From 3eb70ee9ed659fef8c042d09ce4d80b9630c316d Mon Sep 17 00:00:00 2001 From: admin8800 <153331261+admin8800@users.noreply.github.com> Date: Sun, 10 May 2026 06:41:44 +0000 Subject: [PATCH] add --- .dockerignore | 26 + .github/FUNDING.yml | 1 + .github/dependabot.yml | 10 + .github/workflows/docker.yml | 159 + .github/workflows/release.yml | 202 + .github/workflows/windows.yml | 140 + .gitignore | 32 + Dockerfile | 48 + Dockerfile.frontend-artifact | 43 + LICENSE | 674 +++ README.md | 261 +- api/apiHandler.go | 107 + api/apiService.go | 405 ++ api/apiV2Handler.go | 134 + api/session.go | 69 + api/utils.go | 92 + app/app.go | 128 + build.sh | 15 + cmd/admin.go | 64 + cmd/cmd.go | 112 + cmd/migration/1_1.go | 88 + cmd/migration/1_2.go | 317 ++ cmd/migration/1_3.go | 155 + cmd/migration/main.go | 79 + cmd/setting.go | 217 + config/config.go | 68 + config/name | 1 + config/version | 1 + core/box.go | 592 +++ core/endpoint.go | 147 + core/log.go | 242 ++ core/main.go | 95 + core/outbound_check.go | 40 + core/register.go | 139 + core/register_naive.go | 12 + core/register_naive_stub.go | 13 + core/register_tailscale.go | 23 + core/register_tailscale_stub.go | 34 + core/tracker_conn.go | 219 + core/tracker_stats.go | 153 + cronjob/WALCheckpointJob.go | 19 + cronjob/checkCoreJob.go | 17 + cronjob/cronJob.go | 43 + cronjob/delStatsJob.go | 26 + cronjob/depleteJob.go | 30 + cronjob/statsJob.go | 25 + database/backup.go | 302 ++ database/db.go | 123 + database/model/endpoints.go | 63 + database/model/inbounds.go | 103 + database/model/model.go | 73 + database/model/outbounds.go | 53 + database/model/services.go | 90 + docker-build-test.sh | 29 + docker-compose.yml | 22 + entrypoint.sh | 8 + frontend/.browserslistrc | 4 + frontend/.editorconfig | 5 + frontend/.eslintrc.js | 14 + frontend/.gitignore | 22 + frontend/.npmrc | 2 + frontend/LICENSE | 674 +++ frontend/README.md | 76 + frontend/index.html | 22 + frontend/package-lock.json | 3824 +++++++++++++++++ frontend/package.json | 42 + frontend/public/assets/favicon.ico | Bin 0 -> 1562 bytes frontend/src/App.vue | 34 + .../src/assets/Vazirmatn-UI-NL-Regular.woff2 | Bin 0 -> 21840 bytes frontend/src/assets/logo.png | Bin 0 -> 763 bytes frontend/src/assets/logo.svg | 24 + frontend/src/components/Addr.vue | 73 + frontend/src/components/DateTime.vue | 139 + frontend/src/components/Dial.vue | 238 + frontend/src/components/DnsRule.vue | 359 ++ frontend/src/components/Editor.vue | 133 + frontend/src/components/ExpTextarea.vue | 76 + frontend/src/components/Headers.vue | 104 + frontend/src/components/Listen.vue | 149 + frontend/src/components/Main.vue | 295 ++ frontend/src/components/Multiplex.vue | 132 + frontend/src/components/Network.vue | 29 + frontend/src/components/OutJson.vue | 152 + frontend/src/components/Rule.vue | 568 +++ frontend/src/components/SimpleDNS.vue | 61 + frontend/src/components/SubClashExt.vue | 248 ++ frontend/src/components/SubJsonExt.vue | 514 +++ frontend/src/components/Transport.vue | 51 + frontend/src/components/UoT.vue | 29 + frontend/src/components/Users.vue | 42 + frontend/src/components/WgPeer.vue | 113 + frontend/src/components/message.vue | 43 + frontend/src/components/protocols/AnyTls.vue | 80 + frontend/src/components/protocols/Direct.vue | 43 + frontend/src/components/protocols/Http.vue | 50 + .../src/components/protocols/Hysteria.vue | 159 + .../src/components/protocols/Hysteria2.vue | 219 + frontend/src/components/protocols/Naive.vue | 138 + .../src/components/protocols/OutShadowTls.vue | 44 + .../src/components/protocols/Selector.vue | 46 + .../src/components/protocols/ShadowTls.vue | 165 + .../src/components/protocols/Shadowsocks.vue | 77 + frontend/src/components/protocols/Socks.vue | 59 + frontend/src/components/protocols/Ssh.vue | 151 + frontend/src/components/protocols/TProxy.vue | 22 + .../src/components/protocols/Tailscale.vue | 200 + frontend/src/components/protocols/Tor.vue | 115 + frontend/src/components/protocols/Trojan.vue | 24 + frontend/src/components/protocols/Tuic.vue | 89 + frontend/src/components/protocols/Tun.vue | 100 + frontend/src/components/protocols/UrlTest.vue | 121 + frontend/src/components/protocols/Vless.vue | 48 + frontend/src/components/protocols/Vmess.vue | 72 + frontend/src/components/protocols/Warp.vue | 170 + .../src/components/protocols/Wireguard.vue | 197 + frontend/src/components/services/Ccm.vue | 67 + frontend/src/components/services/Derp.vue | 219 + frontend/src/components/services/Ocm.vue | 67 + frontend/src/components/services/SSMAPI.vue | 100 + frontend/src/components/tiles/Gauge.vue | 119 + frontend/src/components/tiles/History.vue | 213 + frontend/src/components/tls/Acme.vue | 259 ++ frontend/src/components/tls/Ech.vue | 165 + frontend/src/components/tls/InTLS.vue | 26 + frontend/src/components/tls/OutTLS.vue | 381 ++ frontend/src/components/transports/Http.vue | 82 + .../src/components/transports/HttpUpgrade.vue | 31 + .../src/components/transports/WebSocket.vue | 69 + frontend/src/components/transports/gRPC.vue | 65 + frontend/src/layouts/default/AppBar.vue | 78 + frontend/src/layouts/default/Default.vue | 38 + frontend/src/layouts/default/Drawer.vue | 69 + frontend/src/layouts/default/View.vue | 14 + frontend/src/layouts/modals/Admin.vue | 97 + frontend/src/layouts/modals/Backup.vue | 99 + frontend/src/layouts/modals/Changes.vue | 145 + frontend/src/layouts/modals/Client.vue | 370 ++ frontend/src/layouts/modals/ClientAddBulk.vue | 228 + .../src/layouts/modals/ClientEditBulk.vue | 196 + frontend/src/layouts/modals/Dns.vue | 210 + frontend/src/layouts/modals/DnsRule.vue | 313 ++ frontend/src/layouts/modals/Endpoint.vue | 227 + frontend/src/layouts/modals/Inbound.vue | 287 ++ frontend/src/layouts/modals/Logs.vue | 90 + frontend/src/layouts/modals/Outbound.vue | 212 + frontend/src/layouts/modals/OutboundBulk.vue | 155 + frontend/src/layouts/modals/QrCode.vue | 150 + frontend/src/layouts/modals/Rule.vue | 324 ++ frontend/src/layouts/modals/RuleImport.vue | 259 ++ frontend/src/layouts/modals/Ruleset.vue | 133 + frontend/src/layouts/modals/RulesetImport.vue | 187 + frontend/src/layouts/modals/Service.vue | 128 + frontend/src/layouts/modals/Stats.vue | 218 + frontend/src/layouts/modals/Tls.vue | 592 +++ frontend/src/layouts/modals/Token.vue | 258 ++ frontend/src/layouts/modals/UsageStats.vue | 101 + frontend/src/layouts/modals/WgQrCode.vue | 129 + frontend/src/locales/en.ts | 637 +++ frontend/src/locales/fa.ts | 635 +++ frontend/src/locales/index.ts | 42 + frontend/src/locales/ru.ts | 640 +++ frontend/src/locales/vi.ts | 635 +++ frontend/src/locales/zhcn.ts | 635 +++ frontend/src/locales/zhtw.ts | 635 +++ frontend/src/main.ts | 55 + frontend/src/plugins/api.ts | 57 + frontend/src/plugins/httputil.ts | 88 + frontend/src/plugins/index.ts | 10 + frontend/src/plugins/randomUtil.ts | 82 + frontend/src/plugins/utils.ts | 104 + frontend/src/plugins/vuetify.ts | 56 + frontend/src/router/index.ts | 123 + frontend/src/store/index.ts | 5 + frontend/src/store/modules/data.ts | 143 + frontend/src/styles/settings.scss | 39 + frontend/src/types/brutal.ts | 5 + frontend/src/types/clients.ts | 185 + frontend/src/types/config.ts | 116 + frontend/src/types/dial.ts | 18 + frontend/src/types/dns.ts | 127 + frontend/src/types/endpoints.ts | 82 + frontend/src/types/inbounds.ts | 240 ++ frontend/src/types/multiplex.ts | 14 + frontend/src/types/outbounds.ts | 268 ++ frontend/src/types/rules.ts | 81 + frontend/src/types/services.ts | 77 + frontend/src/types/tls.ts | 139 + frontend/src/types/transport.ts | 48 + frontend/src/views/Admins.vue | 161 + frontend/src/views/Basics.vue | 324 ++ frontend/src/views/Clients.vue | 424 ++ frontend/src/views/Dns.vue | 372 ++ frontend/src/views/Endpoints.vue | 176 + frontend/src/views/Home.vue | 7 + frontend/src/views/Inbounds.vue | 190 + frontend/src/views/Login.vue | 111 + frontend/src/views/Outbounds.vue | 250 ++ frontend/src/views/Rules.vue | 319 ++ frontend/src/views/Services.vue | 131 + frontend/src/views/Settings.vue | 284 ++ frontend/src/views/Tls.vue | 141 + frontend/src/vite-env.d.ts | 12 + frontend/tsconfig.json | 23 + frontend/tsconfig.node.json | 10 + frontend/vite.config.mts | 64 + go.mod | 199 + go.sum | 493 +++ install.sh | 188 + logger/logger.go | 128 + main.go | 49 + middleware/domainValidator.go | 25 + network/auto_https_conn.go | 67 + network/auto_https_listener.go | 21 + runSUI.sh | 2 + s-ui.service | 14 + s-ui.sh | 934 ++++ service/client.go | 548 +++ service/config.go | 297 ++ service/endpoints.go | 140 + service/inbounds.go | 362 ++ service/outbounds.go | 118 + service/panel.go | 32 + service/server.go | 283 ++ service/services.go | 153 + service/setting.go | 421 ++ service/stats.go | 162 + service/tls.go | 105 + service/user.go | 161 + service/warp.go | 224 + sub/clashService.go | 393 ++ sub/jsonService.go | 326 ++ sub/linkService.go | 74 + sub/sub.go | 162 + sub/subHandler.go | 78 + sub/subService.go | 93 + util/base64.go | 20 + util/common/array.go | 45 + util/common/err.go | 28 + util/common/random.go | 55 + util/genLink.go | 615 +++ util/linkToJson.go | 581 +++ util/outJson.go | 237 + util/subInfo.go | 15 + util/subToJson.go | 97 + web/web.go | 229 + windows/README.md | 23 + windows/build-windows.bat | 73 + windows/build-windows.ps1 | 138 + windows/install-windows.bat | 195 + windows/s-ui-windows.bat | 237 + windows/s-ui-windows.xml | 22 + windows/uninstall-windows.bat | 102 + 252 files changed, 42215 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/FUNDING.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/windows.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Dockerfile.frontend-artifact create mode 100644 LICENSE create mode 100644 api/apiHandler.go create mode 100644 api/apiService.go create mode 100644 api/apiV2Handler.go create mode 100644 api/session.go create mode 100644 api/utils.go create mode 100644 app/app.go create mode 100644 build.sh create mode 100644 cmd/admin.go create mode 100644 cmd/cmd.go create mode 100644 cmd/migration/1_1.go create mode 100644 cmd/migration/1_2.go create mode 100644 cmd/migration/1_3.go create mode 100644 cmd/migration/main.go create mode 100644 cmd/setting.go create mode 100644 config/config.go create mode 100644 config/name create mode 100644 config/version create mode 100644 core/box.go create mode 100644 core/endpoint.go create mode 100644 core/log.go create mode 100644 core/main.go create mode 100644 core/outbound_check.go create mode 100644 core/register.go create mode 100644 core/register_naive.go create mode 100644 core/register_naive_stub.go create mode 100644 core/register_tailscale.go create mode 100644 core/register_tailscale_stub.go create mode 100644 core/tracker_conn.go create mode 100644 core/tracker_stats.go create mode 100644 cronjob/WALCheckpointJob.go create mode 100644 cronjob/checkCoreJob.go create mode 100644 cronjob/cronJob.go create mode 100644 cronjob/delStatsJob.go create mode 100644 cronjob/depleteJob.go create mode 100644 cronjob/statsJob.go create mode 100644 database/backup.go create mode 100644 database/db.go create mode 100644 database/model/endpoints.go create mode 100644 database/model/inbounds.go create mode 100644 database/model/model.go create mode 100644 database/model/outbounds.go create mode 100644 database/model/services.go create mode 100644 docker-build-test.sh create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 frontend/.browserslistrc create mode 100644 frontend/.editorconfig create mode 100644 frontend/.eslintrc.js create mode 100644 frontend/.gitignore create mode 100644 frontend/.npmrc create mode 100644 frontend/LICENSE create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/assets/favicon.ico create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/assets/Vazirmatn-UI-NL-Regular.woff2 create mode 100644 frontend/src/assets/logo.png create mode 100644 frontend/src/assets/logo.svg create mode 100644 frontend/src/components/Addr.vue create mode 100644 frontend/src/components/DateTime.vue create mode 100644 frontend/src/components/Dial.vue create mode 100644 frontend/src/components/DnsRule.vue create mode 100644 frontend/src/components/Editor.vue create mode 100644 frontend/src/components/ExpTextarea.vue create mode 100644 frontend/src/components/Headers.vue create mode 100644 frontend/src/components/Listen.vue create mode 100644 frontend/src/components/Main.vue create mode 100644 frontend/src/components/Multiplex.vue create mode 100644 frontend/src/components/Network.vue create mode 100644 frontend/src/components/OutJson.vue create mode 100644 frontend/src/components/Rule.vue create mode 100644 frontend/src/components/SimpleDNS.vue create mode 100644 frontend/src/components/SubClashExt.vue create mode 100644 frontend/src/components/SubJsonExt.vue create mode 100644 frontend/src/components/Transport.vue create mode 100644 frontend/src/components/UoT.vue create mode 100644 frontend/src/components/Users.vue create mode 100644 frontend/src/components/WgPeer.vue create mode 100644 frontend/src/components/message.vue create mode 100644 frontend/src/components/protocols/AnyTls.vue create mode 100644 frontend/src/components/protocols/Direct.vue create mode 100644 frontend/src/components/protocols/Http.vue create mode 100644 frontend/src/components/protocols/Hysteria.vue create mode 100644 frontend/src/components/protocols/Hysteria2.vue create mode 100644 frontend/src/components/protocols/Naive.vue create mode 100644 frontend/src/components/protocols/OutShadowTls.vue create mode 100644 frontend/src/components/protocols/Selector.vue create mode 100644 frontend/src/components/protocols/ShadowTls.vue create mode 100644 frontend/src/components/protocols/Shadowsocks.vue create mode 100644 frontend/src/components/protocols/Socks.vue create mode 100644 frontend/src/components/protocols/Ssh.vue create mode 100644 frontend/src/components/protocols/TProxy.vue create mode 100644 frontend/src/components/protocols/Tailscale.vue create mode 100644 frontend/src/components/protocols/Tor.vue create mode 100644 frontend/src/components/protocols/Trojan.vue create mode 100644 frontend/src/components/protocols/Tuic.vue create mode 100644 frontend/src/components/protocols/Tun.vue create mode 100644 frontend/src/components/protocols/UrlTest.vue create mode 100644 frontend/src/components/protocols/Vless.vue create mode 100644 frontend/src/components/protocols/Vmess.vue create mode 100644 frontend/src/components/protocols/Warp.vue create mode 100644 frontend/src/components/protocols/Wireguard.vue create mode 100644 frontend/src/components/services/Ccm.vue create mode 100644 frontend/src/components/services/Derp.vue create mode 100644 frontend/src/components/services/Ocm.vue create mode 100644 frontend/src/components/services/SSMAPI.vue create mode 100644 frontend/src/components/tiles/Gauge.vue create mode 100644 frontend/src/components/tiles/History.vue create mode 100644 frontend/src/components/tls/Acme.vue create mode 100644 frontend/src/components/tls/Ech.vue create mode 100644 frontend/src/components/tls/InTLS.vue create mode 100644 frontend/src/components/tls/OutTLS.vue create mode 100644 frontend/src/components/transports/Http.vue create mode 100644 frontend/src/components/transports/HttpUpgrade.vue create mode 100644 frontend/src/components/transports/WebSocket.vue create mode 100644 frontend/src/components/transports/gRPC.vue create mode 100644 frontend/src/layouts/default/AppBar.vue create mode 100644 frontend/src/layouts/default/Default.vue create mode 100644 frontend/src/layouts/default/Drawer.vue create mode 100644 frontend/src/layouts/default/View.vue create mode 100644 frontend/src/layouts/modals/Admin.vue create mode 100644 frontend/src/layouts/modals/Backup.vue create mode 100644 frontend/src/layouts/modals/Changes.vue create mode 100644 frontend/src/layouts/modals/Client.vue create mode 100644 frontend/src/layouts/modals/ClientAddBulk.vue create mode 100644 frontend/src/layouts/modals/ClientEditBulk.vue create mode 100644 frontend/src/layouts/modals/Dns.vue create mode 100644 frontend/src/layouts/modals/DnsRule.vue create mode 100644 frontend/src/layouts/modals/Endpoint.vue create mode 100644 frontend/src/layouts/modals/Inbound.vue create mode 100644 frontend/src/layouts/modals/Logs.vue create mode 100644 frontend/src/layouts/modals/Outbound.vue create mode 100644 frontend/src/layouts/modals/OutboundBulk.vue create mode 100644 frontend/src/layouts/modals/QrCode.vue create mode 100644 frontend/src/layouts/modals/Rule.vue create mode 100644 frontend/src/layouts/modals/RuleImport.vue create mode 100644 frontend/src/layouts/modals/Ruleset.vue create mode 100644 frontend/src/layouts/modals/RulesetImport.vue create mode 100644 frontend/src/layouts/modals/Service.vue create mode 100644 frontend/src/layouts/modals/Stats.vue create mode 100644 frontend/src/layouts/modals/Tls.vue create mode 100644 frontend/src/layouts/modals/Token.vue create mode 100644 frontend/src/layouts/modals/UsageStats.vue create mode 100644 frontend/src/layouts/modals/WgQrCode.vue create mode 100644 frontend/src/locales/en.ts create mode 100644 frontend/src/locales/fa.ts create mode 100644 frontend/src/locales/index.ts create mode 100644 frontend/src/locales/ru.ts create mode 100644 frontend/src/locales/vi.ts create mode 100644 frontend/src/locales/zhcn.ts create mode 100644 frontend/src/locales/zhtw.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/plugins/api.ts create mode 100644 frontend/src/plugins/httputil.ts create mode 100644 frontend/src/plugins/index.ts create mode 100644 frontend/src/plugins/randomUtil.ts create mode 100644 frontend/src/plugins/utils.ts create mode 100644 frontend/src/plugins/vuetify.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/store/index.ts create mode 100644 frontend/src/store/modules/data.ts create mode 100644 frontend/src/styles/settings.scss create mode 100644 frontend/src/types/brutal.ts create mode 100644 frontend/src/types/clients.ts create mode 100644 frontend/src/types/config.ts create mode 100644 frontend/src/types/dial.ts create mode 100644 frontend/src/types/dns.ts create mode 100644 frontend/src/types/endpoints.ts create mode 100644 frontend/src/types/inbounds.ts create mode 100644 frontend/src/types/multiplex.ts create mode 100644 frontend/src/types/outbounds.ts create mode 100644 frontend/src/types/rules.ts create mode 100644 frontend/src/types/services.ts create mode 100644 frontend/src/types/tls.ts create mode 100644 frontend/src/types/transport.ts create mode 100644 frontend/src/views/Admins.vue create mode 100644 frontend/src/views/Basics.vue create mode 100644 frontend/src/views/Clients.vue create mode 100644 frontend/src/views/Dns.vue create mode 100644 frontend/src/views/Endpoints.vue create mode 100644 frontend/src/views/Home.vue create mode 100644 frontend/src/views/Inbounds.vue create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/Outbounds.vue create mode 100644 frontend/src/views/Rules.vue create mode 100644 frontend/src/views/Services.vue create mode 100644 frontend/src/views/Settings.vue create mode 100644 frontend/src/views/Tls.vue create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.mts create mode 100644 go.mod create mode 100644 go.sum create mode 100644 install.sh create mode 100644 logger/logger.go create mode 100644 main.go create mode 100644 middleware/domainValidator.go create mode 100644 network/auto_https_conn.go create mode 100644 network/auto_https_listener.go create mode 100644 runSUI.sh create mode 100644 s-ui.service create mode 100644 s-ui.sh create mode 100644 service/client.go create mode 100644 service/config.go create mode 100644 service/endpoints.go create mode 100644 service/inbounds.go create mode 100644 service/outbounds.go create mode 100644 service/panel.go create mode 100644 service/server.go create mode 100644 service/services.go create mode 100644 service/setting.go create mode 100644 service/stats.go create mode 100644 service/tls.go create mode 100644 service/user.go create mode 100644 service/warp.go create mode 100644 sub/clashService.go create mode 100644 sub/jsonService.go create mode 100644 sub/linkService.go create mode 100644 sub/sub.go create mode 100644 sub/subHandler.go create mode 100644 sub/subService.go create mode 100644 util/base64.go create mode 100644 util/common/array.go create mode 100644 util/common/err.go create mode 100644 util/common/random.go create mode 100644 util/genLink.go create mode 100644 util/linkToJson.go create mode 100644 util/outJson.go create mode 100644 util/subInfo.go create mode 100644 util/subToJson.go create mode 100644 web/web.go create mode 100644 windows/README.md create mode 100644 windows/build-windows.bat create mode 100644 windows/build-windows.ps1 create mode 100644 windows/install-windows.bat create mode 100644 windows/s-ui-windows.bat create mode 100644 windows/s-ui-windows.xml create mode 100644 windows/uninstall-windows.bat diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bd98687 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +.DS_Store +dist/ +release/ +backup/ +bin/ +db/ +sui +web/html +main +tmp +.sync* +*.tar.gz +frontend/node_modules +frontend/.vite + +# local env files +.env.local +.env.*.local + +# Log files +*.log* +.cache + +# Editor directories and files +.idea +.vscode diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8d5df76 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: alireza0 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..153da9a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..db90dff --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,159 @@ +name: Docker Image CI +on: + push: + tags: + - "*" + workflow_dispatch: + +jobs: + frontend-build: + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + with: + submodules: recursive + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 25 + - name: Install dependencies and build frontend + run: | + cd frontend + npm install + npm run build + - name: Upload frontend build artifact + uses: actions/upload-artifact@v7 + with: + name: frontend-dist + path: frontend/dist/ + + build: + needs: frontend-build + strategy: + fail-fast: false + matrix: + include: + - { platform: linux/amd64 } + - { platform: linux/386 } + - { platform: linux/arm64/v8 } + - { platform: linux/arm/v7 } + - { platform: linux/arm/v6 } + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + - name: Download frontend build artifact + uses: actions/download-artifact@v8 + with: + name: frontend-dist + path: frontend_dist + - name: Prepare + run: | + platform="${{ matrix.platform }}" + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + - name: Docker meta + id: meta + uses: docker/metadata-action@v6 + with: + images: | + alireza7/s-ui + ghcr.io/alireza0/s-ui + tags: | + type=ref,event=branch + type=ref,event=tag + type=pep440,pattern={{version}} + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Cache Docker layers + uses: actions/cache@v5 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ matrix.platform }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-${{ matrix.platform }}- + - name: Login to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push by digest + id: build + uses: docker/build-push-action@v7 + with: + context: . + file: Dockerfile.frontend-artifact + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + tags: | + alireza7/s-ui + ghcr.io/alireza0/s-ui + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache,mode=max + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + echo "${digest#sha256:}" > "${{ runner.temp }}/digests/${digest#sha256:}" + - name: Upload digest + uses: actions/upload-artifact@v7 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + needs: build + runs-on: ubuntu-24.04 + steps: + - name: Download digests + uses: actions/download-artifact@v8 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + - name: Login to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Docker meta + id: meta + uses: docker/metadata-action@v6 + with: + images: | + alireza7/s-ui + ghcr.io/alireza0/s-ui + tags: | + type=ref,event=branch + type=ref,event=tag + type=pep440,pattern={{version}} + - name: Create manifest list and push + env: + DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} + working-directory: ${{ runner.temp }}/digests + run: | + set -e + for img in alireza7/s-ui ghcr.io/alireza0/s-ui; do + TAGS_ARGS=$(echo "$DOCKER_METADATA_OUTPUT_JSON" | jq -cr --arg img "$img" '.tags | map(select(startswith($img))) | map("-t " + .) | join(" ")') + DIGEST_REFS=$(for f in *; do echo -n "${img}@sha256:$(cat "$f") "; done) + docker buildx imagetools create $TAGS_ARGS $DIGEST_REFS + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..151fb49 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,202 @@ +name: Release S-UI + +on: + workflow_dispatch: + release: + types: [published] + push: + branches: + - main + tags: + - "*" + paths: + - '.github/workflows/release.yml' + - 'frontend/**' + - '**.sh' + - '**.go' + - 'go.mod' + - 'go.sum' + - 's-ui.service' + +env: + NODE_VERSION: "25" + CRONET_GO_VERSION: "2fef65f9dba90ddb89a87d00a6eb6165487c10c1" + CRONET_GO_REPO: https://github.com/sagernet/cronet-go.git + BOOTLIN_BASE_URL: https://toolchains.bootlin.com/downloads/releases/toolchains + +jobs: + build-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout repository (frontend only) + uses: actions/checkout@v6.0.2 + with: + submodules: recursive + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Build frontend + run: | + cd frontend + npm install + npm run build + cd .. + + - name: Upload frontend dist + uses: actions/upload-artifact@v7 + with: + name: frontend-dist + path: frontend/dist/ + + build-linux: + name: build-${{ matrix.platform }} + needs: build-frontend + strategy: + fail-fast: false + matrix: + include: + - { platform: amd64, arch: amd64, bootlin: x86-64, naive: true } + - { platform: arm64, arch: arm64, bootlin: aarch64, naive: true } + - { platform: armv7, arch: arm, goarm: "7", bootlin: armv7-eabihf, naive: true } + - { platform: armv6, arch: arm, goarm: "6", bootlin: armv6-eabihf, naive: true } + - { platform: armv5, arch: arm, goarm: "5", bootlin: armv5-eabi, naive: false } + - { platform: "386", arch: "386", bootlin: x86-i686, naive: true } + - { platform: s390x, arch: s390x, bootlin: s390x-z13, naive: false } + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + + - name: Download frontend dist + uses: actions/download-artifact@v8 + with: + name: frontend-dist + path: web/html + + - name: Setup Go + uses: actions/setup-go@v6 + with: + cache: false + go-version-file: go.mod + + # Naive platforms: use cronet toolchain only (no Bootlin). + - name: Clone cronet-go (cronet toolchain for naive) + if: matrix.naive + run: | + set -e + git init ~/cronet-go + git -C ~/cronet-go remote add origin ${{ env.CRONET_GO_REPO }} + git -C ~/cronet-go fetch --depth=1 origin "${{ env.CRONET_GO_VERSION }}" + git -C ~/cronet-go checkout FETCH_HEAD + git -C ~/cronet-go submodule update --init --recursive --depth=1 + + - name: Regenerate Debian keyring (cronet sysroot) + if: matrix.naive + run: | + set -e + rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg + cd ~/cronet-go + GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh + + - name: Cache Chromium toolchain + if: matrix.naive + id: cache-chromium-toolchain + uses: actions/cache@v5 + with: + path: | + ~/cronet-go/naiveproxy/src/third_party/llvm-build/ + ~/cronet-go/naiveproxy/src/gn/out/ + ~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/ + ~/cronet-go/naiveproxy/src/out/sysroot-build/ + key: chromium-toolchain-${{ matrix.platform }}-musl-${{ env.CRONET_GO_VERSION }} + + - name: Build cronet lib and set toolchain env (CC, CXX, CGO_LDFLAGS, PATH) + if: matrix.naive + run: | + set -e + cd ~/cronet-go + go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain + go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env | while IFS= read -r line; do + line="${line#export }" + [[ -z "$line" ]] && continue + echo "$line" >> $GITHUB_ENV + done + + - name: Set Go build env (all platforms) + run: | + echo "CGO_ENABLED=1" >> $GITHUB_ENV + echo "GOOS=linux" >> $GITHUB_ENV + echo "GOARCH=${{ matrix.arch }}" >> $GITHUB_ENV + if [ -n "${{ matrix.goarm }}" ]; then echo "GOARM=${{ matrix.goarm }}" >> $GITHUB_ENV; fi + + # Non-naive platforms only: Bootlin musl (armv5, s390x). + - name: Set up Bootlin musl (armv5, s390x) + if: ${{ matrix.naive != true }} + run: | + set -e + BOOTLIN_ARCH="${{ matrix.bootlin }}" + echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})" + TARBALL_BASE="${{ env.BOOTLIN_BASE_URL }}/$BOOTLIN_ARCH/tarballs/" + TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1) + [ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; } + echo "Downloading: $TARBALL_URL" + cd /tmp + curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL" + tar -xf "$(basename "$TARBALL_URL")" + TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1) + TOOLCHAIN_DIR="$(realpath "$TOOLCHAIN_DIR")" + BIN_DIR="$TOOLCHAIN_DIR/bin" + echo "PATH=$BIN_DIR:$PATH" >> $GITHUB_ENV + CC=$(find "$BIN_DIR" -maxdepth 1 \( -name '*-gcc.br_real' -o -name '*-gcc' \) -type f -executable 2>/dev/null | grep -v g++ | head -n1) + [ -z "$CC" ] && { echo "No gcc found in $BIN_DIR" >&2; exit 1; } + echo "CC=$(realpath "$CC")" >> $GITHUB_ENV + SYSROOT="" + F=$(find "$TOOLCHAIN_DIR" -name "libc-header-start.h" 2>/dev/null | head -1) + if [ -n "$F" ]; then SYSROOT=$(dirname "$(dirname "$(dirname "$(dirname "$F")")")"); fi + if [ -n "$SYSROOT" ] && [ -d "$SYSROOT" ]; then + echo "CGO_CFLAGS=--sysroot=$SYSROOT" >> $GITHUB_ENV + echo "CGO_LDFLAGS=--sysroot=$SYSROOT -static" >> $GITHUB_ENV + fi + + - name: Build s-ui + run: | + set -e + BUILD_TAGS="with_quic,with_grpc,with_utls,with_acme,with_gvisor,badlinkname,tfogo_checklinkname0,with_tailscale" + [ "${{ matrix.naive }}" = "true" ] && BUILD_TAGS="${BUILD_TAGS},with_naive_outbound,with_musl" + go build -ldflags="-w -s -checklinkname=0 -linkmode external -extldflags '-static'" -tags "$BUILD_TAGS" -o sui main.go + file sui + ldd sui 2>/dev/null || echo "Static binary confirmed" + + mkdir s-ui + cp sui s-ui/ + cp s-ui.service s-ui/ + cp s-ui.sh s-ui/ + + - name: Package + run: tar -zcvf s-ui-linux-${{ matrix.platform }}.tar.gz s-ui + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: s-ui-linux-${{ matrix.platform }} + path: ./s-ui-linux-${{ matrix.platform }}.tar.gz + retention-days: 30 + + - name: Upload to Release + uses: svenstaro/upload-release-action@v2 + if: | + (github.event_name == 'release' && github.event.action == 'published') || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.event_name == 'release' && github.event.release.tag_name || github.ref_name }} + file: s-ui-linux-${{ matrix.platform }}.tar.gz + asset_name: s-ui-linux-${{ matrix.platform }}.tar.gz + prerelease: true + overwrite: true diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..4aaca53 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,140 @@ +name: Build S-UI for Windows + +on: + workflow_dispatch: + release: + types: [published] + push: + branches: + - main + tags: + - "*" + paths: + - '.github/workflows/windows.yml' + - 'frontend/**' + - '**.go' + - 'go.mod' + - 'go.sum' + - 'windows/**' + +env: + NODE_VERSION: "25" + TAGS: "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0,with_tailscale" + LIBCRONET_BASE_URL: "https://github.com/SagerNet/cronet-go/releases/latest/download" + +jobs: + build-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + with: + submodules: recursive + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: 'https://registry.npmjs.org' + + - name: Build frontend + run: | + cd frontend + npm install + npm run build + cd .. + + - name: Upload frontend artifact + uses: actions/upload-artifact@v7 + with: + name: frontend-dist + path: frontend/dist + retention-days: 1 + + build-windows: + needs: build-frontend + name: build-windows-${{ matrix.arch }} + strategy: + fail-fast: false + matrix: + include: + - { arch: amd64, runner: windows-latest, cgo: "1" } + - { arch: arm64, runner: ubuntu-latest, cgo: "0" } + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + + - name: Download frontend artifact + uses: actions/download-artifact@v8 + with: + name: frontend-dist + path: web/html + + - name: Setup Go + uses: actions/setup-go@v6 + with: + cache: false + go-version-file: go.mod + + - name: Install zip for Windows + if: matrix.arch == 'amd64' + shell: powershell + run: | + # Install Chocolatey if not available + if (!(Get-Command choco -ErrorAction SilentlyContinue)) { + Set-ExecutionPolicy Bypass -Scope Process -Force + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + } + # Install zip + choco install zip -y + + - name: Build s-ui + shell: bash + run: | + export CGO_ENABLED=${{ matrix.cgo }} + export GOOS=windows + export GOARCH=${{ matrix.arch }} + + echo "Building for Windows ${{ matrix.arch }}" + go version + go env GOOS GOARCH + + go build -ldflags="-w -s -checklinkname=0" -tags "${{ env.TAGS }}" -o sui.exe main.go + file sui.exe + + mkdir s-ui-windows + cp sui.exe s-ui-windows/ + cp -r windows/* s-ui-windows/ + + - name: Download libcronet-go + shell: bash + run: | + curl -qsL -o s-ui-windows/libcronet.dll ${{ env.LIBCRONET_BASE_URL }}/libcronet-windows-${{ matrix.arch }}.dll + + - name: Package + shell: bash + run: | + zip -r "s-ui-windows-${{ matrix.arch }}.zip" s-ui-windows + + - name: Upload files to Artifacts + uses: actions/upload-artifact@v7 + with: + name: s-ui-windows-${{ matrix.arch }} + path: ./s-ui-windows-${{ matrix.arch }}.zip + retention-days: 30 + + - name: Upload to Release + uses: svenstaro/upload-release-action@v2 + if: | + (github.event_name == 'release' && github.event.action == 'published') || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref }} + file: s-ui-windows-${{ matrix.arch }}.zip + asset_name: s-ui-windows-${{ matrix.arch }}.zip + prerelease: true + overwrite: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd33dbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +.DS_Store +dist/ +release/ +backup/ +bin/ +db/ +sui +web/html +main +tmp +.sync* +*.tar.gz + +# local env files +.env.local +.env.*.local + +# Log files +*.log* +.cache + +# Windows build artifacts +*.exe +*.zip +s-ui-windows/ +sui-*.exe +sui-*.zip +windows/sui-*.exe + +# Editor directories and files +.idea +.vscode diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e084dfc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +FROM --platform=$BUILDPLATFORM node:alpine AS front-builder +WORKDIR /app +COPY frontend/ ./ +RUN npm install && npm run build + +FROM golang:1.25-alpine AS backend-builder +WORKDIR /app +ARG TARGETARCH +ARG TARGETVARIANT +ENV CGO_ENABLED=1 +ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" +ENV GOARCH=$TARGETARCH + +RUN apk update && apk add --no-cache \ + gcc \ + musl-dev \ + libc-dev \ + make \ + git \ + wget \ + unzip \ + bash \ + curl + +ENV CC=gcc + +RUN CRONET_ARCH="$TARGETARCH" && \ + CRONET_URL="https://github.com/SagerNet/cronet-go/releases/latest/download/libcronet-linux-${CRONET_ARCH}.so"; \ + echo "Downloading $CRONET_URL" && \ + wget -q -O ./libcronet.so "$CRONET_URL" && \ + chmod 755 ./libcronet.so + +COPY . . +COPY --from=front-builder /app/dist/ /app/web/html/ + +RUN if [ "$TARGETARCH" = "arm" ]; then export GOARM=7; [ "$TARGETVARIANT" = "v6" ] && export GOARM=6; fi; \ + go build -ldflags="-w -s" \ + -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_naive_outbound,with_purego,with_tailscale" \ + -o sui main.go + +FROM alpine +LABEL org.opencontainers.image.authors="alireza7@gmail.com" +ENV TZ=Asia/Tehran +WORKDIR /app +RUN set -ex && apk add --no-cache --upgrade bash tzdata ca-certificates nftables +COPY --from=backend-builder /app/sui /app/libcronet.so /app/ +COPY entrypoint.sh /app/ +ENTRYPOINT [ "./entrypoint.sh" ] \ No newline at end of file diff --git a/Dockerfile.frontend-artifact b/Dockerfile.frontend-artifact new file mode 100644 index 0000000..e7395d0 --- /dev/null +++ b/Dockerfile.frontend-artifact @@ -0,0 +1,43 @@ +FROM golang:1.25-alpine AS backend-builder +WORKDIR /app +ARG TARGETARCH +ARG TARGETVARIANT +ENV CGO_ENABLED=1 +ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" +ENV GOARCH=$TARGETARCH + +RUN apk update && apk add --no-cache \ + gcc \ + musl-dev \ + libc-dev \ + make \ + git \ + wget \ + unzip \ + bash \ + curl + +ENV CC=gcc + +RUN CRONET_ARCH="$TARGETARCH" && \ + CRONET_URL="https://github.com/SagerNet/cronet-go/releases/latest/download/libcronet-linux-${CRONET_ARCH}.so"; \ + echo "Downloading $CRONET_URL" && \ + wget -q -O ./libcronet.so "$CRONET_URL" && \ + chmod 755 ./libcronet.so + +COPY . . +COPY frontend_dist/ /app/web/html/ + +RUN if [ "$TARGETARCH" = "arm" ]; then export GOARM=7; [ "$TARGETVARIANT" = "v6" ] && export GOARM=6; fi; \ + go build -ldflags="-w -s" \ + -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_naive_outbound,with_purego,with_tailscale" \ + -o sui main.go + +FROM alpine +LABEL org.opencontainers.image.authors="alireza7@gmail.com" +ENV TZ=Asia/Tehran +WORKDIR /app +RUN set -ex && apk add --no-cache --upgrade bash tzdata ca-certificates nftables +COPY --from=backend-builder /app/sui /app/libcronet.so /app/ +COPY entrypoint.sh /app/ +ENTRYPOINT [ "./entrypoint.sh" ] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3877ae0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index d6dffee..1705268 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,259 @@ -# s-ui -s-ui最后一个版本 +# S-UI +**An Advanced Web Panel • Built on SagerNet/Sing-Box** + +![](https://img.shields.io/github/v/release/alireza0/s-ui.svg) +![S-UI Docker pull](https://img.shields.io/docker/pulls/alireza7/s-ui.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/alireza0/s-ui)](https://goreportcard.com/report/github.com/alireza0/s-ui) +[![Downloads](https://img.shields.io/github/downloads/alireza0/s-ui/total.svg)](https://img.shields.io/github/downloads/alireza0/s-ui/total.svg) +[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +> **Disclaimer:** This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment + +**If you think this project is helpful to you, you may wish to give a**:star2: + +**Want to contribute?** See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding conventions, testing, and the pull request process. + +[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/alireza7) + + + Crypto donation button by NOWPayments + + +## Quick Overview +| Features | Enable? | +| -------------------------------------- | :----------------: | +| Multi-Protocol | :heavy_check_mark: | +| Multi-Language | :heavy_check_mark: | +| Multi-Client/Inbound | :heavy_check_mark: | +| Advanced Traffic Routing Interface | :heavy_check_mark: | +| Client & Traffic & System Status | :heavy_check_mark: | +| Subscription Link (link/json/clash + info)| :heavy_check_mark: | +| Dark/Light Theme | :heavy_check_mark: | +| API Interface | :heavy_check_mark: | + +## Supported Platforms +| Platform | Architecture | Status | +|----------|--------------|---------| +| Linux | amd64, arm64, armv7, armv6, armv5, 386, s390x | ✅ Supported | +| Windows | amd64, 386, arm64 | ✅ Supported | +| macOS | amd64, arm64 | 🚧 Experimental | + +## Screenshots + +!["Main"](https://github.com/alireza0/s-ui-frontend/raw/main/media/main.png) + +[Other UI Screenshots](https://github.com/alireza0/s-ui-frontend/blob/main/screenshots.md) + +## API Documentation + +[API-Documentation Wiki](https://github.com/alireza0/s-ui/wiki/API-Documentation) + +## Default Installation Information +- Panel Port: 2095 +- Panel Path: /app/ +- Subscription Port: 2096 +- Subscription Path: /sub/ +- User/Password: admin + +## Install & Upgrade to Latest Version + +### Linux/macOS +```sh +bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh) +``` + +### Windows +1. Download the latest Windows release from [GitHub Releases](https://github.com/alireza0/s-ui/releases/latest) +2. Extract the ZIP file +3. Run `install-windows.bat` as Administrator +4. Follow the installation wizard + +## Install legacy Version + +**Step 1:** To install your desired legacy version, add the version to the end of the installation command. e.g., ver `1.0.0`: + +```sh +VERSION=1.0.0 && bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/$VERSION/install.sh) $VERSION +``` + +## Manual installation + +### Linux/macOS +1. Get the latest version of S-UI based on your OS/Architecture from GitHub: [https://github.com/alireza0/s-ui/releases/latest](https://github.com/alireza0/s-ui/releases/latest) +2. **OPTIONAL** Get the latest version of `s-ui.sh` [https://raw.githubusercontent.com/alireza0/s-ui/master/s-ui.sh](https://raw.githubusercontent.com/alireza0/s-ui/master/s-ui.sh) +3. **OPTIONAL** Copy `s-ui.sh` to /usr/bin/ and run `chmod +x /usr/bin/s-ui`. +4. Extract s-ui tar.gz file to a directory of your choice and navigate to the directory where you extracted the tar.gz file. +5. Copy *.service files to /etc/systemd/system/ and run `systemctl daemon-reload`. +6. Enable autostart and start S-UI service using `systemctl enable s-ui --now` +7. Start sing-box service using `systemctl enable sing-box --now` + +### Windows +1. Get the latest Windows version from GitHub: [https://github.com/alireza0/s-ui/releases/latest](https://github.com/alireza0/s-ui/releases/latest) +2. Download the appropriate Windows package (e.g., `s-ui-windows-amd64.zip`) +3. Extract the ZIP file to a directory of your choice +4. Run `install-windows.bat` as Administrator +5. Follow the installation wizard +6. Access the panel at http://localhost:2095/app + +## Uninstall S-UI + +```sh +sudo -i + +systemctl disable s-ui --now + +rm -f /etc/systemd/system/sing-box.service +systemctl daemon-reload + +rm -fr /usr/local/s-ui +rm /usr/bin/s-ui +``` + +## Install using Docker + +
+ Click for details + +### Usage + +**Step 1:** Install Docker + +```shell +curl -fsSL https://get.docker.com | sh +``` + +**Step 2:** Install S-UI + +> Docker compose method + +```shell +mkdir s-ui && cd s-ui +wget -q https://raw.githubusercontent.com/alireza0/s-ui/master/docker-compose.yml +docker compose up -d +``` + +> Use docker + +```shell +mkdir s-ui && cd s-ui +docker run -itd \ + -p 2095:2095 -p 2096:2096 -p 443:443 -p 80:80 \ + -v $PWD/db/:/app/db/ \ + -v $PWD/cert/:/root/cert/ \ + --name s-ui --restart=unless-stopped \ + alireza7/s-ui:latest +``` + +> Build your own image + +```shell +git clone https://github.com/alireza0/s-ui +git submodule update --init --recursive +docker build -t s-ui . +``` + +
+ +## Manual run ( contribution ) + +
+ Click for details + +### Build and run whole project +```shell +./runSUI.sh +``` + +### Clone the repository +```shell +# clone repository +git clone https://github.com/alireza0/s-ui +# clone submodules +git submodule update --init --recursive +``` + + +### - Frontend + +Visit [s-ui-frontend](https://github.com/alireza0/s-ui-frontend) for frontend code + +### - Backend +> Please build frontend once before! + +To build backend: +```shell +# remove old frontend compiled files +rm -fr web/html/* +# apply new frontend compiled files +cp -R frontend/dist/ web/html/ +# build +go build -o sui main.go +``` + +To run backend (from root folder of repository): +```shell +./sui +``` + +
+ +## Languages + +- English +- Farsi +- Vietnamese +- Chinese (Simplified) +- Chinese (Traditional) +- Russian + +## Features + +- Supported protocols: + - General: Mixed, SOCKS, HTTP, HTTPS, Direct, Redirect, TProxy + - V2Ray based: VLESS, VMess, Trojan, Shadowsocks + - Other protocols: ShadowTLS, Hysteria, Hysteria2, Naive, TUIC +- Supports XTLS protocols +- An advanced interface for routing traffic, incorporating PROXY Protocol, External, and Transparent Proxy, SSL Certificate, and Port +- An advanced interface for inbound and outbound configuration +- Clients’ traffic cap and expiration date +- Displays online clients, inbounds and outbounds with traffic statistics, and system status monitoring +- Subscription service with ability to add external links and subscription +- HTTPS for secure access to the web panel and subscription service (self-provided domain + SSL certificate) +- Dark/Light theme + +## Environment Variables + +
+ Click for details + +### Usage + +| Variable | Type | Default | +| -------------- | :--------------------------------------------: | :------------ | +| SUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` | +| SUI_DEBUG | `boolean` | `false` | +| SUI_BIN_FOLDER | `string` | `"bin"` | +| SUI_DB_FOLDER | `string` | `"db"` | +| SINGBOX_API | `string` | - | + +
+ +## SSL Certificate + +
+ Click for details + +### Certbot + +```bash +snap install core; snap refresh core +snap install --classic certbot +ln -s /snap/bin/certbot /usr/bin/certbot + +certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d +``` + +
+ +## Stargazers over Time +[![Stargazers over time](https://starchart.cc/alireza0/s-ui.svg)](https://starchart.cc/alireza0/s-ui) diff --git a/api/apiHandler.go b/api/apiHandler.go new file mode 100644 index 0000000..5a32c56 --- /dev/null +++ b/api/apiHandler.go @@ -0,0 +1,107 @@ +package api + +import ( + "strings" + + "github.com/alireza0/s-ui/util/common" + + "github.com/gin-gonic/gin" +) + +type APIHandler struct { + ApiService + apiv2 *APIv2Handler +} + +func NewAPIHandler(g *gin.RouterGroup, a2 *APIv2Handler) { + a := &APIHandler{ + apiv2: a2, + } + a.initRouter(g) +} + +func (a *APIHandler) initRouter(g *gin.RouterGroup) { + g.Use(func(c *gin.Context) { + path := c.Request.URL.Path + if !strings.HasSuffix(path, "login") && !strings.HasSuffix(path, "logout") { + checkLogin(c) + } + }) + g.POST("/:postAction", a.postHandler) + g.GET("/:getAction", a.getHandler) +} + +func (a *APIHandler) postHandler(c *gin.Context) { + loginUser := GetLoginUser(c) + action := c.Param("postAction") + + switch action { + case "login": + a.ApiService.Login(c) + case "changePass": + a.ApiService.ChangePass(c) + case "save": + a.ApiService.Save(c, loginUser) + case "restartApp": + a.ApiService.RestartApp(c) + case "restartSb": + a.ApiService.RestartSb(c) + case "linkConvert": + a.ApiService.LinkConvert(c) + case "subConvert": + a.ApiService.SubConvert(c) + case "importdb": + a.ApiService.ImportDb(c) + case "addToken": + a.ApiService.AddToken(c) + a.apiv2.ReloadTokens() + case "deleteToken": + a.ApiService.DeleteToken(c) + a.apiv2.ReloadTokens() + default: + jsonMsg(c, "failed", common.NewError("unknown action: ", action)) + } +} + +func (a *APIHandler) getHandler(c *gin.Context) { + action := c.Param("getAction") + + switch action { + case "logout": + a.ApiService.Logout(c) + case "load": + a.ApiService.LoadData(c) + case "inbounds", "outbounds", "endpoints", "services", "tls", "clients", "config": + err := a.ApiService.LoadPartialData(c, []string{action}) + if err != nil { + jsonMsg(c, action, err) + } + return + case "users": + a.ApiService.GetUsers(c) + case "settings": + a.ApiService.GetSettings(c) + case "stats": + a.ApiService.GetStats(c) + case "status": + a.ApiService.GetStatus(c) + case "onlines": + a.ApiService.GetOnlines(c) + case "logs": + a.ApiService.GetLogs(c) + case "changes": + a.ApiService.CheckChanges(c) + case "keypairs": + a.ApiService.GetKeypairs(c) + case "getdb": + a.ApiService.GetDb(c) + case "tokens": + a.ApiService.GetTokens(c) + case "singbox-config": + a.ApiService.GetSingboxConfig(c) + case "checkOutbound": + a.ApiService.GetCheckOutbound(c) + default: + jsonMsg(c, "failed", common.NewError("unknown action: ", action)) + } +} diff --git a/api/apiService.go b/api/apiService.go new file mode 100644 index 0000000..2d5091d --- /dev/null +++ b/api/apiService.go @@ -0,0 +1,405 @@ +package api + +import ( + "encoding/json" + "strconv" + "time" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/service" + "github.com/alireza0/s-ui/util" + + "github.com/gin-gonic/gin" +) + +type ApiService struct { + service.SettingService + service.UserService + service.ConfigService + service.ClientService + service.TlsService + service.InboundService + service.OutboundService + service.EndpointService + service.ServicesService + service.PanelService + service.StatsService + service.ServerService +} + +func (a *ApiService) LoadData(c *gin.Context) { + data, err := a.getData(c) + if err != nil { + jsonMsg(c, "", err) + return + } + jsonObj(c, data, nil) +} + +func (a *ApiService) getData(c *gin.Context) (interface{}, error) { + data := make(map[string]interface{}, 0) + lu := c.Query("lu") + isUpdated, err := a.ConfigService.CheckChanges(lu) + if err != nil { + return "", err + } + onlines, err := a.StatsService.GetOnlines() + + sysInfo := a.ServerService.GetSingboxInfo() + if sysInfo["running"] == false { + logs := a.ServerService.GetLogs("1", "debug") + if len(logs) > 0 { + data["lastLog"] = logs[0] + } + } + + if err != nil { + return "", err + } + if isUpdated { + config, err := a.SettingService.GetConfig() + if err != nil { + return "", err + } + clients, err := a.ClientService.GetAll() + if err != nil { + return "", err + } + tlsConfigs, err := a.TlsService.GetAll() + if err != nil { + return "", err + } + inbounds, err := a.InboundService.GetAll() + if err != nil { + return "", err + } + outbounds, err := a.OutboundService.GetAll() + if err != nil { + return "", err + } + endpoints, err := a.EndpointService.GetAll() + if err != nil { + return "", err + } + services, err := a.ServicesService.GetAll() + if err != nil { + return "", err + } + subURI, err := a.SettingService.GetFinalSubURI(getHostname(c)) + if err != nil { + return "", err + } + trafficAge, err := a.SettingService.GetTrafficAge() + if err != nil { + return "", err + } + data["config"] = json.RawMessage(config) + data["clients"] = clients + data["tls"] = tlsConfigs + data["inbounds"] = inbounds + data["outbounds"] = outbounds + data["endpoints"] = endpoints + data["services"] = services + data["subURI"] = subURI + data["enableTraffic"] = trafficAge > 0 + data["onlines"] = onlines + } else { + data["onlines"] = onlines + } + + return data, nil +} + +func (a *ApiService) LoadPartialData(c *gin.Context, objs []string) error { + data := make(map[string]interface{}, 0) + id := c.Query("id") + + for _, obj := range objs { + switch obj { + case "inbounds": + inbounds, err := a.InboundService.Get(id) + if err != nil { + return err + } + data[obj] = inbounds + case "outbounds": + outbounds, err := a.OutboundService.GetAll() + if err != nil { + return err + } + data[obj] = outbounds + case "endpoints": + endpoints, err := a.EndpointService.GetAll() + if err != nil { + return err + } + data[obj] = endpoints + case "services": + services, err := a.ServicesService.GetAll() + if err != nil { + return err + } + data[obj] = services + case "tls": + tlsConfigs, err := a.TlsService.GetAll() + if err != nil { + return err + } + data[obj] = tlsConfigs + case "clients": + clients, err := a.ClientService.Get(id) + if err != nil { + return err + } + data[obj] = clients + case "config": + config, err := a.SettingService.GetConfig() + if err != nil { + return err + } + data[obj] = json.RawMessage(config) + case "settings": + settings, err := a.SettingService.GetAllSetting() + if err != nil { + return err + } + data[obj] = settings + } + } + + jsonObj(c, data, nil) + return nil +} + +func (a *ApiService) GetUsers(c *gin.Context) { + users, err := a.UserService.GetUsers() + if err != nil { + jsonMsg(c, "", err) + return + } + jsonObj(c, *users, nil) +} + +func (a *ApiService) GetSettings(c *gin.Context) { + data, err := a.SettingService.GetAllSetting() + if err != nil { + jsonMsg(c, "", err) + return + } + jsonObj(c, data, err) +} + +func (a *ApiService) GetStats(c *gin.Context) { + resource := c.Query("resource") + tag := c.Query("tag") + limit, err := strconv.Atoi(c.Query("limit")) + if err != nil { + limit = 100 + } + data, err := a.StatsService.GetStats(resource, tag, limit) + if err != nil { + jsonMsg(c, "", err) + return + } + jsonObj(c, data, err) +} + +func (a *ApiService) GetStatus(c *gin.Context) { + request := c.Query("r") + result := a.ServerService.GetStatus(request) + jsonObj(c, result, nil) +} + +func (a *ApiService) GetOnlines(c *gin.Context) { + onlines, err := a.StatsService.GetOnlines() + jsonObj(c, onlines, err) +} + +func (a *ApiService) GetLogs(c *gin.Context) { + count := c.Query("c") + level := c.Query("l") + logs := a.ServerService.GetLogs(count, level) + jsonObj(c, logs, nil) +} + +func (a *ApiService) CheckChanges(c *gin.Context) { + actor := c.Query("a") + chngKey := c.Query("k") + count := c.Query("c") + changes := a.ConfigService.GetChanges(actor, chngKey, count) + jsonObj(c, changes, nil) +} + +func (a *ApiService) GetKeypairs(c *gin.Context) { + kType := c.Query("k") + options := c.Query("o") + keypair := a.ServerService.GenKeypair(kType, options) + jsonObj(c, keypair, nil) +} + +func (a *ApiService) GetDb(c *gin.Context) { + exclude := c.Query("exclude") + db, err := database.GetDb(exclude) + if err != nil { + jsonMsg(c, "", err) + return + } + c.Header("Content-Type", "application/octet-stream") + c.Header("Content-Disposition", "attachment; filename=s-ui_"+time.Now().Format("20060102-150405")+".db") + c.Writer.Write(db) +} + +func (a *ApiService) postActions(c *gin.Context) (string, json.RawMessage, error) { + var data map[string]json.RawMessage + err := c.ShouldBind(&data) + if err != nil { + return "", nil, err + } + return string(data["action"]), data["data"], nil +} + +func (a *ApiService) Login(c *gin.Context) { + remoteIP := getRemoteIp(c) + loginUser, err := a.UserService.Login(c.Request.FormValue("user"), c.Request.FormValue("pass"), remoteIP) + if err != nil { + jsonMsg(c, "", err) + return + } + + sessionMaxAge, err := a.SettingService.GetSessionMaxAge() + if err != nil { + logger.Infof("Unable to get session's max age from DB") + } + + err = SetLoginUser(c, loginUser, sessionMaxAge) + if err == nil { + logger.Info("user ", loginUser, " login success") + } else { + logger.Warning("login failed: ", err) + } + + jsonMsg(c, "", nil) +} + +func (a *ApiService) ChangePass(c *gin.Context) { + id := c.Request.FormValue("id") + oldPass := c.Request.FormValue("oldPass") + newUsername := c.Request.FormValue("newUsername") + newPass := c.Request.FormValue("newPass") + err := a.UserService.ChangePass(id, oldPass, newUsername, newPass) + if err == nil { + logger.Info("change user credentials success") + jsonMsg(c, "save", nil) + } else { + logger.Warning("change user credentials failed:", err) + jsonMsg(c, "", err) + } +} + +func (a *ApiService) Save(c *gin.Context, loginUser string) { + hostname := getHostname(c) + obj := c.Request.FormValue("object") + act := c.Request.FormValue("action") + data := c.Request.FormValue("data") + initUsers := c.Request.FormValue("initUsers") + objs, err := a.ConfigService.Save(obj, act, json.RawMessage(data), initUsers, loginUser, hostname) + if err != nil { + jsonMsg(c, "save", err) + return + } + err = a.LoadPartialData(c, objs) + if err != nil { + jsonMsg(c, obj, err) + } +} + +func (a *ApiService) RestartApp(c *gin.Context) { + err := a.PanelService.RestartPanel(3) + jsonMsg(c, "restartApp", err) +} + +func (a *ApiService) RestartSb(c *gin.Context) { + err := a.ConfigService.RestartCore() + jsonMsg(c, "restartSb", err) +} + +func (a *ApiService) LinkConvert(c *gin.Context) { + link := c.Request.FormValue("link") + result, _, err := util.GetOutbound(link, 0) + jsonObj(c, result, err) +} + +func (a *ApiService) SubConvert(c *gin.Context) { + link := c.Request.FormValue("link") + result, err := util.GetExternalSub(link) + jsonObj(c, result, err) +} + +func (a *ApiService) ImportDb(c *gin.Context) { + file, _, err := c.Request.FormFile("db") + if err != nil { + jsonMsg(c, "", err) + return + } + defer file.Close() + err = database.ImportDB(file) + jsonMsg(c, "", err) +} + +func (a *ApiService) Logout(c *gin.Context) { + loginUser := GetLoginUser(c) + if loginUser != "" { + logger.Infof("user %s logout", loginUser) + } + ClearSession(c) + jsonMsg(c, "", nil) +} + +func (a *ApiService) LoadTokens() ([]byte, error) { + return a.UserService.LoadTokens() +} + +func (a *ApiService) GetTokens(c *gin.Context) { + loginUser := GetLoginUser(c) + tokens, err := a.UserService.GetUserTokens(loginUser) + jsonObj(c, tokens, err) +} + +func (a *ApiService) AddToken(c *gin.Context) { + loginUser := GetLoginUser(c) + expiry := c.Request.FormValue("expiry") + expiryInt, err := strconv.ParseInt(expiry, 10, 64) + if err != nil { + jsonMsg(c, "", err) + return + } + desc := c.Request.FormValue("desc") + token, err := a.UserService.AddToken(loginUser, expiryInt, desc) + jsonObj(c, token, err) +} + +func (a *ApiService) DeleteToken(c *gin.Context) { + tokenId := c.Request.FormValue("id") + err := a.UserService.DeleteToken(tokenId) + jsonMsg(c, "", err) +} + +func (a *ApiService) GetSingboxConfig(c *gin.Context) { + rawConfig, err := a.ConfigService.GetConfig("") + if err != nil { + c.Status(400) + c.Writer.WriteString(err.Error()) + return + } + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", "attachment; filename=config_"+time.Now().Format("20060102-150405")+".json") + c.Writer.Write(*rawConfig) +} + +func (a *ApiService) GetCheckOutbound(c *gin.Context) { + tag := c.Query("tag") + link := c.Query("link") + result := a.ConfigService.CheckOutbound(tag, link) + jsonObj(c, result, nil) +} diff --git a/api/apiV2Handler.go b/api/apiV2Handler.go new file mode 100644 index 0000000..737a94d --- /dev/null +++ b/api/apiV2Handler.go @@ -0,0 +1,134 @@ +package api + +import ( + "encoding/json" + "time" + + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util/common" + + "github.com/gin-gonic/gin" +) + +type TokenInMemory struct { + Token string + Expiry int64 + Username string +} + +type APIv2Handler struct { + ApiService + tokens *[]TokenInMemory +} + +func NewAPIv2Handler(g *gin.RouterGroup) *APIv2Handler { + a := &APIv2Handler{} + a.ReloadTokens() + a.initRouter(g) + return a +} + +func (a *APIv2Handler) initRouter(g *gin.RouterGroup) { + g.Use(func(c *gin.Context) { + a.checkToken(c) + }) + g.POST("/:postAction", a.postHandler) + g.GET("/:getAction", a.getHandler) +} + +func (a *APIv2Handler) postHandler(c *gin.Context) { + username := a.findUsername(c) + action := c.Param("postAction") + + switch action { + case "save": + a.ApiService.Save(c, username) + case "restartApp": + a.ApiService.RestartApp(c) + case "restartSb": + a.ApiService.RestartSb(c) + case "linkConvert": + a.ApiService.LinkConvert(c) + case "subConvert": + a.ApiService.SubConvert(c) + case "importdb": + a.ApiService.ImportDb(c) + default: + jsonMsg(c, "failed", common.NewError("unknown action: ", action)) + } +} + +func (a *APIv2Handler) getHandler(c *gin.Context) { + action := c.Param("getAction") + + switch action { + case "load": + a.ApiService.LoadData(c) + case "inbounds", "outbounds", "endpoints", "services", "tls", "clients", "config": + err := a.ApiService.LoadPartialData(c, []string{action}) + if err != nil { + jsonMsg(c, action, err) + } + return + case "users": + a.ApiService.GetUsers(c) + case "settings": + a.ApiService.GetSettings(c) + case "stats": + a.ApiService.GetStats(c) + case "status": + a.ApiService.GetStatus(c) + case "onlines": + a.ApiService.GetOnlines(c) + case "logs": + a.ApiService.GetLogs(c) + case "changes": + a.ApiService.CheckChanges(c) + case "keypairs": + a.ApiService.GetKeypairs(c) + case "getdb": + a.ApiService.GetDb(c) + case "checkOutbound": + a.ApiService.GetCheckOutbound(c) + default: + jsonMsg(c, "failed", common.NewError("unknown action: ", action)) + } +} + +func (a *APIv2Handler) findUsername(c *gin.Context) string { + token := c.Request.Header.Get("Token") + for index, t := range *a.tokens { + if t.Expiry > 0 && t.Expiry < time.Now().Unix() { + (*a.tokens) = append((*a.tokens)[:index], (*a.tokens)[index+1:]...) + continue + } + if t.Token == token { + return t.Username + } + } + return "" +} + +func (a *APIv2Handler) checkToken(c *gin.Context) { + username := a.findUsername(c) + if username != "" { + c.Next() + return + } + jsonMsg(c, "", common.NewError("invalid token")) + c.Abort() +} + +func (a *APIv2Handler) ReloadTokens() { + tokens, err := a.ApiService.LoadTokens() + if err == nil { + var newTokens []TokenInMemory + err = json.Unmarshal(tokens, &newTokens) + if err != nil { + logger.Error("unable to load tokens: ", err) + } + a.tokens = &newTokens + } else { + logger.Error("unable to load tokens: ", err) + } +} diff --git a/api/session.go b/api/session.go new file mode 100644 index 0000000..957b5f3 --- /dev/null +++ b/api/session.go @@ -0,0 +1,69 @@ +package api + +import ( + "encoding/gob" + + "github.com/alireza0/s-ui/database/model" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +const ( + loginUser = "LOGIN_USER" +) + +func init() { + gob.Register(model.User{}) +} + +func SetLoginUser(c *gin.Context, userName string, maxAge int) error { + options := sessions.Options{ + Path: "/", + Secure: false, + } + if maxAge > 0 { + options.MaxAge = maxAge * 60 + } + + s := sessions.Default(c) + s.Set(loginUser, userName) + s.Options(options) + + return s.Save() +} + +func SetMaxAge(c *gin.Context) error { + s := sessions.Default(c) + s.Options(sessions.Options{ + Path: "/", + }) + return s.Save() +} + +func GetLoginUser(c *gin.Context) string { + s := sessions.Default(c) + obj := s.Get(loginUser) + if obj == nil { + return "" + } + objStr, ok := obj.(string) + if !ok { + return "" + } + return objStr +} + +func IsLogin(c *gin.Context) bool { + return GetLoginUser(c) != "" +} + +func ClearSession(c *gin.Context) { + s := sessions.Default(c) + s.Clear() + s.Options(sessions.Options{ + Path: "/", + MaxAge: -1, + }) + s.Save() +} diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 0000000..da4a859 --- /dev/null +++ b/api/utils.go @@ -0,0 +1,92 @@ +package api + +import ( + "net" + "net/http" + "strings" + + "github.com/alireza0/s-ui/logger" + + "github.com/gin-gonic/gin" +) + +type Msg struct { + Success bool `json:"success"` + Msg string `json:"msg"` + Obj interface{} `json:"obj"` +} + +func getRemoteIp(c *gin.Context) string { + value := c.GetHeader("X-Forwarded-For") + if value != "" { + ips := strings.Split(value, ",") + return ips[0] + } else { + addr := c.Request.RemoteAddr + ip, _, _ := net.SplitHostPort(addr) + return ip + } +} + +func getHostname(c *gin.Context) string { + host := c.Request.Host + if strings.Contains(host, ":") { + host, _, _ = net.SplitHostPort(c.Request.Host) + if strings.Contains(host, ":") { + host = "[" + host + "]" + } + } + return host +} + +func jsonMsg(c *gin.Context, msg string, err error) { + jsonMsgObj(c, msg, nil, err) +} + +func jsonObj(c *gin.Context, obj interface{}, err error) { + jsonMsgObj(c, "", obj, err) +} + +func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) { + m := Msg{ + Obj: obj, + } + if err == nil { + m.Success = true + if msg != "" { + m.Msg = msg + } + } else { + m.Success = false + m.Msg = msg + ": " + err.Error() + logger.Warning("failed :", err) + } + c.JSON(http.StatusOK, m) +} + +func pureJsonMsg(c *gin.Context, success bool, msg string) { + if success { + c.JSON(http.StatusOK, Msg{ + Success: true, + Msg: msg, + }) + } else { + c.JSON(http.StatusOK, Msg{ + Success: false, + Msg: msg, + }) + } +} + +func checkLogin(c *gin.Context) { + if !IsLogin(c) { + if c.GetHeader("X-Requested-With") == "XMLHttpRequest" { + pureJsonMsg(c, false, "Invalid login") + } else { + c.Redirect(http.StatusTemporaryRedirect, "/login") + } + c.Abort() + } else { + c.Next() + } +} diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..6719e18 --- /dev/null +++ b/app/app.go @@ -0,0 +1,128 @@ +package app + +import ( + "log" + + "github.com/alireza0/s-ui/config" + "github.com/alireza0/s-ui/core" + "github.com/alireza0/s-ui/cronjob" + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/service" + "github.com/alireza0/s-ui/sub" + "github.com/alireza0/s-ui/web" + + "github.com/op/go-logging" +) + +type APP struct { + service.SettingService + configService *service.ConfigService + webServer *web.Server + subServer *sub.Server + cronJob *cronjob.CronJob + logger *logging.Logger + core *core.Core +} + +func NewApp() *APP { + return &APP{} +} + +func (a *APP) Init() error { + log.Printf("%v %v", config.GetName(), config.GetVersion()) + + a.initLog() + + err := database.InitDB(config.GetDBPath()) + if err != nil { + return err + } + + // Init Setting + a.SettingService.GetAllSetting() + + a.core = core.NewCore() + + a.cronJob = cronjob.NewCronJob() + a.webServer = web.NewServer() + a.subServer = sub.NewServer() + + a.configService = service.NewConfigService(a.core) + + return nil +} + +func (a *APP) Start() error { + loc, err := a.SettingService.GetTimeLocation() + if err != nil { + return err + } + + trafficAge, err := a.SettingService.GetTrafficAge() + if err != nil { + return err + } + + err = a.cronJob.Start(loc, trafficAge) + if err != nil { + return err + } + + err = a.webServer.Start() + if err != nil { + return err + } + + err = a.subServer.Start() + if err != nil { + return err + } + + err = a.configService.StartCore() + if err != nil { + logger.Error(err) + } + + return nil +} + +func (a *APP) Stop() { + a.cronJob.Stop() + err := a.subServer.Stop() + if err != nil { + logger.Warning("stop Sub Server err:", err) + } + err = a.webServer.Stop() + if err != nil { + logger.Warning("stop Web Server err:", err) + } + err = a.configService.StopCore() + if err != nil { + logger.Warning("stop Core err:", err) + } +} + +func (a *APP) initLog() { + switch config.GetLogLevel() { + case config.Debug: + logger.InitLogger(logging.DEBUG) + case config.Info: + logger.InitLogger(logging.INFO) + case config.Warn: + logger.InitLogger(logging.WARNING) + case config.Error: + logger.InitLogger(logging.ERROR) + default: + log.Fatal("unknown log level:", config.GetLogLevel()) + } +} + +func (a *APP) RestartApp() { + a.Stop() + a.Start() +} + +func (a *APP) GetCore() *core.Core { + return a.core +} diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..7b14643 --- /dev/null +++ b/build.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +cd frontend +npm i +npm run build + +cd .. +echo "Backend" + +mkdir -p web/html +rm -fr web/html/* +cp -R frontend/dist/* web/html/ + +BUILD_TAGS="with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_naive_outbound,with_musl,badlinkname,tfogo_checklinkname0,with_tailscale" +go build -ldflags '-w -s -checklinkname=0 -extldflags "-Wl,-no_warn_duplicate_libraries"' -tags "$BUILD_TAGS" -o sui main.go diff --git a/cmd/admin.go b/cmd/admin.go new file mode 100644 index 0000000..a64f602 --- /dev/null +++ b/cmd/admin.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "fmt" + + "github.com/alireza0/s-ui/config" + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/service" +) + +func resetAdmin() { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + + userService := service.UserService{} + err = userService.UpdateFirstUser("admin", "admin") + if err != nil { + fmt.Println("reset admin credentials failed:", err) + } else { + fmt.Println("reset admin credentials success") + } +} + +func updateAdmin(username string, password string) { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + + if username != "" || password != "" { + userService := service.UserService{} + err := userService.UpdateFirstUser(username, password) + if err != nil { + fmt.Println("reset admin credentials failed:", err) + } else { + fmt.Println("reset admin credentials success") + } + } +} + +func showAdmin() { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + userService := service.UserService{} + userModel, err := userService.GetFirstUser() + if err != nil { + fmt.Println("get current user info failed,error info:", err) + } + username := userModel.Username + userpasswd := userModel.Password + if (username == "") || (userpasswd == "") { + fmt.Println("current username or password is empty") + } + fmt.Println("First admin credentials:") + fmt.Println("\tUsername:\t", username) + fmt.Println("\tPassword:\t", userpasswd) +} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..61ac223 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,112 @@ +package cmd + +import ( + "flag" + "fmt" + "os" + "runtime/debug" + + "github.com/alireza0/s-ui/cmd/migration" + "github.com/alireza0/s-ui/config" +) + +func ParseCmd() { + var showVersion bool + flag.BoolVar(&showVersion, "v", false, "show version") + + adminCmd := flag.NewFlagSet("admin", flag.ExitOnError) + settingCmd := flag.NewFlagSet("setting", flag.ExitOnError) + + var username string + var password string + var port int + var path string + var subPort int + var subPath string + var reset bool + var show bool + settingCmd.BoolVar(&reset, "reset", false, "reset all settings") + settingCmd.BoolVar(&show, "show", false, "show current settings") + settingCmd.IntVar(&port, "port", 0, "set panel port") + settingCmd.StringVar(&path, "path", "", "set panel path") + settingCmd.IntVar(&subPort, "subPort", 0, "set sub port") + settingCmd.StringVar(&subPath, "subPath", "", "set sub path") + + adminCmd.BoolVar(&show, "show", false, "show first admin credentials") + adminCmd.BoolVar(&reset, "reset", false, "reset first admin credentials") + adminCmd.StringVar(&username, "username", "", "set login username") + adminCmd.StringVar(&password, "password", "", "set login password") + + oldUsage := flag.Usage + flag.Usage = func() { + oldUsage() + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" admin set/reset/show first admin credentials") + fmt.Println(" uri Show panel URI") + fmt.Println(" migrate migrate form older version") + fmt.Println(" setting set/reset/show settings") + fmt.Println() + adminCmd.Usage() + fmt.Println() + settingCmd.Usage() + } + + flag.Parse() + if showVersion { + fmt.Println("S-UI Panel\t", config.GetVersion()) + info, ok := debug.ReadBuildInfo() + if ok { + for _, dep := range info.Deps { + if dep.Path == "github.com/sagernet/sing-box" { + fmt.Println("Sing-Box\t", dep.Version) + break + } + } + } + return + } + + switch os.Args[1] { + case "admin": + err := adminCmd.Parse(os.Args[2:]) + if err != nil { + fmt.Println(err) + return + } + switch { + case show: + showAdmin() + case reset: + resetAdmin() + default: + updateAdmin(username, password) + showAdmin() + } + + case "uri": + getPanelURI() + + case "migrate": + migration.MigrateDb() + + case "setting": + err := settingCmd.Parse(os.Args[2:]) + if err != nil { + fmt.Println(err) + return + } + switch { + case show: + showSetting() + case reset: + resetSetting() + default: + updateSetting(port, path, subPort, subPath) + showSetting() + } + default: + fmt.Println("Invalid subcommands") + flag.Usage() + } +} diff --git a/cmd/migration/1_1.go b/cmd/migration/1_1.go new file mode 100644 index 0000000..aa44786 --- /dev/null +++ b/cmd/migration/1_1.go @@ -0,0 +1,88 @@ +package migration + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/alireza0/s-ui/database/model" + + "gorm.io/gorm" +) + +func migrateClientSchema(db *gorm.DB) error { + rows, err := db.Raw("PRAGMA table_info(clients)").Rows() + if err != nil { + fmt.Println(err) + return err + } + defer rows.Close() + + for rows.Next() { + var ( + cid int + cname string + ctype string + notnull int + dfltValue interface{} + pk int + ) + + rows.Scan(&cid, &cname, &ctype, ¬null, &dfltValue, &pk) + if cname == "config" || cname == "inbounds" || cname == "links" { + if ctype == "text" { + fmt.Printf("Column %s has type TEXT\n", cname) + oldData := make([]struct { + Id uint + Data string + }, 0) + db.Model(model.Client{}).Select("id", cname+" as data").Scan(&oldData) + for _, data := range oldData { + var newData []byte + switch cname { + case "inbounds": + inbounds := strings.Split(data.Data, ",") + newData, _ = json.MarshalIndent(inbounds, "", " ") + case "config": + jsonData := map[string]interface{}{} + json.Unmarshal([]byte(data.Data), &jsonData) + newData, _ = json.MarshalIndent(jsonData, "", " ") + case "links": + jsonData := make([]interface{}, 0) + json.Unmarshal([]byte(data.Data), &jsonData) + newData, _ = json.MarshalIndent(jsonData, "", " ") + } + err = db.Model(model.Client{}).Where("id = ?", data.Id).UpdateColumn(cname, newData).Error + if err != nil { + return err + } + } + } + } + } + return nil +} + +func deleteOldWebSecret(db *gorm.DB) error { + return db.Exec("DELETE FROM settings WHERE key = ?", "webSecret").Error +} + +func changesObj(db *gorm.DB) error { + return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error +} + +func to1_1(db *gorm.DB) error { + err := migrateClientSchema(db) + if err != nil { + return err + } + err = deleteOldWebSecret(db) + if err != nil { + return err + } + err = changesObj(db) + if err != nil { + return err + } + return nil +} diff --git a/cmd/migration/1_2.go b/cmd/migration/1_2.go new file mode 100644 index 0000000..e5b6422 --- /dev/null +++ b/cmd/migration/1_2.go @@ -0,0 +1,317 @@ +package migration + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + + "github.com/alireza0/s-ui/database/model" + + "gorm.io/gorm" +) + +type InboundData struct { + Id uint + Tag string + Addrs json.RawMessage + OutJson json.RawMessage +} + +func moveJsonToDb(db *gorm.DB) error { + binFolderPath := os.Getenv("SUI_BIN_FOLDER") + if binFolderPath == "" { + binFolderPath = "bin" + } + dir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + return err + } + configPath := dir + "/" + binFolderPath + "/config.json" + if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) { + return nil + } + + data, err := os.ReadFile(configPath) + if err != nil { + return err + } + var oldConfig map[string]interface{} + err = json.Unmarshal(data, &oldConfig) + if err != nil { + return err + } + + oldInbounds := oldConfig["inbounds"].([]interface{}) + db.Migrator().DropTable(&model.Inbound{}) + db.AutoMigrate(&model.Inbound{}) + for _, inbound := range oldInbounds { + inbObj, _ := inbound.(map[string]interface{}) + tag, _ := inbObj["tag"].(string) + if tlsObj, ok := inbObj["tls"]; ok { + var tls_id uint + err = db.Raw("SELECT id FROM tls WHERE inbounds like ?", `%"`+tag+`"%`).Find(&tls_id).Error + if err != nil { + return err + } + + // Bind or Create tls_id + if tls_id > 0 { + inbObj["tls_id"] = tls_id + } else { + tls_server, _ := json.MarshalIndent(tlsObj, "", " ") + if len(tls_server) > 5 { + newTls := &model.Tls{ + Name: tag, + Server: tls_server, + Client: json.RawMessage("{}"), + } + err = db.Create(newTls).Error + if err != nil { + return err + } + inbObj["tls_id"] = newTls.Id + } + } + } + + var inbData InboundData + db.Raw("select id,addrs,out_json from inbound_data where tag = ?", tag).Find(&inbData) + if inbData.Id > 0 { + inbObj["out_json"] = inbData.OutJson + var addrs []map[string]interface{} + json.Unmarshal(inbData.Addrs, &addrs) + for index, addr := range addrs { + if tlsEnable, ok := addr["tls"].(bool); ok { + newTls := map[string]interface{}{ + "enabled": tlsEnable, + } + if insecure, ok := addr["insecure"].(bool); ok { + newTls["insecure"] = insecure + delete(addrs[index], "insecure") + } + if sni, ok := addr["server_name"].(string); ok { + newTls["server_name"] = sni + delete(addrs[index], "server_name") + } + addrs[index]["tls"] = newTls + } + } + inbObj["addrs"] = addrs + } else { + inbObj["out_json"] = json.RawMessage("{}") + inbObj["addrs"] = json.RawMessage("[]") + } + // Delete deprecated fields + delete(inbObj, "sniff") + delete(inbObj, "sniff_override_destination") + delete(inbObj, "sniff_timeout") + delete(inbObj, "domain_strategy") + inbJson, _ := json.Marshal(inbObj) + + var newInbound model.Inbound + err = newInbound.UnmarshalJSON(inbJson) + if err != nil { + return err + } + err = db.Create(&newInbound).Error + if err != nil { + return err + } + } + delete(oldConfig, "inbounds") + + blockOutboundTags := []string{} + dnsOutboundTags := []string{} + + oldOutbounds := oldConfig["outbounds"].([]interface{}) + db.Migrator().DropTable(&model.Outbound{}, &model.Endpoint{}) + db.AutoMigrate(&model.Outbound{}, &model.Endpoint{}) + for _, outbound := range oldOutbounds { + outType, _ := outbound.(map[string]interface{})["type"].(string) + outboundRaw, _ := json.MarshalIndent(outbound, "", " ") + if outType == "wireguard" { // Check if it is Entrypoint + var newEntrypoint model.Endpoint + err = newEntrypoint.UnmarshalJSON(outboundRaw) + if err != nil { + return err + } + err = db.Create(&newEntrypoint).Error + if err != nil { + return err + } + } else { // It is Outbound + var newOutbound model.Outbound + err = newOutbound.UnmarshalJSON(outboundRaw) + if err != nil { + return err + } + // Delete deprecated fields + if newOutbound.Type == "direct" { + var options map[string]interface{} + json.Unmarshal(newOutbound.Options, &options) + delete(options, "override_address") + delete(options, "override_port") + newOutbound.Options, _ = json.Marshal(options) + } + + switch newOutbound.Type { + case "dns": + dnsOutboundTags = append(dnsOutboundTags, newOutbound.Tag) + case "block": + blockOutboundTags = append(blockOutboundTags, newOutbound.Tag) + default: + err = db.Create(&newOutbound).Error + if err != nil { + return err + } + } + } + } + delete(oldConfig, "outbounds") + + // Check routing rules + if routingRules, ok := oldConfig["route"].(map[string]interface{}); ok { + if rules, hasRules := routingRules["rules"].([]interface{}); hasRules { + hasDns := false + for index, rule := range rules { + ruleObj, _ := rule.(map[string]interface{}) + isBlock := false + isDns := false + outboundTag, _ := ruleObj["outbound"].(string) + for _, tag := range blockOutboundTags { + if tag == outboundTag { + isBlock = true + delete(ruleObj, "outbound") + ruleObj["action"] = "reject" + break + } + } + for _, tag := range dnsOutboundTags { + if tag == outboundTag { + isDns = true + hasDns = true + delete(ruleObj, "outbound") + ruleObj["action"] = "hijack-dns" + break + } + } + if !isBlock && !isDns { + ruleObj["action"] = "route" + } + rules[index] = ruleObj + } + if hasDns { + rules = append(rules, map[string]interface{}{"action": "sniff"}) + } + routingRules["rules"] = rules + } + oldConfig["route"] = routingRules + } + + // Remove v2rayapi and clashapi from experimental config + experimental := oldConfig["experimental"].(map[string]interface{}) + delete(experimental, "v2ray_api") + delete(experimental, "clash_api") + oldConfig["experimental"] = experimental + + // Save the other configs + var otherConfigs json.RawMessage + otherConfigs, err = json.MarshalIndent(oldConfig, "", " ") + if err != nil { + return err + } + + return db.Save(&model.Setting{ + Key: "config", + Value: string(otherConfigs), + }).Error +} + +func migrateTls(db *gorm.DB) error { + if !db.Migrator().HasColumn(&model.Tls{}, "inbounds") { + return nil + } + err := db.Migrator().DropColumn(&model.Tls{}, "inbounds") + if err != nil { + return err + } + var tlsConfig []model.Tls + err = db.Model(model.Tls{}).Scan(&tlsConfig).Error + if err != nil { + return err + } + + for index, tls := range tlsConfig { + var tlsClient map[string]interface{} + err = json.Unmarshal(tls.Client, &tlsClient) + if err != nil { + continue + } + for key := range tlsClient { + switch key { + case "insecure", "disable_sni", "utls", "ech", "reality": + continue + default: + delete(tlsClient, key) + } + } + tlsConfig[index].Client, _ = json.MarshalIndent(tlsClient, "", " ") + } + + return db.Save(&tlsConfig).Error +} + +func dropInboundData(db *gorm.DB) error { + if !db.Migrator().HasTable(&InboundData{}) { + return nil + } + return db.Migrator().DropTable(&InboundData{}) +} + +func migrateClients(db *gorm.DB) error { + var oldClients []model.Client + err := db.Model(model.Client{}).Scan(&oldClients).Error + if err != nil { + return err + } + + for index, oldClient := range oldClients { + var old_inbounds []string + err = json.Unmarshal(oldClient.Inbounds, &old_inbounds) + if err != nil { + return err + } + var inbound_ids []uint + err = db.Raw("SELECT id FROM inbounds WHERE tag in ?", old_inbounds).Find(&inbound_ids).Error + if err != nil { + return err + } + oldClients[index].Inbounds, _ = json.Marshal(inbound_ids) + } + return db.Save(oldClients).Error +} + +func migrateChanges(db *gorm.DB) error { + return db.Migrator().DropColumn(&model.Changes{}, "index") +} + +func to1_2(db *gorm.DB) error { + err := moveJsonToDb(db) + if err != nil { + return err + } + err = migrateTls(db) + if err != nil { + return err + } + err = dropInboundData(db) + if err != nil { + return err + } + err = migrateClients(db) + if err != nil { + return err + } + return migrateChanges(db) +} diff --git a/cmd/migration/1_3.go b/cmd/migration/1_3.go new file mode 100644 index 0000000..f11c331 --- /dev/null +++ b/cmd/migration/1_3.go @@ -0,0 +1,155 @@ +package migration + +import ( + "encoding/json" + "net/url" + "strconv" + "strings" + + "github.com/alireza0/s-ui/database/model" + + "gorm.io/gorm" +) + +func migrate_dns(db *gorm.DB) error { + var configStr string + err := db.Model(model.Setting{}).Select("value").Where("key = ?", "config").First(&configStr).Error + if err != nil { + return err + } + if configStr == "" { + return nil + } + var config map[string]interface{} + err = json.Unmarshal([]byte(configStr), &config) + if err != nil { + return err + } + if dnsConfig, ok := config["dns"].(map[string]interface{}); ok { + if dnsServers, ok := dnsConfig["servers"].([]interface{}); ok { + for index, dnsServer := range dnsServers { + if dnsServer, ok := dnsServer.(map[string]interface{}); ok { + if addr, ok := dnsServer["address"].(string); ok && addr != "" { + switch addr { + case "local": + delete(dnsServer, "address") + dnsServer["type"] = "local" + case "fakeip": + delete(dnsServer, "address") + dnsServer["type"] = "fakeip" + default: + addrParsed, err := url.Parse(addr) + if err != nil { + continue + } + switch addrParsed.Scheme { + case "": + dnsServer["type"] = "udp" + dnsServer["server"] = addr + case "udp", "tcp", "tls", "quic", "https", "h3": + dnsServer["type"] = addrParsed.Scheme + dnsServer["server"] = addrParsed.Host + case "dhcp": + dnsServer["type"] = addrParsed.Scheme + if addrParsed.Host != "auto" && addrParsed.Host != "" { + dnsServer["interface"] = addrParsed.Host + } + case "rcode": + dnsServer["type"] = "predefined" + dnsServer["responses"] = []map[string]string{ + { + "rcode": strings.ToUpper(addrParsed.Host), + }, + } + } + delete(dnsServer, "address") + if addrParsed.Port() != "" { + port, err := strconv.Atoi(addrParsed.Port()) + if err == nil { + dnsServer["server_port"] = port + } + } + if address_resolver, ok := dnsServer["address_resolver"].(string); ok && address_resolver != "" { + delete(dnsServer, "address_resolver") + dnsServer["domain_resolver"] = address_resolver + } + delete(dnsServer, "strategy") + } + dnsServers[index] = dnsServer + } + } + } + dnsConfig["servers"] = dnsServers + } + config["dns"] = dnsConfig + } else { + return nil + } + + // save changes + configs, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + + return db.Model(model.Setting{}).Where("key = ?", "config").Update("value", string(configs)).Error +} + +func remove_outbound_strategy(db *gorm.DB) error { + var outbounds []model.Outbound + err := db.Find(&outbounds).Where("json_extract(options, '$.domain_strategy') IS NOT NULL").Error + if err != nil { + return err + } + for _, outbound := range outbounds { + var restFields map[string]json.RawMessage + if err := json.Unmarshal(outbound.Options, &restFields); err != nil { + return err + } + delete(restFields, "domain_strategy") + outbound.Options, _ = json.MarshalIndent(restFields, "", " ") + db.Save(&outbound) + } + return nil +} + +func anytls_user_config(db *gorm.DB) error { + var clients []model.Client + err := db.Model(model.Client{}).Find(&clients).Error + if err != nil { + return err + } + for index, client := range clients { + var configs map[string]json.RawMessage + if err := json.Unmarshal(client.Config, &configs); err != nil { + return err + } + if configs["anytls"] != nil { + continue + } + configs["anytls"] = configs["trojan"] + configJson, err := json.MarshalIndent(configs, "", " ") + if err != nil { + return err + } + clients[index].Config = configJson + db.Save(&clients[index]) + } + return nil +} + +func to1_3(db *gorm.DB) error { + err := anytls_user_config(db) + if err != nil { + return err + } + err = migrate_dns(db) + if err != nil { + return err + } + err = remove_outbound_strategy(db) + if err != nil { + return err + } + return nil +} diff --git a/cmd/migration/main.go b/cmd/migration/main.go new file mode 100644 index 0000000..a692867 --- /dev/null +++ b/cmd/migration/main.go @@ -0,0 +1,79 @@ +package migration + +import ( + "fmt" + "log" + "os" + + "github.com/alireza0/s-ui/config" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func MigrateDb() { + // void running on first install + path := config.GetDBPath() + _, err := os.Stat(path) + if err != nil { + println("Database not found") + return + } + + db, err := gorm.Open(sqlite.Open(path)) + if err != nil { + log.Fatal(err) + return + } + tx := db.Begin() + defer func() { + if err == nil { + tx.Commit() + } else { + tx.Rollback() + } + }() + currentVersion := config.GetVersion() + dbVersion := "" + tx.Raw("SELECT value FROM settings WHERE key = ?", "version").Find(&dbVersion) + fmt.Println("Current version:", currentVersion, "\nDatabase version:", dbVersion) + + if currentVersion == dbVersion { + fmt.Println("Database is up to date, no need to migrate") + return + } + + fmt.Println("Start migrating database...") + + // Before 1.2 + if dbVersion == "" { + err = to1_1(tx) + if err != nil { + log.Fatal("Migration to 1.1 failed: ", err) + return + } + err = to1_2(tx) + if err != nil { + log.Fatal("Migration to 1.2 failed: ", err) + return + } + dbVersion = "1.2" + } + + // Before 1.3 + if dbVersion[0:3] == "1.2" { + err = to1_3(tx) + if err != nil { + log.Fatal("Migration to 1.3 failed: ", err) + return + } + } + + // Set version + err = tx.Exec("UPDATE settings SET value = ? WHERE key = ?", currentVersion, "version").Error + if err != nil { + log.Fatal("Update version failed: ", err) + return + } + fmt.Println("Migration done!") +} diff --git a/cmd/setting.go b/cmd/setting.go new file mode 100644 index 0000000..d1d14a5 --- /dev/null +++ b/cmd/setting.go @@ -0,0 +1,217 @@ +package cmd + +import ( + "fmt" + "io" + "net/http" + "strings" + "sync" + "time" + + "github.com/alireza0/s-ui/config" + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/service" + + "github.com/shirou/gopsutil/v4/net" +) + +func resetSetting() { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + + settingService := service.SettingService{} + err = settingService.ResetSettings() + if err != nil { + fmt.Println("reset setting failed:", err) + } else { + fmt.Println("reset setting success") + } +} + +func updateSetting(port int, path string, subPort int, subPath string) { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + + settingService := service.SettingService{} + + if port > 0 { + err := settingService.SetPort(port) + if err != nil { + fmt.Println("set port failed:", err) + } else { + fmt.Println("set port success") + } + } + if path != "" { + err := settingService.SetWebPath(path) + if err != nil { + fmt.Println("set path failed:", err) + } else { + fmt.Println("set path success") + } + } + if subPort > 0 { + err := settingService.SetSubPort(subPort) + if err != nil { + fmt.Println("set sub port failed:", err) + } else { + fmt.Println("set sub port success") + } + } + if subPath != "" { + err := settingService.SetSubPath(subPath) + if err != nil { + fmt.Println("set sub path failed:", err) + } else { + fmt.Println("set sub path success") + } + } +} + +func showSetting() { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + settingService := service.SettingService{} + allSetting, err := settingService.GetAllSetting() + if err != nil { + fmt.Println("get current port failed,error info:", err) + } + fmt.Println("Current panel settings:") + fmt.Println("\tPanel port:\t", (*allSetting)["webPort"]) + fmt.Println("\tPanel path:\t", (*allSetting)["webPath"]) + if (*allSetting)["webListen"] != "" { + fmt.Println("\tPanel IP:\t", (*allSetting)["webListen"]) + } + if (*allSetting)["webDomain"] != "" { + fmt.Println("\tPanel Domain:\t", (*allSetting)["webDomain"]) + } + if (*allSetting)["webURI"] != "" { + fmt.Println("\tPanel URI:\t", (*allSetting)["webURI"]) + } + fmt.Println() + fmt.Println("Current subscription settings:") + fmt.Println("\tSub port:\t", (*allSetting)["subPort"]) + fmt.Println("\tSub path:\t", (*allSetting)["subPath"]) + if (*allSetting)["subListen"] != "" { + fmt.Println("\tSub IP:\t", (*allSetting)["subListen"]) + } + if (*allSetting)["subDomain"] != "" { + fmt.Println("\tSub Domain:\t", (*allSetting)["subDomain"]) + } + if (*allSetting)["subURI"] != "" { + fmt.Println("\tSub URI:\t", (*allSetting)["subURI"]) + } +} + +func getPublicIP() string { + apis := []string{ + "https://api64.ipify.org", + "https://ip.sb", + "https://icanhazip.com", + "https://ipinfo.io/ip", + "https://checkip.amazonaws.com", + } + type result struct { + ip string + err error + } + ch := make(chan result, len(apis)) + var wg sync.WaitGroup + client := &http.Client{Timeout: 3 * time.Second} + + for _, api := range apis { + wg.Add(1) + go func(url string) { + defer wg.Done() + resp, err := client.Get(url) + if err != nil { + ch <- result{"", err} + return + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + ch <- result{"", err} + return + } + ch <- result{string(body), nil} + }(api) + } + + go func() { + wg.Wait() + close(ch) + }() + + for res := range ch { + if res.err == nil && res.ip != "" { + return strings.TrimSpace(res.ip) + } + } + return "" +} + +func getPanelURI() { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + settingService := service.SettingService{} + Port, _ := settingService.GetPort() + BasePath, _ := settingService.GetWebPath() + Listen, _ := settingService.GetListen() + Domain, _ := settingService.GetWebDomain() + KeyFile, _ := settingService.GetKeyFile() + CertFile, _ := settingService.GetCertFile() + TLS := false + if KeyFile != "" && CertFile != "" { + TLS = true + } + Proto := "" + if TLS { + Proto = "https://" + } else { + Proto = "http://" + } + PortText := fmt.Sprintf(":%d", Port) + if (Port == 443 && TLS) || (Port == 80 && !TLS) { + PortText = "" + } + if len(Domain) > 0 { + fmt.Println(Proto + Domain + PortText + BasePath) + return + } + if len(Listen) > 0 { + fmt.Println(Proto + Listen + PortText + BasePath) + return + } + fmt.Println("Local address:") + netInterfaces, _ := net.Interfaces() + for i := 0; i < len(netInterfaces); i++ { + if len(netInterfaces[i].Flags) > 2 && netInterfaces[i].Flags[0] == "up" && netInterfaces[i].Flags[1] != "loopback" { + addrs := netInterfaces[i].Addrs + for _, address := range addrs { + IP := strings.Split(address.Addr, "/")[0] + if strings.Contains(address.Addr, ".") { + fmt.Println(Proto + IP + PortText + BasePath) + } else if address.Addr[0:6] != "fe80::" { + fmt.Println(Proto + "[" + IP + "]" + PortText + BasePath) + } + } + } + } + pubIP := getPublicIP() + if pubIP != "" { + fmt.Printf("\nGlobal address:\n%s%s%s\n", Proto, pubIP, PortText+BasePath) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..af4140f --- /dev/null +++ b/config/config.go @@ -0,0 +1,68 @@ +package config + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" +) + +//go:embed version +var version string + +//go:embed name +var name string + +type LogLevel string + +const ( + Debug LogLevel = "debug" + Info LogLevel = "info" + Warn LogLevel = "warn" + Error LogLevel = "error" +) + +func GetVersion() string { + return strings.TrimSpace(version) +} + +func GetName() string { + return strings.TrimSpace(name) +} + +func GetLogLevel() LogLevel { + if IsDebug() { + return Debug + } + logLevel := os.Getenv("SUI_LOG_LEVEL") + if logLevel == "" { + return Info + } + return LogLevel(logLevel) +} + +func IsDebug() bool { + return os.Getenv("SUI_DEBUG") == "true" +} + +func GetDBFolderPath() string { + dbFolderPath := os.Getenv("SUI_DB_FOLDER") + if dbFolderPath == "" { + dir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + // Cross-platform fallback path + if runtime.GOOS == "windows" { + return "C:\\Program Files\\s-ui\\db" + } + return "/usr/local/s-ui/db" + } + dbFolderPath = filepath.Join(dir, "db") + } + return dbFolderPath +} + +func GetDBPath() string { + return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) +} diff --git a/config/name b/config/name new file mode 100644 index 0000000..037c3b9 --- /dev/null +++ b/config/name @@ -0,0 +1 @@ +s-ui \ No newline at end of file diff --git a/config/version b/config/version new file mode 100644 index 0000000..13175fd --- /dev/null +++ b/config/version @@ -0,0 +1 @@ +1.4.1 \ No newline at end of file diff --git a/core/box.go b/core/box.go new file mode 100644 index 0000000..99ef17d --- /dev/null +++ b/core/box.go @@ -0,0 +1,592 @@ +package core + +import ( + "context" + "errors" + "fmt" + "io" + "time" + + "github.com/alireza0/s-ui/util/common" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/adapter/outbound" + boxService "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/common/certificate" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport/local" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/cachefile" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/direct" + "github.com/sagernet/sing-box/route" + sbCommon "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" +) + +var _ adapter.SimpleLifecycle = (*Box)(nil) + +type Box struct { + createdAt time.Time + logFactory log.Factory + logger log.ContextLogger + network *route.NetworkManager + endpoint *endpoint.Manager + inbound *inbound.Manager + outbound *outbound.Manager + service *boxService.Manager + dnsTransport *dns.TransportManager + dnsRouter *dns.Router + connection *route.ConnectionManager + router *route.Router + internalService []adapter.LifecycleService + statsTracker *StatsTracker + connTracker *ConnTracker + done chan struct{} +} + +type Options struct { + option.Options + Context context.Context +} + +func Context( + ctx context.Context, + inboundRegistry adapter.InboundRegistry, + outboundRegistry adapter.OutboundRegistry, + endpointRegistry adapter.EndpointRegistry, + dnsTransportRegistry adapter.DNSTransportRegistry, + serviceRegistry adapter.ServiceRegistry, +) context.Context { + if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || + service.FromContext[adapter.InboundRegistry](ctx) == nil { + ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry) + ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry) + } + if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil || + service.FromContext[adapter.OutboundRegistry](ctx) == nil { + ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry) + ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry) + } + if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil || + service.FromContext[adapter.EndpointRegistry](ctx) == nil { + ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry) + ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry) + } + if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil { + ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry) + ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry) + } + if service.FromContext[adapter.ServiceRegistry](ctx) == nil { + ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry) + ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry) + } + return ctx +} + +func NewBox(options Options) (*Box, error) { + var err error + createdAt := time.Now() + ctx := options.Context + if ctx == nil { + ctx = context.Background() + } + ctx = service.ContextWithDefaultRegistry(ctx) + + endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx) + inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) + outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) + dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx) + serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx) + + if endpointRegistry == nil { + return nil, common.NewError("missing endpoint registry in context") + } + if inboundRegistry == nil { + return nil, common.NewError("missing inbound registry in context") + } + if outboundRegistry == nil { + return nil, common.NewError("missing outbound registry in context") + } + if dnsTransportRegistry == nil { + return nil, common.NewError("missing DNS transport registry in context") + } + if serviceRegistry == nil { + return nil, common.NewError("missing service registry in context") + } + + ctx = pause.WithDefaultManager(ctx) + experimentalOptions := sbCommon.PtrValueOrDefault(options.Experimental) + var needCacheFile bool + var needClashAPI bool + var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled { + needCacheFile = true + } + if experimentalOptions.ClashAPI != nil { + needClashAPI = true + } + if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { + needV2RayAPI = true + } + platformInterface := service.FromContext[adapter.PlatformInterface](ctx) + var defaultLogWriter io.Writer + if platformInterface != nil { + defaultLogWriter = io.Discard + } + var logFactory log.Factory + logFactory, err = NewFactory(log.Options{ + Context: ctx, + Options: sbCommon.PtrValueOrDefault(options.Log), + DefaultWriter: defaultLogWriter, + BaseTime: createdAt, + }) + if err != nil { + return nil, common.NewError("create log factory", err) + } + factory = logFactory + + var internalServices []adapter.LifecycleService + certificateOptions := sbCommon.PtrValueOrDefault(options.Certificate) + if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem || + len(certificateOptions.Certificate) > 0 || + len(certificateOptions.CertificatePath) > 0 || + len(certificateOptions.CertificateDirectoryPath) > 0 { + certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions) + if err != nil { + return nil, err + } + service.MustRegister[adapter.CertificateStore](ctx, certificateStore) + internalServices = append(internalServices, certificateStore) + } + + routeOptions := sbCommon.PtrValueOrDefault(options.Route) + dnsOptions := sbCommon.PtrValueOrDefault(options.DNS) + endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry) + inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) + outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final) + dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final) + serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry) + + service.MustRegister[adapter.EndpointManager](ctx, endpointManager) + service.MustRegister[adapter.InboundManager](ctx, inboundManager) + service.MustRegister[adapter.OutboundManager](ctx, outboundManager) + service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) + service.MustRegister[adapter.ServiceManager](ctx, serviceManager) + + dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) + service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) + + networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) + if err != nil { + return nil, common.NewError("initialize network manager", err) + } + service.MustRegister[adapter.NetworkManager](ctx, networkManager) + connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection")) + service.MustRegister[adapter.ConnectionManager](ctx, connectionManager) + router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions) + service.MustRegister[adapter.Router](ctx, router) + err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet) + if err != nil { + return nil, common.NewError("initialize router", err) + } + for i, transportOptions := range dnsOptions.Servers { + var tag string + if transportOptions.Tag != "" { + tag = transportOptions.Tag + } else { + tag = F.ToString(i) + } + err = dnsTransportManager.Create( + ctx, + logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")), + tag, + transportOptions.Type, + transportOptions.Options, + ) + if err != nil { + return nil, common.NewError("initialize DNS server[", i, "]", err) + } + } + err = dnsRouter.Initialize(dnsOptions.Rules) + if err != nil { + return nil, common.NewError("initialize dns router", err) + } + for i, endpointOptions := range options.Endpoints { + var tag string + if endpointOptions.Tag != "" { + tag = endpointOptions.Tag + } else { + tag = F.ToString(i) + } + err = endpointManager.Create( + ctx, + router, + logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")), + tag, + endpointOptions.Type, + endpointOptions.Options, + ) + if err != nil { + return nil, common.NewError("initialize endpoint["+F.ToString(i)+"] "+tag, err) + } + } + for i, inboundOptions := range options.Inbounds { + var tag string + if inboundOptions.Tag != "" { + tag = inboundOptions.Tag + } else { + tag = F.ToString(i) + } + err = inboundManager.Create( + ctx, + router, + logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), + tag, + inboundOptions.Type, + inboundOptions.Options, + ) + if err != nil { + return nil, common.NewError("initialize inbound[", i, "] ", tag, err) + } + } + for i, outboundOptions := range options.Outbounds { + var tag string + if outboundOptions.Tag != "" { + tag = outboundOptions.Tag + } else { + tag = F.ToString(i) + } + outboundCtx := ctx + if tag != "" { + // TODO: remove this + outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{ + Outbound: tag, + }) + } + err = outboundManager.Create( + outboundCtx, + router, + logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")), + tag, + outboundOptions.Type, + outboundOptions.Options, + ) + if err != nil { + return nil, common.NewError("initialize outbound["+F.ToString(i)+"] "+tag, err) + } + } + for i, serviceOptions := range options.Services { + var tag string + if serviceOptions.Tag != "" { + tag = serviceOptions.Tag + } else { + tag = F.ToString(i) + } + err = serviceManager.Create( + ctx, + logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")), + tag, + serviceOptions.Type, + serviceOptions.Options, + ) + if err != nil { + return nil, common.NewError("initialize service["+F.ToString(i)+"]"+tag, err) + } + } + outboundManager.Initialize(func() (adapter.Outbound, error) { + return direct.NewOutbound( + ctx, + router, + logFactory.NewLogger("outbound/direct"), + "direct", + option.DirectOutboundOptions{}, + ) + }) + dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) { + return local.NewTransport( + ctx, + logFactory.NewLogger("dns/local"), + "local", + option.LocalDNSServerOptions{}, + ) + }) + if platformInterface != nil { + err = platformInterface.Initialize(networkManager) + if err != nil { + return nil, common.NewError("initialize platform interface", err) + } + } + statsTracker := NewStatsTracker() + connTracker := NewConnTracker() + router.AppendTracker(statsTracker) + router.AppendTracker(connTracker) + + if needCacheFile { + cacheFile := cachefile.New(ctx, sbCommon.PtrValueOrDefault(experimentalOptions.CacheFile)) + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + internalServices = append(internalServices, cacheFile) + } + if needClashAPI { + clashAPIOptions := sbCommon.PtrValueOrDefault(experimentalOptions.ClashAPI) + clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) + clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions) + if err != nil { + return nil, common.NewError(err, "create clash-server") + } + router.AppendTracker(clashServer) + service.MustRegister[adapter.ClashServer](ctx, clashServer) + internalServices = append(internalServices, clashServer) + } + if needV2RayAPI { + v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), sbCommon.PtrValueOrDefault(experimentalOptions.V2RayAPI)) + if err != nil { + return nil, common.NewError(err, "create v2ray-server") + } + if v2rayServer.StatsService() != nil { + router.AppendTracker(v2rayServer.StatsService()) + internalServices = append(internalServices, v2rayServer) + service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) + } + } + ntpOptions := sbCommon.PtrValueOrDefault(options.NTP) + if ntpOptions.Enabled { + ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain()) + if err != nil { + return nil, common.NewError(err, "create NTP service") + } + timeService := ntp.NewService(ntp.Options{ + Context: ctx, + Dialer: ntpDialer, + Logger: logFactory.NewLogger("ntp"), + Server: ntpOptions.ServerOptions.Build(), + Interval: time.Duration(ntpOptions.Interval), + WriteToSystem: ntpOptions.WriteToSystem, + }) + service.MustRegister[ntp.TimeService](ctx, timeService) + internalServices = append(internalServices, adapter.NewLifecycleService(timeService, "ntp service")) + } + return &Box{ + network: networkManager, + endpoint: endpointManager, + inbound: inboundManager, + outbound: outboundManager, + dnsTransport: dnsTransportManager, + service: serviceManager, + dnsRouter: dnsRouter, + connection: connectionManager, + router: router, + createdAt: createdAt, + logFactory: logFactory, + logger: logFactory.Logger(), + internalService: internalServices, + statsTracker: statsTracker, + connTracker: connTracker, + done: make(chan struct{}), + }, nil +} + +func (s *Box) PreStart() error { + err := s.preStart() + if err != nil { + // TODO: remove catch error + defer func() { + v := recover() + if v != nil { + s.logger.Error(err.Error()) + s.logger.Error("panic on early close: " + fmt.Sprint(v)) + } + }() + s.Close() + return err + } + s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)") + return nil +} + +func (s *Box) Start() error { + err := s.start() + if err != nil { + return err + } + s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)") + return nil +} + +func (s *Box) preStart() error { + monitor := taskmonitor.New(s.logger, C.StartTimeout) + monitor.Start("start logger") + err := s.logFactory.Start() + monitor.Finish() + if err != nil { + return common.NewError(err, "start logger") + } + err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router) + if err != nil { + return err + } + return nil +} + +func (s *Box) start() error { + err := s.preStart() + if err != nil { + return err + } + err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service) + if err != nil { + return err + } + err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) + if err != nil { + return err + } + err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService) + if err != nil { + return err + } + return nil +} + +func (s *Box) Close() error { + select { + case <-s.done: + return nil + default: + close(s.done) + } + var err error + s.logger.Info("closing sing-box") + for _, closeItem := range []struct { + name string + service adapter.Lifecycle + }{ + {"service", s.service}, + {"endpoint", s.endpoint}, + {"inbound", s.inbound}, + {"outbound", s.outbound}, + {"router", s.router}, + {"connection", s.connection}, + {"dns-router", s.dnsRouter}, + {"dns-transport", s.dnsTransport}, + {"network", s.network}, + } { + if closeItem.service == nil { + continue + } + func() { + defer func() { + if v := recover(); v != nil { + err = errors.Join(err, common.NewError(fmt.Errorf("panic: %v", v), "close "+closeItem.name)) + s.logger.Error("panic closing ", closeItem.name, ": ", v) + } + }() + s.logger.Trace("close ", closeItem.name) + startTime := time.Now() + closeErr := closeItem.service.Close() + if closeErr != nil { + closeErr = common.NewError(closeErr, "close "+closeItem.name) + } + err = errors.Join(err, closeErr) + s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + }() + } + for _, lifecycleService := range s.internalService { + if lifecycleService == nil { + continue + } + func() { + defer func() { + if v := recover(); v != nil { + err = errors.Join(err, common.NewError(fmt.Errorf("panic: %v", v), "close "+lifecycleService.Name())) + s.logger.Error("panic closing ", lifecycleService.Name(), ": ", v) + } + }() + s.logger.Trace("close ", lifecycleService.Name()) + startTime := time.Now() + closeErr := lifecycleService.Close() + if closeErr != nil { + closeErr = common.NewError(closeErr, "close "+lifecycleService.Name()) + } + err = errors.Join(err, closeErr) + s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + }() + } + s.logger.Trace("close logger") + startTime := time.Now() + closeErr := s.logFactory.Close() + if closeErr != nil { + closeErr = common.NewError(closeErr, "close logger") + } + err = errors.Join(err, closeErr) + s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + s.logger.Info("sing-box closed (live time: ", F.Seconds(time.Since(s.createdAt).Seconds()), "s)") + if s.statsTracker != nil { + s.statsTracker.Reset() + } + if s.connTracker != nil { + s.connTracker.Reset() + } + return err +} + +func (s *Box) Uptime() uint32 { + return uint32(time.Since(s.createdAt).Seconds()) +} + +func (s *Box) Network() adapter.NetworkManager { + return s.network +} + +func (s *Box) Router() adapter.Router { + return s.router +} + +func (s *Box) Inbound() adapter.InboundManager { + return s.inbound +} + +func (s *Box) Outbound() adapter.OutboundManager { + return s.outbound +} + +func (s *Box) Endpoint() adapter.EndpointManager { + return s.endpoint +} + +func (s *Box) StatsTracker() *StatsTracker { + return s.statsTracker +} + +func (s *Box) ConnTracker() *ConnTracker { + return s.connTracker +} diff --git a/core/endpoint.go b/core/endpoint.go new file mode 100644 index 0000000..f501ea3 --- /dev/null +++ b/core/endpoint.go @@ -0,0 +1,147 @@ +package core + +import ( + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util/common" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" +) + +func (c *Core) AddInbound(config []byte) error { + if !c.isRunning { + return common.NewError("sing-box is not running") + } + var err error + var inbound_config option.Inbound + err = inbound_config.UnmarshalJSONContext(c.GetCtx(), config) + if err != nil { + return err + } + + err = inbound_manager.Create( + c.GetCtx(), + router, + factory.NewLogger("inbound/"+inbound_config.Type+"["+inbound_config.Tag+"]"), + inbound_config.Tag, + inbound_config.Type, + inbound_config.Options) + if err != nil { + return err + } + + return nil +} + +func (c *Core) RemoveInbound(tag string) error { + if !c.isRunning { + return common.NewError("sing-box is not running") + } + logger.Info("remove inbound: ", tag) + return inbound_manager.Remove(tag) +} + +func (c *Core) AddOutbound(config []byte) error { + if !c.isRunning { + return common.NewError("sing-box is not running") + } + var err error + var outbound_config option.Outbound + + err = outbound_config.UnmarshalJSONContext(c.GetCtx(), config) + if err != nil { + return err + } + + outboundCtx := adapter.WithContext(c.GetCtx(), &adapter.InboundContext{ + Outbound: outbound_config.Tag, + }) + + err = outbound_manager.Create( + outboundCtx, + router, + factory.NewLogger("outbound/"+outbound_config.Type+"["+outbound_config.Tag+"]"), + outbound_config.Tag, + outbound_config.Type, + outbound_config.Options) + if err != nil { + return err + } + + return nil +} + +func (c *Core) RemoveOutbound(tag string) error { + if !c.isRunning { + return common.NewError("sing-box is not running") + } + logger.Info("remove outbound: ", tag) + return outbound_manager.Remove(tag) +} + +func (c *Core) AddEndpoint(config []byte) error { + if !c.isRunning { + return common.NewError("sing-box is not running") + } + var err error + var endpoint_config option.Endpoint + + err = endpoint_config.UnmarshalJSONContext(c.GetCtx(), config) + if err != nil { + return err + } + + err = endpoint_manager.Create( + c.GetCtx(), + router, + factory.NewLogger("endpoint/"+endpoint_config.Type+"["+endpoint_config.Tag+"]"), + endpoint_config.Tag, + endpoint_config.Type, + endpoint_config.Options) + if err != nil { + return err + } + + return nil +} + +func (c *Core) RemoveEndpoint(tag string) error { + if !c.isRunning { + return common.NewError("sing-box is not running") + } + logger.Info("remove endpoint: ", tag) + return endpoint_manager.Remove(tag) +} + +func (c *Core) AddService(config []byte) error { + if !c.isRunning { + return common.NewError("sing-box is not running") + } + var err error + var srv_config option.Service + + err = srv_config.UnmarshalJSONContext(c.GetCtx(), config) + if err != nil { + return err + } + + err = service_manager.Create( + c.GetCtx(), + factory.NewLogger("service/"+srv_config.Type+"["+srv_config.Tag+"]"), + srv_config.Tag, + srv_config.Type, + srv_config.Options) + if err != nil { + return err + } + + return nil +} + +func (c *Core) RemoveService(tag string) error { + if !c.isRunning { + return common.NewError("sing-box is not running") + } + logger.Info("remove service: ", tag) + return service_manager.Remove(tag) +} diff --git a/core/log.go b/core/log.go new file mode 100644 index 0000000..bd4e6fe --- /dev/null +++ b/core/log.go @@ -0,0 +1,242 @@ +package core + +import ( + "context" + "io" + "os" + "time" + + suiLog "github.com/alireza0/s-ui/logger" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/service/filemanager" +) + +type PlatformWriter struct{} + +func (p PlatformWriter) DisableColors() bool { + return true +} +func (p PlatformWriter) WriteMessage(level log.Level, message string) { + switch level { + case log.LevelInfo: + suiLog.Info(message) + case log.LevelWarn: + suiLog.Warning(message) + case log.LevelPanic: + case log.LevelFatal: + case log.LevelError: + suiLog.Error(message) + default: + suiLog.Debug(message) + } +} + +func NewFactory(options log.Options) (log.Factory, error) { + logOptions := options.Options + + if logOptions.Disabled { + return log.NewNOPFactory(), nil + } + + var logWriter io.Writer + var logFilePath string + + switch logOptions.Output { + case "": + logWriter = options.DefaultWriter + if logWriter == nil { + logWriter = os.Stderr + } + case "stderr": + logWriter = os.Stderr + case "stdout": + logWriter = os.Stdout + default: + logFilePath = logOptions.Output + } + logFormatter := log.Formatter{ + BaseTime: options.BaseTime, + DisableColors: logOptions.DisableColor || logFilePath != "", + DisableTimestamp: !logOptions.Timestamp && logFilePath != "", + FullTimestamp: logOptions.Timestamp, + TimestampFormat: "-0700 2006-01-02 15:04:05", + } + factory := NewDefaultFactory( + options.Context, + logFormatter, + logWriter, + logFilePath, + ) + if logOptions.Level != "" { + logLevel, err := log.ParseLevel(logOptions.Level) + if err != nil { + return nil, common.Error("parse log level", err) + } + factory.SetLevel(logLevel) + } else { + factory.SetLevel(log.LevelTrace) + } + return factory, nil +} + +var _ log.Factory = (*defaultFactory)(nil) + +type defaultFactory struct { + ctx context.Context + formatter log.Formatter + writer io.Writer + file *os.File + filePath string + level log.Level + subscriber *observable.Subscriber[log.Entry] + observer *observable.Observer[log.Entry] +} + +func NewDefaultFactory( + ctx context.Context, + formatter log.Formatter, + writer io.Writer, + filePath string, +) log.ObservableFactory { + factory := &defaultFactory{ + ctx: ctx, + formatter: formatter, + writer: writer, + filePath: filePath, + level: log.LevelTrace, + subscriber: observable.NewSubscriber[log.Entry](128), + } + return factory +} + +func (f *defaultFactory) Start() error { + if f.filePath != "" { + logFile, err := filemanager.OpenFile(f.ctx, f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + f.writer = logFile + f.file = logFile + } + return nil +} + +func (f *defaultFactory) Close() error { + return common.Close( + common.PtrOrNil(f.file), + f.subscriber, + ) +} + +func (f *defaultFactory) Level() log.Level { + return f.level +} + +func (f *defaultFactory) SetLevel(level log.Level) { + f.level = level +} + +func (f *defaultFactory) Logger() log.ContextLogger { + return f.NewLogger("") +} + +func (f *defaultFactory) NewLogger(tag string) log.ContextLogger { + return &observableLogger{f, tag} +} + +func (f *defaultFactory) Subscribe() (subscription observable.Subscription[log.Entry], done <-chan struct{}, err error) { + return f.observer.Subscribe() +} + +func (f *defaultFactory) UnSubscribe(sub observable.Subscription[log.Entry]) { + f.observer.UnSubscribe(sub) +} + +type observableLogger struct { + *defaultFactory + tag string +} + +func (l *observableLogger) Log(ctx context.Context, level log.Level, args []any) { + level = log.OverrideLevelFromContext(level, ctx) + if level > l.level { + return + } + msg := F.ToString(args...) + switch level { + case log.LevelInfo: + suiLog.Info(l.tag, msg) + case log.LevelWarn: + suiLog.Warning(l.tag, msg) + case log.LevelPanic: + case log.LevelFatal: + case log.LevelError: + suiLog.Error(l.tag, msg) + default: + suiLog.Debug(l.tag, msg) + } + if (l.filePath != "" || l.writer != os.Stderr) && l.writer != nil { + message := l.formatter.Format(ctx, level, l.tag, msg, time.Now()) + l.writer.Write([]byte(message)) + } +} + +func (l *observableLogger) Trace(args ...any) { + l.TraceContext(context.Background(), args...) +} + +func (l *observableLogger) Debug(args ...any) { + l.DebugContext(context.Background(), args...) +} + +func (l *observableLogger) Info(args ...any) { + l.InfoContext(context.Background(), args...) +} + +func (l *observableLogger) Warn(args ...any) { + l.WarnContext(context.Background(), args...) +} + +func (l *observableLogger) Error(args ...any) { + l.ErrorContext(context.Background(), args...) +} + +func (l *observableLogger) Fatal(args ...any) { + l.FatalContext(context.Background(), args...) +} + +func (l *observableLogger) Panic(args ...any) { + l.PanicContext(context.Background(), args...) +} + +func (l *observableLogger) TraceContext(ctx context.Context, args ...any) { + l.Log(ctx, log.LevelTrace, args) +} + +func (l *observableLogger) DebugContext(ctx context.Context, args ...any) { + l.Log(ctx, log.LevelDebug, args) +} + +func (l *observableLogger) InfoContext(ctx context.Context, args ...any) { + l.Log(ctx, log.LevelInfo, args) +} + +func (l *observableLogger) WarnContext(ctx context.Context, args ...any) { + l.Log(ctx, log.LevelWarn, args) +} + +func (l *observableLogger) ErrorContext(ctx context.Context, args ...any) { + l.Log(ctx, log.LevelError, args) +} + +func (l *observableLogger) FatalContext(ctx context.Context, args ...any) { + l.Log(ctx, log.LevelFatal, args) +} + +func (l *observableLogger) PanicContext(ctx context.Context, args ...any) { + l.Log(ctx, log.LevelPanic, args) +} diff --git a/core/main.go b/core/main.go new file mode 100644 index 0000000..1bef45b --- /dev/null +++ b/core/main.go @@ -0,0 +1,95 @@ +package core + +import ( + "context" + + "github.com/alireza0/s-ui/logger" + + sb "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/adapter" + _ "github.com/sagernet/sing-box/experimental/clashapi" + _ "github.com/sagernet/sing-box/experimental/v2rayapi" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + _ "github.com/sagernet/sing-box/transport/v2rayquic" + "github.com/sagernet/sing/service" +) + +var ( + globalCtx context.Context + inbound_manager adapter.InboundManager + outbound_manager adapter.OutboundManager + service_manager adapter.ServiceManager + endpoint_manager adapter.EndpointManager + router adapter.Router + factory log.Factory +) + +type Core struct { + isRunning bool + instance *Box +} + +func NewCore() *Core { + globalCtx = context.Background() + globalCtx = sb.Context(globalCtx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry()) + return &Core{ + isRunning: false, + instance: nil, + } +} + +func (c *Core) GetCtx() context.Context { + return globalCtx +} + +func (c *Core) GetInstance() *Box { + return c.instance +} + +func (c *Core) Start(sbConfig []byte) error { + var opt option.Options + err := opt.UnmarshalJSONContext(globalCtx, sbConfig) + if err != nil { + logger.Error("Unmarshal config err:", err.Error()) + } + + c.instance, err = NewBox(Options{ + Context: globalCtx, + Options: opt, + }) + if err != nil { + return err + } + + err = c.instance.Start() + if err != nil { + _ = c.instance.Close() + c.instance = nil + return err + } + + globalCtx = service.ContextWith(globalCtx, c) + inbound_manager = service.FromContext[adapter.InboundManager](globalCtx) + outbound_manager = service.FromContext[adapter.OutboundManager](globalCtx) + service_manager = service.FromContext[adapter.ServiceManager](globalCtx) + endpoint_manager = service.FromContext[adapter.EndpointManager](globalCtx) + router = service.FromContext[adapter.Router](globalCtx) + + c.isRunning = true + return nil +} + +func (c *Core) Stop() error { + c.isRunning = false + if c.instance == nil { + return nil + } + err := c.instance.Close() + c.instance = nil + return err +} + +func (c *Core) IsRunning() bool { + return c.isRunning +} diff --git a/core/outbound_check.go b/core/outbound_check.go new file mode 100644 index 0000000..74fe50d --- /dev/null +++ b/core/outbound_check.go @@ -0,0 +1,40 @@ +package core + +import ( + "context" + "time" + + urltest "github.com/sagernet/sing-box/common/urltest" +) + +const checkTimeout = 15 * time.Second + +type CheckOutboundResult struct { + OK bool + Delay uint16 + Error string +} + +func CheckOutbound(ctx context.Context, tag string, link string) (result CheckOutboundResult) { + if outbound_manager == nil { + result.Error = "core not running" + return result + } + ob, ok := outbound_manager.Outbound(tag) + if !ok { + result.Error = "outbound not found" + return result + } + + ctx, cancel := context.WithTimeout(ctx, checkTimeout) + defer cancel() + + delay, err := urltest.URLTest(ctx, link, ob) + if err != nil { + result.Error = err.Error() + return result + } + result.OK = true + result.Delay = delay + return result +} diff --git a/core/register.go b/core/register.go new file mode 100644 index 0000000..7f0201e --- /dev/null +++ b/core/register.go @@ -0,0 +1,139 @@ +package core + +import ( + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing-box/dns/transport/dhcp" + "github.com/sagernet/sing-box/dns/transport/fakeip" + "github.com/sagernet/sing-box/dns/transport/hosts" + "github.com/sagernet/sing-box/dns/transport/local" + "github.com/sagernet/sing-box/dns/transport/quic" + "github.com/sagernet/sing-box/protocol/anytls" + "github.com/sagernet/sing-box/protocol/block" + "github.com/sagernet/sing-box/protocol/direct" + "github.com/sagernet/sing-box/protocol/group" + "github.com/sagernet/sing-box/protocol/http" + "github.com/sagernet/sing-box/protocol/hysteria" + "github.com/sagernet/sing-box/protocol/hysteria2" + "github.com/sagernet/sing-box/protocol/mixed" + "github.com/sagernet/sing-box/protocol/naive" + _ "github.com/sagernet/sing-box/protocol/naive/quic" + "github.com/sagernet/sing-box/protocol/redirect" + "github.com/sagernet/sing-box/protocol/shadowsocks" + "github.com/sagernet/sing-box/protocol/shadowtls" + "github.com/sagernet/sing-box/protocol/socks" + "github.com/sagernet/sing-box/protocol/ssh" + "github.com/sagernet/sing-box/protocol/tor" + "github.com/sagernet/sing-box/protocol/trojan" + "github.com/sagernet/sing-box/protocol/tuic" + "github.com/sagernet/sing-box/protocol/tun" + "github.com/sagernet/sing-box/protocol/vless" + "github.com/sagernet/sing-box/protocol/vmess" + "github.com/sagernet/sing-box/protocol/wireguard" + "github.com/sagernet/sing-box/service/ccm" + "github.com/sagernet/sing-box/service/ocm" + "github.com/sagernet/sing-box/service/resolved" + "github.com/sagernet/sing-box/service/ssmapi" + _ "github.com/sagernet/sing-box/transport/v2rayquic" +) + +func InboundRegistry() *inbound.Registry { + registry := inbound.NewRegistry() + + tun.RegisterInbound(registry) + redirect.RegisterRedirect(registry) + redirect.RegisterTProxy(registry) + direct.RegisterInbound(registry) + + socks.RegisterInbound(registry) + http.RegisterInbound(registry) + mixed.RegisterInbound(registry) + + shadowsocks.RegisterInbound(registry) + vmess.RegisterInbound(registry) + trojan.RegisterInbound(registry) + naive.RegisterInbound(registry) + shadowtls.RegisterInbound(registry) + vless.RegisterInbound(registry) + anytls.RegisterInbound(registry) + + hysteria.RegisterInbound(registry) + tuic.RegisterInbound(registry) + hysteria2.RegisterInbound(registry) + + return registry +} + +func OutboundRegistry() *outbound.Registry { + registry := outbound.NewRegistry() + + direct.RegisterOutbound(registry) + + block.RegisterOutbound(registry) + + group.RegisterSelector(registry) + group.RegisterURLTest(registry) + + socks.RegisterOutbound(registry) + http.RegisterOutbound(registry) + shadowsocks.RegisterOutbound(registry) + vmess.RegisterOutbound(registry) + trojan.RegisterOutbound(registry) + registerNaiveOutbound(registry) + tor.RegisterOutbound(registry) + ssh.RegisterOutbound(registry) + shadowtls.RegisterOutbound(registry) + vless.RegisterOutbound(registry) + anytls.RegisterOutbound(registry) + + hysteria.RegisterOutbound(registry) + tuic.RegisterOutbound(registry) + hysteria2.RegisterOutbound(registry) + + return registry +} + +func EndpointRegistry() *endpoint.Registry { + registry := endpoint.NewRegistry() + + wireguard.RegisterEndpoint(registry) + registerTailscaleEndpoint(registry) + + return registry +} + +func DNSTransportRegistry() *dns.TransportRegistry { + registry := dns.NewTransportRegistry() + + transport.RegisterTCP(registry) + transport.RegisterUDP(registry) + transport.RegisterTLS(registry) + transport.RegisterHTTPS(registry) + hosts.RegisterTransport(registry) + local.RegisterTransport(registry) + fakeip.RegisterTransport(registry) + + quic.RegisterTransport(registry) + quic.RegisterHTTP3Transport(registry) + dhcp.RegisterTransport(registry) + registerTailscaleTransport(registry) + + return registry +} + +func ServiceRegistry() *service.Registry { + registry := service.NewRegistry() + + resolved.RegisterService(registry) + ssmapi.RegisterService(registry) + + registerDERPService(registry) + ccm.RegisterService(registry) + ocm.RegisterService(registry) + + return registry +} diff --git a/core/register_naive.go b/core/register_naive.go new file mode 100644 index 0000000..76e5127 --- /dev/null +++ b/core/register_naive.go @@ -0,0 +1,12 @@ +//go:build with_naive_outbound + +package core + +import ( + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/protocol/naive" +) + +func registerNaiveOutbound(registry *outbound.Registry) { + naive.RegisterOutbound(registry) +} diff --git a/core/register_naive_stub.go b/core/register_naive_stub.go new file mode 100644 index 0000000..a0bed54 --- /dev/null +++ b/core/register_naive_stub.go @@ -0,0 +1,13 @@ +//go:build !with_naive_outbound + +package core + +import ( + "github.com/alireza0/s-ui/logger" + "github.com/sagernet/sing-box/adapter/outbound" +) + +func registerNaiveOutbound(registry *outbound.Registry) { + // naive outbound is disabled when built without with_naive_outbound tag + logger.Error("naive outbound is disabled when built without with_naive_outbound tag") +} diff --git a/core/register_tailscale.go b/core/register_tailscale.go new file mode 100644 index 0000000..b0b772f --- /dev/null +++ b/core/register_tailscale.go @@ -0,0 +1,23 @@ +//go:build with_tailscale + +package core + +import ( + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/protocol/tailscale" + "github.com/sagernet/sing-box/service/derp" +) + +func registerTailscaleEndpoint(registry *endpoint.Registry) { + tailscale.RegisterEndpoint(registry) +} + +func registerTailscaleTransport(registry *dns.TransportRegistry) { + tailscale.RegistryTransport(registry) +} + +func registerDERPService(registry *service.Registry) { + derp.Register(registry) +} diff --git a/core/register_tailscale_stub.go b/core/register_tailscale_stub.go new file mode 100644 index 0000000..9d5e2eb --- /dev/null +++ b/core/register_tailscale_stub.go @@ -0,0 +1,34 @@ +//go:build !with_tailscale + +package core + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/adapter/service" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func registerTailscaleEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) { + return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) + }) +} + +func registerTailscaleTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.TailscaleDNSServerOptions](registry, C.DNSTypeTailscale, func(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) { + return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) + }) +} + +func registerDERPService(registry *service.Registry) { + service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) { + return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`) + }) +} diff --git a/core/tracker_conn.go b/core/tracker_conn.go new file mode 100644 index 0000000..e5def7a --- /dev/null +++ b/core/tracker_conn.go @@ -0,0 +1,219 @@ +package core + +import ( + "context" + "errors" + "io" + "net" + "sync" + + "github.com/gofrs/uuid/v5" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/network" +) + +type ConnectionInfo struct { + ID string + Conn net.Conn + PacketConn network.PacketConn + Inbound string + Type string // "tcp" or "udp" +} + +type ConnTracker struct { + access sync.Mutex + connections map[string]*ConnectionInfo +} + +func NewConnTracker() *ConnTracker { + return &ConnTracker{ + connections: make(map[string]*ConnectionInfo), + } +} + +func (c *ConnTracker) Reset() { + c.access.Lock() + defer c.access.Unlock() + for _, connInfo := range c.connections { + if connInfo.Conn != nil { + _ = connInfo.Conn.Close() + } + if connInfo.PacketConn != nil { + _ = connInfo.PacketConn.Close() + } + } + c.connections = make(map[string]*ConnectionInfo) +} + +func (c *ConnTracker) generateConnectionID() string { + return uuid.Must(uuid.NewV4()).String() +} + +func (c *ConnTracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { + connID := c.generateConnectionID() + connInfo := &ConnectionInfo{ + ID: connID, + Conn: conn, + Inbound: metadata.Inbound, + Type: "tcp", + } + + c.trackConnection(connID, connInfo) + + return c.createWrappedConn(conn, connID) +} + +func (c *ConnTracker) RoutedPacketConnection(ctx context.Context, conn network.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) network.PacketConn { + connID := c.generateConnectionID() + connInfo := &ConnectionInfo{ + ID: connID, + PacketConn: conn, + Inbound: metadata.Inbound, + Type: "udp", + } + + c.trackConnection(connID, connInfo) + + return c.createWrappedPacketConn(conn, connID) +} + +func (c *ConnTracker) CloseConnByInbound(inbound string) int { + c.access.Lock() + defer c.access.Unlock() + + closedCount := 0 + for connID, connInfo := range c.connections { + if connInfo.Inbound == inbound { + if connInfo.Conn != nil { + connInfo.Conn.Close() + } + if connInfo.PacketConn != nil { + connInfo.PacketConn.Close() + } + delete(c.connections, connID) + closedCount++ + } + } + return closedCount +} + +func (c *ConnTracker) trackConnection(connID string, connInfo *ConnectionInfo) { + c.access.Lock() + defer c.access.Unlock() + c.connections[connID] = connInfo +} + +func (c *ConnTracker) untrackConnection(connID string) { + c.access.Lock() + defer c.access.Unlock() + delete(c.connections, connID) +} + +// shouldUntrackIOErr reports whether err indicates the connection is done (peer closed, reset, etc.). +func shouldUntrackIOErr(err error) bool { + if err == nil { + return false + } + if errors.Is(err, io.EOF) { + return true + } + var ne net.Error + if errors.As(err, &ne) { + return !ne.Temporary() + } + return true +} + +func (c *ConnTracker) createWrappedConn(conn net.Conn, connID string) *wrappedConn { + return &wrappedConn{ + Conn: conn, + tracker: c, + connID: connID, + } +} + +func (c *ConnTracker) createWrappedPacketConn(conn network.PacketConn, connID string) *wrappedPacketConn { + return &wrappedPacketConn{ + PacketConn: conn, + tracker: c, + connID: connID, + } +} + +type wrappedConn struct { + net.Conn + tracker *ConnTracker + connID string + untrackOnce sync.Once +} + +func (w *wrappedConn) doUntrack() { + w.untrackOnce.Do(func() { + w.tracker.untrackConnection(w.connID) + }) +} + +func (w *wrappedConn) Read(b []byte) (int, error) { + n, err := w.Conn.Read(b) + if shouldUntrackIOErr(err) { + w.doUntrack() + } + return n, err +} + +func (w *wrappedConn) Write(b []byte) (int, error) { + n, err := w.Conn.Write(b) + if err != nil && shouldUntrackIOErr(err) { + w.doUntrack() + } + return n, err +} + +func (w *wrappedConn) Close() error { + w.doUntrack() + return w.Conn.Close() +} + +func (w *wrappedConn) Upstream() any { + return w.Conn +} + +type wrappedPacketConn struct { + network.PacketConn + tracker *ConnTracker + connID string + untrackOnce sync.Once +} + +func (w *wrappedPacketConn) doUntrack() { + w.untrackOnce.Do(func() { + w.tracker.untrackConnection(w.connID) + }) +} + +func (w *wrappedPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { + dest, err := w.PacketConn.ReadPacket(buffer) + if shouldUntrackIOErr(err) { + w.doUntrack() + } + return dest, err +} + +func (w *wrappedPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + err := w.PacketConn.WritePacket(buffer, destination) + if err != nil && shouldUntrackIOErr(err) { + w.doUntrack() + } + return err +} + +func (w *wrappedPacketConn) Close() error { + w.doUntrack() + return w.PacketConn.Close() +} + +func (w *wrappedPacketConn) Upstream() any { + return w.PacketConn +} diff --git a/core/tracker_stats.go b/core/tracker_stats.go new file mode 100644 index 0000000..23070fa --- /dev/null +++ b/core/tracker_stats.go @@ -0,0 +1,153 @@ +package core + +import ( + "context" + "net" + "sync" + "time" + + "github.com/alireza0/s-ui/database/model" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/atomic" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/network" +) + +type Counter struct { + read *atomic.Int64 + write *atomic.Int64 +} + +type StatsTracker struct { + access sync.Mutex + inbounds map[string]Counter + outbounds map[string]Counter + users map[string]Counter +} + +func NewStatsTracker() *StatsTracker { + return &StatsTracker{ + inbounds: make(map[string]Counter), + outbounds: make(map[string]Counter), + users: make(map[string]Counter), + } +} + +func (c *StatsTracker) Reset() { + c.access.Lock() + defer c.access.Unlock() + c.inbounds = make(map[string]Counter) + c.outbounds = make(map[string]Counter) + c.users = make(map[string]Counter) +} + +func (c *StatsTracker) getReadCounters(inbound string, outbound string, user string) ([]*atomic.Int64, []*atomic.Int64) { + var readCounter []*atomic.Int64 + var writeCounter []*atomic.Int64 + c.access.Lock() + defer c.access.Unlock() + + if inbound != "" { + readCounter = append(readCounter, c.loadOrCreateCounter(&c.inbounds, inbound).read) + writeCounter = append(writeCounter, c.inbounds[inbound].write) + } + if outbound != "" { + readCounter = append(readCounter, c.loadOrCreateCounter(&c.outbounds, outbound).read) + writeCounter = append(writeCounter, c.outbounds[outbound].write) + } + if user != "" { + readCounter = append(readCounter, c.loadOrCreateCounter(&c.users, user).read) + writeCounter = append(writeCounter, c.users[user].write) + } + return readCounter, writeCounter +} + +func (c *StatsTracker) loadOrCreateCounter(obj *map[string]Counter, name string) Counter { + counter, loaded := (*obj)[name] + if loaded { + return counter + } + counter = Counter{read: &atomic.Int64{}, write: &atomic.Int64{}} + (*obj)[name] = counter + return counter +} + +func (c *StatsTracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { + readCounter, writeCounter := c.getReadCounters(metadata.Inbound, matchOutbound.Tag(), metadata.User) + return bufio.NewInt64CounterConn(conn, readCounter, writeCounter) +} + +func (c *StatsTracker) RoutedPacketConnection(ctx context.Context, conn network.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) network.PacketConn { + readCounter, writeCounter := c.getReadCounters(metadata.Inbound, matchOutbound.Tag(), metadata.User) + return bufio.NewInt64CounterPacketConn(conn, readCounter, nil, writeCounter, nil) +} + +func (c *StatsTracker) GetStats() *[]model.Stats { + c.access.Lock() + defer c.access.Unlock() + + dt := time.Now().Unix() + + s := []model.Stats{} + for inbound, counter := range c.inbounds { + down := counter.write.Swap(0) + up := counter.read.Swap(0) + if down > 0 || up > 0 { + s = append(s, model.Stats{ + DateTime: dt, + Resource: "inbound", + Tag: inbound, + Direction: false, + Traffic: down, + }, model.Stats{ + DateTime: dt, + Resource: "inbound", + Tag: inbound, + Direction: true, + Traffic: up, + }) + } + } + + for outbound, counter := range c.outbounds { + down := counter.write.Swap(0) + up := counter.read.Swap(0) + if down > 0 || up > 0 { + s = append(s, model.Stats{ + DateTime: dt, + Resource: "outbound", + Tag: outbound, + Direction: false, + Traffic: down, + }, model.Stats{ + DateTime: dt, + Resource: "outbound", + Tag: outbound, + Direction: true, + Traffic: up, + }) + } + } + + for user, counter := range c.users { + down := counter.write.Swap(0) + up := counter.read.Swap(0) + if down > 0 || up > 0 { + s = append(s, model.Stats{ + DateTime: dt, + Resource: "user", + Tag: user, + Direction: false, + Traffic: down, + }, model.Stats{ + DateTime: dt, + Resource: "user", + Tag: user, + Direction: true, + Traffic: up, + }) + } + } + return &s +} diff --git a/cronjob/WALCheckpointJob.go b/cronjob/WALCheckpointJob.go new file mode 100644 index 0000000..e79da15 --- /dev/null +++ b/cronjob/WALCheckpointJob.go @@ -0,0 +1,19 @@ +package cronjob + +import ( + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/logger" +) + +type WALCheckpointJob struct{} + +func NewWALCheckpointJob() *WALCheckpointJob { + return &WALCheckpointJob{} +} + +func (s *WALCheckpointJob) Run() { + db := database.GetDB() + if err := db.Exec("PRAGMA wal_checkpoint(FULL)").Error; err != nil { + logger.Error("Error checkpointing WAL: ", err.Error()) + } +} diff --git a/cronjob/checkCoreJob.go b/cronjob/checkCoreJob.go new file mode 100644 index 0000000..b16c4fe --- /dev/null +++ b/cronjob/checkCoreJob.go @@ -0,0 +1,17 @@ +package cronjob + +import ( + "github.com/alireza0/s-ui/service" +) + +type CheckCoreJob struct { + service.ConfigService +} + +func NewCheckCoreJob() *CheckCoreJob { + return &CheckCoreJob{} +} + +func (s *CheckCoreJob) Run() { + s.ConfigService.StartCore() +} diff --git a/cronjob/cronJob.go b/cronjob/cronJob.go new file mode 100644 index 0000000..5ab45e8 --- /dev/null +++ b/cronjob/cronJob.go @@ -0,0 +1,43 @@ +package cronjob + +import ( + "time" + + "github.com/robfig/cron/v3" +) + +type CronJob struct { + cron *cron.Cron +} + +func NewCronJob() *CronJob { + return &CronJob{} +} + +func (c *CronJob) Start(loc *time.Location, trafficAge int) error { + c.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds()) + c.cron.Start() + + go func() { + // Start stats job + c.cron.AddJob("@every 10s", NewStatsJob(trafficAge > 0)) + // Start expiry job + c.cron.AddJob("@every 1m", NewDepleteJob()) + // Start deleting old stats + if trafficAge > 0 { + c.cron.AddJob("@daily", NewDelStatsJob(trafficAge)) + } + // Start core if it is not running + c.cron.AddJob("@every 5s", NewCheckCoreJob()) + // database WAL checkpoint + c.cron.AddJob("@every 10m", NewWALCheckpointJob()) + }() + + return nil +} + +func (c *CronJob) Stop() { + if c.cron != nil { + c.cron.Stop() + } +} diff --git a/cronjob/delStatsJob.go b/cronjob/delStatsJob.go new file mode 100644 index 0000000..ac80a82 --- /dev/null +++ b/cronjob/delStatsJob.go @@ -0,0 +1,26 @@ +package cronjob + +import ( + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/service" +) + +type DelStatsJob struct { + service.StatsService + trafficAge int +} + +func NewDelStatsJob(ta int) *DelStatsJob { + return &DelStatsJob{ + trafficAge: ta, + } +} + +func (s *DelStatsJob) Run() { + err := s.StatsService.DelOldStats(s.trafficAge) + if err != nil { + logger.Warning("Deleting old statistics failed: ", err) + return + } + logger.Debug("Stats older than ", s.trafficAge, " days were deleted") +} diff --git a/cronjob/depleteJob.go b/cronjob/depleteJob.go new file mode 100644 index 0000000..3f9438e --- /dev/null +++ b/cronjob/depleteJob.go @@ -0,0 +1,30 @@ +package cronjob + +import ( + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/service" +) + +type DepleteJob struct { + service.ClientService + service.InboundService +} + +func NewDepleteJob() *DepleteJob { + return new(DepleteJob) +} + +func (s *DepleteJob) Run() { + inboundIds, err := s.ClientService.DepleteClients() + if err != nil { + logger.Warning("Disable depleted users failed: ", err) + return + } + if len(inboundIds) > 0 { + err := s.InboundService.RestartInbounds(database.GetDB(), inboundIds) + if err != nil { + logger.Error("unable to restart inbounds: ", err) + } + } +} diff --git a/cronjob/statsJob.go b/cronjob/statsJob.go new file mode 100644 index 0000000..232a245 --- /dev/null +++ b/cronjob/statsJob.go @@ -0,0 +1,25 @@ +package cronjob + +import ( + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/service" +) + +type StatsJob struct { + service.StatsService + enableTraffic bool +} + +func NewStatsJob(saveTraffic bool) *StatsJob { + return &StatsJob{ + enableTraffic: saveTraffic, + } +} + +func (s *StatsJob) Run() { + err := s.StatsService.SaveStats(s.enableTraffic) + if err != nil { + logger.Warning("Get stats failed: ", err) + return + } +} diff --git a/database/backup.go b/database/backup.go new file mode 100644 index 0000000..1517b00 --- /dev/null +++ b/database/backup.go @@ -0,0 +1,302 @@ +package database + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" + "runtime" + "strings" + "syscall" + "time" + + "github.com/alireza0/s-ui/cmd/migration" + "github.com/alireza0/s-ui/config" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util/common" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func GetDb(exclude string) ([]byte, error) { + exclude_changes, exclude_stats := false, false + for _, table := range strings.Split(exclude, ",") { + if table == "changes" { + exclude_changes = true + } else if table == "stats" { + exclude_stats = true + } + } + + dir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + return nil, err + } + dbPath := dir + config.GetName() + "_" + time.Now().Format("20060102-200203") + ".db" + + backupDb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + return nil, err + } + defer os.Remove(dbPath) + + err = backupDb.AutoMigrate( + &model.Setting{}, + &model.Tls{}, + &model.Inbound{}, + &model.Outbound{}, + &model.Endpoint{}, + &model.User{}, + &model.Stats{}, + &model.Client{}, + &model.Changes{}, + ) + if err != nil { + return nil, err + } + + var settings []model.Setting + var tls []model.Tls + var inbound []model.Inbound + var outbound []model.Outbound + var endpoint []model.Endpoint + var users []model.User + var clients []model.Client + var stats []model.Stats + var changes []model.Changes + + // Perform scans and handle errors + if err := db.Model(&model.Setting{}).Scan(&settings).Error; err != nil { + return nil, err + } else if len(settings) > 0 { + if err := backupDb.Save(settings).Error; err != nil { + return nil, err + } + } + if err := db.Model(&model.Tls{}).Scan(&tls).Error; err != nil { + return nil, err + } else if len(tls) > 0 { + if err := backupDb.Save(tls).Error; err != nil { + return nil, err + } + } + if err := db.Model(&model.Inbound{}).Scan(&inbound).Error; err != nil { + return nil, err + } else if len(inbound) > 0 { + if err := backupDb.Save(inbound).Error; err != nil { + return nil, err + } + } + if err := db.Model(&model.Outbound{}).Scan(&outbound).Error; err != nil { + return nil, err + } else if len(outbound) > 0 { + if err := backupDb.Save(outbound).Error; err != nil { + return nil, err + } + } + if err := db.Model(&model.Endpoint{}).Scan(&endpoint).Error; err != nil { + return nil, err + } else if len(endpoint) > 0 { + if err := backupDb.Save(endpoint).Error; err != nil { + return nil, err + } + } + if err := db.Model(&model.User{}).Scan(&users).Error; err != nil { + return nil, err + } else if len(users) > 0 { + if err := backupDb.Save(users).Error; err != nil { + return nil, err + } + } + if err := db.Model(&model.Client{}).Scan(&clients).Error; err != nil { + return nil, err + } else if len(clients) > 0 { + if err := backupDb.Save(clients).Error; err != nil { + return nil, err + } + } + + if !exclude_stats { + if err := db.Model(&model.Stats{}).Scan(&stats).Error; err != nil { + return nil, err + } + if len(stats) > 0 { + if err := backupDb.Save(stats).Error; err != nil { + return nil, err + } + } + } + if !exclude_changes { + if err := db.Model(&model.Changes{}).Scan(&changes).Error; err != nil { + return nil, err + } + if len(changes) > 0 { + if err := backupDb.Save(changes).Error; err != nil { + return nil, err + } + } + } + + // Update WAL + err = backupDb.Exec("PRAGMA wal_checkpoint;").Error + if err != nil { + return nil, err + } + + bdb, _ := backupDb.DB() + bdb.Close() + + // Open the file for reading + file, err := os.Open(dbPath) + if err != nil { + return nil, err + } + defer file.Close() + + // Read the file contents + fileContents, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + return fileContents, nil +} + +func ImportDB(file multipart.File) error { + // Check if the file is a SQLite database + isValidDb, err := IsSQLiteDB(file) + if err != nil { + return common.NewErrorf("Error checking db file format: %v", err) + } + if !isValidDb { + return common.NewError("Invalid db file format") + } + + // Reset the file reader to the beginning + _, err = file.Seek(0, 0) + if err != nil { + return common.NewErrorf("Error resetting file reader: %v", err) + } + + // Save the file as temporary file + tempPath := fmt.Sprintf("%s.temp", config.GetDBPath()) + // Remove the existing fallback file (if any) before creating one + _, err = os.Stat(tempPath) + if err == nil { + errRemove := os.Remove(tempPath) + if errRemove != nil { + return common.NewErrorf("Error removing existing temporary db file: %v", errRemove) + } + } + // Create the temporary file + tempFile, err := os.Create(tempPath) + if err != nil { + return common.NewErrorf("Error creating temporary db file: %v", err) + } + defer tempFile.Close() + + // Remove temp file before returning + defer os.Remove(tempPath) + + // Close old DB + old_db, _ := db.DB() + old_db.Close() + + // Save uploaded file to temporary file + _, err = io.Copy(tempFile, file) + if err != nil { + return common.NewErrorf("Error saving db: %v", err) + } + + // Check if we can init db or not + newDb, err := gorm.Open(sqlite.Open(tempPath), &gorm.Config{}) + if err != nil { + return common.NewErrorf("Error checking db: %v", err) + } + newDb_db, _ := newDb.DB() + newDb_db.Close() + + // Backup the current database for fallback + fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath()) + // Remove the existing fallback file (if any) + _, err = os.Stat(fallbackPath) + if err == nil { + errRemove := os.Remove(fallbackPath) + if errRemove != nil { + return common.NewErrorf("Error removing existing fallback db file: %v", errRemove) + } + } + // Move the current database to the fallback location + err = os.Rename(config.GetDBPath(), fallbackPath) + if err != nil { + return common.NewErrorf("Error backing up temporary db file: %v", err) + } + + // Remove the temporary file before returning + defer os.Remove(fallbackPath) + + // Move temp to DB path + err = os.Rename(tempPath, config.GetDBPath()) + if err != nil { + errRename := os.Rename(fallbackPath, config.GetDBPath()) + if errRename != nil { + return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename) + } + return common.NewErrorf("Error moving db file: %v", err) + } + + // Migrate DB + migration.MigrateDb() + err = InitDB(config.GetDBPath()) + if err != nil { + errRename := os.Rename(fallbackPath, config.GetDBPath()) + if errRename != nil { + return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename) + } + return common.NewErrorf("Error migrating db: %v", err) + } + + // Restart app + err = SendSighup() + if err != nil { + return common.NewErrorf("Error restarting app: %v", err) + } + + return nil +} + +func IsSQLiteDB(file io.Reader) (bool, error) { + signature := []byte("SQLite format 3\x00") + buf := make([]byte, len(signature)) + _, err := file.Read(buf) + if err != nil { + return false, err + } + return bytes.Equal(buf, signature), nil +} + +func SendSighup() error { + // Get the current process + process, err := os.FindProcess(os.Getpid()) + if err != nil { + return err + } + + // Send SIGHUP to the current process + go func() { + time.Sleep(3 * time.Second) + if runtime.GOOS == "windows" { + err = process.Kill() + } else { + err = process.Signal(syscall.SIGHUP) + } + if err != nil { + logger.Error("send signal SIGHUP failed:", err) + } + }() + return nil +} diff --git a/database/db.go b/database/db.go new file mode 100644 index 0000000..ee7f40b --- /dev/null +++ b/database/db.go @@ -0,0 +1,123 @@ +package database + +import ( + "encoding/json" + "os" + "path" + "strings" + "time" + + "github.com/alireza0/s-ui/config" + "github.com/alireza0/s-ui/database/model" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var db *gorm.DB + +func initUser() error { + var count int64 + err := db.Model(&model.User{}).Count(&count).Error + if err != nil { + return err + } + if count == 0 { + user := &model.User{ + Username: "admin", + Password: "admin", + } + return db.Create(user).Error + } + return nil +} + +func OpenDB(dbPath string) error { + dir := path.Dir(dbPath) + err := os.MkdirAll(dir, 01740) + if err != nil { + return err + } + + var gormLogger logger.Interface + + if config.IsDebug() { + gormLogger = logger.Default + } else { + gormLogger = logger.Discard + } + + c := &gorm.Config{ + Logger: gormLogger, + } + sep := "?" + if strings.Contains(dbPath, "?") { + sep = "&" + } + dsn := dbPath + sep + "_busy_timeout=10000&_journal_mode=WAL" + db, err = gorm.Open(sqlite.Open(dsn), c) + if err != nil { + return err + } + + sqlDB, err := db.DB() + if err != nil { + return err + } + sqlDB.SetMaxOpenConns(25) + sqlDB.SetMaxIdleConns(5) + sqlDB.SetConnMaxLifetime(time.Hour) + + if config.IsDebug() { + db = db.Debug() + } + return nil +} + +func InitDB(dbPath string) error { + err := OpenDB(dbPath) + if err != nil { + return err + } + + // Default Outbounds + if !db.Migrator().HasTable(&model.Outbound{}) { + db.Migrator().CreateTable(&model.Outbound{}) + defaultOutbound := []model.Outbound{ + {Type: "direct", Tag: "direct", Options: json.RawMessage(`{}`)}, + } + db.Create(&defaultOutbound) + } + + err = db.AutoMigrate( + &model.Setting{}, + &model.Tls{}, + &model.Inbound{}, + &model.Outbound{}, + &model.Service{}, + &model.Endpoint{}, + &model.User{}, + &model.Tokens{}, + &model.Stats{}, + &model.Client{}, + &model.Changes{}, + ) + if err != nil { + return err + } + err = initUser() + if err != nil { + return err + } + + return nil +} + +func GetDB() *gorm.DB { + return db +} + +func IsNotFound(err error) bool { + return err == gorm.ErrRecordNotFound +} diff --git a/database/model/endpoints.go b/database/model/endpoints.go new file mode 100644 index 0000000..21017d7 --- /dev/null +++ b/database/model/endpoints.go @@ -0,0 +1,63 @@ +package model + +import ( + "encoding/json" +) + +type Endpoint struct { + Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Type string `json:"type" form:"type"` + Tag string `json:"tag" form:"tag" gorm:"unique"` + Options json.RawMessage `json:"-" form:"-"` + Ext json.RawMessage `json:"ext" form:"ext"` +} + +func (o *Endpoint) UnmarshalJSON(data []byte) error { + var err error + var raw map[string]interface{} + if err = json.Unmarshal(data, &raw); err != nil { + return err + } + + // Extract fixed fields and store the rest in Options + if val, exists := raw["id"].(float64); exists { + o.Id = uint(val) + } + delete(raw, "id") + o.Type, _ = raw["type"].(string) + delete(raw, "type") + o.Tag = raw["tag"].(string) + delete(raw, "tag") + o.Ext, _ = json.MarshalIndent(raw["ext"], "", " ") + delete(raw, "ext") + + // Remaining fields + o.Options, err = json.MarshalIndent(raw, "", " ") + return err +} + +// MarshalJSON customizes marshalling +func (o Endpoint) MarshalJSON() ([]byte, error) { + // Combine fixed fields and dynamic fields into one map + combined := make(map[string]interface{}) + switch o.Type { + case "warp": + combined["type"] = "wireguard" + default: + combined["type"] = o.Type + } + combined["tag"] = o.Tag + + if o.Options != nil { + var restFields map[string]json.RawMessage + if err := json.Unmarshal(o.Options, &restFields); err != nil { + return nil, err + } + + for k, v := range restFields { + combined[k] = v + } + } + + return json.Marshal(combined) +} diff --git a/database/model/inbounds.go b/database/model/inbounds.go new file mode 100644 index 0000000..9ad5b06 --- /dev/null +++ b/database/model/inbounds.go @@ -0,0 +1,103 @@ +package model + +import ( + "encoding/json" +) + +type Inbound struct { + Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Type string `json:"type" form:"type"` + Tag string `json:"tag" form:"tag" gorm:"unique"` + + // Foreign key to tls table + TlsId uint `json:"tls_id" form:"tls_id"` + Tls *Tls `json:"tls" form:"tls" gorm:"foreignKey:TlsId;references:Id"` + + Addrs json.RawMessage `json:"addrs" form:"addrs"` + OutJson json.RawMessage `json:"out_json" form:"out_json"` + Options json.RawMessage `json:"-" form:"-"` +} + +func (i *Inbound) UnmarshalJSON(data []byte) error { + var err error + var raw map[string]interface{} + if err = json.Unmarshal(data, &raw); err != nil { + return err + } + + // Extract fixed fields and store the rest in Options + if val, exists := raw["id"].(float64); exists { + i.Id = uint(val) + } + delete(raw, "id") + i.Type, _ = raw["type"].(string) + delete(raw, "type") + i.Tag, _ = raw["tag"].(string) + delete(raw, "tag") + + // TlsId + if val, exists := raw["tls_id"].(float64); exists { + i.TlsId = uint(val) + } + delete(raw, "tls_id") + delete(raw, "tls") + delete(raw, "users") + + // Addrs + i.Addrs, _ = json.MarshalIndent(raw["addrs"], "", " ") + delete(raw, "addrs") + + // OutJson + i.OutJson, _ = json.MarshalIndent(raw["out_json"], "", " ") + delete(raw, "out_json") + + // Remaining fields + i.Options, err = json.MarshalIndent(raw, "", " ") + return err +} + +// MarshalJSON customizes marshalling +func (i Inbound) MarshalJSON() ([]byte, error) { + // Combine fixed fields and dynamic fields into one map + combined := make(map[string]interface{}) + combined["type"] = i.Type + combined["tag"] = i.Tag + if i.Tls != nil { + combined["tls"] = i.Tls.Server + } + + if i.Options != nil { + var restFields map[string]json.RawMessage + if err := json.Unmarshal(i.Options, &restFields); err != nil { + return nil, err + } + + for k, v := range restFields { + combined[k] = v + } + } + + return json.Marshal(combined) +} + +func (i Inbound) MarshalFull() (*map[string]interface{}, error) { + combined := make(map[string]interface{}) + combined["id"] = i.Id + combined["type"] = i.Type + combined["tag"] = i.Tag + combined["tls_id"] = i.TlsId + combined["addrs"] = i.Addrs + combined["out_json"] = i.OutJson + + if i.Options != nil { + var restFields map[string]interface{} + if err := json.Unmarshal(i.Options, &restFields); err != nil { + return nil, err + } + + for k, v := range restFields { + combined[k] = v + } + } + return &combined, nil +} diff --git a/database/model/model.go b/database/model/model.go new file mode 100644 index 0000000..788b88a --- /dev/null +++ b/database/model/model.go @@ -0,0 +1,73 @@ +package model + +import "encoding/json" + +type Setting struct { + Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Key string `json:"key" form:"key"` + Value string `json:"value" form:"value"` +} + +type Tls struct { + Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" form:"name"` + Server json.RawMessage `json:"server" form:"server"` + Client json.RawMessage `json:"client" form:"client"` +} + +type User struct { + Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Username string `json:"username" form:"username"` + Password string `json:"password" form:"password"` + LastLogins string `json:"lastLogin"` +} + +type Client struct { + Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Enable bool `json:"enable" form:"enable"` + Name string `json:"name" form:"name"` + Config json.RawMessage `json:"config,omitempty" form:"config"` + Inbounds json.RawMessage `json:"inbounds" form:"inbounds"` + Links json.RawMessage `json:"links,omitempty" form:"links"` + Volume int64 `json:"volume" form:"volume"` + Expiry int64 `json:"expiry" form:"expiry"` + Down int64 `json:"down" form:"down"` + Up int64 `json:"up" form:"up"` + Desc string `json:"desc" form:"desc"` + Group string `json:"group" form:"group"` + + // Delay start and periodic reset + DelayStart bool `json:"delayStart" form:"delayStart" gorm:"default:false;not null"` + AutoReset bool `json:"autoReset" form:"autoReset" gorm:"default:false;not null"` + ResetDays int `json:"resetDays" form:"resetDays" gorm:"default:0;not null"` + NextReset int64 `json:"nextReset" form:"nextReset" gorm:"default:0;not null"` + TotalUp int64 `json:"totalUp" form:"totalUp" gorm:"default:0;not null"` + TotalDown int64 `json:"totalDown" form:"totalDown" gorm:"default:0;not null"` +} + +type Stats struct { + Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"` + DateTime int64 `json:"dateTime"` + Resource string `json:"resource"` + Tag string `json:"tag"` + Direction bool `json:"direction"` + Traffic int64 `json:"traffic"` +} + +type Changes struct { + Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"` + DateTime int64 `json:"dateTime"` + Actor string `json:"actor"` + Key string `json:"key"` + Action string `json:"action"` + Obj json.RawMessage `json:"obj"` +} + +type Tokens struct { + Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Desc string `json:"desc" form:"desc"` + Token string `json:"token" form:"token"` + Expiry int64 `json:"expiry" form:"expiry"` + UserId uint `json:"userId" form:"userId"` + User *User `json:"user" gorm:"foreignKey:UserId;references:Id"` +} diff --git a/database/model/outbounds.go b/database/model/outbounds.go new file mode 100644 index 0000000..fdb93e3 --- /dev/null +++ b/database/model/outbounds.go @@ -0,0 +1,53 @@ +package model + +import "encoding/json" + +type Outbound struct { + Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Type string `json:"type" form:"type"` + Tag string `json:"tag" form:"tag" gorm:"unique"` + Options json.RawMessage `json:"-" form:"-"` +} + +func (o *Outbound) UnmarshalJSON(data []byte) error { + var err error + var raw map[string]interface{} + if err = json.Unmarshal(data, &raw); err != nil { + return err + } + + // Extract fixed fields and store the rest in Options + if val, exists := raw["id"].(float64); exists { + o.Id = uint(val) + } + delete(raw, "id") + o.Type, _ = raw["type"].(string) + delete(raw, "type") + o.Tag = raw["tag"].(string) + delete(raw, "tag") + + // Remaining fields + o.Options, err = json.MarshalIndent(raw, "", " ") + return err +} + +// MarshalJSON customizes marshalling +func (o Outbound) MarshalJSON() ([]byte, error) { + // Combine fixed fields and dynamic fields into one map + combined := make(map[string]interface{}) + combined["type"] = o.Type + combined["tag"] = o.Tag + + if o.Options != nil { + var restFields map[string]json.RawMessage + if err := json.Unmarshal(o.Options, &restFields); err != nil { + return nil, err + } + + for k, v := range restFields { + combined[k] = v + } + } + + return json.Marshal(combined) +} diff --git a/database/model/services.go b/database/model/services.go new file mode 100644 index 0000000..113c74f --- /dev/null +++ b/database/model/services.go @@ -0,0 +1,90 @@ +package model + +import ( + "encoding/json" +) + +type Service struct { + Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Type string `json:"type" form:"type"` + Tag string `json:"tag" form:"tag" gorm:"unique"` + + // Foreign key to tls table + TlsId uint `json:"tls_id" form:"tls_id"` + Tls *Tls `json:"tls" form:"tls" gorm:"foreignKey:TlsId;references:Id"` + + Options json.RawMessage `json:"-" form:"-"` +} + +func (i *Service) UnmarshalJSON(data []byte) error { + var err error + var raw map[string]interface{} + if err = json.Unmarshal(data, &raw); err != nil { + return err + } + + // Extract fixed fields and store the rest in Options + if val, exists := raw["id"].(float64); exists { + i.Id = uint(val) + } + delete(raw, "id") + i.Type, _ = raw["type"].(string) + delete(raw, "type") + i.Tag, _ = raw["tag"].(string) + delete(raw, "tag") + + // TlsId + if val, exists := raw["tls_id"].(float64); exists { + i.TlsId = uint(val) + } + delete(raw, "tls_id") + delete(raw, "tls") + + // Remaining fields + i.Options, err = json.MarshalIndent(raw, "", " ") + return err +} + +// MarshalJSON customizes marshalling +func (i Service) MarshalJSON() ([]byte, error) { + // Combine fixed fields and dynamic fields into one map + combined := make(map[string]interface{}) + combined["type"] = i.Type + combined["tag"] = i.Tag + if i.Tls != nil { + combined["tls"] = i.Tls.Server + } + + if i.Options != nil { + var restFields map[string]json.RawMessage + if err := json.Unmarshal(i.Options, &restFields); err != nil { + return nil, err + } + + for k, v := range restFields { + combined[k] = v + } + } + + return json.Marshal(combined) +} + +func (i Service) MarshalFull() (*map[string]interface{}, error) { + combined := make(map[string]interface{}) + combined["id"] = i.Id + combined["type"] = i.Type + combined["tag"] = i.Tag + combined["tls_id"] = i.TlsId + + if i.Options != nil { + var restFields map[string]interface{} + if err := json.Unmarshal(i.Options, &restFields); err != nil { + return nil, err + } + + for k, v := range restFields { + combined[k] = v + } + } + return &combined, nil +} diff --git a/docker-build-test.sh b/docker-build-test.sh new file mode 100644 index 0000000..f761a1c --- /dev/null +++ b/docker-build-test.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Test Docker multi-platform build (linux/amd64, 386, arm64, arm/v7, arm/v6) +# Requires: frontend_dist/ (run from repo root after building frontend) + +set -e +cd "$(dirname "$0")/.." + +echo "==> Preparing frontend_dist..." +if [ ! -d "frontend_dist" ] || [ -z "$(ls -A frontend_dist 2>/dev/null)" ]; then + echo "Building frontend..." + (cd frontend && npm install --prefer-offline --no-audit && npm run build) + rm -rf frontend_dist + mkdir -p frontend_dist + cp -R frontend/dist/* frontend_dist/ + echo "frontend_dist ready." +else + echo "frontend_dist exists, skipping frontend build." +fi + +PLATFORMS="linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6" +echo "==> Testing Docker build for: $PLATFORMS" +docker buildx build \ + --platform "$PLATFORMS" \ + -f Dockerfile.frontend-artifact \ + --build-arg CRONET_RELEASE=latest \ + --progress=plain \ + . 2>&1 | tee docker-build-test.log + +echo "==> Done. Check docker-build-test.log for full output." diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..04e7582 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +--- +services: + s-ui: + image: alireza7/s-ui + container_name: s-ui + hostname: "s-ui" + volumes: + - "./db:/app/db" + - "./cert:/app/cert" + tty: true + restart: unless-stopped + ports: + - "2095:2095" + - "2096:2096" + networks: + - s-ui + entrypoint: "./entrypoint.sh" + +networks: + s-ui: + driver: bridge + \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..e56494e --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +DB_PATH="${SUI_DB_FOLDER:-/app/db}/s-ui.db" +if [ -f "$DB_PATH" ]; then + ./sui migrate +fi + +exec ./sui \ No newline at end of file diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc new file mode 100644 index 0000000..9a86b04 --- /dev/null +++ b/frontend/.browserslistrc @@ -0,0 +1,4 @@ +> 1% +last 2 versions +not dead +not ie 11 diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..8f88145 --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,5 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js new file mode 100644 index 0000000..3be380c --- /dev/null +++ b/frontend/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + root: true, + env: { + node: true, + }, + extends: [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/eslint-config-typescript', + ], + rules: { + 'vue/multi-word-component-names': 'off', + }, +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..67accf1 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,22 @@ +.DS_Store +node_modules +dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..6acebdb --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,2 @@ +engine-strict=false + diff --git a/frontend/LICENSE b/frontend/LICENSE new file mode 100644 index 0000000..3877ae0 --- /dev/null +++ b/frontend/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..429d60b --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,76 @@ +# S-UI-Frontend +** A frontend for S-UI ** + +[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +> **Disclaimer:** This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment + +## [Screenshots](./screenshots.md) + +## Project setup + +``` +# yarn +yarn + +# npm +npm install + +# pnpm +pnpm install + +# bun +bun install +``` + +### Compiles and hot-reloads for development + +``` +# yarn +yarn dev + +# npm +npm run dev + +# pnpm +pnpm dev + +# bun +pnpm run dev +``` + +### Compiles and minifies for production + +``` +# yarn +yarn build + +# npm +npm run build + +# pnpm +pnpm build + +# bun +pnpm run build +``` + +### Lints and fixes files + +``` +# yarn +yarn lint + +# npm +npm run lint + +# pnpm +pnpm lint + +# bun +pnpm run lint +``` + +### Customize configuration + +See [Configuration Reference](https://vitejs.dev/config/). diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..1326d57 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,22 @@ + + + + + + + + + S-UI + + + +
+ + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..6d90599 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3824 @@ +{ + "name": "s-ui-frontend", + "version": "1.4.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "s-ui-frontend", + "version": "1.4.1", + "dependencies": { + "@mdi/font": "7.4.47", + "axios": "^1.14.0", + "chart.js": "^4.5.1", + "clipboard": "^2.0.11", + "core-js": "^3.49.0", + "moment": "^2.30.1", + "notivue": "^2.4.5", + "pinia": "^3.0.4", + "qrcode.vue": "^3.8.0", + "roboto-fontface": "^0.10.0", + "vue": "^3.5.31", + "vue-chartjs": "^5.3.3", + "vue-i18n": "^11.3.0", + "vue-router": "^5.0.4", + "vue3-persian-datetime-picker": "^1.2.2", + "vuetify": "^4.0.4", + "yaml": "^2.8.3" + }, + "devDependencies": { + "@babel/types": "^7.29.0", + "@types/node": "^25.5.0", + "@vitejs/plugin-vue": "^6.0.5", + "eslint-plugin-vue": "^10.8.0", + "material-design-icons-iconfont": "^6.7.0", + "sass": "^1.98.0", + "typescript": "^6.0.2", + "vite": "^8.0.3", + "vite-plugin-vuetify": "^2.1.3", + "vue-tsc": "^3.2.6" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@intlify/core-base": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz", + "integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==", + "license": "MIT", + "dependencies": { + "@intlify/devtools-types": "11.3.0", + "@intlify/message-compiler": "11.3.0", + "@intlify/shared": "11.3.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/devtools-types": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.0.tgz", + "integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.3.0", + "@intlify/shared": "11.3.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz", + "integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.3.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz", + "integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@mdi/font": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz", + "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==", + "license": "Apache-2.0" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue-macros/common": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz", + "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==", + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", + "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.31", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", + "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz", + "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.31", + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz", + "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz", + "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz", + "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz", + "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz", + "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/runtime-core": "3.5.31", + "@vue/shared": "3.5.31", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz", + "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "vue": "3.5.31" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", + "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "license": "MIT" + }, + "node_modules/@vuetify/loader-shared": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.1.2.tgz", + "integrity": "sha512-X+1jBLmXHkpQEnC0vyOb4rtX2QSkBiFhaFXz8yhQqN2A4vQ6k2nChxN4Ol7VAY5KoqMdFoRMnmNdp/1qYXDQig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "upath": "^2.0.1" + }, + "peerDependencies": { + "vue": "^3.0.0", + "vuetify": ">=3" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz", + "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "ast-kit": "^2.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "license": "MIT", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz", + "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.0", + "semver": "^7.6.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.0.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "license": "MIT", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jalaali-js": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz", + "integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/material-design-icons-iconfont": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz", + "integrity": "sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-jalaali": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.9.6.tgz", + "integrity": "sha512-v8wXjQplvk5ez+sUqgsWIrafwIf1BEXXvzTYwsg1wHcqh27nSgKPCJ6FnZRrCz03MoNyB9N31L0oms+vE8Rq7g==", + "license": "MIT", + "dependencies": { + "jalaali-js": "^1.1.0", + "moment": "^2.22.2", + "moment-timezone": "^0.5.21", + "rimraf": "^3.0.2" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/notivue": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/notivue/-/notivue-2.4.5.tgz", + "integrity": "sha512-7yBdaKesUZIwdcQP3nv1oWYyisI2bURkZ+D9KfLgeNqguHUzkQ1WdhGcTj59PBZa8mqa1/K5Mh8YsphSToMKcQ==", + "license": "MIT", + "peerDependencies": { + "@nuxt/kit": ">=3.5.0", + "@nuxt/schema": ">=3.5.0", + "defu": ">=6" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@nuxt/schema": { + "optional": true + }, + "defu": { + "optional": true + } + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode.vue": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-3.8.0.tgz", + "integrity": "sha512-+XKbSKvQu158zlHsJm+HObGUQM3Q+9Oq6yb2op/6lMM2gwOiiG9uIhujvrSV5UoTjrzR0BcNxCdW2kk7KE4NEg==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roboto-fontface": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz", + "integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==", + "license": "Apache-2.0" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/upath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vuetify": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.3.tgz", + "integrity": "sha512-Q4SC/4TqbNvaZIFb9YsfBqkGlYHbJJJ6uU3CnRBZqLUF3s5eCMVZAaV4GkTbehIH/bhSj42lMXztOwc71u6rVw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vuetify/loader-shared": "^2.1.2", + "debug": "^4.3.3", + "upath": "^2.0.1" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": ">=5", + "vue": "^3.0.0", + "vuetify": ">=3" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz", + "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-sfc": "3.5.31", + "@vue/runtime-dom": "3.5.31", + "@vue/server-renderer": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-chartjs": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz", + "integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-i18n": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz", + "integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.3.0", + "@intlify/devtools-types": "11.3.0", + "@intlify/shared": "11.3.0", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-i18n/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.4.tgz", + "integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.6", + "@vue-macros/common": "^3.1.1", + "@vue/devtools-api": "^8.0.6", + "ast-walker-scope": "^0.8.3", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "mlly": "^1.8.0", + "muggle-string": "^0.4.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "scule": "^1.3.0", + "tinyglobby": "^0.2.15", + "unplugin": "^3.0.0", + "unplugin-utils": "^0.3.1", + "yaml": "^2.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.17", + "pinia": "^3.0.4", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz", + "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.1" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-kit": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-shared": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", + "license": "MIT" + }, + "node_modules/vue-router/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/vue-router/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/vue-router/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/vue-tsc": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz", + "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.6" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue3-persian-datetime-picker": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vue3-persian-datetime-picker/-/vue3-persian-datetime-picker-1.2.2.tgz", + "integrity": "sha512-d7nkj5vgtUvEXZboSdRmP1uwBfXvXgXqdvsOOMQb34jiMZU/aBDrTYWTEe1N+XKF9pvTTJn8Rws9ttJmyhK/hw==", + "license": "MIT", + "dependencies": { + "moment-jalaali": "^0.9.4" + } + }, + "node_modules/vuetify": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.0.4.tgz", + "integrity": "sha512-sO2ux9RG0C1HKaP1HqDMro3+vbbmUJwzcKXuzaxQmUERAT/0FR0yfbwnj4PrMwWy1qc2WPJq01h4cr86FmNrFA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "typescript": ">=4.7", + "vite-plugin-vuetify": ">=2.1.0", + "vue": "^3.5.0", + "webpack-plugin-vuetify": ">=3.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vite-plugin-vuetify": { + "optional": true + }, + "webpack-plugin-vuetify": { + "optional": true + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..98f9306 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,42 @@ +{ + "name": "s-ui-frontend", + "version": "1.4.1", + "private": true, + "scripts": { + "dev": "vite --host", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "lint": "eslint . --fix --ignore-path .gitignore" + }, + "dependencies": { + "@mdi/font": "7.4.47", + "axios": "^1.14.0", + "chart.js": "^4.5.1", + "clipboard": "^2.0.11", + "core-js": "^3.49.0", + "moment": "^2.30.1", + "notivue": "^2.4.5", + "pinia": "^3.0.4", + "qrcode.vue": "^3.8.0", + "roboto-fontface": "^0.10.0", + "vue": "^3.5.31", + "vue-chartjs": "^5.3.3", + "vue-i18n": "^11.3.0", + "vue-router": "^5.0.4", + "vue3-persian-datetime-picker": "^1.2.2", + "vuetify": "^4.0.4", + "yaml": "^2.8.3" + }, + "devDependencies": { + "@babel/types": "^7.29.0", + "@types/node": "^25.5.0", + "@vitejs/plugin-vue": "^6.0.5", + "eslint-plugin-vue": "^10.8.0", + "material-design-icons-iconfont": "^6.7.0", + "sass": "^1.98.0", + "typescript": "^6.0.2", + "vite": "^8.0.3", + "vite-plugin-vuetify": "^2.1.3", + "vue-tsc": "^3.2.6" + } +} diff --git a/frontend/public/assets/favicon.ico b/frontend/public/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2e250185d002727a6f100d6d9a2366bc46d53473 GIT binary patch literal 1562 zcmV+#2Ictx0096204^^80096X00agA02TlM0EtjeM-2)Z3IG5A4M|8uQUCw|E&u=k zF9-$z005lj_>uqs1j?jS0rtI@n^wP&LCq;)CZtz#yR}On z2P;@Q9h?G(K(cr|-Y0;szyMg!nfGzqx3We-Qnba7NTjLbt%AtoEJe~AoN{azN1Zy^ zmN`Hd6fNm@WgV=uv$Gs0{{Z|Aa;X{Uye+PW%Hk6A#&saL1PAulFjx$3dL93fjOuF0v1*vi*MlF0bN0 zslsz}b1nHGcepJPBsSCR7>I1bWug_nX3aICAVYs}#U=L-u9w{!TEZ~-*fd;;Ra$bYNI?Gs$mz%M)s z%ZmV)sh!{qAfXrvar$QH^VZhZ$FOfb7!>4*v*3MjmqI)VT(;%Wv?0~zH6LVxbJpl9 zewfaq8V%3!LinLD;z2H#TX5V8aDY3kcmcnH7ZmcbR6#&Kcn>J66nU}V3G%i6c!R6M zB3p2!hPVZNd!=YTn~>83CI$Ix;2ZF$N=#?N@e4sCxPGhS;-8EBGf;5_ov(9!p_ft-#{x$)3cEH*R8yT&*z)K5Pvf}JKK_{=6M*b zRR~j?ZUVjblBg8t52-g(r#n?!TWfk1w+I9RlT}q!!JM2N>jEwrp*c@HpZ9K%WrPN` zt@K{vrWO}=ilcT3dD&f$Cy*(Qzyn+$=o@YptD7rvVdn(=)cu&wdTur%$O{%17h9Ew zn$H>y7kzPSD=I45+|trA9_?=*^Al^(3Ohe>CC&%NR#a338XFr&qbZO1&y^V!7Xp@} zjeb#6Q!|LtC%xft(R7}had9Dt%4aXWv9EFB&ppZyg2l5U&SBGK48MYbU*JtsZ>}R_ z`>crzp-Z@&(Eyf-d(%F2=93YMAB>7Pf>xUmu6ZEyNic4eHyob|BDsJ?4%yD)ICvcx z62i`dj0oiK$TdUL*!`N~>s*QBz(j}8C)|h&^1T}3nQq0ubO_CLE6&*)G{m>L6_>3Q zTGV?H=lp-dh`*>KPV?o>w%`ee(3heh9coRZ>s4+ zyxT##Uy_x=e&o4S_2F*Jn>!_eG&fnLBdsC+mxFkgkg<$>#4lBKGP;4t!0pyKNnp9I zw39e;);dVjW0n|d!{8mBG198meBF~SZfk2h?WUS&i2EGGY2HH%LC3<~P$)Muw4yrQ z`|0M_t2lDrP)WD9x9`9)pBlo=y6X*r&x`U1|D&Oy;bAx84$>o-S+*OY<%f-iV)e_7 z{8M6G_xt^mb#-+g>5Ai+2SBS#8d*Jgd3kidNl!NxMn%{rPeXNqcv7FwH{Q|FL5~RE z25F}7*bo0t#9b0qp?$J30nKg*cl*?i>G_fC2LzE^Ks?xLhMu_TOqN8(AiN(JMD3Us zh0| zRz)ywFYP6BrBJVP&-EsZzO;OHnP2I>-jjv7+<5e*(e6@2qh5sDe=h=tB^^g}N&o-= M07*qoM6N<$f;H>)5&!@I literal 0 HcmV?d00001 diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..2c1f2c3 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/assets/Vazirmatn-UI-NL-Regular.woff2 b/frontend/src/assets/Vazirmatn-UI-NL-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3ce577ae8add0f5496932a4953764c669a0fc679 GIT binary patch literal 21840 zcmV)5K*_&%Pew8T0RR91098-`4*&oF0NPXl095k;Appey00000000000000000000 z0000Qfm$2JavYR?24Db(BoPP-f$<=L(G>wU0we>GR11h~00bZfjCu#N91I5=)xD{Ke!?Zs#C;kAm*QV0xOo+IxAMl(Se~uE@C%iiIZ7r7~)P7chu~V-e1T<1(k)z z*>g#$)AEcxc~+h8J2e{hCQJY`Q{n?g+`FWm^aTZpnY1n@`=mE<%tnI6*|Dz7t>aBtx@bpj<7Ya0mPfdyDaZkAk|OHaNn^aOUQ-k?s?7ue|rf;!7! zU}u{S>awBZ=sSEu2B=#`6E0i^=DPpFjAyF=UM|95K2BT>_8*sx`j|w%`DWLwpa?)M z5EuP%WWrDtklcM;6QGHARlWYS>i|(487?}hj|oImu&wA)(lWBg7qpzbf{NX3H)1Wa znOKv4vmmD}^&&+wx}Y2fPzg~LRp(RE%mM9n>H+|Yn%$^E(lotE>EN<-OH!InlhgPG zFa@4(!V`Pvq$_+&RaEb;kIh zJd)O_DcK#xng;0!GsG|}CAdl?C#lG+9Tx1FvU2J>=ef)+1KfAf(;k5%35nz4E14>! z%%z&&S-s2J0@zG?WCIOyb_C1T4Gmr-HZ6d-G$V{N-V{NCg@}=eV^}{JTK)+n6h+Nm z(6DflaPjcHb?VZuOQ~+UGc5;dSw}gj-ly|0!;Lh`BowjYO{maehFq=FhJCi{mwyK> zB+Ak&TR*W(KY190vOfO>jMZAvWglR$2}t53NW!(GF*Rnm#6>d}#?lm+I`h~ZkiGU< z*Bb(Q*&c=)(In~vYSgwDdRE)0BZPycar?nOAQ{#>aGJP`tUBUIRYp)%C zX)^=OfsBc}s8Q^j)n<3B4jm;PAs|cBCvK}~(YxYki>crgZW>Q|h>n+H)3QT=jwC{Ggn&xv9=j<4t8KASIt2U;{8O&3AE3nZd6AR%Ztm8?i8{JtDHDmubpj`LGRYT^mBurM@tnp~rhmG_=_y^h#`SLXjL*`-44?hE zX4U@lzQDsS<`M-6HS$*cEnV&v8%pt2Uj4QDHvFjU2aCV%8@zD=c^0tj-5ig2`?*_- z#GTTpsUZObT;^^zT2Af<9>j*D_icyy9xqYxPhg#!9k%d9sEsFp{-YcR5)?rI;3pue z)b%$oTG5Ybtm7E>_$D|J!8K=@lqNZu$+rX-DT0L3vpCghOnZ98ZkfrVa;pnz`514{#{5n zv{6sG@zW9%eCG(DY%vB@Xct>*7#|5LZ*7~~+5V1px{E#TdUtyaAw{J;*mdSLz;(24 z8VpMg47bp7Yi+jEen-VTOkE;h#`@5a*GU) zQb|3nbTh~@Rf9&*-m`bZo80_X5sB?w|9Q7HcheWVb=rs$ z>n2_wB)T)np@@9%#!0E5ny7}FYpb)K`WveIH_CWZ&F)nf6a#WP9U}}KM{(ElJ!iu2 zK*Mnq-SYOJCdAO$9l5T#H^-+Sw0?Uz(R*JqF%bScIFemnO23Lm-7tOs*j3}s3_U)- zCg&e-lq(T#712?f^Yf9Ib)!DqH2M@64Niqw!F|i57FFZeX>|JDWAwaFD9>ZBG4HnD zs(tH{8q9ikm#g-kR7^Il79ZU@^6z!rUPebcDHc@?iGIaQLqvtx$L?_|u_A~6CB=~| zk-PewCe!XNaE)$QV~G;niY8fhh`MjnU9Lu5{DGj%Y`5=fe%r32@D;p3A-sx#EChmN zStoaeYx&`;6aW8dSh+vWG@{v3+MPLC!POMUHXVJ5Z>lqyMBDJCmbm5LcQx<6;3&B7 zq%R{mMXgbjI>W^K3bXI4EQ(@(u?ri@+%eH?E9|?1$i;XE zWDHT#*6mcqwl}kFOxdvht*P#GNfz-tGcXv1U7_B%MJoz}vMqfY$|~Mix3~#*N(Z~7 zr6_5L4exA;atFUsD5eY|1!C2-j}X2+w9X%P2w0+mD#CFSb3Qo^Vp$BuSNy3HrV7tZ zJ6kh%oa16Ibg4Zv03a8Yc*RBMR5ZXWXD^$T?3Ee2Ni#jx5god-=SRBpQ`Cak`yLZG zZ7eE2mbmvc2_xMsLl1rCAYgL|%vP)G(^@~YEBZ=|8L==Q3 z|B(rq$%t%Bp2VV&8}|rjS?d?P@CA29-5q+3R+|3K*v!&-&csosxNsgPKJ~?Bb(= zO1E4SIdLpoGNHvr8M`95k8sKoC;e|j%fd5_|NKl_o!yr2U;yOUNrLWqlC@4GW zr!z6U0e-n;q`Ws+a0fGepyyL=)$(fkRmf9$P|e$U(iRu-mb;QCrLt4@$Oj?&4@j>1 z|6b)#k(g&3)Z}x%p$?6lO$*x5gRW#Q~0ShD+Sw zHV=8uZ}xZd9YO#XsqemB^a6`z?0)#MybmrO^$#KBu!JiD5sOTeq7mH~$1?VDjducr zB_=VhCNb&Br9YLak5asCX-?}8-C`RznDNYJIh)zfX-N=5neOGr-5igHO#frCrXoHQ z6wcee2I!Y8*Lu~zfn_UCg(^|Gs#L4`HLZ0W>t5dmH=_7-r!?axH?x_ue60B`ZFL*l z-rf#(vh!W-b`N_Fpion&sj-jQMaor-Y~mp{pumD}z=tD>6MKrl1P6SGAoYo#{<&ZL zm0ynnYM?)kA?DcPj3@qt5=A_zWVf>_q?}rs>7-v>vh8i@wY82m`(dUmt9ivzy~^ z^B7^|v5ad14Tv?CnbI_-J7Zwk)qbw?Uf^#Z!`c|{Tfh4UU!oSX#HBBH#j9NXTGze7 zjc<0#+uZ(6B_RlM8M&#ca1o+))k}W^Ws^rCC6rS|E%i0kT1VaWHP{Golg%{WQmbvW z-Cl>C6qxs~y6vI=e9$rG#+};_pTGV5^FIv}NAvTENck1iP`{zM?cg{on|@lit7hDv zf09@2FmLC|w9SArQc_bgTx=Jrfg6t<;Y;$8z3yMim-_Yi(!SnZdY8#%bvayam(LY+ zMO?90vbyM=w^z~Zw?O9e48&C&GHrK=;c!@-Km7&WH)+>ANjQfOD8ntpk144nl~Md6 zInVF(RfF2k9gA5WvgAyemYg`*cs?-Ww-{ZyI%rJ_fPZ~(yXSX9a?lcbiKxK8%#^nFiP70>pTZ*8Nr5~$ zG~q<#wrCAO4Zl?t)42JO#JdADr9s&tBmV*6(4Fyzq3VvNb7u*(U^OjdQX8Y`TAIj& zfO%Lf^R*rGaP=%Mx{n^kCRD9P!~+uXz8mSXT``>|G{ec}St^Tq*%7PA1CwT!w9rjj zK0EanzBllh_|2iwj@etqN*|Vpx&Zw%I{JxIUf)`!0hn%SP1l+0S1_J0-6-yEbmO8! z28`GJVJkuLi!GjJi&j|Wdy2Q1tBzjm?gHXu&Uz`((+1JIrlV2UQ-ZV94C#CnT0TTi zh=tY+xVGUUiTd*C@gu1J=7*_GeeOd`w7gm^Aj!V0^3{Uo8SxYCWA&!n z8~JUkFZz{(cJvGnRR)~PSwfSkWU5)R&MlV2Q`scgR{RreiByTO8++VQxS7gdjBAuK zHn%Bdm|Z0_Wf46}4ld5jXLP#ee=Ao9P%mz*xX&_o2fm@D!SXyx^H$)T>ZIqnjYm(#NxCj3v zs~?{)oC6Lo-L&F(D69?SaaDppeG4jCmj*iHml*VPkY=vLyV-F&Fr(6Kc!U>@l9e-& zITU6Ndxo(;8dl3d>6AFKY-ofAkzT)(id%rSN+_~U1d^@Dk%5FDb|~e#tDG(+69XBp zc>4L)xs5`9u8N0KC9+7G{OQOp5yn3u*o3?Y(EP|gr#=EDa z+V^Y@G$%x(-$~A9kEJ#F63(;rP1>#}xYspot@ATCPZ>o^a(&dPx zTEE2UKnZj4cXrsLlhQ`^1`#8%L~6DeS(gj)ip<;Jx4%FJ-0!{;{-p22-@G>6xGvpC zJmHGK%wTvH8O4jy8L9-Re-zI!Wbanb$erMW^Aprp@op*p3%t_&tuTcL2q1$B23SY{ z59ts?f)Z4r0j=mlKZY@h9azE|4qzRR<5^JL^J^Uzi_)(IXZz|QE#eC$<~y_8bMU5< z>!cep7FE(#4iecXWtSOgk*dnF-Z>U+u?XASLI+QAbLvP-W_L)&zczd(m8IrVTi4Xg zeQ(4%jWGPe1~R}u3(`*5}M*#Ur%;%HbltWw#S#F^^ z_dKL)7k=LpkBaJaF|6sqFD+Yf zo8A?t?Cd;OABHFI<-Jw!zCifYWXAmVX%I^v|4_@VUN>jnE{k?sC3DzOr;M96Z`rDK zn?Bib;KYTm{_sza`VCQ{tcq%y$t(w-O(#pvPgtV`bcvFp#kN>t%j)5aZ?AUUF>yMD zz~m~W8yR<>pQUudUMcccu3CK;`PO! z$`{`P$9r@qw$nd;e*Rvs~ptSNT&3I-Shw4;MQ z=#52eT4}8I>Cn?t+)g90x#4${#!8V`Mz0I&M}d9dqTz-bu1ryi(3|B}ME}t(JNTBm zEO?TJnZ^ZWUG%|hg}UW7B3x|@9xBb9n){v=mdt28qE>u0mq6^V8RK4 zH0uD_Z(W}uBjX#YC@s-wfURw$XUWrE0aU^c4+Nx;jk@X0EG&+kq=dg}3#}n)-U2#Y zn))G6hcq3gv%68LX{ao{eQzM^{139sX;|9WxRUV+h%H-b?-H9ZX~wJti$2@3YtNx0 zr_NmZ3Hb0|z1Dw9S_xWBE00^PH=6NGqhvcdk~}IPHhGoJ=vhy}Qz#6MM6szbrf0Ya zmogMbvJ?X8imett@L`87;N8DDn1T|el~rCXGZ}Wcxei}X7==8fOuxqDs)JO)nzPxq znROmvw=okp{rJv<^dl?yA&`ret5mDr@R7OCUb|T)Z*exr5EE`+oB~>mEtynY0xyFa zK?#V7>g1lA3N%RurCAd``lKRfK9+y{Yci@fUG_RK#GImBCY7B#{--sMyo^56NnlKl z9R}qq<}6<|7w2_RUObk^@veP=WC`qb>>r!UR9MoY5A91ySw%O!3^2@?`h#rLK#)z^ zKFIEd+>N*!Yt%%Lt(s!nw}5RRQ*UqxlWiJE(f zK)ri2_EN4=wOWCKg$fs$^|f%2W_znprAB}tA;Lt!rO%Kt6Bf)_v1ZQ>l0(#7EVr_5 zwlLe}k^ABZT_GTBeg*@$zMZ;#?r-SzxUK`CS8A$F0i6c)pr8NOb^dR|5U=xNFmb~% zesI!#=)@HPFr|?)ZnCeO_`H5OKS))b_0(H`ql~p0$G8gZmWBR5Vjjsb%)>c6b9Wxh zqj@v$$9|l~eZ0nJ{3m3>CvFlZc~U2R@}_XAre+$aZMvra`}g0AEzZX|n%~JLZ(c*c z1b4R4;(fu!(EcJJl6bPIq>nLX*v$&-Jmd*4c*pPmK40H{{{Ktb-96kd`p2*Tf7bw= zfPxb+EnXvsqyyJY#nJo(D2GM|PZ*yBa)mP1@fi>pS2pM^UM3sANrip^?l@QkF{D#Q zGo$1Y&#}x=Zt$4ryygF&{$JLY*G0XxNBU*|@x`e9@y9w)Th0AIk)<|yNeL|$WnQKQ zEB(?b?Z5MXHw*mTLE!J!vMuW+BA^2W%>txUxYlJ4D5V=)Y)&) ze!Rm;Hjash=W=D+kA;umEos{EPY!sCsJO$`XV`dJE7n2|6H zA8Qh^Js7ccB-N=^2S$qDb4NQH)cZ-x7k)$=V2~F{h#8dsJ@weK;C9wSt&1qi8AR~h zc2C7!?s&+7!Ux(k0x;r@$hjxRi_>u3_{||&)Rf+C2B=^8_(tZq?ku8CV@p=6Rhz2S z4XS#c14+|i6m=PF+C+8)D$0VU6t^f>StQ;+hFSM(N;bOGXygr|)C_(xP{7uD^k1g4@e~jVxy;&{(^YP>~or%+MG<4ZV{bUfE zJbvUirPrml&>MfF3*R2GgnZofK!q`oqv0m^@c?oKITBdMfsc6)Ecsu(p2!cYE}jSN zE7l(lv^}qpx7;qz!>)=3WQ4m1%7>~!i0F>en^3|SfJ(i7XR(d=fS<=km;eH$FffEE z%#;DM`=DC;+XaNPXrEXdCk#sg8XmL}3egjOCQ4vw5gbS0Br1yy7=Xonl1zk%)^PU# zIh7Jvs@_vWvN|;5Dt(`%fl==8+eCRJM#B*j4YRnF$xvlvvqW>bC7n*k?SdTK8o&~~ zH<%|UuRnakhb+guWt9g-0>??vaatDN4r28W~6($Yc>n#Cnz^IDJe3r!bzHTdiK>Y)q^EKzC#k%}&4x<_fdQgTNl{()rz03IHsohhJjEN)Pb^_&EH z^Mt+hc$Z_eQ7gy`L$sJqzKX7XmziG5T94rTSJPLsb;md5`GE57D7{Y#2?6qX37U%C zt`6sE&khakT!CwiotZtHeMb#0u~74%iS3y)$QurWs+xJZo?H*G(o#5js7&KJvOSOV zn%t}O7urtLR}1O@l&~($u-ad+qPhRsw*r~(w?UT5bY3`N+*~-ixzNdn!+PNOP9(rM zPt$^2adP!MB}6n(sp^K2Rw4t;BX!0f-7Zft2#7qmj0d}ATL;o@=iVj8jcEZDXIL8yQBer>c`p-$5mt#~j8hZwkbfCh z|MKI@qRpJ^IN9M!j~g8F0osur zJWNp8r}1S}T&=i|@Ff}kaN|rN$OI4;L!3bcdJd6kU}Y%M_Hq|4WpUP4Ca7mW=adPY zv8o9s?^&D)-oi9wL=U@Y9Xuo%cMQ@`WIZhdVX;2SZ|PPJv1-P2*1R@EWw}{b-cVQB z@YzYDU}c&1;lM=eL@|bzHu6eWmPCg7!+T8}6Yd z9_s%Lc6u+_9l-`2*s00a&3&az&(5+KKo{zVcG!|lWN?;&O|?&tb&4sl`)3J6Y9^0G zgEG%_s3kV0n&O#dI8;kJ-paKXZowoENtL>6VOyqwti3)3M*?H+dginQSYIwGu=r<& zqdOQ^jsV1vVdz7z@(KkGWIj)d-FC57R~|BT&aaCbUKy!QP@9WNU?~|U0LkCQQ{MX) z_aw1*rpR>GoHR}Bc%eNC)tgzxAN7j@3%!VrDZH#{(FC>Fgey~d`zKGOP8^!V4womN zD0_{qHbfo_dMuU_@Mz+Mhx`qJ@73$K1A3@4$y(b`477c}?w~-ohFV4!lBa3uefbhgu;MLbsX4K z9p~(J=SaT&d>HPocp%gJWA|-|xZ}xng(+SEyY2MmJ~~`QyD@T_8k>5`$Jw<$CU!o3 zM(7SVl_x2}sB4|cU(r-g_S!_>pk56o)z2hXr$lVgEDL1|!fL5k?dH?6z&9BF&0~IZ zi-_slEbKEjZ8X^yi1y^jr_IcmKWbEafm3fFRQPyOmmUjYfQz%-)Sx03AwCL9a6h9y z0*c`>KaVD-bxBnAR5YcLa#m?aMtJAslFSf}Zp5Zh-%Y(S@*nZz3okW=+(=|lK#Y7| zj38UH$T6|@-~I5$LD`e$0lkR<>b{-Ua`h=Ayc&I6_@&3OQqM1tn{>v*X>bEeF`v3$ z-Sm95+UY}E1Sq+{CB~^!9SnCX#(kVe{n^o{qJ985@`QIInyqJNiyzC^WXjs-Y%jEf zeEa@q^&vYBsC{tLedRCoOnzET*+g&2#q8$CI``=yMGv3x!;cN#xsi>xH~Rb0kz0Pd z!cM5wxKVBX)2_(sR<~WNXxo{4IUd*{X!Xu**<@f*C|ng)79_s|HYg}XhDzL8Z%}$| zXF{^VyjkuRM@ThG+V4r}7nAzFIT@fckVOm^7%`LxOOn6oas-fKDQe+)7 z^jd}v(U8sw?Q+mb&>}Oe=y%d#CUb6OO5EKUcRoV&)wGps;#CyCo?f(M%8kmZmTix^ zA8TonH!qb!A{jQbcfPVC6y(+Viu5PbCF=+N3MS(^QY{m;zhy&>AVP_2&tx$OR6>j< zV{HCQ{8MZ~ia-L<`X}e7G>pEn8*BivhFO7fU(Oek4~W@ljz$T!2L@;$UG$ zO0ybU-l6|aE;+)eT+Jy|Gj&x}*K@17o@4d(TrIV~E>@qh{1>5EcX$_mB$;GufJ+Z+ zPFx)^!U-A)R{T#WH#RqJmAz|!VUK4Cez*H=)$Ll0&;1KH-F5HPF? z`w+~@#AkSu7?)CVxdqZ(jFn#~4d&~88;H5QE<&F=j7kWg(v~tSwT_3L+0@A#hq}e5 zJ*|W%=yP)mn3vUf$Cr@utaF1dqo-i%qSWlRjefskQ_2og<1g8xz64`CsTc%OjqeZc z+iJ+J(q|sE4BVz&dS8awf0fnQeju-?Fwhy_t`jpMON^jVi_gKSa!n&%s4OJ)UXADO zikmaYlQk6|iiJ%#&1aQ|+^Ji~;u$0^r9zRqC3hzn$=tm8H1}7a&$ZHd;a|?xdY{bu zbO=*2O(2t_2hKeJ8h5nQcIw6t*#||joK56Rx!!}A>hpeu2H^mK4-|#U z-zMGWD3`GuNF*JMssY>RM<)o2Jm|2^0r*Xpk++dyxrt9Q=b!1e3FscloBUtU!!BYTr}p~E4nYNa!? z)pdYuAn;@yVrFc&&yTS$&GB8mRRaqO+O;cS!U-!};STQM$pG~i)5^#gf`aN3bTzDx zx&!4MlnzM42g2h9;_#os!E(hILK!m(R$xH^5tWG{+5Uch!T!v^*wE(c>*-8@kezD7 z*?Q!*;ehJezMIj>Xb#)LBFTXGeG#$5$`&!f9ov+cr2PnR?Z|&Wft5H4G19hsK*vx+ z(I`kK40|)0i{`K`Ew}*+yd+z)OLA#*Mt*3hT?ECQ2uLi3m$n&Q2wx?>^AY@J&I23U z?_{U>>mA#gu3SHd?lHltdrrc&>+Ef>e_kdaMrX*b6OCQJibe?&Vm9nWJj(yI$%GcqIOZi!%thQO%EL5TLL z&Y@%gGuvxQ_o?~aYPHY4%6R#Vtic7AAZ);$>`m`{DYn70%h}=1-gFj|CGc)fL(U;n ze2e2E<7vJr+C5n0=Wfg{R3JYTMK(+{W9Z$PFE>R9j|xp@9ZA+ib1Kojh{TTW>}1CT z71$nhZOmdBI)tPq07Jvax3$efh?s;(DT#qaAv|h)ZWXH=6?|$=h#Vr46FeNuQhw!o zkKXPE_9;M#HxK_21z_k27T`wvXa4@}>w^3ZbhO+P01sFTC6{Giw=f`=?x_ z^=LqmBg`+^|K7wv=$nv&J;g=-YhgD8_Zc8)wdM@>=(R^Bj1f@xx6SAw`J*OHB4b&d zDZrZejDE+TQI)an4#0Uco*@^H%t_6EoCZnbfRYUV3#66u%H3D*%8y8tN3xtA+h>jj zm?t8SC5q`QvdpnLNK9Q$D-L*LM%x+ad&n#%HwB-0k4p0NA-+o4gt&Zq%Va5$uHb~k8>=1L9&}Km=}Tj#`OaC z%`)rLB0jNc4Fz9}_fJ^B@+;!<3qYvMc@&G;8E{+vWzlkS%8ndx-@GmfgnaT3d^OrP z4q%=<YQ$a#Qa+hAs|aX*(n@?I)WJJGteS7ToiiH6_I zOv_z>GRJyZIjLvpuR>0{L_m*8Elktw(`49A0(}_=;sEzNXms~FGAdkGN|(;(SJ>?@ zFaPOxcc!HL!hdR(_3hie|1%I^fV8$k4ujq$i&4e+BpSi>C6T(378x%7A+b4L5;G^| zLdSdywyTA1UZhS0ymL2>972_vQ;-a0HlXsP1WlyEfp-nrAKUF8wNnzH4 zwlKRj#-C01t}qN1J9l~5QOH!ldcn_Zt{0y@l)siTaAkGp(uJK3((<#HjIm^1DvyvD zD{Q6vq_DGd_)Orsyo*{><}>=rN%tabd!j%Wk?j$R?v1Ia42wu^=Om6{&anA$F(ztr z!ad1kYNUA%h9>X>%YPfdlc)1ZaBW@yJl1uxx2UtaLGlI7#lpxK3WpaG!bu4AG9_QTu0kPRC$e_@4MTjiK`O!BSV-Wro0zF+Q26<_F+YhR zUUy0B_@zlr7pM;6+{x^-+<=7J`|qb8Fl;o1DaWWe*c8}Xt5Xsabe$>jA>s&=KS}T7 z9Gz(tVVKLU2#rB|`%_4gVG>6JXu6e}E~LKJq<3Y8pwdt%w%}sF;1L2X{6T#Z_8+Jwqo0s=^xdpBl>gEjq9LmUCYM%eNOlA0wJC2FTKeyLc%) zfH!D1*kX}*?9ykgg4uusCZWQ%RlC$yTG-Lo#|)`0eetks^{a?y0@F2@(?l=0^0Z16ILMKh z1{Y|%+KTyNIDIdN(_`o0<9?qSaA>%3Fq@&qX`2$tW)!g8XL z<)dED(fK~n+-GG$gM%UmY?*>4QiBWPNya^zp^HvBO#Q*&64JrGEP!Rq>TADKKYr8@ zGR|#aqNwhS-83E#;#G_Di*Sl;u)Keg`uz8|h_3o`b^T*uJoQztg#c-%~k7su{;T0@2ttM z=+{gE7O68?!2_wbCU){T4*l+ib5Y<6QShn=I+`vL)9GSye3-R)Eg_5|((^ghl zd1xfRU68QD>1KYPE0oVY2yY3imTJgjp|G(+0dpv}sMUlJcl@1myv~M7uw!>LKiEGs z94^8J@-aUBYh;`nw?MwuH;B^0@b|7v5mgc?!PP{5mUndkGpLCWEQtj+DNr(^USq_H zZb~;hW;w6n5TlEi{Ga=XC@(KI2J4<2#heMV%IXYp^kK8Tfl!v;YC??TO1`u)kAAeT zMzkLs4;66tTQ}uFe{!%1zbA`$3kUJh!1LK18Th(ua}f<1quvS2mItvNYIjTkLPi8d*O>mY?Gv`@e54P)JUy+*&qQMDzZ~I>^oe9rEq6?TYjln{cpcav*Ji5BC3> z0!{hIn@X0Z{cz)vt7g%fqB8L%wOLEPA0AVPTQgW!2z{NO%?d!fCc#4e^gH64SKJmG z9qse=o5kVCw;|Z2sxqOp{z|Zp8EDFPxvpeHg%?ki>IRyb^(Dn?;&Bj8+TmlI;o;!~ zaL3Nph}qG1>BfP{h@hv zmp^pw^G>JoJQTCeI zMY%Lu3~Fyt0$a+qLR*1Hf7M>C2t-^+Z4wJXbR)*#?UY;Fwseg*^!0j~=4T|@=@@X( zsmzr89EK>{*@TyqW>X@UVN7Q@C%dS7SPIlF!z>N3w*DDrn$%gKrLX zWA6Btk_fAWkrB&;ThB0JtTTn|>`Z>;C>aWc%liW#a8oE765r(^ln=`lg`=>nGOqo^ z1lN{7QY?y|o2d{gR1D3@q&pDVk6{FDVLF889W=$6E|LQ{)|5tBbH>MQIBYnuv0h%T zuCBgGDpgidRx#d9>gpS;qP$62(|!9O{>-zdUbqMM?%xl&6Bd3o7_)Tx6(6s7UlqRM z_&<4hV{}O~uX+1ptLIg@amb~C^b2*Z^x4z-`wx1M9@__jih}>ES1(^Zdp3Eo9~y_c zE|a;>*4H;~EreeL8s3-(l1P+ZmWCyDbeul3LR=szOx;#F`e`5*XnqvMXl!IeMFG4y zqkgpvMEW*P-^~*PxUp(zLaL`U&@Kp_Yv>=`0Gl;h*zDX~^okxDW-z8^kQ1fXnCmbB-Nmg$ns{R?rN7 zOT)RkEH}GL_WIL}pDd2+Szmp+_+njK+vKQr(6n|0NF0U56oiD=_s#BvF7}V!J&@V; z-~!<1c(m`>vZIkw6OyhH>KBir<5Di#c;jQ~vn!nO9XlQmGr_|wWVtq1 zBuY67l~&QD4xSz(F_B0wN_H?&?CsYZcR*Xo`e&d+FhUhQaYI6`&-ywmP>S9CN!vz#(W`=rML7aF8kfcU=4#H-1K_SZiUPE>mXqcwTG16hQC zSh5428ikE0COOXI9(TRK-5enArOl zhE3HZIrIkm7BP#-C}u=RxK;4n9eS^+XA_J@)CmY6>X^`)Jb|I?-el9C2bq;oQa|)% z7T|Y~)#5Z5$b#3BdYi*k)UIW{ZZ#F3@1U>M{Wg?svxn7gYk{gCB31ja9jaEUs#R)@ zs)^MdNYtI`#XD%R`tt~-smtYOGWR9p7ZTm^5$OUtI9pUsRRsIxyJBs(+v7wW&q%o! zm-j(XW^~Y$PDpV3edl1IV24)-1WN(^sPy2JZAn@CYCg6E@(YD30{tv1ufrrg5@HNp z_D`P~yzQ;CRC7g?BXm9*D=976BKu1fS7PX8qEgCQK=f}b6u9#L-q)H6fL;H!2+57< zfT3m}$C8p5#Veyl>c*%0`@$is+|9i3mLpL}YHIp|o_9BZ zkVJ9s#A07L`{GGn>AX?84|uX?q+?uOGM0G8^m=BRkvka*te;6|FmlV)X%f=ug@&&) z){t#eQ#WvtDd~w252Ft>=ku$#tZd=XR;zoaViWYE3m0R(zJahMQa?bMXWxLqcRcrO zC$Ne!7po0eE%A}PaUuRRfbS;$qb1X2w?kxVgSlOyWblyCT)-vp8|DhGQFCrX_rtp! zrT!EjU4#U`Vs%Ul_-;l*>~))ZK9SuQ#ZUNm*_c2Vi|8|mUzbwQziGOEH0oR%`OLy- zGMsB0Pu&8`wTWkRsw`W0rm8BK%1mj?rZ7|HvZ>3Iyll#{<){2rXL#bO=kwR8!z}yK zT-E>_wb0-!#7eNm*apb42Alj?TaFDjZ~yP9 zW>oH&RZNknBry&LURjQsznLw>~)Tlp^#&QthZ2J{ddy zwDN_`9t&FD97&F+g!^V;$gx04IyG;-nbvNX>!3Q2anRt1u|gCbictok{@p=0<=Mdq zw|z68-D*_Oj}y_AVLW50w0);Lxw%#M#2NH{t17A%xLdVs%ODP#=8F}ZuvU#%(z2rl zv|sq4uER`X{r0tGw3yGGuWHIuHVX&V&-u8d*uU?M9B3Qp({Iba+vPdD zc3_bTJ_WTUct8kYoo7(&_6j_--h&~_{z3g7pu1oS{r>OtaZ?R z+pu51Na}n8ec8^!6AlMx7gMXegH5&(n%&cE4E>U*a!cQ=acOm#3f)cLjc3X){&|`) z8eL7f&wY;Tm;(`Hr^TrP8=|FL)DU)yC**tayhy+8lt|Kcdk(t#N&=H2Q=_Qw%Uwg% z=Y|+jU;)|D$q{m7^6f*yXhE@eqb7rMHOyoZ({v%w2}-i-)vA;HcqKbz=QFaHy`uY7 z!|3E78Ul*j>{qHMpL7L!AK58}GiUX+pQ;}_U1T-$`h0lGb|j~q;|{{j1fKsYsbWr94yFUVO3tit7x}VR?P&6 zkS2pcRAIE~}^{2+29FG^Bs37N-* z+3c{?v;g`tR!Q{c^uzOquCCrZ9u3rGyXKd2Vmb@pkY!$dID!VqlysZ;7>LD+A;Swm zP;Iq3J5@q9gUtMj+_J4IN&GW8VhK9m{y4o!gE8s-y=!N6h-Avw@EN1j9We>9L~1c3 z8x0{TiS&@t;O5xU3VKrRbXLM@?$q?2Q?9O&WK!n9k-sAG#NNJ)^ah|WV}d#QDfP`- zF+(W_BY(k07LQnT=%?_6L{rF⪼DyE~g66^)LHHBIQ#^Taal@zz4geBRqgS_@t9v_z^sb*q zVcp6T=3mons0Do7mH`?SZCbXl_9#Tdq+(n)p;9 zhOZN1`6I445oqLmo~ti>mtRAv9MO}+$ZGwLUmB;uiMy*yCKQ+ijne-pbY_0mfcK&s z1DkXpklFvRpbr&dfP^5|s~E~Ke}0+m)1Zp!+Wxz}5b*LJqNb1_BZos%zx7v^m&)XQ z3HtKa^D%1*MK-0Rcl@v3@TUR%-PLrj>w-+rO4(#@v~@)ImS_6oXuu3aBb&jv5{ZU> zxq@&dxn+02WIYUeS@oB*k3jytP{?v_7;z|apj&H&Y#AhUYFj)O*>H%9$}|#)PsnO1 z?-XWXkbOl#I-=yhR|ihZQc0e4hM&!95~G_rA!3|F$Ta5{qC^O_?1TBa$mIFnI$;ve zn(I0_*0gWy6{W3;j!!Y93y!;5EGP>R;^Le zPZEfAm&t8Iu*%HyFJoBm(4 z58$0AK~%n&2sbG;qk)+vta@UDeU!v{v`N5o9(XZLD}lyc@(|xN|5sw2vr)z(2EPcs z<~B9@m&)j^Qp9V@DA5gykanGMSiBNb-+x#7J@4?5NT8`N8|S*q%!iHLTGrg~qd;yk z-pe6^+w=FGQ)E{m4&|gbZm<+XA6F9TzIAUkP|Z9KgaVpct)td=?BWpKPuy3+4Ox4G zYh2e$M0|-5!H)}({9PALL@1>O*CcKUB7Q=M;cvKNMQCFjl!A>H3C1DkqZZqlPE$Mz zAE72F7&vo#GTm?uIv{iGb~GOORVF!#kYX{2J!&%GyK{J&ZLK1;Tp((^9g&<`-5lRU zQD0<_Mq>hm&W_~!!G?jn4l}NmkJOU!86{gfw4~o#YY;m|L^q#PAeu`0Spu>(HEg=pn9 z+|1YqIOEw{JRy^Qewezg%~I|STsM45Bux56Izu~2ZOGzDAXY>#W=fRwrSbQUdD;W9 zt6QrF;sBf&Gb?n&8)J9B7kFc~DQ>gMfSG10Xb>Y!=>7`>=RHA-4qTEgkmzl)IX>&&R$7zcS z_Jv+g2>XJhC|un>YU@zwVfb*l-ir*bQD%``+O9Xd163zRjlId222cl>S;!SKM~Blt)j$4B!qd~8$P;EL5}pOCbt%aB=* z4`IqVv=#9GV^JdXT(>EJ0T}x)_UG38z5mPcmxI;6f6D)F?w|iUy07Hz2RxT-v;HyQ z_7CA-?6ZGAPUCkZ^rV*m?328_f50!B8T`GkkJ>R{zs8R?(LZ-%ThDp|HovOctDO@4 z95#?+)j|S>`>l&u2IhNylvp2vqY{+mk@Z1GW=~H5MSE?&9Y8#ni)SETB<}Da+3$v9 znU)(~3z9MpSD|Jl01Go!}HuD|yLa&#t07GnJc&HyGNg zvxP>kxBPg)gbS^*(7D5^)>?v=^W3=i9Zp zo1`ll)>rTUvH{psp=n4j!jy9n6o@R6CZB6^^{pm|-DhBI7i&gXMOe%vl$!~T$Rh5@ zU`(ilEulqg(bAY4iD$#jzD&I2vbM=ebFw|n-t=|1ix@Yl5(<)VlfQ{_+L|?pgh$x$ zUv@p&IK(^^k41t;Z2ZaG_P+GdPYrF`m9Et^HkH)+m_3}BoQG4ig(P|}Oy({=j9q=T z52f*u#wR`nCB(rlaC9QX_D)PYL>cv2PsnvahM)Dczxd;A2*GwjY2K?`C1PisKTe`D z#q%>RCDfwu5{Xc8>5ou+!Z(Srq8GYkgR`o&Zo7Z%nU0;`1|3IYbKv_XI-{B!^Ac&EN4WQ z0Pv+VJYoUNH!kAvs1hHFw4QHNh-3jPU`|zSKldl_;Dij5eYd z@x9~Lw?3k|RK##22&94HOar{wV}n@R3v3=0$ya>IXH9{4g6NYwh_!uTK$VtPD@ysc zmem4IR(h@Xk_9Kj9d^Q zdN$AXQMNv#G3T7s#81eNiRB}dq!R=ak^PxY&IT8s1pguFM}mE4K0(iI9I<>d0|XJ& z5UjWLOXdrf)V|%ILbF1g{OmT3xM>`Y>GR3)+bm%rF%Cd3?ypiJ>^*BFcsz3i&lH!4 zWNAr9%|04E7f|O%=(~(58(xXeHkoe2hc!f5byywwVq_Up$nrS$kFXxYwo(yM~6pu65XuG9f0 zXvayC)nVZP!l4X25Ah}v37$? zhUYk*`k*#580N*UAz_ncYvZ+gHo6|k742`Z)I<_c8$D`07h4<|X*8897D-2S0si3+ z7-werGiImLso6Wl(P1DK0y!OXwRJ?vD&FjE>RSSxWaJp0j`o&kP@CZkb}4UoD|eRskzE;pdqi^gWvGNYW?keRiA&&@c(Q+txvcV zLGnS1<}FwORY?p~hn!Me2E=EAid>ch49Q4_1gXe1>GCQYFO>AzCAn$NO+iJM4`8zh z%R1+{5Q@J~jYs z{JEtf7c=K)YEngUAuhkv1?j)?JIB6*4r~9$!WC-9aXxVJS~~$1`N#C$O%ys}-4t(# zqIefA-N7%^1#r}}mY5_RTB@P%;OBKdyrOxvRi8E014L5^v8q1J^+Cmvqsruw;KL>x z2n{sM9Z-!fBB&A+PhMb-4)Y0ncHGtkGfR0^L_^pOxRo$2+p(5{pz~CglA10a5SS9l zB2L=Y#p$OkH5hCi+Ri8+9yL^-EjY-B9^`UcgD%WvSoPE6%XEmHF1M^flN^s43n8|2 zc$yU(7*k)GHc{5a8~14k<4Y7uu=~1dPE42)39488iKr?x#U*J4-zVQuuOyLZscy3x zD$T4&4X9aQr_rLt3k3`O0&U7#lQx0`VpyG87yYOkD1vI5A*KDme4D*9Jw(@}&6hdL zE5v4zc-Ro;6vSmSVs||5(|?WngQ!-37~!DN85o%`#WXX_GRHg%EV7Pn<4j=TP7Ugz zd8O`ErW{@R{h+V0tekoUB~}w7#$ZgCGGoqyB`a{&Y}m464*?0qfg>kq7+B75T)1+B z=gxyCFW!9kBJg|WV9S3vNXRGx1ql{{iY8PTx^N6k5h6v2#=;gOR-AYV5+&i_O2(6d zPasvAbV3<2Wf2j}mLpf5d=dqu3Kc0 z(rv3Az54XqX278Bh721qYRtF^lcr3YF>B5a^LE;0!J;L*EnBh2sx>l0lgB>b;dBiv ztpHkwZo`;f2!TXdNibQQodi=PSxVR(l}2YUS!@oMXEVVJMOG8cRAx89ELD~h%vNVR zVW`n$wpeWj6uugtF0O9w9?(k?X}QvK=gG*-o0Ts+e~!|@fY;?}w{P8Ee=r=4C)1e~ zng5&GAC9N<<$Ak6p07|i5{<%LyVLFU2SbDjWn4&gFw%x; z*^cY^!Qs*I$?4ho#pTsC075W=VmLukG{bVdAWE{LYPw-sw&Qw!5Jqv5W_eLob<=kJ zFi!KbZu@aw_v86`10Vz=D25XxMKdhN3!)?|s-_#JWjn6t2VoQ^X_gmdRX1(d592g1 z>$V@~<$AjV3J97@cY6%eYx!)yL(u}ENGy@c!lALYnag@-%wwsk38M zQJ;;W_yOeD?5gS(Cf1UZc^k&-J(C~Am0Fsg{}g)d>ie0u1y&dKmNCYcJt#6*q?Yf! z)+|l(A~iryZUaszbDltafQ|OV%J7O5dGf7BMt#)7FncJ?_^k3Q{m{gwhId>4co>*! zes#WPprv9>3sG4*Xdz6f?JzDRD7SR*v=AoLb{H2?ZMS79GEEie$Vmc&u~slvC=|`aAJ~#CbiRc(qYMX@)1sgiaTnsH+RszJ+xX` zojUbp&ok8`JTPio&FkzelzVGy=uxLjrBbO(9lDfFnsjVf_&Dlr&-6v`SaPNgmJ~bf zVT_P1y4R+!a^g1T(4%aZN@c-HE3LGKC~s4iB1I0FGG$6gNJvnnAE#C-&Fk!(d7M#o zDNF&&tdtx-oQ~rCWq;^&T6=1%^0Yvnm}&)$^Q51Fx_6q^$KWdGZUft4!-*J64GC?A z69$eI2?h6ly?ze6D}IHpaLDK<$1a__Ot)nSl!zTJ2eD4oto%$ zIWvnClhCnD$pEU{=hxr&9Oonb?3I!hN`w=WU*vBbR`-6qql~dsr^{JPr@rZ`OUEBR z>9Az{!e5_F2X#|5@1S$+GL@Y}m$JA@=XaVjB`72)ck7Sx+vVBPZ@ORo(Ww*5cEa8o z>T4uZ=I8QEf8E+Nd-mjWZyd&jgi>)rLa8w0LV|KjhffP(LfvOQM(k$Q8^NT$q<+wND^rnz%p_HFT{ zbTn;vf6ufKCbq-4kf7YsVbemGP}^Z#NKkI+@EPl^t9Fe%r)SdZ*5XWqA=HjDjt1>& z+HgzCa75%-N({J!wwRoY5vy4UA%qY@2qA=!BPT7Tlu}A5rIbeQ(aZf|dIYilczJ#pg1hK7dz{(c~P!h{Jxl@P^1kvVhbgoK35pFbZ+ z0+nTFX9F2PP+nf1n3&k!-ad2YOb7c@20$xSOM?7@865WiZ)o`Mu>bsl1qc4`IRD^( z!G-(Zf8Kw;pyB@i|3LBQ_ZR%HEPbE|)WBKb5n0T@z;^_M8K-LVNi#4oF?zZU}_Jc)%}&cd}l?ADDl zon38a=jZOYJhl3f@$HlC-qxo*YY+6!jD8nvo_gf$oY$O}qqKU>e!QP(E*|^%!R%Q% zGLNr(^+J10rs;~J~-QK`Mv2!!vx*i-tq}@IgOXs=6+9O{khm-96)EA9`|i&`-w5>tDX#4!C!^=Xl%3KA!ez zGaTak&0P-qtNLyEZS3)|{7`q7MV-}(7J)c#H3dSRxL^7e5`$@JLJlgu|( na^zY+iqMEOuHW_4?;Gp81CnArajL)s!oc9^>gTe~DWM4f= + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/Addr.vue b/frontend/src/components/Addr.vue new file mode 100644 index 0000000..cb50323 --- /dev/null +++ b/frontend/src/components/Addr.vue @@ -0,0 +1,73 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/DateTime.vue b/frontend/src/components/DateTime.vue new file mode 100644 index 0000000..deb4c61 --- /dev/null +++ b/frontend/src/components/DateTime.vue @@ -0,0 +1,139 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Dial.vue b/frontend/src/components/Dial.vue new file mode 100644 index 0000000..b97be8c --- /dev/null +++ b/frontend/src/components/Dial.vue @@ -0,0 +1,238 @@ + + + diff --git a/frontend/src/components/DnsRule.vue b/frontend/src/components/DnsRule.vue new file mode 100644 index 0000000..08ffd59 --- /dev/null +++ b/frontend/src/components/DnsRule.vue @@ -0,0 +1,359 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Editor.vue b/frontend/src/components/Editor.vue new file mode 100644 index 0000000..e6692b2 --- /dev/null +++ b/frontend/src/components/Editor.vue @@ -0,0 +1,133 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/ExpTextarea.vue b/frontend/src/components/ExpTextarea.vue new file mode 100644 index 0000000..f34894e --- /dev/null +++ b/frontend/src/components/ExpTextarea.vue @@ -0,0 +1,76 @@ + + + diff --git a/frontend/src/components/Headers.vue b/frontend/src/components/Headers.vue new file mode 100644 index 0000000..01bede3 --- /dev/null +++ b/frontend/src/components/Headers.vue @@ -0,0 +1,104 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Listen.vue b/frontend/src/components/Listen.vue new file mode 100644 index 0000000..5b169b9 --- /dev/null +++ b/frontend/src/components/Listen.vue @@ -0,0 +1,149 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Main.vue b/frontend/src/components/Main.vue new file mode 100644 index 0000000..25116f7 --- /dev/null +++ b/frontend/src/components/Main.vue @@ -0,0 +1,295 @@ + + + diff --git a/frontend/src/components/Multiplex.vue b/frontend/src/components/Multiplex.vue new file mode 100644 index 0000000..335895e --- /dev/null +++ b/frontend/src/components/Multiplex.vue @@ -0,0 +1,132 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Network.vue b/frontend/src/components/Network.vue new file mode 100644 index 0000000..28bd0f4 --- /dev/null +++ b/frontend/src/components/Network.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/OutJson.vue b/frontend/src/components/OutJson.vue new file mode 100644 index 0000000..2deef0a --- /dev/null +++ b/frontend/src/components/OutJson.vue @@ -0,0 +1,152 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Rule.vue b/frontend/src/components/Rule.vue new file mode 100644 index 0000000..517cecc --- /dev/null +++ b/frontend/src/components/Rule.vue @@ -0,0 +1,568 @@ + + + diff --git a/frontend/src/components/SimpleDNS.vue b/frontend/src/components/SimpleDNS.vue new file mode 100644 index 0000000..7195139 --- /dev/null +++ b/frontend/src/components/SimpleDNS.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/SubClashExt.vue b/frontend/src/components/SubClashExt.vue new file mode 100644 index 0000000..68f6538 --- /dev/null +++ b/frontend/src/components/SubClashExt.vue @@ -0,0 +1,248 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/SubJsonExt.vue b/frontend/src/components/SubJsonExt.vue new file mode 100644 index 0000000..7a61678 --- /dev/null +++ b/frontend/src/components/SubJsonExt.vue @@ -0,0 +1,514 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Transport.vue b/frontend/src/components/Transport.vue new file mode 100644 index 0000000..9009631 --- /dev/null +++ b/frontend/src/components/Transport.vue @@ -0,0 +1,51 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/UoT.vue b/frontend/src/components/UoT.vue new file mode 100644 index 0000000..51b9fac --- /dev/null +++ b/frontend/src/components/UoT.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Users.vue b/frontend/src/components/Users.vue new file mode 100644 index 0000000..7812edb --- /dev/null +++ b/frontend/src/components/Users.vue @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/WgPeer.vue b/frontend/src/components/WgPeer.vue new file mode 100644 index 0000000..f37f1ca --- /dev/null +++ b/frontend/src/components/WgPeer.vue @@ -0,0 +1,113 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/message.vue b/frontend/src/components/message.vue new file mode 100644 index 0000000..0d39e42 --- /dev/null +++ b/frontend/src/components/message.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/AnyTls.vue b/frontend/src/components/protocols/AnyTls.vue new file mode 100644 index 0000000..6fe32b5 --- /dev/null +++ b/frontend/src/components/protocols/AnyTls.vue @@ -0,0 +1,80 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Direct.vue b/frontend/src/components/protocols/Direct.vue new file mode 100644 index 0000000..52f4bdf --- /dev/null +++ b/frontend/src/components/protocols/Direct.vue @@ -0,0 +1,43 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Http.vue b/frontend/src/components/protocols/Http.vue new file mode 100644 index 0000000..0a77632 --- /dev/null +++ b/frontend/src/components/protocols/Http.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Hysteria.vue b/frontend/src/components/protocols/Hysteria.vue new file mode 100644 index 0000000..1cf775e --- /dev/null +++ b/frontend/src/components/protocols/Hysteria.vue @@ -0,0 +1,159 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Hysteria2.vue b/frontend/src/components/protocols/Hysteria2.vue new file mode 100644 index 0000000..7b0055a --- /dev/null +++ b/frontend/src/components/protocols/Hysteria2.vue @@ -0,0 +1,219 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Naive.vue b/frontend/src/components/protocols/Naive.vue new file mode 100644 index 0000000..c294128 --- /dev/null +++ b/frontend/src/components/protocols/Naive.vue @@ -0,0 +1,138 @@ + + + diff --git a/frontend/src/components/protocols/OutShadowTls.vue b/frontend/src/components/protocols/OutShadowTls.vue new file mode 100644 index 0000000..73ceace --- /dev/null +++ b/frontend/src/components/protocols/OutShadowTls.vue @@ -0,0 +1,44 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Selector.vue b/frontend/src/components/protocols/Selector.vue new file mode 100644 index 0000000..125e5f9 --- /dev/null +++ b/frontend/src/components/protocols/Selector.vue @@ -0,0 +1,46 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/ShadowTls.vue b/frontend/src/components/protocols/ShadowTls.vue new file mode 100644 index 0000000..cfa44dc --- /dev/null +++ b/frontend/src/components/protocols/ShadowTls.vue @@ -0,0 +1,165 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Shadowsocks.vue b/frontend/src/components/protocols/Shadowsocks.vue new file mode 100644 index 0000000..162410f --- /dev/null +++ b/frontend/src/components/protocols/Shadowsocks.vue @@ -0,0 +1,77 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Socks.vue b/frontend/src/components/protocols/Socks.vue new file mode 100644 index 0000000..41ab7e1 --- /dev/null +++ b/frontend/src/components/protocols/Socks.vue @@ -0,0 +1,59 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Ssh.vue b/frontend/src/components/protocols/Ssh.vue new file mode 100644 index 0000000..222c154 --- /dev/null +++ b/frontend/src/components/protocols/Ssh.vue @@ -0,0 +1,151 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/TProxy.vue b/frontend/src/components/protocols/TProxy.vue new file mode 100644 index 0000000..11bf1d2 --- /dev/null +++ b/frontend/src/components/protocols/TProxy.vue @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Tailscale.vue b/frontend/src/components/protocols/Tailscale.vue new file mode 100644 index 0000000..47c5a25 --- /dev/null +++ b/frontend/src/components/protocols/Tailscale.vue @@ -0,0 +1,200 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Tor.vue b/frontend/src/components/protocols/Tor.vue new file mode 100644 index 0000000..0d8aac5 --- /dev/null +++ b/frontend/src/components/protocols/Tor.vue @@ -0,0 +1,115 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Trojan.vue b/frontend/src/components/protocols/Trojan.vue new file mode 100644 index 0000000..5c4f017 --- /dev/null +++ b/frontend/src/components/protocols/Trojan.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Tuic.vue b/frontend/src/components/protocols/Tuic.vue new file mode 100644 index 0000000..c3736ae --- /dev/null +++ b/frontend/src/components/protocols/Tuic.vue @@ -0,0 +1,89 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Tun.vue b/frontend/src/components/protocols/Tun.vue new file mode 100644 index 0000000..f853176 --- /dev/null +++ b/frontend/src/components/protocols/Tun.vue @@ -0,0 +1,100 @@ + + + diff --git a/frontend/src/components/protocols/UrlTest.vue b/frontend/src/components/protocols/UrlTest.vue new file mode 100644 index 0000000..8c4f99f --- /dev/null +++ b/frontend/src/components/protocols/UrlTest.vue @@ -0,0 +1,121 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Vless.vue b/frontend/src/components/protocols/Vless.vue new file mode 100644 index 0000000..b3a3115 --- /dev/null +++ b/frontend/src/components/protocols/Vless.vue @@ -0,0 +1,48 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Vmess.vue b/frontend/src/components/protocols/Vmess.vue new file mode 100644 index 0000000..2ce85ff --- /dev/null +++ b/frontend/src/components/protocols/Vmess.vue @@ -0,0 +1,72 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Warp.vue b/frontend/src/components/protocols/Warp.vue new file mode 100644 index 0000000..6bdcbc4 --- /dev/null +++ b/frontend/src/components/protocols/Warp.vue @@ -0,0 +1,170 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/protocols/Wireguard.vue b/frontend/src/components/protocols/Wireguard.vue new file mode 100644 index 0000000..4e31593 --- /dev/null +++ b/frontend/src/components/protocols/Wireguard.vue @@ -0,0 +1,197 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/services/Ccm.vue b/frontend/src/components/services/Ccm.vue new file mode 100644 index 0000000..49be011 --- /dev/null +++ b/frontend/src/components/services/Ccm.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/components/services/Derp.vue b/frontend/src/components/services/Derp.vue new file mode 100644 index 0000000..1c64dea --- /dev/null +++ b/frontend/src/components/services/Derp.vue @@ -0,0 +1,219 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/services/Ocm.vue b/frontend/src/components/services/Ocm.vue new file mode 100644 index 0000000..1e91e7a --- /dev/null +++ b/frontend/src/components/services/Ocm.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/components/services/SSMAPI.vue b/frontend/src/components/services/SSMAPI.vue new file mode 100644 index 0000000..e0443d5 --- /dev/null +++ b/frontend/src/components/services/SSMAPI.vue @@ -0,0 +1,100 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/tiles/Gauge.vue b/frontend/src/components/tiles/Gauge.vue new file mode 100644 index 0000000..d7eefad --- /dev/null +++ b/frontend/src/components/tiles/Gauge.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/frontend/src/components/tiles/History.vue b/frontend/src/components/tiles/History.vue new file mode 100644 index 0000000..604ecc5 --- /dev/null +++ b/frontend/src/components/tiles/History.vue @@ -0,0 +1,213 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/tls/Acme.vue b/frontend/src/components/tls/Acme.vue new file mode 100644 index 0000000..e8e3027 --- /dev/null +++ b/frontend/src/components/tls/Acme.vue @@ -0,0 +1,259 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/tls/Ech.vue b/frontend/src/components/tls/Ech.vue new file mode 100644 index 0000000..856836e --- /dev/null +++ b/frontend/src/components/tls/Ech.vue @@ -0,0 +1,165 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/tls/InTLS.vue b/frontend/src/components/tls/InTLS.vue new file mode 100644 index 0000000..1982bfe --- /dev/null +++ b/frontend/src/components/tls/InTLS.vue @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/tls/OutTLS.vue b/frontend/src/components/tls/OutTLS.vue new file mode 100644 index 0000000..e02b1dd --- /dev/null +++ b/frontend/src/components/tls/OutTLS.vue @@ -0,0 +1,381 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/transports/Http.vue b/frontend/src/components/transports/Http.vue new file mode 100644 index 0000000..66f4778 --- /dev/null +++ b/frontend/src/components/transports/Http.vue @@ -0,0 +1,82 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/transports/HttpUpgrade.vue b/frontend/src/components/transports/HttpUpgrade.vue new file mode 100644 index 0000000..a52a085 --- /dev/null +++ b/frontend/src/components/transports/HttpUpgrade.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/transports/WebSocket.vue b/frontend/src/components/transports/WebSocket.vue new file mode 100644 index 0000000..ff56859 --- /dev/null +++ b/frontend/src/components/transports/WebSocket.vue @@ -0,0 +1,69 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/transports/gRPC.vue b/frontend/src/components/transports/gRPC.vue new file mode 100644 index 0000000..aef1c91 --- /dev/null +++ b/frontend/src/components/transports/gRPC.vue @@ -0,0 +1,65 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/default/AppBar.vue b/frontend/src/layouts/default/AppBar.vue new file mode 100644 index 0000000..fc76534 --- /dev/null +++ b/frontend/src/layouts/default/AppBar.vue @@ -0,0 +1,78 @@ + + + diff --git a/frontend/src/layouts/default/Default.vue b/frontend/src/layouts/default/Default.vue new file mode 100644 index 0000000..3a57e3d --- /dev/null +++ b/frontend/src/layouts/default/Default.vue @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/layouts/default/Drawer.vue b/frontend/src/layouts/default/Drawer.vue new file mode 100644 index 0000000..ca0e7b5 --- /dev/null +++ b/frontend/src/layouts/default/Drawer.vue @@ -0,0 +1,69 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/default/View.vue b/frontend/src/layouts/default/View.vue new file mode 100644 index 0000000..0889462 --- /dev/null +++ b/frontend/src/layouts/default/View.vue @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Admin.vue b/frontend/src/layouts/modals/Admin.vue new file mode 100644 index 0000000..6f74eda --- /dev/null +++ b/frontend/src/layouts/modals/Admin.vue @@ -0,0 +1,97 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Backup.vue b/frontend/src/layouts/modals/Backup.vue new file mode 100644 index 0000000..45cabb8 --- /dev/null +++ b/frontend/src/layouts/modals/Backup.vue @@ -0,0 +1,99 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Changes.vue b/frontend/src/layouts/modals/Changes.vue new file mode 100644 index 0000000..0f00624 --- /dev/null +++ b/frontend/src/layouts/modals/Changes.vue @@ -0,0 +1,145 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Client.vue b/frontend/src/layouts/modals/Client.vue new file mode 100644 index 0000000..44e67be --- /dev/null +++ b/frontend/src/layouts/modals/Client.vue @@ -0,0 +1,370 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/ClientAddBulk.vue b/frontend/src/layouts/modals/ClientAddBulk.vue new file mode 100644 index 0000000..af7042b --- /dev/null +++ b/frontend/src/layouts/modals/ClientAddBulk.vue @@ -0,0 +1,228 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/ClientEditBulk.vue b/frontend/src/layouts/modals/ClientEditBulk.vue new file mode 100644 index 0000000..e5718b0 --- /dev/null +++ b/frontend/src/layouts/modals/ClientEditBulk.vue @@ -0,0 +1,196 @@ + + + diff --git a/frontend/src/layouts/modals/Dns.vue b/frontend/src/layouts/modals/Dns.vue new file mode 100644 index 0000000..ad109e9 --- /dev/null +++ b/frontend/src/layouts/modals/Dns.vue @@ -0,0 +1,210 @@ + + + diff --git a/frontend/src/layouts/modals/DnsRule.vue b/frontend/src/layouts/modals/DnsRule.vue new file mode 100644 index 0000000..951fdf2 --- /dev/null +++ b/frontend/src/layouts/modals/DnsRule.vue @@ -0,0 +1,313 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Endpoint.vue b/frontend/src/layouts/modals/Endpoint.vue new file mode 100644 index 0000000..f9248f5 --- /dev/null +++ b/frontend/src/layouts/modals/Endpoint.vue @@ -0,0 +1,227 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Inbound.vue b/frontend/src/layouts/modals/Inbound.vue new file mode 100644 index 0000000..b4292d3 --- /dev/null +++ b/frontend/src/layouts/modals/Inbound.vue @@ -0,0 +1,287 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Logs.vue b/frontend/src/layouts/modals/Logs.vue new file mode 100644 index 0000000..e8b6e00 --- /dev/null +++ b/frontend/src/layouts/modals/Logs.vue @@ -0,0 +1,90 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Outbound.vue b/frontend/src/layouts/modals/Outbound.vue new file mode 100644 index 0000000..96d68ce --- /dev/null +++ b/frontend/src/layouts/modals/Outbound.vue @@ -0,0 +1,212 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/OutboundBulk.vue b/frontend/src/layouts/modals/OutboundBulk.vue new file mode 100644 index 0000000..4b8a2ae --- /dev/null +++ b/frontend/src/layouts/modals/OutboundBulk.vue @@ -0,0 +1,155 @@ + + + diff --git a/frontend/src/layouts/modals/QrCode.vue b/frontend/src/layouts/modals/QrCode.vue new file mode 100644 index 0000000..762b6bd --- /dev/null +++ b/frontend/src/layouts/modals/QrCode.vue @@ -0,0 +1,150 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Rule.vue b/frontend/src/layouts/modals/Rule.vue new file mode 100644 index 0000000..a332890 --- /dev/null +++ b/frontend/src/layouts/modals/Rule.vue @@ -0,0 +1,324 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/RuleImport.vue b/frontend/src/layouts/modals/RuleImport.vue new file mode 100644 index 0000000..69bba22 --- /dev/null +++ b/frontend/src/layouts/modals/RuleImport.vue @@ -0,0 +1,259 @@ + + + diff --git a/frontend/src/layouts/modals/Ruleset.vue b/frontend/src/layouts/modals/Ruleset.vue new file mode 100644 index 0000000..ae5404f --- /dev/null +++ b/frontend/src/layouts/modals/Ruleset.vue @@ -0,0 +1,133 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/RulesetImport.vue b/frontend/src/layouts/modals/RulesetImport.vue new file mode 100644 index 0000000..0410bbe --- /dev/null +++ b/frontend/src/layouts/modals/RulesetImport.vue @@ -0,0 +1,187 @@ + + + diff --git a/frontend/src/layouts/modals/Service.vue b/frontend/src/layouts/modals/Service.vue new file mode 100644 index 0000000..fe79478 --- /dev/null +++ b/frontend/src/layouts/modals/Service.vue @@ -0,0 +1,128 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Stats.vue b/frontend/src/layouts/modals/Stats.vue new file mode 100644 index 0000000..86fcb68 --- /dev/null +++ b/frontend/src/layouts/modals/Stats.vue @@ -0,0 +1,218 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Tls.vue b/frontend/src/layouts/modals/Tls.vue new file mode 100644 index 0000000..f179c4d --- /dev/null +++ b/frontend/src/layouts/modals/Tls.vue @@ -0,0 +1,592 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Token.vue b/frontend/src/layouts/modals/Token.vue new file mode 100644 index 0000000..3cf954a --- /dev/null +++ b/frontend/src/layouts/modals/Token.vue @@ -0,0 +1,258 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/UsageStats.vue b/frontend/src/layouts/modals/UsageStats.vue new file mode 100644 index 0000000..7c3b951 --- /dev/null +++ b/frontend/src/layouts/modals/UsageStats.vue @@ -0,0 +1,101 @@ + + + diff --git a/frontend/src/layouts/modals/WgQrCode.vue b/frontend/src/layouts/modals/WgQrCode.vue new file mode 100644 index 0000000..d44d794 --- /dev/null +++ b/frontend/src/layouts/modals/WgQrCode.vue @@ -0,0 +1,129 @@ + + + \ No newline at end of file diff --git a/frontend/src/locales/en.ts b/frontend/src/locales/en.ts new file mode 100644 index 0000000..c03ac0f --- /dev/null +++ b/frontend/src/locales/en.ts @@ -0,0 +1,637 @@ +export default { + success: "success", + failed: "failed", + enable: "Enable", + disable: "Disable", + none: "None", + all: "All", + loading: "Loading...", + confirm: "Are you sure ?", + yes: "yes", + no: "no", + unlimited: "infinite", + type: "Type", + protocol: "Protocol", + submit: "Submit", + reset: "Reset", + now: "Now", + network: "Network", + copyToClipboard: "Copy to clipboard", + noData: "No data!", + invalidLogin: "Invalid Login!", + online: "Online", + version: "Version", + email: "Email", + commaSeparated: "(comma separated)", + count: "Count", + template: "Template", + editor: "Editor", + error: { + dplData: "Duplicate Data", + core: "Sing-Box Error", + invalidData: "Invalid Data", + }, + theme: { + light: "Light", + dark: "Dark", + system: "System", + }, + pages: { + login: "Login", + home: "Home", + inbounds: "Inbounds", + outbounds: "Outbounds", + services: "Services", + endpoints: "Endpoints", + clients: "Clients", + rules: "Rules", + tls: "TLS Settings", + basics: "Basics", + dns: "DNS", + admins: "Admins", + settings: "Settings", + }, + main: { + tiles: "Tiles", + gauges: "Gauges", + charts: "Charts", + infos: "Information", + gauge: { + cpu: "CPU Gauge", + mem: "RAM Gauge", + dsk: "Disk Gauge", + swp: "Swap Gauge", + }, + chart: { + cpu: "CPU Monitor", + mem: "RAM Monitor", + net: "Network Bandwidth", + pnet: "Network Packets", + dio: "Disk I/O", + }, + info: { + sys: "System Info", + sbd: "Sing-Box Info", + host: "Host", + cpu: "CPU", + core: "Core", + uptime: "Uptime", + startupTime: "Startup time", + threads: "Threads", + memory: "Memory", + running: "Running" + }, + backup: { + title: "Backup & Restore", + backup: "Download Backup", + restore: "Restore Backup", + exclStats: "Exclude graphs", + exclChanges: "Exclude changes", + sbConfig: "Download Sing-Box Config", + }, + stats: { + title: "Usage & Counts", + totalUsage: "Total Usage", + }, + }, + objects: { + inbound: "Inbound", + client: "Client", + outbound: "Outbound", + endpoint: "Endpoint", + config: "Config", + rule: "Rule", + ruleset: "Ruleset", + service: "Service", + dnsserver: "DNS Server", + dnsrule: "DNS Rule", + user: "User", + tag: "Tag", + listen: "Listen", + dial: "Dial", + tls: "TLS", + multiplex: "Multiplex", + transport: "Transport", + headers: "Headers", + key: "Key", + value: "Value", + }, + actions: { + action: "Action", + add: "Add", + addbulk: "Add Bulk", + editbulk: "Edit Bulk", + delbulk: "Delete Bulk", + new: "New", + edit: "Edit", + del: "Delete", + clone: "Clone", + test: "Test", + testAll: "Test all", + save: "Save", + update: "Update", + submit: "Submit", + set: "Set", + generate: "Generate", + disable: "Disable", + close: "Close", + restartApp: "Restart App", + restartSb: "Restart Singbox", + }, + login: { + title: "Login", + username: "Username", + unRules: "Username can not be empty", + password: "Password", + pwRules: "Password can not be empty", + }, + menu: { + logout: "Logout", + }, + admin: { + changeCred: "Change credentials", + oldPass: "Current Password", + newUname: "New Username", + newPass: "New Password", + lastLogin: "Last login", + date: "Date", + time: "Time", + changes: "Changes", + actor: "Actor", + key: "Key", + action: "Action", + api: { + title: "API Tokens", + msg: "Please copy the token below and store it somewhere safe. It will not be shown again.", + token: "Token", + }, + }, + setting: { + interface: "Interface", + sub: "Subscription", + addr: "Address", + port: "Port", + webPath: "Base URI", + domain: "Domain", + sslKey: "SSL Key Path", + sslCert: "SSL Certificate Path", + webUri: "Panel URI", + sessionAge: "Session Maximum Age", + trafficAge: "Traffic Maximum Age", + timeLoc: "Timezone Location", + subEncode: "Enable Encoding", + subInfo: "Enable Client Info", + path: "Default Path", + update: "Automatic Update Time", + subUri: "Subscription URI", + jsonSub: "JSON Subscription", + toDirect: "Route to Direct", + toBlock: "Route to Block", + timestamp: "Timestamp", + globalDns: "Global DNS", + directDns: "Direct DNS", + toDirectDns: "Route to Direct DNS", + jsonSubOptions: "Other Options", + excludePkg: "Exclude Packages", + clashSub: "Clash Subscription", + mixedPort: "Mixed Inbound Port", + tun: "Tun Inbound", + }, + client: { + name: "Name", + desc: "Description", + group: "Group", + inboundTags: "Inbound Tags", + basics: "Basics", + config: "Config", + links: "Links", + external: "External Link", + sub: "External Subscription", + delayStart: "Delay Start", + autoReset: "Auto Reset", + resetDays: "Reset Days", + nextReset: "Next Reset", + }, + bulk: { + order: "Order", + random: "Random", + changeLimits: "Change limits", + addInbounds: "Add inbounds", + removeInbounds: "Remove inbounds", + addDays: "Add days", + addVolume: "Add volume", + }, + types: { + un: "Username", + pw: "Password", + direct: { + overrideAddr: "Override Address", + overridePort: "Override Port", + }, + hy: { + obfs: "Obfuscated Password", + auth: "Authentication Password", + hyOptions: "Hysteria Options", + hy2Options: "Hysteria2 Options", + ignoreBw: "Ignore Client Bandwidth", + }, + shdwTls: { + hs: "Handshake Server", + addHS: "Add Handshake Server", + }, + ssh: { + passphrase: "Passphrase", + hostKey: "Host Keys", + algorithm: "Key Algorithms", + clientVer: "Client Version", + options: "SSH Options", + }, + tor: { + execPath: "Executable File Path", + dataDir: "Data Directory", + extArgs: "Extra Args", + }, + tuic: { + congControl: "Congestion Control", + authTimeout: "Authentication Timeout", + hb: "Heartbeat", + }, + tun: { + addr: "Addresses", + ifName: "Interface Name", + excludeMptcp: "Exclude MPTCP", + fallbackRuleIndex: "iproute2 Fallback Rule Index", + }, + vless: { + flow: "Flow", + udpEnc: "UDP Packet Encoding", + }, + vmess: { + security: "Security", + globalPadding: "Global Padding", + authLen: "Encryptrd Length", + }, + wg: { + privKey: "Private Key", + pubKey: "Peer Public Key", + psk: "Pre-Shared Key", + localIp: "Local IPs", + worker: "Workers", + ifName: "Interface Name", + sysIf: "System Interface", + options: "Wireguard Options", + allowedIp: "Allowed IPs", + peer: "Peer", + peers: "Peers", + }, + lb: { + defaultOut: "Default Outbound", + interruptConn: "Interrupt exist connections", + testUrl: "Test URL", + interval: "Interval", + tolerance: "Tolerance", + urlTestOptions: "URLTest Options" + }, + ts: { + options: "Tailscale Options", + stateDir: "State Directory", + authKey: "Authentication Key", + relayServer: "Relay Server", + relayServerPort: "Relay Server Port", + relayEndpoints: "Relay Static Endpoints", + systemInterface: "System Interface", + sysIfName: "Interface Name", + sysIfMtu: "Interface MTU", + controlUrl: "Control URL", + ephemeral: "Ephemeral Mode", + hostname: "Hostname", + acceptRoutes: "Accept Routes", + exitNode: "Exit Node", + allowLanAccess: "Allow LAN Access", + advRoutes: "Advertise Routes", + advExitNode: "Advertise Exit Node", + udpTimeout: "UDP Timeout", + }, + ocm: { + credentialPath: "Credential Path", + usagesPath: "Usages Path", + users: "Users", + userName: "Name", + userToken: "Token", + }, + ccm: { + credentialPath: "Credential Path", + usagesPath: "Usages Path", + users: "Users", + userName: "Name", + userToken: "Token", + }, + derp: { + configPath: "Config Path", + verifyClientEndpoint: "Verify Client Endpoint", + verifyClientUrl: "Verify Client URL", + meshWith: "Mesh With", + meshPsk: "Mesh PSK", + meshPskFile: "Mesh PSK File", + stun: "STUN Server", + options: "DERP Options", + }, + anytls: { + idleInterval: "Idle Session Check Interval", + idleTimeout: "Idle Session Timeout", + minIdle: "Minimum Idle Session", + }, + naive: { + insecureConcurrency: "Insecure Concurrency", + quic: "QUIC", + quicCongestion: "QUIC Congestion Control", + udpOverTcp: "UDP over TCP", + }, + }, + in: { + addr: "Address", + port: "Port", + ssMethod: "Method", + ssManageable: "Manageable", + sSide: "Server Side", + cSide: "Client Side", + multiDomain: "Multi Domain", + remark: "Remark", + mdOption: "Multi Domain Options", + }, + listen: { + options: "Listen Options", + tcpOptions: "TCP Options", + udpOptions: "UDP Options", + detour: "Detour", + detourText: "Forward to inbound", + disableTcpKeepAlive: "Disable TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "TCP Keep Alive Interval", + }, + dial: { + bindIf: "Bind to Network Interface", + bindIp4: "Bind to IPv4", + bindIp6: "Bind to IPv6", + bindNoPort: "Bind Address No Port", + reuseAddr: "Reuse Listener Address", + connTimeout: "Connection Timeout", + disableTcpKeepAlive: "Disable TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "TCP Keep Alive Interval", + domainResolver: "Domain Resolver", + options: "Dial Options", + detourText: "Forward to outbound", + }, + transport: { + enable: "Enable Transport", + host: "Host", + hosts: "Hosts", + path: "Path", + httpMethod: "Request Method", + idleTimeout: "Idle Timeout", + pingTimeout: "Ping Timeout", + grpcServiceName: "Service Name", + grpcPws: "Permit Without Stream", + }, + mux: { + enable: "Enable Multiplex", + maxConn: "Max Connections", + minStr: "Min Streams", + maxStr: "Max Streams", + padding: "Only padding", + enableBrutal: "Enable Brutal", + }, + out: { + addr: "Server Address", + port: "Server Port", + addUrlTest: "Add URLTest", + delay: "Delay", + }, + rule: { + add: "Add Rule", + simple: "Simple", + logical: "Logical", + mode: "Mode", + invert: "Invert", + ipVer: "IP Version", + domain: "Domains", + domainSufix: "Domain Suffixes", + domainKw: "Domain Keywords", + domainRgx: "Domain Regexes", + ip: "IP CIDRs", + privateIp: "Invalid IP Ranges", + port: "Ports", + portRange: "Port Ranges", + srcCidr: "Source IP CIDRs", + srcPrivateIp: "Invalid Source IPs", + srcPort: "Source Ports", + srcPortRange: "Source Port Ranges", + ruleset: "Rulesets", + rulesetMatchSrc: "Ruleset IPcidr Match Source", + preferredBy: "Preferred By (Outbound)", + interfaceAddr: "Interface Address", + options: "Rule Options", + domainRules: "Domain/IP", + srcIpRules: "Source IP", + srcPortRules: "Source Port", + udpDisableDomainUnmapping: "UDP Disable Domain Unmapping", + udpConnect: "UDP Connect", + udpTimeout: "UDP Timeout", + method: "Method", + noDrop: "No Drop", + sniffer: "Sniffer", + timeout: "Timeout", + strategy: "Strategy", + etaHint: "Enter one item per line. Blank lines and duplicate entries will be skipped.", + import: { + title: "Mass import of rulesets", + rulesTitle: "Importing rules", + urlsHint: "One URL per line. The tag is determined from the file name without the extension.", + fileHint: "Upload it .a txt file with URLs, one per line.", + jsonHint: "Insert a JSON object with the rules and/or rule_set arrays. You can insert the entire \"route\" block: {'{'}...{'}'} or just its contents.", + fileJsonHint: "Upload it .a json file with the route block.", + urlHint: "Specify a direct link to the JSON file (for example, a raw GitHub link).", + preview: "Preview", + skipped: "already exist, shown in gray", + conflict: "Conflicts detected", + merge: "Merge - add imported rules (skip duplicate set tags)", + replace: "Replace - delete existing rules and sets, and import them again", + pasteUrls: "Insert URL", + uploadTxt: "Upload .txt", + uploadFile: "Upload a file", + fromUrl: "By URL", + selectTxt: "Choose .txt file", + selectJson: "Choose .json file", + parse: "Parse", + conflictDesc: "The config already has {rules} of rules and {rulesets} of rule sets. Select an action:", + finalOutbound: "Outbound by default (final)", + applyFinal: "Apply as outbound by default", + errNoArrays: 'No "rules" or "rule_set" arrays found.', + errJsonParse: "JSON parse error: {message}", + errNoArraysFetched: 'No "rules" or "rule_set" found in fetched JSON.', + errFetch: "Fetch error: {message}", + errNoFile: "No file selected.", + errNoArraysInFile: 'No "rules" or "rule_set" found in the file.', + }, + }, + ruleset: { + add: "Add Ruleset", + format: "Data Format", + interval: "Update Intervals", + remote: "Remote", + local: "Local", + }, + dns: { + add: "Add Dns Server", + title: "Dns Servers", + final: "Final", + server: "Server", + firstServer: "First Server", + cacheCapacity: "Cache Capacity", + disableCache: "Disable Cache", + disableExpire: "Disable Expire", + independentCache: "Independent Cache", + reverseMapping: "Reverse Mapping", + domainStrategy: "Domain Strategy", + local: { + preferGo: "Prefer Go", + }, + rule: { + add: "Add Dns Rule", + title: "Dns Rules", + inet4Range: "IPv4 Range", + inet6Range: "IPv6 Range", + acceptDefault: "Accept Default Resolvers", + action: { + title: "Action", + route: "Route", + routeOptions: "Route Options", + reject: "Reject", + predefined: "Predefined", + rewriteTtl: "Rewrite TTL", + clientSubnet: "Client Subnet", + rcode: "Response Code", + rcodes: { + noError: "Ok", + formerr: "Bad request", + servFail: "Server failure", + nxDomain: "Not found", + refused: "Refused", + notImp: "Not Implemented", + }, + answer: "Answers", + ns: "Nameservers", + extra: "Extra", + }, + } + }, + basic: { + log: { + title: "Logs", + level: "Level", + output: "Output", + timestamp: "Enable Timestamp", + }, + routing: { + title: "Routing", + defaultOut: "Default Outbound", + defaultIf: "Default NIC", + defaultRm: "Default Routing Mark", + defaultDns: "Default DNS Resolver", + autoBind: "Auto Bind NIC", + }, + exp: { + storeFakeIp: "Store Fake IP", + extController: "External Controller", + extUi: "External UI", + extUiDownloadUrl: "UI Download URL", + extUiDownloadDetour: "UI Download detour", + secret: "Secret", + defaultMode: "Default Mode", + allowOrigin: "Allow Origin", + allowPrivate: "Allow Private Network", + }, + }, + tls: { + enable: "Enable TLS", + usePath: "Use Path", + useText: "Use Text", + certPath: "Certificate File Path", + keyPath: "Key File Path", + cert: "Certificate", + key: "Key", + options: "TLS Options", + minVer: "Minimum Version", + maxVer: "Maximum Version", + cs: "Cipher suits", + privKey: "Private Key", + pubKey: "Public Key", + disableSni: "Disable SNI", + insecure: "Allow Insecure", + fragment: "Fragment", + fragmentDelay: "Fragment Fallback Delay", + recordFragment: "Multiple records Fragmentation", + store: "Root Store", + ktls: "Kernel TLS", + kernelTx: "TX", + kernelRx: "RX", + queryServerName: "ECH Query Server Name", + acme: { + options: "ACME Options", + dataDir: "Data Directory", + defaultDomain: "Default Domain", + disableChallenges: "Disable Challenges", + httpChallenge: "Disable HTTP Challenge", + tlsChallenge: "Disable TLS Challenge", + altPorts: "Alternative Ports", + altHport: "Alternative HTTP Port", + altTport: "Alternative TLS Port", + caProvider: "CA Provider", + customCa: "Custom CA Provider", + extAcc: "External Account", + dns01: "DNS01 Challenge", + dns01Provider: "DNS01 Challenge Provider", + dns01Params: { + api_token: "API Token", + zone_token: "Zone Token", + access_key_id: "Access Key ID", + access_key_secret: "Access Key Secret", + region_id: "Region ID", + security_token: "Security Token", + username: "Username", + password: "Password", + subdomain: "Subdomain", + server_url: "Server URL", + }, + }, + }, + stats: { + upload: "Upload", + download: "Download", + volume: "Volume", + usage: "Usage", + enable: "Enable Statistics", + graphTitle: "Traffic Chart", + B: "B", + KB: "KB", + MB: "MB", + GB: "GB", + TB: "TB", + PB: "PB", + p: "p", + Kp: "Kp", + Mp: "Mp", + Gp: "Gp", + Mbps: "Mbps", + }, + date: { + expiry: "Expiry", + expired: "Expired", + d: "d", + h: "h", + m: "m", + s: "s", + ms: "ms", + }, +} diff --git a/frontend/src/locales/fa.ts b/frontend/src/locales/fa.ts new file mode 100644 index 0000000..ad6072d --- /dev/null +++ b/frontend/src/locales/fa.ts @@ -0,0 +1,635 @@ +export default { + success: "موفق", + failed: "خطا", + enable: "فعال", + disable: "غیرفعال", + none: "هیچ", + all: "همه", + loading: "در حال بارگذاری...", + confirm: "آیا مطمئن هستید ؟", + yes: "بله", + no: "خیر", + unlimited: "نامحدود", + type: "مدل", + protocol: "پروتکل", + submit: "تایید", + reset: "ریست", + now: "اکنون", + network: "شبکه", + copyToClipboard: "کپی در حافظه", + noData: "بدون داده!", + invalidLogin: "ورود نامعتبر!", + online: "آنلاین", + version: "نسخه", + email: "ایمیل", + commaSeparated: "(جداشده با کاما)", + count: "تعداد", + template: "الگو", + editor: "ویرایشگر", + error: { + dplData: "داده تکراری", + core: "خطا در سینگ‌باکس", + invalidData: "داده نامعتبر", + }, + theme: { + light: "روشن", + dark: "تیره", + system: "سیستم", + }, + pages: { + login: "ورود", + home: "خانه", + inbounds: "ورودی‌ها", + outbounds: "خروجی‌ها", + endpoints: "درگاه‌ها", + services: "خدمات", + clients: "کاربران", + rules: "قوانین", + tls: "رمزنگاری‌ها", + basics: "ترازها", + dns: "DNS", + admins: "ادمین‌ها", + settings: "پیکربندی", + }, + main: { + tiles: "کاشی‌ها", + gauges: "سنجش‌ها", + charts: "نمودارها", + infos: "داده‌ها", + gauge: { + cpu: "سنجش پردازنده", + mem: "سنجش حافظه", + dsk: "سنجش دیسک", + swp: "سنجش Swap", + }, + chart: { + cpu: "نمودار پردازنده", + mem: "نمودار حافظه", + net: "ترافیک شبکه", + pnet: "بسته‌های شبکه", + dio: "نمودار دیسک", + }, + info: { + sys: "داده‌های سیستم", + sbd: "داده‌های سینگ‌باکس", + host: "نام", + cpu: "پردازنده", + core: "هسته", + uptime: "مدت‌", + startupTime: "زمان راه‌اندازی", + threads: "نخ‌ها", + memory: "حافظه", + running: "اجرا" + }, + backup: { + title: "پشتیبان‌گیری و بازیابی", + backup: "دریافت پشتیبان", + restore: "بازیابی پشتیبان", + exclStats: "بدون گراف‌ها", + exclChanges: "بدون تغییرات", + sbConfig: "دریافت پیکربندی سینگ‌باکس", + }, + stats: { + title: "آمار و تعداد", + totalUsage: "مجموع مصرف", + }, + }, + objects: { + inbound: "ورودی‌", + client: "کاربر", + outbound: "خروجی‌", + endpoint: "درگاه", + config: "پیکربندی", + rule: "قانون", + ruleset: "مجموعه", + service: "خدمت", + dnsserver: "سرور DNS", + dnsrule: "قانون DNS", + user: "کاربر", + tag: "برچسب", + listen: "گوش‌دادن", + dial: "تماس", + tls: "رمزنگاری", + multiplex: "تسهیم", + transport: "انتقال", + headers: "سربرگ‌ها", + key: "نام", + value: "مقدار", + }, + actions: { + action: "فرمان", + add: "ایجاد", + addbulk: "ایجاد انبوه", + editbulk: "ویرایش انبوه", + delbulk: "حذف انبوه", + new: "جدید", + edit: "ویرایش", + del: "حذف", + clone: "شبیه‌سازی", + test: "تست", + testAll: "تست همه", + save: "ذخیره", + update: "بروزرسانی", + submit: "ارسال", + set: "تنظیم", + generate: "تولید", + disable: "غیرفعال", + close: "بستن", + restartApp: "ریستارت پنل", + restartSb: "ریستارت سینگ‌باکس", + }, + login: { + title: "ورود", + username: "نام کاربری", + unRules: "نام کاربری نمی‌تواند خالی باشد", + password: "کلمه عبور", + pwRules: "کلمه عبور نمی‌تواند خالی باشد", + }, + menu: { + logout: "خروج", + }, + admin: { + changeCred: "ویرایش داده‌ها", + oldPass: "رمز کنونی", + newUname: "نام کاربری جدید", + newPass: "رمز جدید", + lastLogin: "آخرین ورود", + date: "تاریخ", + time: "ساعت", + changes: "تغییرات", + actor: "مجری", + key: "کلید", + action: "عمل", + api: { + title: "توکن‌های API", + msg: "لطفا توکن زیر را کپی کنید و در یک مکان امن نگهدارید. این توکن دیگر نمایش داده نخواهد شد.", + token: "توکن", + }, + }, + setting: { + interface: "نما", + sub: "سابسکریپشن", + addr: "آدرس", + port: "پورت", + webPath: "مسیر پایه", + domain: "دامنه", + sslKey: "مسیر فایل کلید", + sslCert: "مسیر فایل گواهی", + webUri: "آدرس نهایی پنل", + sessionAge: "بیشینه زمان لاگین ماندن", + trafficAge: "بیشینه زمان ذخیره ترافیک", + timeLoc: "منطقه زمانی", + subEncode: "رمزگذاری", + subInfo: "نمایش اطلاعات کاربر", + path: "مسیر پیشفرض", + update: "زمان بروزرسانی خودکار", + subUri: "آدرس نهایی سابسکریپشن", + jsonSub: "سابسکریپشن JSON", + toDirect: "هدایت مستقیم", + toBlock: "بستن مسیر", + timestamp: "نمایش زمان", + globalDns: "DNS کلی", + directDns: "DNS مستقیم", + toDirectDns: "هدایت به DNS مستقیم", + jsonSubOptions: "گزینه‌های دیگر", + excludePkg: "نرم‌افزارهای استثنا", + clashSub: "سابسکریپشن CLASH", + mixedPort: "ورودی Mixed", + tun: "ورودی TUN", + }, + client: { + name: "نام", + desc: "شرح", + group: "گروه", + inboundTags: "برچسب‌های ورودی", + basics: "پایه", + config: "تنظیم", + links: "لینک‌ها", + external: "لینک‌ خارجی", + sub: "سابسکریپشن خارجی", + delayStart: "تأخیر شروع", + autoReset: "بازنشانی خودکار", + resetDays: "روزهای بازنشانی", + nextReset: "بازنشانی بعدی", + }, + bulk: { + order: "ترتیب", + random: "تصادفی", + changeLimits: "تغییر محدودیت‌ها", + addInbounds: "افزودن اینباندها", + removeInbounds: "حذف اینباندها", + addDays: "افزودن روز", + addVolume: "افزودن حجم", + }, + types: { + un: "نام کاربری", + pw: "رمز", + direct: { + overrideAddr: "جایگزین آدرس", + overridePort: "جایگزین پورت", + }, + hy: { + obfs: "رمز مبهم کننده", + auth: "رمز احراز هویت", + hyOptions: "گزینه‌های Hysteria", + hy2Options: "گزینه‌های Hysteria2", + ignoreBw: "نادیده‌گرفتن پهنای‌باند کاربر", + }, + shdwTls: { + hs: "سرور دست‌تکانی", + addHS: "افزودن سرور دست‌تکانی", + }, + ssh: { + passphrase: "عبارت عبور", + hostKey: "کلیدهای هاست‌ها", + algorithm: "الگوریتم‌ها", + clientVer: "نسخه کلاینت", + options: "گزینه‌های SSH", + }, + tor: { + execPath: "مسیر فایل اجرایی", + dataDir: "پوشه داده‌ها", + extArgs: "آرگومان‌های اضافی", + }, + tuic: { + congControl: "کنترل ازدحام", + authTimeout: "مهلت احراز هویت", + hb: "ضربان قلب", + }, + tun: { + addr: "آدرس‌ها", + ifName: "نام اینترفیس", + excludeMptcp: "حذف MPTCP", + fallbackRuleIndex: "شاخص قوانین fallback در iproute2", + }, + vless: { + flow: "جریان", + udpEnc: "کدگذاری بسته UDP", + }, + vmess: { + security: "امنیت", + globalPadding: "لایه بندی کلی", + authLen: "رمزگذاری اندازه بسته", + }, + wg: { + privKey: "کلید خصوصی", + pubKey: "کلید عمومی همتا", + psk: "کلید مشترک", + localIp: "آدرس‌های محلی", + worker: "عملگرها", + ifName: "نام اینترفیس", + sysIf: "استفاده از اینترفیس سیستم", + options: "گزینه‌های Wireguard", + allowedIp: "آدرس‌های مجاز", + peer: "همتا", + peers: "همتاها", + }, + lb: { + defaultOut: "خروجی پیش‌فرض", + interruptConn: "قطع ارتباط موجود", + testUrl: "URL تست", + interval: "فاصله زمانی", + tolerance: "تحمل", + urlTestOptions: "گزینه‌های URLTest" + }, + ts: { + options: "گزینه‌های Tailscale", + stateDir: "مسیر پوشه وضعیت", + authKey: "کلید احراز هویت", + relayServer: "سرور رله", + relayServerPort: "پورت سرور رله", + relayEndpoints: "نقاط انتهایی ثابت رله", + systemInterface: "رابط سیستمی", + sysIfName: "نام رابط", + sysIfMtu: "MTU رابط", + controlUrl: "درگاه کنترل", + ephemeral: "حالت موقتی", + hostname: "نام هاست", + acceptRoutes: "پذیرش مسیرها", + exitNode: "درگاه خروج", + allowLanAccess: "دسترسی به LAN", + advRoutes: "تبلیغ مسیرها", + advExitNode: "تبلیغ درگاه خروج", + udpTimeout: "مهلت UDP", + }, + ocm: { + credentialPath: "مسیر اعتبارنامه", + usagesPath: "مسیر آمار استفاده", + users: "کاربران", + userName: "نام", + userToken: "توکن", + }, + ccm: { + credentialPath: "مسیر اعتبارنامه", + usagesPath: "مسیر آمار استفاده", + users: "کاربران", + userName: "نام", + userToken: "توکن", + }, + derp: { + configPath: "مسیر پیکربندی", + verifyClientEndpoint: "درگاه تایید کلاینت", + verifyClientUrl: "URL تایید کلاینت", + meshWith: "شبکه مش", + meshPsk: "کلید پیش‌اشتراک‌گذاری", + meshPskFile: "فایل کلید پیش‌اشتراک‌گذاری", + stun: "سرور STUN", + options: "گزینه‌های DERP", + }, + naive: { + insecureConcurrency: "همزمانی ناامن", + quic: "QUIC", + quicCongestion: "کنترل تراکم QUIC", + udpOverTcp: "UDP روی TCP", + }, + anytls: { + idleInterval: "فاصله بررسی جلسه غیرفعال", + idleTimeout: "زمان پایان جلسه غیرفعال", + minIdle: "حداقل جلسات غیرفعال", + }, + }, + in: { + addr: "آدرس", + port: "پورت", + ssMethod: "روش", + ssManageable: "قابل مدیریت", + sSide: "سمت سرور", + cSide: "سمت کاربر", + multiDomain: "دامنه چندگانه", + remark: "شرح", + mdOption: "گزینه‌های دامنه چندگانه", + }, + listen: { + options: "گزینه‌های گوش‌دادن", + tcpOptions: "گزینه‌های TCP", + udpOptions: "گزینه‌های UDP", + detour: "انحراف مسیر", + detourText: "ارسال به ورودی دیگر", + disableTcpKeepAlive: "غیرفعال‌سازی TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "فاصله TCP Keep Alive", + }, + dial: { + bindIf: "اتصال به کارت شبکه", + bindIp4: "اتصال به IPv4", + bindIp6: "اتصال به IPv6", + bindNoPort: "اتصال آدرس بدون پورت", + reuseAddr: "استفاده مجدد از آدرس", + connTimeout: "مهلت ارتباط", + disableTcpKeepAlive: "غیرفعال‌سازی TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "فاصله TCP Keep Alive", + domainResolver: "حل‌کننده دامنه", + options: "گزینه‌های تماس", + detourText: "ارسال به خروجی دیگر", + }, + transport: { + enable: "فعال‌سازی انتقال", + host: "دامنه", + hosts: "دامنه‌ها", + path: "مسیر", + httpMethod: "متد درخواست", + idleTimeout: "مهلت بیکاری", + pingTimeout: "مهلت پینگ", + grpcServiceName: "نام سرویس", + grpcPws: "حفظ ارتباط بدون دیتا", + }, + mux: { + enable: "فعال‌سازی تسهیم", + maxConn: "بیشینه ارتباطات", + minStr: "کمینه استریم", + maxStr: "بیشینه استریم", + padding: "فقط با پدینگ", + enableBrutal: "فعال‌سازی شدت", + }, + out: { + addr: "آدرس سرور", + port: "پورت سرور", + addUrlTest: "افزودن URLTest", + delay: "تأخیر" + }, + rule: { + add: "ایجاد قانون", + simple: "ساده", + logical: "منطقی", + mode: "حالت", + invert: "برعکس", + ipVer: "نسخه IP", + domain: "دامنه‌ها", + domainSufix: "پسوند‌های دامنه", + domainKw: "کلمات کلیدی دامنه", + domainRgx: "رجکس دامنه", + ip: "محدوده‌های IP", + privateIp: "آدرس های IP نامعتبر", + port: "پورت‌ها", + portRange: "محدوده‌های پورت", + srcCidr: "محدوده‌های آدرس IP مبدا", + srcPrivateIp: "آدرس‌های IP مبدا نامعتبر", + srcPort: "پورت‌های مبدا", + srcPortRange: "محدوده پورتهای منبع", + ruleset: "مجموعه‌ها", + rulesetMatchSrc: "تطابق آدرس‌های مبدا با مجموعه قوانین", + preferredBy: "ترجیح داده شده توسط", + interfaceAddr: "آدرس رابط شبکه", + options: "گزینه‌های قوانین", + domainRules: "دامنه/آدرس", + srcIpRules: "آدرس مبدا", + srcPortRules: "پورت مبدا", + udpDisableDomainUnmapping: "عدم تبدیل مسیریابی دامنه", + udpConnect: "اتصال UDP", + udpTimeout: "مهلت UDP", + method: "روش", + noDrop: "عدم رهاکردن", + sniffer: "شنود کننده", + timeout: "مهلت", + strategy: "استراتژی", + etaHint: "یک آیتم برای هر خط وارد کنید. خطوط خالی و تکراری حذف خواهند شد.", + import: { + title: "ورود دسته‌ای مجموعه‌های قانون", + rulesTitle: "ورود قوانین", + urlsHint: "در هر خط یک URL. برچسب از نام فایل بدون پسوند تعیین می‌شود.", + fileHint: "فایل .txt حاوی URLها را بارگذاری کنید؛ در هر خط یک مورد.", + jsonHint: "یک شی JSON با آرایه‌های rules و/یا rule_set قرار دهید. می‌توانید کل بلوک «route»: {'{'}...{'}'} یا فقط محتوایش را بچسبانید.", + fileJsonHint: "فایل .json حاوی بلوک route را بارگذاری کنید.", + urlHint: "لینک مستقیم فایل JSON را وارد کنید (مثلاً لینک raw گیت‌هاب).", + preview: "پیش‌نمایش", + skipped: "از قبل وجود دارند، به‌صورت خاکستری نمایش داده شده‌اند", + conflict: "تعارض شناسایی شد", + merge: "ادغام — افزودن قوانین واردشده (رد برچسب تکراری مجموعه‌ها)", + replace: "جایگزینی — حذف قوانین و مجموعه‌های موجود و ورود دوباره", + pasteUrls: "چسباندن URL", + uploadTxt: "بارگذاری .txt", + uploadFile: "بارگذاری فایل", + fromUrl: "از طریق لینک", + selectTxt: "انتخاب فایل .txt", + selectJson: "انتخاب فایل .json", + parse: "تجزیه", + conflictDesc: "پیکربندی از قبل {rules} قانون و {rulesets} مجموعه قانون دارد. عمل را انتخاب کنید:", + finalOutbound: "خروجی پیش‌فرض (final)", + applyFinal: "اعمال به‌عنوان خروجی پیش‌فرض", + errNoArrays: 'هیچ آرایهٔ «rules» یا «rule_set» یافت نشد.', + errJsonParse: "خطای تجزیهٔ JSON:

{message}

", + errNoArraysFetched: 'در JSON دریافت‌شده «rules» یا «rule_set» یافت نشد.', + errFetch: "خطای دریافت:

{message}

", + errNoFile: "فایلی انتخاب نشده است.", + errNoArraysInFile: 'در فایل «rules» یا «rule_set» یافت نشد.', + }, + }, + ruleset: { + add: "ایجاد مجموعه", + format: "فرمت داده‌ها", + interval: "بازه بروزرسانی‌ها", + remote: "راه دور", + local: "محلی", + }, + dns: { + add: "ایجاد سرور DNS", + title: "سرورهای DNS", + final: "سرور نهایی", + server: "سرور", + firstServer: "سرور نخست", + cacheCapacity: "ظرفیت cache", + disableCache: "غیرفعال‌سازی cache", + disableExpire: "بدون انقضا", + independentCache: "استقلال cache", + reverseMapping: "نگاشت معکوس", + domainStrategy: "استراتژی دامنه", + local: { preferGo: "ترجیح Go" }, + rule: { + add: "ایجاد قانون DNS", + title: "قوانین DNS", + inet4Range: "محدوده IPv4", + inet6Range: "محدوده IPv6", + acceptDefault: "پذیرش پیش‌فرض", + action: { + title: "عملیات", + route: "مسیریابی", + routeOptions: "گزینه‌های مسیریابی", + reject: "رد کردن", + predefined: "پیش تعریف شده", + rewriteTtl: "بازنویسی TTL", + clientSubnet: "زیر شبکه کاربر", + rcode: "کد جواب", + rcodes: { + noError: "درست", + formerr: "درخواست نامعتبر", + servFail: "خطای سرور", + nxDomain: "یافت نشد", + refused: "رد شده", + notImp: "پیاده‌سازی نشده", + }, + answer: "پاسخ‌ها", + ns: "سرورهای دامنه", + extra: "اضافی", + }, + }, + }, + basic: { + log: { + title: "گزارش‌ها", + level: "سطح", + output: "خروجی", + timestamp: "فعال‌سازی ثبت زمان", + }, + routing: { + title: "مسیریابی", + defaultOut: "خروجی پیش‌فرض", + defaultIf: "کارت شبکه پیش‌فرض", + defaultRm: "Routing Mark پیش‌فرض", + defaultDns: "DNS پیش‌فرض", + autoBind: "انتخاب اتوماتیک کارت شبکه", + }, + exp: { + storeFakeIp: "ذخیره آدرس‌های نامعتبر", + extController: "کنترل‌گر خارجی", + extUi: "رابط‌کاربری خارجی", + extUiDownloadUrl: "آدرس دانلود رابط‌کاربری", + extUiDownloadDetour: "خروجی دانلود رابط‌کاربری", + secret: "رمز", + defaultMode: "حالت پیش‌فرض", + allowOrigin: "اجازه از مبدا", + allowPrivate: "اجازه شبکه خصوصی", + }, + }, + tls: { + enable: "فعالسازی رمزنگاری", + usePath: "مسیر فایل", + useText: "متن گواهی", + certPath: "مسیر فایل گواهی", + keyPath: "مسیر فایل کلید", + cert: "گواهی", + key: "کلید", + options: "گزینه‌های رمز‌نگاری", + minVer: "کمینه نسخه", + maxVer: "بیشینه نسخه", + cs: "مدل‌های رمزنگاری", + privKey: "کلید خصوصی", + pubKey: "کلید عمومی", + disableSni: "غیرفعال‌سازی SNI", + insecure: "تایید ارتباط ناامن", + fragment: "تکه‌بندی", + fragmentDelay: "تاخیر تکه‌بندی جایگزین", + recordFragment: "تکه‌بندی چندگانه", + store: "انبار ریشه", + ktls: "TLS هسته", + kernelTx: "ارسال", + kernelRx: "دریافت", + queryServerName: "نام سرور ECH برای جستجو", + acme: { + options: "گزینه‌های ACME", + dataDir: "مسیر داده‌ها", + defaultDomain: "دامنه پیش‌فرض", + disableChallenges: "بستن چالش‌ها", + httpChallenge: "بستن چالش HTTP", + tlsChallenge: "بستن چالش TLS", + altPorts: "پورت‌های جایگزین", + altHport: "پورت جایگزین HTTP", + altTport: "پورت جایگزین TLS", + caProvider: "فراهم کننده گواهی", + customCa: "فراهم کننده دیگر", + extAcc: "حساب خارجی", + dns01: "چالش DNS01", + dns01Provider: "فراهم کننده چالش DNS01", + dns01Params: { + api_token: "توکن API", + zone_token: "توکن زون", + access_key_id: "شناسه کلید دسترسی", + access_key_secret: "رمز کلید دسترسی", + region_id: "شناسه منطقه", + security_token: "توکن امنیتی", + username: "نام کاربری", + password: "رمز عبور", + subdomain: "زیردامنه", + server_url: "آدرس سرور", + }, + }, + }, + stats: { + upload: "آپلود", + download: "دانلود", + volume: "حجم", + usage: "استفاده", + enable: "فعال سازی گزارش ترافیک", + graphTitle: "نمودار ترافیک", + B: "ب", + KB: "ک‌ب", + MB: "م‌ب", + GB: "گ‌ب", + TB: "ت‌ب", + PB: "پ‌ب", + p: "پ", + Kp: "ک‌پ", + Mp: "م‌پ", + Gp: "گ‌پ", + Mbps: "م‌ب/ث", + }, + date: { + expiry: "انقضا", + expired: "منقضی", + d: "ر", + h: "س", + m: "د", + s: "ث", + ms: "م‌ث", + } +} diff --git a/frontend/src/locales/index.ts b/frontend/src/locales/index.ts new file mode 100644 index 0000000..9005f59 --- /dev/null +++ b/frontend/src/locales/index.ts @@ -0,0 +1,42 @@ +import { createI18n } from 'vue-i18n' +import en from './en' +import fa from './fa' +import vi from './vi' +import zhcn from './zhcn' +import zhtw from './zhtw' +import ru from './ru' + +export const i18n = createI18n({ + legacy: false, + locale: localStorage.getItem("locale") ?? 'en', + fallbackLocale: 'en', + messages: { + en: en, + fa: fa, + vi: vi, + zhHans: zhcn, + zhHant: zhtw, + ru: ru + }, +}) + +export const locale = (() => { + const l = i18n.global.locale.value + switch (l) { + case "zhHans": + return "zh-cn" + case "zhHant": + return "zh-tw" + default: + return l + } +})() + +export const languages = [ + { title: 'English', value: 'en' }, + { title: 'فارسی', value: 'fa' }, + { title: 'Tiếng Việt', value: 'vi' }, + { title: '简体中文', value: 'zhHans' }, + { title: '繁體中文', value: 'zhHant' }, + { title: 'Русский', value: 'ru' }, +] diff --git a/frontend/src/locales/ru.ts b/frontend/src/locales/ru.ts new file mode 100644 index 0000000..2029e43 --- /dev/null +++ b/frontend/src/locales/ru.ts @@ -0,0 +1,640 @@ +export default { + success: "успех", + failed: "ошибка", + enable: "Включить", + disable: "Отключить", + none: "Никакие", + all: "Все", + loading: "Загрузка...", + confirm: "Вы уверены?", + yes: "да", + no: "нет", + unlimited: "бесконечный", + type: "Тип", + protocol: "Протокол", + submit: "Отправить", + reset: "Сбросить", + now: "Сейчас", + network: "Сеть", + copyToClipboard: "Копировать в буфер обмена", + noData: "Нет данных!", + invalidLogin: "Неверный логин!", + online: "В сети", + version: "Версия", + email: "Электронная почта", + commaSeparated: "(разделено запятыми)", + count: "Количество", + template: "Шаблон", + editor: "Редактор", + error: { + dplData: "Дублирующие данные", + core: "Ошибка Sing-Box", + invalidData: "Неверные данные", + }, + theme: { + light: "Светлый", + dark: "Темный", + system: "Система", + }, + pages: { + login: "Вход", + home: "Главная", + inbounds: "Входящие", + outbounds: "Исходящие", + services: "Устройства", + endpoints: "Эндпоинты", + clients: "Клиенты", + rules: "Правила", + tls: "Настройки TLS", + basics: "Основы", + dns: "DNS", + admins: "Администраторы", + settings: "Настройки", + }, + main: { + tiles: "Плитки", + gauges: "Датчики", + charts: "Графики", + infos: "Информация", + gauge: { + cpu: "Загрузка ЦП", + mem: "Загрузка ОЗУ", + dsk: "Загрузка диска", + swp: "Загрузка Swap", + }, + chart: { + cpu: "Мониторинг ЦП", + mem: "Мониторинг ОЗУ", + net: "Сетевой трафик", + pnet: "Сетевые пакеты", + dio: "Мониторинг диска", + }, + info: { + sys: "Информация о системе", + sbd: "Информация о Sing-Box", + host: "Хост", + cpu: "ЦП", + core: "Ядро", + uptime: "Время работы", + startupTime: "Время запуска", + threads: "Потоки", + memory: "Память", + running: "Работает" + }, + backup: { + title: "Резервное копирование и восстановление", + backup: "Скачать резервную копию", + restore: "Восстановить резервную копию", + exclStats: "Исключить графики", + exclChanges: "Исключить изменения", + sbConfig: "Скачать конфигурацию Sing-Box", + }, + stats: { + title: "Использование и количество", + totalUsage: "Общее использование", + }, + }, + objects: { + inbound: "Входящий", + client: "Клиент", + outbound: "Исходящий", + endpoint: "Точка входа", + config: "Настройки", + rule: "Правило", + ruleset: "Набор правил", + service: "Устройство", + dnsserver: "DNS сервер", + dnsrule: "Правило DNS", + user: "Пользователь", + tag: "Тег", + listen: "Прослушивание", + dial: "Звонок", + tls: "TLS", + multiplex: "Мультиплекс", + transport: "Транспорт", + headers: "Заголовки", + key: "Ключ", + value: "Значение", + }, + actions: { + action: "Действие", + add: "Добавить", + addbulk: "Добавить пакетно", + editbulk: "Редактировать пакетно", + delbulk: "Удалить пакетно", + new: "Новый", + edit: "Редактировать", + del: "Удалить", + clone: "Клонировать", + test: "Тест", + testAll: "Тестировать все", + save: "Сохранить", + update: "Обновить", + submit: "Отправить", + set: "Установить", + generate: "Генерировать", + disable: "Отключить", + close: "Закрыть", + restartApp: "Перезапустить приложение", + restartSb: "Перезапустить Singbox", + apply: "Применить", + }, + login: { + title: "Вход", + username: "Имя пользователя", + unRules: "Имя пользователя не может быть пустым", + password: "Пароль", + pwRules: "Пароль не может быть пустым", + }, + menu: { + logout: "Выйти", + }, + admin: { + changeCred: "Изменить учетные данные", + oldPass: "Текущий пароль", + newUname: "Новое имя пользователя", + newPass: "Новый пароль", + lastLogin: "Последний вход", + date: "Дата", + time: "Время", + changes: "Изменения", + actor: "Исполнитель", + key: "Ключ", + action: "Действие", + api: { + title: "Токены API", + msg: "Пожалуйста, скопируйте токен ниже и сохраните его в безопасном месте. Он не будет показан заново.", + token: "Токен", + }, + }, + setting: { + interface: "Интерфейс", + sub: "Подписка", + addr: "Адрес", + port: "Порт", + webPath: "Базовый URI", + domain: "Домен", + sslKey: "Путь к SSL ключу", + sslCert: "Путь к SSL сертификату", + webUri: "URI панели", + sessionAge: "Максимальная длительность сессии", + trafficAge: "Максимальная длительность трафика", + timeLoc: "Часовой пояс", + subEncode: "Включить кодирование", + subInfo: "Включить информацию о клиенте", + path: "Путь по умолчанию", + update: "Время автоматического обновления", + subUri: "URI подписки", + jsonSub: "JSON подписка", + toDirect: "Маршрутизация на Direct", + toBlock: "Маршрутизация на Block", + timestamp: "Метка времени", + globalDns: "Глобальный DNS", + directDns: "Прямой DNS", + toDirectDns: "Маршрутизация на Direct DNS", + jsonSubOptions: "Другие параметры", + excludePkg: "Исключить пакеты", + clashSub: "Clash подписка", + mixedPort: "Смешанный порт", + tun: "Tun инбоунд", + }, + client: { + name: "Имя", + desc: "Описание", + group: "Группа", + inboundTags: "Теги входящих", + basics: "Основы", + config: "Конфигурация", + links: "Ссылки", + external: "Внешняя ссылка", + sub: "Внешняя подписка", + delayStart: "Отложенный старт", + autoReset: "Авто сброс", + resetDays: "Дней до сброса", + nextReset: "Следующий сброс", + }, + bulk: { + order: "Порядок", + random: "Случайный", + changeLimits: "Изменить лимиты", + addInbounds: "Добавить входящие", + removeInbounds: "Удалить входящие", + addDays: "Добавить дни", + addVolume: "Добавить объём", + }, + types: { + un: "Имя пользователя", + pw: "Пароль", + direct: { + overrideAddr: "Переопределить адрес", + overridePort: "Переопределить порт", + }, + hy: { + obfs: "Обфусцированный пароль", + auth: "Пароль аутентификации", + hyOptions: "Параметры Hysteria", + hy2Options: "Параметры Hysteria2", + ignoreBw: "Игнорировать пропускную способность клиента", + }, + shdwTls: { + hs: "Сервер рукопожатий", + addHS: "Добавить сервер рукопожатий", + }, + ssh: { + passphrase: "Парольная фраза", + hostKey: "Ключи хоста", + algorithm: "Алгоритмы ключей", + clientVer: "Версия клиента", + options: "Параметры SSH", + }, + tor: { + execPath: "Путь к исполняемому файлу", + dataDir: "Каталог данных", + extArgs: "Дополнительные аргументы", + }, + tuic: { + congControl: "Контроль перегрузок", + authTimeout: "Таймаут аутентификации", + hb: "Сердцебиение", + }, + tun: { + addr: "Адреса", + ifName: "Имя интерфейса", + excludeMptcp: "Исключить MPTCP", + fallbackRuleIndex: "Индекс правила iproute2 fallback", + }, + vless: { + flow: "Поток", + udpEnc: "Кодирование UDP пакетов", + }, + vmess: { + security: "Безопасность", + globalPadding: "Глобальная подкладка", + authLen: "Длина шифрования", + }, + wg: { + privKey: "Приватный ключ", + pubKey: "Публичный ключ пира", + psk: "Предварительно разделенный ключ", + localIp: "Локальные IP", + worker: "Работники", + ifName: "Имя интерфейса", + sysIf: "Системный интерфейс", + options: "Параметры Wireguard", + allowedIp: "Разрешенные IP", + peer: "Пир", + peers: "Пиры", + }, + lb: { + defaultOut: "Исходящий по умолчанию", + interruptConn: "Прервать существующие соединения", + testUrl: "Тестовый URL", + interval: "Интервал", + tolerance: "Толерантность", + urlTestOptions: "Параметры URLTest" + }, + ts: { + options: "Параметры Tailscale", + stateDir: "Каталог состояния", + authKey: "Ключ аутентификации", + relayServer: "Сервер ретрансляции", + relayServerPort: "Порт сервера ретрансляции", + relayEndpoints: "Статические точки ретрансляции", + systemInterface: "Системный интерфейс", + sysIfName: "Имя интерфейса", + sysIfMtu: "MTU интерфейса", + controlUrl: "URL управления", + ephemeral: "Эфемерный", + hostname: "Имя хоста", + acceptRoutes: "Принять маршруты", + exitNode: "Выходной узел", + allowLanAccess: "Разрешить доступ LAN", + advRoutes: "Рекламируемые маршруты", + advExitNode: "Рекламируемый выходной узел", + udpTimeout: "Таймаут UDP", + }, + ocm: { + credentialPath: "Путь к учетным данным", + usagesPath: "Путь к статистике", + users: "Пользователи", + userName: "Имя", + userToken: "Токен", + }, + ccm: { + credentialPath: "Путь к учетным данным", + usagesPath: "Путь к статистике", + users: "Пользователи", + userName: "Имя", + userToken: "Токен", + }, + derp: { + configPath: "Путь к конфигурации", + verifyClientEndpoint: "Проверить конечную точку клиента", + verifyClientUrl: "Проверить URL клиента", + meshWith: "Сеть с", + meshPsk: "Предварительно разделенный ключ", + meshPskFile: "Файл предварительно разделенного ключа", + stun: "Сервер STUN", + options: "Параметры DERP", + }, + naive: { + insecureConcurrency: "Небезопасная параллельность", + quic: "QUIC", + quicCongestion: "Управление перегрузкой QUIC", + udpOverTcp: "UDP через TCP", + }, + anytls: { + idleInterval: "Интервал проверки неактивных сессий", + idleTimeout: "Тайм-аут неактивной сессии", + minIdle: "Минимум неактивных сессий" + }, + }, + in: { + addr: "Адрес", + port: "Порт", + ssMethod: "Метод", + ssManageable: "Управляемый", + sSide: "Сторона сервера", + cSide: "Сторона клиента", + multiDomain: "Мультидомен", + remark: "Примечание", + mdOption: "Параметры мультидомена", + }, + listen: { + options: "Параметры прослушивания", + tcpOptions: "Параметры TCP", + udpOptions: "Параметры UDP", + detour: "Обход", + detourText: "Переадресация на входящий", + disableTcpKeepAlive: "Отключить TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "Интервал TCP Keep Alive", + }, + dial: { + bindIf: "Привязка к сетевому интерфейсу", + bindIp4: "Привязка к IPv4", + bindIp6: "Привязка к IPv6", + bindNoPort: "Привязка адреса без порта", + reuseAddr: "Повторное использование адреса слушателя", + connTimeout: "Таймаут подключения", + disableTcpKeepAlive: "Отключить TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "Интервал TCP Keep Alive", + domainResolver: "Разрешение домена", + options: "Параметры вызова", + detourText: "Переадресация на исходящий", + }, + transport: { + enable: "Включить транспорт", + host: "Хост", + hosts: "Хосты", + path: "Путь", + httpMethod: "Метод запроса", + idleTimeout: "Таймаут бездействия", + pingTimeout: "Таймаут пинга", + grpcServiceName: "Имя службы", + grpcPws: "Разрешить без потока", + }, + mux: { + enable: "Включить мультиплекс", + maxConn: "Максимальное количество соединений", + minStr: "Минимальное количество потоков", + maxStr: "Максимальное количество потоков", + padding: "Только подкладка", + enableBrutal: "Включить Brutal", + }, + out: { + addr: "Адрес сервера", + port: "Порт сервера", + addUrlTest: "Добавить URLTest", + delay: "Задержка", + }, + rule: { + add: "Добавить правило", + simple: "Простое", + logical: "Логическое", + mode: "Режим", + invert: "Инвертировать", + ipVer: "Версия IP", + domain: "Домены", + domainSufix: "Суффиксы доменов", + domainKw: "Ключевые слова домена", + domainRgx: "Регулярные выражения домена", + ip: "CIDR IP", + privateIp: "Недействительные диапазоны IP", + port: "Порты", + portRange: "Диапазоны портов", + srcCidr: "CIDR исходного IP", + srcPrivateIp: "Недействительные исходные IP", + srcPort: "Исходные порты", + srcPortRange: "Диапазоны исходных портов", + ruleset: "Наборы правил", + rulesetMatchSrc: "Набор правил для соответствия источника IPcidr", + preferredBy: "Предпочтительный исходящий", + interfaceAddr: "Адрес интерфейса", + options: "Параметры правила", + domainRules: "Домен/IP", + srcIpRules: "Источник IP", + srcPortRules: "Источник порта", + udpDisableDomainUnmapping: "Отключить перенос доменных имен", + udpConnect: "Подключение UDP", + udpTimeout: "Таймаут UDP", + method: "Метод", + noDrop: "Не сбрасывать", + sniffer: "Обнаружение", + timeout: "Таймаут", + strategy: "Стратегия", + etaHint: "По одной записи в строке. Пустые строки и дубликаты игнорируются.", + import: { + title: "Массовый импорт наборов правил", + rulesTitle: "Импорт правил", + urlsHint: "По одному URL в строке. Тег определяется из имени файла без расширения.", + fileHint: "Загрузите .txt файл с URL-адресами, по одному в строке.", + jsonHint: "Вставьте JSON-объект с массивами rules и/или rule_set. Можно вставить весь блок \"route\": {'{'}...{'}'} или только его содержимое.", + fileJsonHint: "Загрузите .json файл с блоком route.", + urlHint: "Укажите прямую ссылку на JSON-файл (например, raw-ссылку GitHub).", + preview: "Предпросмотр", + skipped: "уже существуют, показаны серым", + conflict: "Обнаружены конфликты", + merge: "Объединить — добавить импортируемые правила (пропустить дубликаты тегов наборов)", + replace: "Заменить — удалить существующие правила и наборы, импортировать заново", + pasteUrls: "Вставить URL", + uploadTxt: "Загрузить .txt", + uploadFile: "Загрузить файл", + fromUrl: "По ссылке", + selectTxt: "Выберите .txt файл", + selectJson: "Выберите .json файл", + parse: "Разобрать", + conflictDesc: "В конфиге уже есть {rules} правил и {rulesets} наборов правил. Выберите действие:", + finalOutbound: "Outbound по умолчанию (final)", + applyFinal: "Применить как outbound по умолчанию", + errNoArrays: 'Массивы "rules" или "rule_set" не найдены.', + errJsonParse: "Ошибка разбора JSON: {message}", + errNoArraysFetched: 'В полученном JSON нет "rules" или "rule_set".', + errFetch: "Ошибка загрузки: {message}", + errNoFile: "Файл не выбран.", + errNoArraysInFile: 'В файле нет "rules" или "rule_set".', + }, + }, + ruleset: { + add: "Добавить набор правил", + format: "Формат данных", + interval: "Интервалы обновления", + remote: "Удаленный", + local: "Локальный", + }, + dns: { + add: "Добавить DNS сервер", + title: "DNS серверы", + final: "Итоговый", + server: "Сервер", + firstServer: "Первый сервер", + cacheCapacity: "Вместимость кэша", + disableCache: "Отключить кэш", + disableExpire: "Отключить истечение", + independentCache: "Независимый кэш", + reverseMapping: "Обратное отображение", + domainStrategy: "Стратегия домена", + local: { preferGo: "Предпочитать Go" }, + rule: { + add: "Добавить правило DNS", + title: "Правила DNS", + inet4Range: "Диапазон IPv4", + inet6Range: "Диапазон IPv6", + acceptDefault: "Принять резолверы по умолчанию", + action: { + title: "Действие", + route: "Маршрутизация", + routeOptions: "Параметры маршрутизации", + reject: "Отклонить", + predefined: "Предопределенные", + rewriteTtl: "Перезаписать TTL", + clientSubnet: "Подсеть клиента", + rcode: "Код ответа", + rcodes: { + noError: "ОК", + formerr: "Неверный запрос", + servFail: "Сбой сервера", + nxDomain: "Не найдено", + refused: "Отклонено", + notImp: "Не реализовано" + }, + answer: "Ответ", + ns: "Серверы имён", + extra: "Дополнительные", + }, + } + }, + basic: { + log: { + title: "Журналы", + level: "Уровень", + output: "Вывод", + timestamp: "Включить метку времени", + }, + routing: { + title: "Маршрутизация", + defaultOut: "Исходящий по умолчанию", + defaultIf: "Сетевой интерфейс по умолчанию", + defaultRm: "Маршрут по умолчанию", + defaultDns: "DNS по умолчанию", + autoBind: "Автопривязка сетевого интерфейса", + }, + exp: { + storeFakeIp: "Хранить поддельный IP", + extController: "Внешний контроллер", + extUi: "Внешний интерфейс", + extUiDownloadUrl: "URL загрузки интерфейса", + extUiDownloadDetour: "Обход загрузки интерфейса", + secret: "Секрет", + defaultMode: "Режим по умолчанию", + allowOrigin: "Разрешить источник", + allowPrivate: "Разрешить частную сеть" + }, + }, + tls: { + enable: "Включить TLS", + usePath: "Использовать путь", + useText: "Использовать текст", + certPath: "Путь к файлу сертификата", + keyPath: "Путь к файлу ключа", + cert: "Сертификат", + key: "Ключ", + options: "Параметры TLS", + minVer: "Минимальная версия", + maxVer: "Максимальная версия", + cs: "Наборы шифров", + privKey: "Приватный ключ", + pubKey: "Публичный ключ", + disableSni: "Отключить SNI", + insecure: "Разрешить небезопасное", + fragment: "Фрагментация", + fragmentDelay: "Задержка фрагментации", + recordFragment: "Фрагментация записей", + store: "Корневое хранилище", + ktls: "Ядро TLS", + kernelTx: "TX", + kernelRx: "RX", + queryServerName: "ECH имя сервера для запроса", + acme: { + options: "Параметры ACME", + dataDir: "Каталог данных", + defaultDomain: "Домен по умолчанию", + disableChallenges: "Отключить вызовы", + httpChallenge: "Отключить HTTP вызов", + tlsChallenge: "Отключить TLS вызов", + altPorts: "Альтернативные порты", + altHport: "Альтернативный HTTP порт", + altTport: "Альтернативный TLS порт", + caProvider: "Поставщик CA", + customCa: "Пользовательский поставщик CA", + extAcc: "Внешний аккаунт", + dns01: "DNS01 вызов", + dns01Provider: "Поставщик DNS01 вызова", + dns01Params: { + api_token: "API Токен", + zone_token: "Токен зоны", + access_key_id: "ID ключа доступа", + access_key_secret: "Секрет ключа доступа", + region_id: "ID региона", + security_token: "Токен безопасности", + username: "Имя пользователя", + password: "Пароль", + subdomain: "Поддомен", + server_url: "URL сервера", + }, + }, + }, + stats: { + upload: "Загрузка", + download: "Скачивание", + volume: "Объем", + usage: "Использование", + enable: "Включить статистику", + graphTitle: "График трафика", + B: "Б", + KB: "КБ", + MB: "МБ", + GB: "ГБ", + TB: "ТБ", + PB: "ПБ", + p: "п", + Kp: "Кп", + Mp: "Мп", + Gp: "Гп", + Mbps: "Мб/с", + }, + date: { + expiry: "Срок действия", + expired: "Истек", + d: "д", + h: "ч", + m: "м", + s: "с", + ms: "мс", + }, +} + + + + diff --git a/frontend/src/locales/vi.ts b/frontend/src/locales/vi.ts new file mode 100644 index 0000000..11cbaa2 --- /dev/null +++ b/frontend/src/locales/vi.ts @@ -0,0 +1,635 @@ +export default { + success: "Thành công", + failed: "Thất bại", + enable: "Kích hoạt", + disable: "Vô hiệu hóa", + none: "Không", + all: "Tất cả", + loading: "Đang tải...", + confirm: "Bạn chắc chắn chứ?", + yes: "có", + no: "không", + unlimited: "vô hạn", + type: "Loại", + protocol: "Giao thức", + submit: "Gửi", + reset: "Đặt lại", + now: "Hiện tại", + network: "Mạng", + copyToClipboard: "Sao chép vào clipboard", + noData: "Không có dữ liệu!", + invalidLogin: "Đăng nhập không hợp lệ!", + online: "Trực tuyến", + version: "Phiên bản", + email: "Email", + commaSeparated: "(được phân tách bằng dấu phẩy)", + count: "Đếm", + template: "Mẫu", + editor: "Bản sử dụng", + error: { + dplData: "Dữ liệu trùng lặp", + core: "Lỗi Sing-Box", + invalidData: "Dữ liệu khỏ hợp lệ", + }, + theme: { + light: "Nhật", + dark: "Xanh", + system: "Phòng bán", + }, + pages: { + login: "Đăng nhập", + home: "Trang chủ", + inbounds: "Đầu Vào", + outbounds: "Đầu ra", + services: "Thiết bị", + endpoints: "Câu hình", + clients: "Khách hàng", + rules: "Quy tắc", + tls: "Cài đặt TLS", + basics: "Cơ bản", + dns: "DNS", + admins: "Quản trị viên", + settings: "Cài đặt", + }, + main: { + tiles: "OHB", + gauges: "Đồng hồ đo", + charts: "Biểu đồ", + infos: "Thông tin", + gauge: { + cpu: "Đồng hồ CPU", + mem: "Đồng hồ RAM", + dsk: "Đồng hồ Disk", + swp: "Đồng hồ Swap", + }, + chart: { + cpu: "Máy theo dõi CPU", + mem: "Máy theo dõi RAM", + net: "Băng thông mạng", + pnet: "Gói mạng", + dio: "Disk I/O", + }, + info: { + sys: "Thông tin hệ thống", + sbd: "Thông tin Sing-Box", + host: "Máy chủ", + cpu: "CPU", + core: "Nhân", + uptime: "Thời gian hoạt động", + startupTime: "Thời gian khởi động", + threads: "Luồng", + memory: "Bộ nhớ", + running: "Đang chạy" + }, + backup: { + title: "Sao lưu và khôi phục", + backup: "Tải xuống bản sao lưu", + restore: "Khôi phục bản sao lưu", + exclStats: "Loại trừ các biểu đồ", + exclChanges: "Loại trừ các thay đổi", + sbConfig: "Tải xuống cấu hình Sing-Box", + }, + stats: { + title: "Sử dụng và số lượng", + totalUsage: "Tổng sử dụng", + }, + }, + objects: { + inbound: "Đầu Vào", + client: "Máy Khách hàng", + outbound: "Đầu Ra", + endpoint: "Điểm cuối", + config: "Câu hình", + rule: "Quy tắc", + ruleset: "Bộ quy tắc", + service: "Dịch vụ", + dnsserver: "Máy chủ DNS", + dnsrule: "Quy tắc DNS", + user: "Người dùng", + tag: "Thẻ", + listen: "Nghe", + dial: "Quay số", + tls: "TLS", + multiplex: "Ghép đa truyền thông ", + transport: "Giao thông", + headers: "Tiêu đề", + key: "Chìa khóa", + value: "Giá trị", + }, + actions: { + action: "Hành động", + add: "Thêm", + addbulk: "Thêm Hàng loạt", + editbulk: "Chỉnh sửa hàng loạt", + delbulk: "Xóa hàng loạt", + new: "Mới", + edit: "Chỉnh sửa", + del: "Xóa", + clone: "Nhân bản", + test: "Kiểm tra", + testAll: "Kiểm tra tất cả", + save: "Lưu", + update: "Cập nhật", + submit: "Gửi", + set: "Đặt", + generate: "Tạo ra", + disable: "Vô hiệu hóa", + close: "Đóng", + restartApp: "Khởi động lại ứng dụng", + restartSb: "Khởi động lại Singbox", + }, + login: { + title: "Đăng nhập", + username: "Tên người dùng", + unRules: "Tên người dùng không thể trống", + password: "Mật khẩu", + pwRules: "Mật khẩu không thể trống", + }, + menu: { + logout: "Đăng xuất", + }, + admin: { + changeCred: "Thay đổi thông tin đăng nhập", + oldPass: "Mật khẩu hiện tại", + newUname: "Tên người dùng mới", + newPass: "Mật khẩu mới", + lastLogin: "Lân đăng nhập cuôi", + date: "Ngày", + time: "Thời gian", + changes: "Thay đổi", + actor: "Diễn viên", + key: "Khóa", + action: "Hành động", + api: { + title: "Mã thông báo API", + msg: "Vui lòng sao chép mã thông báo bên dưới và lưu trữ nó ở nơi an toàn. Nó sẽ không được hiển thị lại.", + token: "Mã thông báo" + }, + }, + setting: { + interface: "Giao diện", + sub: "Đăng ký", + addr: "Địa chỉ", + port: "Cổng", + webPath: "Đường dẫn gốc", + domain: "Miền", + sslKey: "Đường dẫn khóa SSL", + sslCert: "Đường dẫn chứng chỉ SSL", + webUri: "URI bảng điều khiển", + sessionAge: "Tuổi tối đa của phiên", + trafficAge: "Tuổi lưu thông tối đa", + timeLoc: "Vị trí múi giờ", + subEncode: "Kích hoạt mã hóa", + subInfo: "Kích hoạt thông tin khách hàng", + path: "Đường dẫn mặc định", + update: "Thời gian cập nhật tự động", + subUri: "URI đăng ký", + jsonSub: "Đăng ký JSON", + toDirect: "Chuyển hướng tới Trực tiếp", + toBlock: "Chuyển hướng tới Chặn", + timestamp: "Dấu thời gian", + globalDns: "DNS Toàn cầu", + directDns: "DNS Trực tiếp", + toDirectDns: "Chuyển hướng tới DNS Trực tiếp", + jsonSubOptions: "Tùy chọn Khác", + excludePkg: "Loại trừ Gói", + clashSub: "Clash đăng ký", + mixedPort: "Cổng khóa", + tun: "Tun đăng ký", + }, + client: { + name: "Tên", + desc: "Mô tả", + group: "Nhóm", + inboundTags: "Thẻ đầu vào", + basics: "Cơ bản", + config: "Cấu hình", + links: "Liên kết", + external: "Liên kết bên ngoài", + sub: "Đăng ký bên ngoài", + delayStart: "Trì hoãn bắt đầu", + autoReset: "Tự động đặt lại", + resetDays: "Số ngày đặt lại", + nextReset: "Đặt lại lần sau", + }, + bulk: { + order: "Sắp xếp", + random: "Ngẫu nhiên", + changeLimits: "Thay đổi giới hạn", + addInbounds: "Thêm inbound", + removeInbounds: "Xóa inbound", + addDays: "Thêm ngày", + addVolume: "Thêm dung lượng", + }, + types: { + un: "Tên người dùng", + pw: "Mật khẩu", + direct: { + overrideAddr: "Ghi đè Địa chỉ", + overridePort: "Ghi đè Cổng", + }, + hy: { + obfs: "Mật khẩu đã được Ẩn", + auth: "Mật khẩu Xác thực", + hyOptions: "Tùy chọn Hysteria", + hy2Options: "Tùy chọn Hysteria2", + ignoreBw: "Bỏ qua Băng thông của Client", + }, + shdwTls: { + hs: "Máy chủ Handshake", + addHS: "Thêm Máy chủ Handshake", + }, + ssh: { + passphrase: "Cụm từ mật khẩu", + hostKey: "Khóa Máy chủ", + algorithm: "Thuật toán Khóa", + clientVer: "Phiên bản Client", + options: "Tùy chọn SSH", + }, + tor: { + execPath: "Đường dẫn File thực thi", + dataDir: "Thư mục Dữ liệu", + extArgs: "Đối số Bổ sung", + }, + tuic: { + congControl: "Kiểm soát Tắc nghẽn", + authTimeout: "Thời gian chờ Xác thực", + hb: "Nhịp tim", + }, + tun: { + addr: "Địa chỉ", + ifName: "Tên Giao diện", + excludeMptcp: "Loại trừ MPTCP", + fallbackRuleIndex: "Chỉ số quy tắc dự phòng iproute2", + }, + vless: { + flow: "Luồng", + udpEnc: "Mã hóa Gói UDP", + }, + vmess: { + security: "Bảo mật", + globalPadding: "Đệm Toàn cầu", + authLen: "Chiều dài Mã hóa", + }, + wg: { + privKey: "Khóa Riêng tư", + pubKey: "Khóa Công khai của Đối tác", + psk: "Khóa được Chia sẻ trước", + localIp: "IPs Cục bộ", + worker: "Công nhân", + ifName: "Tên Giao diện", + sysIf: "Giao diện Hệ thống", + options: "Tùy chọn Wireguard", + allowedIp: "IPs được Phép", + peer: "Đối tác", + peers: "Đối tác", + }, + lb: { + defaultOut: "Đầu ra Mặc định", + interruptConn: "Ngắt kết nối hiện tại", + testUrl: "URL Kiểm tra", + interval: "Khoảng thời gian", + tolerance: "Sự dung hòa", + urlTestOptions: "Tùy chọn URLTest", + }, + ts: { + options: "Tùy chọn Tailscale", + stateDir: "Thư mục Trạng thái", + authKey: "Khóa Xác thực", + relayServer: "Máy chủ chuyển tiếp", + relayServerPort: "Cổng máy chủ chuyển tiếp", + relayEndpoints: "Điểm cuối tĩnh chuyển tiếp", + systemInterface: "Giao diện hệ thống", + sysIfName: "Tên giao diện", + sysIfMtu: "MTU giao diện", + controlUrl: "URL Cấu hình", + ephemeral: "Tạm thời", + hostname: "Tên máy chủ", + acceptRoutes: "Chấp nhận Đường dẫn", + exitNode: "Nút thoát", + allowLanAccess: "Cho phép Truy cập LAN", + advRoutes: "Quảng bá Đường dẫn", + advExitNode: "Quảng bá Nút thoát", + udpTimeout: "Thời gian Chờ UDP", + }, + ocm: { + credentialPath: "Đường dẫn Thông tin xác thực", + usagesPath: "Đường dẫn Thống kê", + users: "Người dùng", + userName: "Tên", + userToken: "Token", + }, + ccm: { + credentialPath: "Đường dẫn Thông tin xác thực", + usagesPath: "Đường dẫn Thống kê", + users: "Người dùng", + userName: "Tên", + userToken: "Token", + }, + derp: { + configPath: "Đường dẫn Cấu hình", + verifyClientEndpoint: "Điểm cuối Xác minh Máy khách", + verifyClientUrl: "URL Xác minh Máy khách", + meshWith: "Kết nối Mesh với", + meshPsk: "Khóa PSK Mesh", + meshPskFile: "Tệp Khóa PSK Mesh", + stun: "Máy chủ STUN", + options: "Tùy chọn DERP", + }, + naive: { + insecureConcurrency: "Đồng thời không an toàn", + quic: "QUIC", + quicCongestion: "Điều khiển tắc nghẽn QUIC", + udpOverTcp: "UDP qua TCP", + }, + anytls: { + idleInterval: "Khoảng kiểm tra phiên nhàn rỗi", + idleTimeout: "Thời gian chờ phiên nhàn rỗi", + minIdle: "Số phiên nhàn rỗi tối thiểu" + }, + }, + in: { + addr: "Địa chỉ", + port: "Cổng", + ssMethod: "Phương thức", + ssManageable: "Quản lý được", + sSide: "Phía Máy chủ", + cSide: "Phía Khách hàng", + multiDomain: "Nhiều Tên miền", + remark: "Ghi chú", + mdOption: "Tùy chọn Nhiều Tên miền", + }, + listen: { + options: "Tùy chọn Nghe", + tcpOptions: "Tùy chọn TCP", + udpOptions: "Tùy chọn UDP", + detour: "Lạc đạo", + detourText: "Chuyển tiếp tới đầu vào", + disableTcpKeepAlive: "Tắt TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "Khoảng thời gian TCP Keep Alive", + }, + dial: { + bindIf: "Ràng buộc tới Giao diện Mạng", + bindIp4: "Ràng buộc tới IPv4", + bindIp6: "Ràng buộc tới IPv6", + bindNoPort: "Ràng buộc địa chỉ không cổng", + reuseAddr: "Sử dụng lại Địa chỉ Nghe", + connTimeout: "Thời gian Chờ Kết nối", + disableTcpKeepAlive: "Tắt TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "Khoảng thời gian TCP Keep Alive", + domainResolver: "Trình phân giải Tên miền", + options: "Tùy chọn Gọi", + detourText: "Chuyển tiếp tới thư đi", + }, + transport: { + enable: "Kích hoạt vận chuyển", + host: "Máy chủ", + hosts: "Máy chủ", + path: "Đường dẫn", + httpMethod: "Phương thức Yêu cầu", + idleTimeout: "Thời gian Chờ Chờ đợi", + pingTimeout: "Thời gian Chờ Ping", + grpcServiceName: "Tên Dịch vụ", + grpcPws: "Cho phép mà không Có Luồng", + }, + mux: { + enable: "Bật Multiplex", + maxConn: "Số kết nối Tối đa", + minStr: "Số Luồng Tối thiểu", + maxStr: "Số Luồng Tối đa", + padding: "Chỉ đệm", + enableBrutal: "Bật Brutal", + }, + out: { + addr: "Địa chỉ Máy chủ", + port: "Cổng Máy chủ", + addUrlTest: "Thêm URLTest", + delay: "Độ trễ", + }, + rule: { + add: "Thêm Quy tắc", + simple: "Đơn giản", + logical: "Logic", + mode: "Chế độ", + invert: "Nghịch đảo", + ipVer: "Phiên bản IP", + domain: "Tên miền", + domainSufix: "Hậu tố Miền", + domainKw: "Từ khóa Miền", + domainRgx: "Regex Miền", + ip: "CIDRs IP", + privateIp: "Dải IP Không hợp lệ", + port: "Cổng", + portRange: "Dải Cổng", + srcCidr: "CIDRs IP Nguồn", + srcPrivateIp: "IP Nguồn Không hợp lệ", + srcPort: "Cổng Nguồn", + srcPortRange: "Dải Cổng Nguồn", + ruleset: "Bộ quy tắc", + rulesetMatchSrc: "Bộ quy tắc IPcidr Phù hợp Nguồn", + preferredBy: "Ưu tiên theo đầu ra", + interfaceAddr: "Địa chỉ giao diện", + options: "Tùy chọn Quy tắc", + domainRules: "Tên miền/IP", + srcIpRules: "IP Nguồn", + srcPortRules: "Cổng Nguồn", + udpDisableDomainUnmapping: "Không màm mạng tiền lập tên miền", + udpConnect: "Kết nối UDP", + udpTimeout: "Thời gian Chờ UDP", + method: "Phương pháp", + noDrop: "Không Tháp", + sniffer: "Kiểm tra Sniffer", + timeout: "Thời gian Chờ Sniffing", + strategy: "Chiến lệ", + etaHint: "Mỗi dòng một mục. Dòng trống và mục trùng lặp sẽ bị bỏ qua.", + import: { + title: "Nhập hàng loạt bộ quy tắc", + rulesTitle: "Nhập quy tắc", + urlsHint: "Một URL mỗi dòng. Thẻ được lấy từ tên tệp (không gồm phần mở rộng).", + fileHint: "Tải lên tệp .txt chứa URL, mỗi dòng một URL.", + jsonHint: "Dán đối tượng JSON có mảng rules và/hoặc rule_set. Có thể dán cả khối \"route\": {'{'}...{'}'} hoặc chỉ nội dung bên trong.", + fileJsonHint: "Tải lên tệp .json có khối route.", + urlHint: "Nhập liên kết trực tiếp tới tệp JSON (ví dụ liên kết raw GitHub).", + preview: "Xem trước", + skipped: "đã tồn tại, hiển thị màu xám", + conflict: "Phát hiện xung đột", + merge: "Gộp — thêm quy tắc đã nhập (bỏ qua thẻ bộ trùng)", + replace: "Thay thế — xóa quy tắc và bộ hiện có, nhập lại", + pasteUrls: "Dán URL", + uploadTxt: "Tải lên .txt", + uploadFile: "Tải lên tệp", + fromUrl: "Theo URL", + selectTxt: "Chọn tệp .txt", + selectJson: "Chọn tệp .json", + parse: "Phân tích", + conflictDesc: "Cấu hình đã có {rules} quy tắc và {rulesets} bộ quy tắc. Chọn thao tác:", + finalOutbound: "Outbound mặc định (final)", + applyFinal: "Áp dụng làm outbound mặc định", + errNoArrays: 'Không tìm thấy mảng "rules" hoặc "rule_set".', + errJsonParse: "Lỗi phân tích JSON: {message}", + errNoArraysFetched: 'Không tìm thấy "rules" hoặc "rule_set" trong JSON đã tải.', + errFetch: "Lỗi tải: {message}", + errNoFile: "Chưa chọn tệp.", + errNoArraysInFile: 'Không tìm thấy "rules" hoặc "rule_set" trong tệp.', + }, + }, + ruleset: { + add: "Thêm Bộ quy tắc", + format: "Định dạng Dữ liệu", + interval: "Khoảng cách Cập nhật", + remote: "Xa", + local: "Cục bộ", + }, + dns: { + add: "Thêm Máy chủ DNS", + title: "Máy chủ DNS", + final: "Cuối cùng", + server: "Máy chủ", + firstServer: "Máy chủ Đầu tiên", + cacheCapacity: "Nội dung bộ nhớ", + disableCache: "Vô hiệu hóa bộ nhớ đệm", + disableExpire: "Vô hiệu hóa hệ thống", + independentCache: "Bộ nhớ rẽ", + reverseMapping: "Màm mạng tên lập", + domainStrategy: "Chiến lược Domain", + local: { preferGo: "Ưu tiên Go" }, + rule: { + add: "Thêm Quy tắc DNS", + title: "Quy tắc DNS", + inet4Range: "Dải CIDR IPv4", + inet6Range: "Dải CIDR IPv6", + acceptDefault: "Chấp nhận Mặc định", + action: { + title: "Hành động", + route: "Định tuyến", + routeOptions: "Tùy chọn định tuyến", + reject: "Từ chối", + predefined: "Định nghĩa sẵn", + rewriteTtl: "Ghi đè TTL", + clientSubnet: "Subnet của máy khách", + rcode: "Máy chủ tên", + rcodes: { + noError: "OK", + formerr: "Yêu cầu không hợp lệ", + servFail: "Lỗi máy chủ", + nxDomain: "Không tìm thấy", + refused: "Bị từ chối", + notImp: "Chưa được hỗ trợ" + }, + answer: "Câu trả lời", + ns: "Máy chủ tên", + extra: "Bổ sung" + } + } + }, + basic: { + log: { + title: "Nhật ký", + level: "Mức độ", + output: "Đầu ra", + timestamp: "Bật Dấu thời gian", + }, + routing: { + title: "Định tuyến", + defaultOut: "Ra ngoài Mặc định", + defaultIf: "NIC Mặc định", + defaultRm: "Đánh dấu Định tuyến Mặc định", + defaultDns: "Máy chủ DNS Mặc định", + autoBind: "Tự động Ràng buộc NIC", + }, + exp: { + storeFakeIp: "Lưu IP Giả mạo", + extController: "Trình điều khiển bên ngoài", + extUi: "Giao diện người dùng bên ngoài", + extUiDownloadUrl: "URL tải giao diện", + extUiDownloadDetour: "Chuyển hướng tải giao diện", + secret: "Mã bí mật", + defaultMode: "Chế độ mặc định", + allowOrigin: "Cho phép nguồn gốc", + allowPrivate: "Cho phép mạng riêng", + }, + }, + tls : { + enable: "Kích hoạt TLS", + usePath: "Sử dụng đường dẫn", + useText: "Sử dụng văn bản", + certPath: "Đường dẫn tệp chứng chỉ", + keyPath: "Đường dẫn tệp khóa", + cert: "Chứng chỉ", + key: "Khóa", + options: "Tùy chọn TLS", + minVer: "Phiên bản Tối thiểu", + maxVer: "Phiên bản Tối đa", + cs: "Các bộ mã hóa", + privKey: "Khóa riêng", + pubKey: "Khóa Công khai", + disableSni: "Tắt SNI", + insecure: "Cho phép Không an toàn", + fragment: "Kiểm tra hệ thống", + fragmentDelay: "Khoảng thời gian hệ thống", + recordFragment: "Kiểm tra bộ nhớ", + store: "Kho lưu trữ gốc", + ktls: "TLS nhân", + kernelTx: "TX", + kernelRx: "RX", + queryServerName: "Tên máy chủ truy vấn ECH", + acme: { + options: "Tùy chọn ACME", + dataDir: "Thư mục Dữ liệu", + defaultDomain: "Tên miền Mặc định", + disableChallenges: "Vô hiệu hóa Thách thức", + httpChallenge: "Vô hiệu hóa Thách thức HTTP", + tlsChallenge: "Vô hiệu hóa Thách thức TLS", + altPorts: "Cổng Thay thế", + altHport: "Cổng HTTP Thay thế", + altTport: "Cổng TLS Thay thế", + caProvider: "Nhà cung cấp CA", + customCa: "Nhà cung cấp CA Tùy chỉnh", + extAcc: "Tài khoản Bên ngoài", + dns01: "Thách thức DNS01", + dns01Provider: "Nhà cung cấp Thách thức DNS01", + dns01Params: { + api_token: "Mã API", + zone_token: "Mã Vùng", + access_key_id: "ID Khóa Truy cập", + access_key_secret: "Bí mật Khóa Truy cập", + region_id: "ID Khu vực", + security_token: "Mã Bảo mật", + username: "Tên đăng nhập", + password: "Mật khẩu", + subdomain: "Tên miền phụ", + server_url: "URL Máy chủ", + }, + }, + }, + stats: { + upload: "Tải lên", + download: "Tải xuống", + volume: "Thể tích", + usage: "Sử dụng", + enable: "Kích hoạt thống kê", + graphTitle: "Biểu đồ lưu lượng", + B: "B", + KB: "KB", + MB: "MB", + GB: "GB", + TB: "TB", + PB: "PB", + p: "ph", + Kp: "Kph", + Mp: "Mph", + Gp: "Gph", + Mbps: "Mbps", + }, + date: { + expiry: "Hết hạn", + expired: "Đã hết hạn", + d: "ng", + h: "g", + m: "p", + s: "s", + ms: "ms", + }, +} diff --git a/frontend/src/locales/zhcn.ts b/frontend/src/locales/zhcn.ts new file mode 100644 index 0000000..0a9f012 --- /dev/null +++ b/frontend/src/locales/zhcn.ts @@ -0,0 +1,635 @@ +export default { + success: "成功", + failed: "失败", + enable: "启用", + disable: "禁用", + none: "无", + all: "全部", + loading: "加载中...", + confirm: "是否确定?", + yes: "确认", + no: "取消", + unlimited: "无限", + type: "类型", + protocol: "协议", + submit: "提交", + reset: "重置", + now: "当前", + network: "网络", + copyToClipboard: "复制到剪贴板", + noData: "无数据!", + invalidLogin: "登录无效!", + online: "在线", + version: "版本", + email: "电子邮件", + commaSeparated: "(逗号分隔)", + count: "计数", + template: "模板", + editor: "编辑器", + error: { + dplData: "重复数据", + core: "Sing-Box 错误", + invalidData: "无效数据", + }, + theme: { + light: "浅色", + dark: "深色", + system: "跟随系统", + }, + pages: { + login: "登录", + home: "主页", + inbounds: "入站管理", + outbounds: "出站管理", + services: "服务管理", + endpoints: "节点管理", + clients: "用户管理", + rules: "路由列表", + tls: "TLS 设置", + basics: "基础信息", + dns: "DNS", + admins: "管理员", + settings: "设置", + }, + main: { + tiles: "信息卡", + gauges: "仪表板", + charts: "图表", + infos: "信息", + gauge: { + cpu: "CPU 仪表", + mem: "RAM 仪表", + dsk: "Disk 仪表", + swp: "Swap 仪表", + }, + chart: { + cpu: "CPU 监视器", + mem: "RAM 监视器", + net: "网络带宽", + pnet: "网络数据包", + dio: "Disk I/O", + }, + info: { + sys: "系统信息", + sbd: "运行信息", + host: "主机", + cpu: "CPU", + core: "核心", + uptime: "运行时间", + startupTime: "启动时间", + threads: "线程", + memory: "内存", + running: "运行状态" + }, + backup: { + title: "备份与恢复", + backup: "下载备份", + restore: "恢复备份", + exclStats: "排除图表数据", + exclChanges: "排除变更数据", + sbConfig: "下载 Sing-Box 配置", + }, + stats: { + title: "使用量与统计", + totalUsage: "总用量", + }, + }, + objects: { + inbound: "入站", + client: "客户端", + outbound: "出站", + endpoint: "节点", + config: "配置", + rule: "规则", + ruleset: "规则集", + service: "服务", + dnsserver: "DNS 服务器", + dnsrule: "DNS规则", + user: "用户", + tag: "标签", + listen: "监听", + dial: "拨号", + tls: "TLS", + multiplex: "多路复用", + transport: "传输", + headers: "标头", + key: "键", + value: "值", + }, + actions: { + action: "操作", + add: "添加", + addbulk: "批量添加", + editbulk: "批量编辑", + delbulk: "批量删除", + new: "新建", + edit: "编辑", + del: "删除", + clone: "克隆", + test: "测试", + testAll: "测试全部", + save: "保存", + update: "更新", + submit: "提交", + set: "设置", + generate: "生成", + disable: "禁用", + close: "关闭", + restartApp: "重启面板", + restartSb: "重启 Singbox", + }, + login: { + title: "登录", + username: "用户名", + unRules: "用户名不能为空", + password: "密码", + pwRules: "密码不能为空", + }, + menu: { + logout: "退出登录", + }, + admin: { + changeCred: "更改凭据", + oldPass: "当前密码", + newUname: "新用户名", + newPass: "新密码", + lastLogin: "上次登录", + date: "日期", + time: "时间", + changes: "更改", + actor: "执行者", + key: "键", + action: "操作", + api: { + title: "API 令牌", + msg: "请复制令牌并保存到安全的地方。它将不再显示。", + token: "令牌", + }, + }, + setting: { + interface: "界面", + sub: "订阅", + addr: "地址", + port: "端口", + webPath: "面板路径", + domain: "域名", + sslKey: "SSL 密钥 (Key) 路径", + sslCert: "SSL 证书 (cert) 路径", + webUri: "面板 URI", + sessionAge: "会话超时时限", + trafficAge: "流量过期时限", + timeLoc: "时区", + subEncode: "启用 Base64 编码", + subInfo: "启用用户信息", + path: "默认路径", + update: "自动更新时间", + subUri: "订阅 URI", + jsonSub: "JSON 订阅", + toDirect: "路由到直连", + toBlock: "路由到阻止", + timestamp: "时间戳", + globalDns: "全局 DNS", + directDns: "直连 DNS", + toDirectDns: "路由到直连 DNS", + jsonSubOptions: "其他选项", + excludePkg: "排除包", + clashSub: "Clash 订阅", + mixedPort: "混合入站端口", + tun: "Tun 入站", + }, + client: { + name: "名称", + desc: "描述", + group: "组", + inboundTags: "入站标签", + basics: "基础", + config: "配置", + links: "链接", + external: "外部链接", + sub: "外部订阅", + delayStart: "延迟启动", + autoReset: "自动重置", + resetDays: "重置天数", + nextReset: "下次重置", + }, + bulk: { + order: "排序", + random: "随机", + changeLimits: "更改限制", + addInbounds: "添加入站", + removeInbounds: "移除入站", + addDays: "增加天数", + addVolume: "增加流量", + }, + types: { + un: "用户名", + pw: "密码", + direct: { + overrideAddr: "覆盖地址", + overridePort: "覆盖端口", + }, + hy: { + obfs: "混淆密码", + auth: "认证密码", + hyOptions: "Hysteria 选项", + hy2Options: "Hysteria2 选项", + ignoreBw: "忽略客户端带宽", + }, + shdwTls: { + hs: "握手服务器", + addHS: "添加握手服务器", + }, + ssh: { + passphrase: "密码短语", + hostKey: "主机密钥", + algorithm: "密钥算法", + clientVer: "客户端版本", + options: "SSH 选项", + }, + tor: { + execPath: "可执行文件路径", + dataDir: "数据目录", + extArgs: "额外参数", + }, + tuic: { + congControl: "拥塞控制", + authTimeout: "认证超时", + hb: "心跳包", + }, + tun: { + addr: "地址", + ifName: "接口名称", + excludeMptcp: "排除 MPTCP", + fallbackRuleIndex: "iproute2 回退规则索引", + }, + vless: { + flow: "流控", + udpEnc: "UDP 数据包编码", + }, + vmess: { + security: "安全性", + globalPadding: "全局填充", + authLen: "加密长度", + }, + wg: { + privKey: "私钥", + pubKey: "对等方公钥", + psk: "预共享密钥", + localIp: "本地 IP 地址", + worker: "工作线程", + ifName: "接口名称", + sysIf: "系统接口", + options: "WireGuard 选项", + allowedIp: "允许的 IP 地址", + peer: "对等体", + peers: "对等体", + }, + lb: { + defaultOut: "默认出站", + interruptConn: "中断现有连接", + testUrl: "测试 URL", + interval: "间隔", + tolerance: "容错", + urlTestOptions: "URLTest 选项", + }, + ts: { + options: "Tailscale 选项", + stateDir: "状态目录", + authKey: "认证密钥", + relayServer: "中继服务器", + relayServerPort: "中继服务器端口", + relayEndpoints: "中继静态端点", + systemInterface: "系统接口", + sysIfName: "接口名称", + sysIfMtu: "接口 MTU", + controlUrl: "控制 URL", + ephemeral: "临时节点", + hostname: "主机名", + acceptRoutes: "接受路由", + exitNode: "出口节点", + allowLanAccess: "允许 LAN 访问", + advRoutes: "广告路由", + advExitNode: "广告出口节点", + udpTimeout: "UDP 超时", + }, + ocm: { + credentialPath: "凭据路径", + usagesPath: "用量统计路径", + users: "用户", + userName: "名称", + userToken: "令牌", + }, + ccm: { + credentialPath: "凭据路径", + usagesPath: "用量统计路径", + users: "用户", + userName: "名称", + userToken: "令牌", + }, + derp: { + configPath: "配置路径", + verifyClientEndpoint: "验证客户端端点", + verifyClientUrl: "验证客户端 URL", + meshWith: "与其他 DERP 节点网格连接", + meshPsk: "网格预共享密钥", + meshPskFile: "网格预共享密钥文件", + stun: "STUN 服务器", + options: "DERP 选项", + }, + naive: { + insecureConcurrency: "不安全并发数", + quic: "QUIC", + quicCongestion: "QUIC 拥塞控制", + udpOverTcp: "UDP over TCP", + }, + anytls: { + idleInterval: "空闲会话检查间隔", + idleTimeout: "空闲会话超时", + minIdle: "最小空闲会话数" + }, + }, + in: { + addr: "地址", + port: "端口", + ssMethod: "方法", + ssManageable: "可管理的", + sSide: "服务器端", + cSide: "客户端", + multiDomain: "多域名", + remark: "备注", + mdOption: "多域名选项", + }, + listen: { + options: "监听选项", + tcpOptions: "TCP 选项", + udpOptions: "UDP 选项", + detour: "转发", + detourText: "转发到入站", + disableTcpKeepAlive: "禁用 TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "TCP Keep Alive 间隔", + }, + dial: { + bindIf: "绑定到网络接口", + bindIp4: "绑定到 IPv4", + bindIp6: "绑定到 IPv6", + bindNoPort: "绑定地址不占端口", + reuseAddr: "重用监听地址", + connTimeout: "连接超时", + disableTcpKeepAlive: "禁用 TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "TCP Keep Alive 间隔", + domainResolver: "域名解析器", + options: "拨号选项", + detourText: "转发至出站", + }, + transport: { + enable: "启用传输", + host: "主机域名", + hosts: "主机域名列表", + path: "HTTP 请求路径", + httpMethod: "HTTP 请求方法", + idleTimeout: "空闲超时", + pingTimeout: "Ping 超时", + grpcServiceName: "gRPC 服务名称", + grpcPws: "允许无流时保持连接", + }, + mux: { + enable: "启用多路复用", + maxConn: "最大连接数", + minStr: "最小流数", + maxStr: "最大流数", + padding: "仅允许填充连接", + enableBrutal: "启用 TCP Brutal", + }, + out: { + addr: "服务器地址", + port: "服务器端口", + addUrlTest: "添加 URLTest", + delay: "延迟", + }, + rule: { + add: "添加规则", + simple: "简单", + logical: "逻辑", + mode: "模式", + invert: "反选结果", + ipVer: "IP 版本", + domain: "域名", + domainSufix: "域名后缀", + domainKw: "域名关键词", + domainRgx: "域名正则表达式", + ip: "IP CIDR", + privateIp: "匹配非公开 IP", + port: "端口", + portRange: "端口范围", + srcCidr: "源 IP CIDR", + srcPrivateIp: "匹配非公开源 IP", + srcPort: "源端口", + srcPortRange: "源端口范围", + ruleset: "规则集", + rulesetMatchSrc: "规则集 IP CIDR 匹配源 IP", + preferredBy: "优选出站", + interfaceAddr: "接口地址", + options: "规则选项", + domainRules: "域名/IP", + srcIpRules: "源 IP", + srcPortRules: "源端口", + udpDisableDomainUnmapping: "禁用域名解析映射", + udpConnect: "启用 UDP 连接", + udpTimeout: "UDP 超时", + method: "方法", + noDrop: "不丢弃", + sniffer: "嗅探", + timeout: "超时", + strategy: "策略", + etaHint: "每行一项。空行和重复项将被忽略。", + import: { + title: "批量导入规则集", + rulesTitle: "导入规则", + urlsHint: "每行一个 URL。标签由文件名(不含扩展名)决定。", + fileHint: "上传包含 URL 的 .txt 文件,每行一个。", + jsonHint: "粘贴包含 rules 和/或 rule_set 数组的 JSON 对象。可粘贴整个 \"route\" 块:{'{'}...{'}'} 或仅其内容。", + fileJsonHint: "上传包含 route 块的 .json 文件。", + urlHint: "填写 JSON 文件的直链(例如 GitHub raw 链接)。", + preview: "预览", + skipped: "已存在,以灰色显示", + conflict: "检测到冲突", + merge: "合并 — 添加导入的规则(跳过重复的规则集标签)", + replace: "替换 — 删除现有规则与规则集后重新导入", + pasteUrls: "粘贴 URL", + uploadTxt: "上传 .txt", + uploadFile: "上传文件", + fromUrl: "通过链接", + selectTxt: "选择 .txt 文件", + selectJson: "选择 .json 文件", + parse: "解析", + conflictDesc: "配置中已有 {rules} 条规则和 {rulesets} 个规则集。请选择操作:", + finalOutbound: "默认出站(final)", + applyFinal: "设为默认出站", + errNoArrays: '未找到 "rules" 或 "rule_set" 数组。', + errJsonParse: "JSON 解析错误:{message}", + errNoArraysFetched: '获取的 JSON 中未找到 "rules" 或 "rule_set"。', + errFetch: "获取失败:{message}", + errNoFile: "未选择文件。", + errNoArraysInFile: '文件中未找到 "rules" 或 "rule_set"。', + }, + }, + ruleset: { + add: "添加规则集", + format: "数据格式", + interval: "更新间隔", + remote: "远程", + local: "本地", + }, + dns: { + add: "添加 DNS 服务器", + title: "DNS 服务器", + final: "最终", + server: "服务器", + firstServer: "首选服务器", + cacheCapacity: "缓存容量", + disableCache: "禁用缓存", + disableExpire: "禁用过期", + independentCache: "独立缓存", + reverseMapping: "反向映射", + domainStrategy: "域名解析策略", + local: { preferGo: "优先使用 Go" }, + rule: { + add: "添加 DNS 规则", + title: "DNS 规则", + inet4Range: "IPv4 范围", + inet6Range: "IPv6 范围", + acceptDefault: "接受默认", + action: { + title: "操作", + route: "路由", + routeOptions: "路由选项", + reject: "拒绝", + predefined: "预定义", + rewriteTtl: "重写 TTL", + clientSubnet: "客户端子网", + rcode: "响应码", + rcodes: { + noError: "正常", + formerr: "请求错误", + servFail: "服务器故障", + nxDomain: "未找到", + refused: "被拒绝", + notImp: "未实现" + }, + answer: "回答", + ns: "名称服务器", + extra: "附加" + } + } + }, + basic: { + log: { + title: "日志", + level: "级别", + output: "输出", + timestamp: "启用时间戳", + }, + routing: { + title: "路由", + defaultOut: "默认出站", + defaultIf: "默认网卡", + defaultRm: "默认路由标记", + defaultDns: "默认 DNS 解析器", + autoBind: "自动绑定网卡", + }, + exp: { + storeFakeIp: "持久化 Fake-IP", + extController: "外部控制器", + extUi: "外部界面", + extUiDownloadUrl: "界面下载地址", + extUiDownloadDetour: "界面下载绕行", + secret: "密钥", + defaultMode: "默认模式", + allowOrigin: "允许来源", + allowPrivate: "允许私有网络", + }, + }, + tls : { + enable: "启用 TLS", + usePath: "使用外部路径", + useText: "使用文件内容", + certPath: "证书文件路径", + keyPath: "私钥文件路径", + cert: "证书文件内容", + key: "私钥文件内容", + options: "TLS 选项", + minVer: "最低版本", + maxVer: "最高版本", + cs: "密码套件", + privKey: "私钥", + pubKey: "公钥", + disableSni: "禁用 SNI", + insecure: "允许不安全", + fragment: "启用", + fragmentDelay: "启用后延迟", + recordFragment: "启用", + store: "根证书库", + ktls: "内核 TLS", + kernelTx: "发送", + kernelRx: "接收", + queryServerName: "ECH 查询服务器名称", + acme: { + options: "ACME 选项", + dataDir: "数据目录", + defaultDomain: "默认域名", + disableChallenges: "禁用挑战", + httpChallenge: "禁用 HTTP 挑战", + tlsChallenge: "禁用 TLS 挑战", + altPorts: "替代端口", + altHport: "替代 HTTP 端口", + altTport: "替代 TLS 端口", + caProvider: "CA 提供商", + customCa: "自定义 CA 提供商", + extAcc: "外部账户", + dns01: "DNS01 挑战", + dns01Provider: "DNS01 挑战提供商", + dns01Params: { + api_token: "API 令牌", + zone_token: "Zone 令牌", + access_key_id: "访问密钥 ID", + access_key_secret: "访问密钥密文", + region_id: "区域 ID", + security_token: "安全令牌", + username: "用户名", + password: "密码", + subdomain: "子域名", + server_url: "服务器 URL", + }, + }, + }, + stats: { + upload: "上传", + download: "下载", + volume: "流量", + usage: "已用", + enable: "启用统计", + graphTitle: "流量图表", + B: "B", + KB: "KB", + MB: "MB", + GB: "GB", + TB: "TB", + PB: "PB", + p: "p", + Kp: "Kp", + Mp: "Mp", + Gp: "Gp", + Mbps: "Mbps", + }, + date: { + expiry: "到期", + expired: "已到期", + d: "天", + h: "时", + m: "分", + s: "秒", + ms: "毫秒", + }, +} diff --git a/frontend/src/locales/zhtw.ts b/frontend/src/locales/zhtw.ts new file mode 100644 index 0000000..727cef3 --- /dev/null +++ b/frontend/src/locales/zhtw.ts @@ -0,0 +1,635 @@ +export default { + success: "成功", + failed: "失敗", + enable: "啟用", + disable: "禁用", + none: "無", + all: "全部", + loading: "加載中...", + confirm: "是否確定?", + yes: "確認", + no: "取消", + unlimited: "無限", + type: "類型", + protocol: "協定", + submit: "提交", + reset: "重置", + now: "當前", + network: "網絡", + copyToClipboard: "復製到剪貼板", + noData: "無數據!", + invalidLogin: "登錄無效!", + online: "在線", + version: "版本", + email: "電子郵件", + commaSeparated: "(逗號分隔)", + count: "計數", + template: "模板", + editor: "編輯器", + error: { + dplData: "重複數據", + core: "Sing-Box 錯誤", + invalidData: "無效數據", + }, + theme: { + light: "明亮", + dark: "暗黑", + system: "系統", + }, + pages: { + login: "登錄", + home: "主頁", + inbounds: "入站管理", + outbounds: "出站管理", + services: "服務管理", + endpoints: "端點管理", + clients: "用戶管理", + rules: "路由列表", + tls: "TLS 設置", + basics: "基礎信息", + dns: "DNS", + admins: "管理員", + settings: "設置", + }, + main: { + tiles: "信息卡", + gauges: "儀表板", + charts: "圖表", + infos: "信息", + gauge: { + cpu: "CPU 儀表", + mem: "RAM 儀表", + dsk: "Disk 儀表", + swp: "Swap 儀表", + }, + chart: { + cpu: "CPU 監視器", + mem: "RAM 監視器", + net: "網絡帶寬", + pnet: "網絡數據包", + dio: "Disk I/O", + }, + info: { + sys: "系統信息", + sbd: "運行信息", + host: "主機", + cpu: "CPU", + core: "核心", + uptime: "運行時間", + startupTime: "啟動時間", + threads: "線程", + memory: "內存", + running: "運行狀態" + }, + backup: { + title: "備份與恢復", + backup: "下載備份", + restore: "恢復備份", + exclStats: "排除圖表記錄", + exclChanges: "排除更改記錄", + sbConfig: "下載 Sing-Box 配置", + }, + stats: { + title: "使用量與統計", + totalUsage: "總用量", + }, + }, + objects: { + inbound: "入站", + client: "客戶端", + outbound: "出站", + endpoint: "端點", + config: "配置", + rule: "規則", + ruleset: "規則集", + service: "服務", + dnsserver: "DNS 服務器", + dnsrule: "DNS 規則", + user: "用戶", + tag: "標簽", + listen: "聽", + dial: "撥號", + tls: "TLS", + multiplex: "多路復用", + transport: "傳輸", + headers: "方法", + key: "鑰匙", + value: "價值", + }, + actions: { + action: "操作", + add: "添加", + addbulk: "批量添加", + editbulk: "批量編輯", + delbulk: "批量刪除", + new: "新建", + edit: "編輯", + del: "刪除", + clone: "克隆", + test: "測試", + testAll: "測試全部", + save: "保存", + update: "更新", + submit: "提交", + set: "設置", + generate: "生成", + disable: "禁用", + close: "關閉", + restartApp: "重啟面板", + restartSb: "重啟 Singbox", + }, + login: { + title: "登錄", + username: "用戶名", + unRules: "用戶名不能為空", + password: "密碼", + pwRules: "密碼不能為空", + }, + menu: { + logout: "退出登錄", + }, + admin: { + changeCred: "更改憑據", + oldPass: "當前密碼", + newUname: "新用戶名", + newPass: "新密碼", + lastLogin: "上次登入", + date: "日期", + time: "時間", + changes: "更改", + actor: "執行者", + key: "鍵", + action: "操作", + api: { + title: "API 憑據", + msg: "請複製下面的憑據並儲存在安全的地方。它將不再顯示。", + token: "憑據", + }, + }, + setting: { + interface: "界面", + sub: "訂閱", + addr: "地址", + port: "端口", + webPath: "基本 URI", + domain: "域名", + sslKey: "SSL 密鑰 (Key) 路徑", + sslCert: "SSL 證書 (cert) 路徑", + webUri: "面板 URI", + sessionAge: "會話最大連接數", + trafficAge: "流量最大年齡", + timeLoc: "時區", + subEncode: "啟用編碼", + subInfo: "啟用用戶信息", + path: "默認路徑", + update: "自動更新時間", + subUri: "訂閱 URL", + jsonSub: "JSON 訂閱", + toDirect: "路由到直連", + toBlock: "路由到阻止", + timestamp: "時間戳", + globalDns: "全局 DNS", + directDns: "直連 DNS", + toDirectDns: "路由到直連 DNS", + jsonSubOptions: "其他選項", + excludePkg: "排除包", + clashSub: "Clash 訂閱", + mixedPort: "混合入站端口", + tun: "Tun 入站", + }, + client: { + name: "名稱", + desc: "描述", + group: "組", + inboundTags: "入站標簽", + basics: "基礎", + config: "配置", + links: "鏈接", + external: "外部鏈接", + sub: "外部訂閱", + delayStart: "延遲啟動", + autoReset: "自動重置", + resetDays: "重置天數", + nextReset: "下次重置", + }, + bulk: { + order: "排序", + random: "隨機", + changeLimits: "變更限制", + addInbounds: "添加入站", + removeInbounds: "移除入站", + addDays: "增加天數", + addVolume: "增加流量", + }, + types: { + un: "用戶名", + pw: "密碼", + direct: { + overrideAddr: "覆蓋地址", + overridePort: "覆蓋端口", + }, + hy: { + obfs: "混淆密碼", + auth: "驗證密碼", + hyOptions: "Hysteria 選項", + hy2Options: "Hysteria2 選項", + ignoreBw: "忽略客戶端帶寬", + }, + shdwTls: { + hs: "握手服務器", + addHS: "添加握手服務器", + }, + ssh: { + passphrase: "密語", + hostKey: "主機密鑰", + algorithm: "密鑰算法", + clientVer: "客戶端版本", + options: "SSH 選項", + }, + tor: { + execPath: "可執行文件路徑", + dataDir: "數據目錄", + extArgs: "額外參數", + }, + tuic: { + congControl: "擁塞控制", + authTimeout: "身份驗證超時", + hb: "心跳", + }, + tun: { + addr: "地址", + ifName: "介面名稱", + excludeMptcp: "排除 MPTCP", + fallbackRuleIndex: "iproute2 回退規則索引", + }, + vless: { + flow: "流量", + udpEnc: "UDP 封包編碼", + }, + vmess: { + security: "安全性", + globalPadding: "全局填充", + authLen: "加密長度", + }, + wg: { + privKey: "私鑰", + pubKey: "對等方公鑰", + psk: "預共享密鑰", + localIp: "本地 IP", + worker: "工作線程", + ifName: "介面名稱", + sysIf: "系統介面", + options: "Wireguard 選項", + allowedIp: "允許的 IP", + peer: "對等方", + peers: "對等方", + }, + lb: { + defaultOut: "默認外部", + interruptConn: "中斷現有連接", + testUrl: "測試 URL", + interval: "間隔", + tolerance: "容忍度", + urlTestOptions: "URLTest 選項" + }, + ts: { + options: "Tailscale 選項", + stateDir: "狀態目錄", + authKey: "授權密鑰", + relayServer: "轉發伺服器", + relayServerPort: "轉發伺服器端口", + relayEndpoints: "轉發靜態端點", + systemInterface: "系統介面", + sysIfName: "介面名稱", + sysIfMtu: "介面 MTU", + controlUrl: "控制 URL", + ephemeral: "臨時節點", + hostname: "主機名", + acceptRoutes: "接受路由", + exitNode: "出口節點", + allowLanAccess: "允許 LAN 訪問", + advRoutes: "廣告路由", + advExitNode: "廣告出口節點", + udpTimeout: "UDP 超時", + }, + ocm: { + credentialPath: "憑證路徑", + usagesPath: "用量統計路徑", + users: "用戶", + userName: "名稱", + userToken: "令牌", + }, + ccm: { + credentialPath: "憑證路徑", + usagesPath: "用量統計路徑", + users: "用戶", + userName: "名稱", + userToken: "令牌", + }, + derp: { + configPath: "配置路徑", + verifyClientEndpoint: "驗證客戶端端點", + verifyClientUrl: "驗證客戶端 URL", + meshWith: "網狀連接", + meshPsk: "網狀 PSK", + meshPskFile: "網狀 PSK 文件", + stun: "STUN 服務器", + options: "DERP 選項", + }, + naive: { + insecureConcurrency: "不安全並發數", + quic: "QUIC", + quicCongestion: "QUIC 擁塞控制", + udpOverTcp: "UDP over TCP", + }, + anytls: { + idleInterval: "閒置會話檢查間隔", + idleTimeout: "閒置會話逾時", + minIdle: "最小閒置會話數" + }, + }, + in: { + addr: "地址", + port: "端口", + ssMethod: "方法", + ssManageable: "可管理的", + sSide: "服務器端", + cSide: "客戶端", + multiDomain: "多域名", + remark: "備註", + mdOption: "多域名選項", + }, + listen: { + options: "監聽選項", + tcpOptions: "TCP 選項", + udpOptions: "UDP 選項", + detour: "繞道", + detourText: "轉發到入站", + disableTcpKeepAlive: "停用 TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "TCP Keep Alive 間隔", + }, + dial: { + bindIf: "綁定到網路接口", + bindIp4: "綁定到 IPv4", + bindIp6: "綁定到 IPv6", + bindNoPort: "綁定地址不佔用端口", + reuseAddr: "重用監聽地址", + connTimeout: "連接超時", + disableTcpKeepAlive: "停用 TCP Keep Alive", + tcpKeepAlive: "TCP Keep Alive", + tcpKeepAliveInterval: "TCP Keep Alive 間隔", + domainResolver: "域名解析器", + options: "撥號選項", + detourText: "轉寄至出站", + }, + transport: { + enable: "啟用傳輸", + host: "主機", + hosts: "主機列表", + path: "路徑", + httpMethod: "請求方法", + idleTimeout: "閒置超時", + pingTimeout: "Ping 超時", + grpcServiceName: "服務名稱", + grpcPws: "允許無流", + }, + mux: { + enable: "啟用多路徑", + maxConn: "最大連接數", + minStr: "最小串流數", + maxStr: "最大串流數", + padding: "僅填充", + enableBrutal: "啟用暴力", + }, + out: { + addr: "伺服器地址", + port: "伺服器端口", + addUrlTest: "新增 URLTest", + delay: "延遲", + }, + rule: { + add: "添加規則", + simple: "簡單", + logical: "邏輯", + mode: "模式", + invert: "反轉", + ipVer: "IP 版本", + domain: "域名", + domainSufix: "域名後綴", + domainKw: "域名關鍵詞", + domainRgx: "域名正則表達式", + ip: "IP CIDR", + privateIp: "無效 IP 範圍", + port: "端口", + portRange: "端口範圍", + srcCidr: "源 IP CIDR", + srcPrivateIp: "無效源 IP", + srcPort: "源端口", + srcPortRange: "源端口範圍", + ruleset: "規則集", + rulesetMatchSrc: "規則集 IP 範圍匹配源", + preferredBy: "優選出站", + interfaceAddr: "介面地址", + options: "規則選項", + domainRules: "域名/IP", + srcIpRules: "源 IP", + srcPortRules: "源端口", + udpDisableDomainUnmapping: "禁用域名解析映射", + udpConnect: "啟用 UDP 連接", + udpTimeout: "UDP 超時", + method: "方法", + noDrop: "不丟弃", + sniffer: "嗅探", + timeout: "超時", + strategy: "策略", + etaHint: "每行一項。空白行與重複項目將被略過。", + import: { + title: "批次匯入規則集", + rulesTitle: "匯入規則", + urlsHint: "每行一個 URL。標籤由檔名(不含副檔名)決定。", + fileHint: "上傳含 URL 的 .txt 檔案,每行一個。", + jsonHint: "貼上含 rules 及/或 rule_set 陣列的 JSON 物件。可貼上整個 \"route\" 區塊:{'{'}...{'}'} 或僅其內容。", + fileJsonHint: "上傳含 route 區塊的 .json 檔案。", + urlHint: "填寫 JSON 檔案的直連(例如 GitHub raw 連結)。", + preview: "預覽", + skipped: "已存在,以灰色顯示", + conflict: "偵測到衝突", + merge: "合併 — 加入匯入的規則(略過重複的規則集標籤)", + replace: "取代 — 刪除現有規則與規則集後重新匯入", + pasteUrls: "貼上 URL", + uploadTxt: "上傳 .txt", + uploadFile: "上傳檔案", + fromUrl: "透過連結", + selectTxt: "選擇 .txt 檔案", + selectJson: "選擇 .json 檔案", + parse: "解析", + conflictDesc: "設定中已有 {rules} 條規則與 {rulesets} 個規則集。請選擇操作:", + finalOutbound: "預設出站(final)", + applyFinal: "設為預設出站", + errNoArrays: '找不到 "rules" 或 "rule_set" 陣列。', + errJsonParse: "JSON 解析錯誤:{message}", + errNoArraysFetched: '取得的 JSON 中找不到 "rules" 或 "rule_set"。', + errFetch: "取得失敗:{message}", + errNoFile: "未選擇檔案。", + errNoArraysInFile: '檔案中找不到 "rules" 或 "rule_set"。', + }, + }, + ruleset: { + add: "添加規則集", + format: "數據格式", + interval: "更新間隔", + remote: "遠端", + local: "本地", + }, + dns: { + add: "添加 DNS 服務器", + title: "DNS 服務器", + final: "最終", + server: "服務器", + firstServer: "首選服務器", + cacheCapacity: "快取容量", + disableCache: "停用快取", + disableExpire: "停用過期", + independentCache: "獨立快取", + reverseMapping: "反向映射", + domainStrategy: "域名策略", + local: { preferGo: "優先使用 Go" }, + rule: { + add: "添加 DNS 規則", + title: "DNS 規則", + inet4Range: "IPv4 範圍", + inet6Range: "IPv6 範圍", + acceptDefault: "接受默認", + action: { + title: "操作", + route: "路由", + routeOptions: "路由選項", + reject: "拒絕", + predefined: "預設", + rewriteTtl: "重寫 TTL", + clientSubnet: "用戶端子網", + rcode: "回應碼", + rcodes: { + noError: "正常", + formerr: "請求錯誤", + servFail: "伺服器故障", + nxDomain: "未找到", + refused: "被拒絕", + notImp: "尚未實作" + }, + answer: "回應", + ns: "名稱伺服器", + extra: "額外資訊" + } + }, + }, + basic: { + log: { + title: "日誌", + level: "級別", + output: "輸出", + timestamp: "啟用時間戳記", + }, + routing: { + title: "路由", + defaultOut: "默認外部", + defaultIf: "默認網卡", + defaultRm: "默認路由標記", + defaultDns: "默認 DNS 解析器", + autoBind: "自動綁定網卡", + }, + exp: { + storeFakeIp: "存儲假 IP", + extController: "外部控制器", + extUi: "外部介面", + extUiDownloadUrl: "介面下載網址", + extUiDownloadDetour: "介面下載繞行", + secret: "密鑰", + defaultMode: "預設模式", + allowOrigin: "允許來源", + allowPrivate: "允許私人網路", + }, + }, + tls : { + enable: "啟用 TLS", + usePath: "使用外部路徑", + useText: "使用文件內容", + certPath: "證書文件路徑", + keyPath: "私鑰文件路徑", + cert: "證書文件內容", + key: "私鑰文件內容", + options: "TLS 選項", + minVer: "最低版本", + maxVer: "最高版本", + cs: "加密套件", + privKey: "私鑰", + pubKey: "公鑰", + disableSni: "停用 SNI", + insecure: "允許不安全連線", + fragment: "分段", + fragmentDelay: "分段回應延遲", + recordFragment: "多筆記錄分段", + store: "根憑證庫", + ktls: "內核 TLS", + kernelTx: "發送", + kernelRx: "接收", + queryServerName: "ECH 查詢伺服器名稱", + acme: { + options: "ACME 選項", + dataDir: "數據目錄", + defaultDomain: "默認域名", + disableChallenges: "禁用挑戰", + httpChallenge: "禁用 HTTP 挑戰", + tlsChallenge: "禁用 TLS 挑戰", + altPorts: "替代端口", + altHport: "替代 HTTP 端口", + altTport: "替代 TLS 端口", + caProvider: "CA 提供商", + customCa: "自定義 CA 提供商", + extAcc: "外部賬戶", + dns01: "DNS01 挑戰", + dns01Provider: "DNS01 挑戰提供商", + dns01Params: { + api_token: "API 令牌", + zone_token: "Zone 令牌", + access_key_id: "存取金鑰 ID", + access_key_secret: "存取金鑰密文", + region_id: "區域 ID", + security_token: "安全令牌", + username: "用戶名", + password: "密碼", + subdomain: "子網域", + server_url: "伺服器 URL", + }, + }, + }, + stats: { + upload: "上傳", + download: "下載", + volume: "流量", + usage: "已用", + enable: "啟用統計", + graphTitle: "流量圖表", + B: "B", + KB: "KB", + MB: "MB", + GB: "GB", + TB: "TB", + PB: "PB", + p: "p", + Kp: "Kp", + Mp: "Mp", + Gp: "Gp", + Mbps: "Mbps", + }, + date: { + expiry: "到期", + expired: "已到期", + d: "天", + h: "時", + m: "分", + s: "秒", + ms: "毫秒", + }, +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..fb7d25d --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,55 @@ +/** + * main.ts + * + * Bootstraps Vuetify and other plugins then mounts the App` + */ + +// Composables +import { createApp, ref } from 'vue' + +// Components +import App from './App.vue' + +// Use router +import router from './router' + +// Store +import store from './store' + +// Plugins +import { registerPlugins } from '@/plugins' + +// Locale +import { i18n } from '@/locales' +import Vue3PersianDatetimePicker from 'vue3-persian-datetime-picker' + +// Notivue +import { createNotivue } from 'notivue' +import 'notivue/notification.css' +import 'notivue/animations.css' +const notivue = createNotivue({ + position: 'bottom-center', + limit: 4, + enqueue: false, + avoidDuplicates: true, + notifications: { + global: { + duration: 3000 + } + }, +}) + +const loading = ref(false) + +const app = createApp(App) +app.provide('loading', loading) + +registerPlugins(app) + +app + .use(router) + .use(store) + .use(i18n) + .use(notivue) + .component('DatePicker', Vue3PersianDatetimePicker) + .mount('#app') diff --git a/frontend/src/plugins/api.ts b/frontend/src/plugins/api.ts new file mode 100644 index 0000000..52b9e81 --- /dev/null +++ b/frontend/src/plugins/api.ts @@ -0,0 +1,57 @@ +import axios from 'axios' + +axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' +axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' + +axios.defaults.baseURL = "./" +const pendingRequests = new Map() + +axios.interceptors.request.use( + (config) => { + // Generate a unique key for the request + const requestKey = `${config.method}:${config.url}` + + // Check if there is already a pending request with the same key + if (pendingRequests.has(requestKey)) { + const cancelSource = pendingRequests.get(requestKey) + cancelSource.cancel('Duplicate request cancelled') + } + + // Create a new cancel token for the request + const cancelSource = axios.CancelToken.source() + config.cancelToken = cancelSource.token + + // Store the cancel token in the pending requests map + pendingRequests.set(requestKey, cancelSource) + + if (config.data instanceof FormData) { + config.headers['Content-Type'] = 'multipart/form-data' + } + return config + }, + (error) => Promise.reject(error), +) + +axios.interceptors.response.use( + (response) => { + // Remove the request from the pending requests map + const requestKey = `${response.config.method}:${response.config.url}` + pendingRequests.delete(requestKey) + return response + }, + (error) => { + if (axios.isCancel(error)) { + // Handle duplicate request cancellation here if needed + console.warn(error.message) + } else { + // Remove the request from the pending requests map on error + const requestKey = `${error.config.method}:${error.config.url}` + pendingRequests.delete(requestKey) + } + return Promise.reject(error) + } +) + +const api = axios.create() + +export default api diff --git a/frontend/src/plugins/httputil.ts b/frontend/src/plugins/httputil.ts new file mode 100644 index 0000000..99f0b52 --- /dev/null +++ b/frontend/src/plugins/httputil.ts @@ -0,0 +1,88 @@ +import api from './api' +import { i18n } from '@/locales' +import router from '@/router' +import { push } from 'notivue' + +export interface Msg { + success: boolean + msg: string + obj: any | null +} + +function _handleMsg(msg: any): void { + if (!isMsg(msg)) { + return + } + if(msg.msg){ + if (!msg.success && msg.msg == "Invalid login") { + push.error({ + title: i18n.global.t('invalidLogin'), + }) + logout() + return + } + if (msg.success) { + push.success({ + message: i18n.global.t('success') + ": " + i18n.global.t('actions.' + msg.msg), + }) + } else { + push.error({ + title: i18n.global.t('failed'), + message: msg.msg + }) + } + } +} + +export const logout = async () => { + const response = await HttpUtils.get('api/logout') + if(response.success){ + router.push('/login') + } +} + +function _respToMsg(resp: any): Msg { + const data = resp.data + if (data == null) { + return { success: true, msg: "", obj: null } + } else if (isMsg(data)) { + if (data.hasOwnProperty('success')) { + return { success: data.success, msg: data.msg, obj: data.obj || null } + } else { + return data + } + } else { + return { success: false, msg: `unknown data: ${data}`, obj: null } + } +} + +function isMsg(obj: any): obj is Msg { + return Object.hasOwn(obj,'success') && Object.hasOwn(obj,'msg') && Object.hasOwn(obj, 'obj') +} + +const HttpUtils = { + async get(url: string, data: object = {}, options: any[] = []): Promise { + let msg: Msg + try { + const resp = await api.get(url, { params: data, ...options }) + msg = _respToMsg(resp) + } catch (e: any) { + msg = { success: false, msg: e.toString(), obj: null } + } + _handleMsg(msg) + return msg + }, + async post(url: string, data: object | null, options: any = undefined): Promise { + let msg: Msg + try { + const resp = await api.post(url, data, options) + msg = _respToMsg(resp) + } catch (e: any) { + msg = { success: false, msg: e.toString(), obj: null } + } + _handleMsg(msg) + return msg + }, +} + +export default HttpUtils \ No newline at end of file diff --git a/frontend/src/plugins/index.ts b/frontend/src/plugins/index.ts new file mode 100644 index 0000000..d4cc1ee --- /dev/null +++ b/frontend/src/plugins/index.ts @@ -0,0 +1,10 @@ +// Plugins +import vuetify from './vuetify' + +// Types +import type { App } from 'vue' + +export function registerPlugins (app: App) { + app + .use(vuetify) +} diff --git a/frontend/src/plugins/randomUtil.ts b/frontend/src/plugins/randomUtil.ts new file mode 100644 index 0000000..2ac7582 --- /dev/null +++ b/frontend/src/plugins/randomUtil.ts @@ -0,0 +1,82 @@ +const seq = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') + +const RandomUtil = { + randomIntRange(min: number, max: number): number { + if (!Number.isSafeInteger(min)){ + return this.randomIntRange(Number.MIN_SAFE_INTEGER, max) + } + if (!Number.isSafeInteger(max)){ + return this.randomIntRange(min, Number.MAX_SAFE_INTEGER) + } + if (max < min) { + return this.randomIntRange(max, min) + } + const array = new Uint32Array(2); + window.crypto.getRandomValues(array); + const highbits = array[0] + const lowbits = array[1] >>> 11 + const random = (highbits * 2 ** 21 + lowbits) / (Number.MAX_SAFE_INTEGER + 1) + return Math.floor(random * (max - min + 1) + min) + }, + randomInt(n: number) { + return this.randomIntRange(0, n) + }, + randomSeq(count: number): string { + if (count <= 0) { + return '' + } + let str = '' + for (let i = 0; i < count; ++i) { + str += seq[this.randomInt(62)] + } + return str + }, + randomLowerAndNum(count: number): string { + if (count <= 0) { + return '' + } + let str = '' + for (let i = 0; i < count; ++i) { + str += seq[this.randomInt(36)] + } + return str + }, + randomUUID(): string { + const rng = new Uint8Array(16); + window.crypto.getRandomValues(rng); + rng[6] = (rng[6] & 0x0f) | 0x40; + rng[8] = (rng[8] & 0x3f) | 0x80; + return ( + byteToHex[rng[0]] + byteToHex[rng[1]] + byteToHex[rng[2]] + byteToHex[rng[3]] + '-' + + byteToHex[rng[4]] + byteToHex[rng[5]] + '-' + + byteToHex[rng[6]] + byteToHex[rng[7]] + '-' + + byteToHex[rng[8]] + byteToHex[rng[9]] + '-' + + byteToHex[rng[10]] + byteToHex[rng[11]] + byteToHex[rng[12]] + + byteToHex[rng[13]] + byteToHex[rng[14]] + byteToHex[rng[15]] + ); + }, + randomShadowsocksPassword(n: number): string { + const array = new Uint8Array(n) + window.crypto.getRandomValues(array) + return btoa(String.fromCharCode(...array)) + }, + randomShortId(): string[] { + let shortIds = new Array(24).fill('') + for (var ii = 1; ii < 24; ii++) { + for (var jj = 0; jj <= this.randomInt(7); jj++){ + let randomNum = this.randomInt(256) + shortIds[ii] += ('0' + randomNum.toString(16)).slice(-2) + } + } + return shortIds + } +} + +const byteToHex = Array.from( + { length: 256 }, + (_, i) => (i + 0x100) + .toString(16) + .slice(1) +) + +export default RandomUtil \ No newline at end of file diff --git a/frontend/src/plugins/utils.ts b/frontend/src/plugins/utils.ts new file mode 100644 index 0000000..d1578ed --- /dev/null +++ b/frontend/src/plugins/utils.ts @@ -0,0 +1,104 @@ +import { i18n } from "@/locales" + +type OBJ = { + [key: string]: any +} + +export const FindDiff = { + deepCompare(obj1: any, obj2: any): boolean { + // Check if the types of both objects are the same + if (typeof obj1 !== typeof obj2) { + return false + } + + // Check if both objects are arrays + if (Array.isArray(obj1) && Array.isArray(obj2)) { + if (obj1.length !== obj2.length) { + return false + } + + for (let i = 0; i < obj1.length; i++) { + if (!this.deepCompare(obj1[i], obj2[i])) { + return false + } + } + return true + } + + // Check if both objects are plain objects + if (typeof obj1 === 'object' && typeof obj2 === 'object' && obj1 !== null && obj2 !== null) { + const keys1 = Object.keys(obj1).filter(key => obj1[key] !== undefined) + const keys2 = Object.keys(obj2).filter(key => obj2[key] !== undefined) + + if (keys1.length !== keys2.length) { + return false + } + + for (const key of keys1) { + if (!keys2.includes(key) || !this.deepCompare(obj1[key], obj2[key])) { + return false + } + } + return true + } + + // Check primitive values + return obj1 === obj2 + } +} + +const ONE_KB = 1024 +const ONE_MB = ONE_KB * 1024 +const ONE_GB = ONE_MB * 1024 +const ONE_TB = ONE_GB * 1024 +const ONE_PB = ONE_TB * 1024 + +export const HumanReadable = { + sizeFormat(size:number, fix:number=2) { + if (!size || size<0) return "-" + if (size < ONE_KB) { + return size.toFixed(0) + " " + i18n.global.t('stats.B') + } else if (size < ONE_MB) { + return (size / ONE_KB).toFixed(fix) + " " + i18n.global.t('stats.KB') + } else if (size < ONE_GB) { + return (size / ONE_MB).toFixed(fix) + " " + i18n.global.t('stats.MB') + } else if (size < ONE_TB) { + return (size / ONE_GB).toFixed(fix) + " " + i18n.global.t('stats.GB') + } else if (size < ONE_PB) { + return (size / ONE_TB).toFixed(fix) + " " + i18n.global.t('stats.TB') + } else { + return (size / ONE_PB).toFixed(fix) + " " + i18n.global.t('stats.PB') + } + }, + packetFormat(size:number, fix:number=2) { + if (!size || size<0) return "-" + if (size < 1000) { + return size.toFixed(0) + " " + i18n.global.t('stats.p') + } else if (size < 1000000) { + return (size / 1000).toFixed(fix) + " " + i18n.global.t('stats.Kp') + } else if (size < 1000000000) { + return (size / 1000000).toFixed(fix) + " " + i18n.global.t('stats.Mp') + } else { + return (size / 1000000000).toFixed(fix) + " " + i18n.global.t('stats.Gp') + } + }, + formatSecond(second:number): string { + if (!second || second<0) return "-" + if (second < 60) { + return second.toFixed(0) + i18n.global.t('date.s') + } else if (second < 3600) { + return (second / 60).toFixed(0) + i18n.global.t('date.m') + } else if (second < 3600 * 24) { + return (second / 3600).toFixed(0) + i18n.global.t('date.h') + } + const day = Math.floor(second / 3600 / 24) + const remain = Math.floor((second/3600) - (day*24)) + return day + i18n.global.t('date.d') + (remain > 0 ? ' ' + remain + i18n.global.t('date.h') : '') + }, + remainedDays(exp:number): string { + if (exp == 0) return i18n.global.t('unlimited') + const now = Date.now()/1000 + if (exp < now) return i18n.global.t('date.expired') + return Math.floor((exp - now) / (3600*24)) + " " + i18n.global.t('date.d') + } +} \ No newline at end of file diff --git a/frontend/src/plugins/vuetify.ts b/frontend/src/plugins/vuetify.ts new file mode 100644 index 0000000..fdcde87 --- /dev/null +++ b/frontend/src/plugins/vuetify.ts @@ -0,0 +1,56 @@ +/** + * plugins/vuetify.ts + * + * Framework documentation: https://vuetifyjs.com` + */ + +// Styles +import '@mdi/font/css/materialdesignicons.css' +import 'vuetify/styles/main.css' + +import colors from 'vuetify/util/colors' +import { fa, en, vi, zhHans, zhHant, ru } from 'vuetify/locale' + +// Composables +import { createVuetify } from 'vuetify' + +// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides +export default createVuetify({ + defaults: { + VRow: { density: 'compact' }, + VTextField: { + variant: 'solo-filled', + }, + VSelect: { + variant: 'solo-filled', + }, + VCombobox: { + variant: 'solo-filled', + }, + VTextarea: { + variant: 'solo-filled', + }, + }, + theme: { + defaultTheme: localStorage.getItem('theme') ?? 'system', + themes: { + light: { + colors: { + error: '#FF5252', + background: colors.grey.lighten4, + }, + }, + dark: { + colors: { + primary: colors.blue.darken4, + error: colors.red.accent3, + }, + }, + }, + }, + locale: { + locale: localStorage.getItem("locale") ?? 'en', + fallback: 'en', + messages: { en, fa, vi, zhHans, zhHant, ru }, + }, +}) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..69088b3 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,123 @@ +// Composables +import { createRouter, createWebHistory } from 'vue-router' +import Login from '@/views/Login.vue' +import Data from '@/store/modules/data' + +const routes = [ + { + path: '/login', + name: 'pages.login', + component: Login, + }, + { + path: '/', + component: () => import('@/layouts/default/Default.vue'), + meta: { requiresAuth: true }, + children: [ + { + path: '/', + name: 'pages.home', + component: () => import('@/views/Home.vue'), + }, + { + path: '/inbounds', + name: 'pages.inbounds', + component: () => import('@/views/Inbounds.vue'), + }, + { + path: '/clients', + name: 'pages.clients', + component: () => import('@/views/Clients.vue'), + }, + { + path: '/outbounds', + name: 'pages.outbounds', + component: () => import('@/views/Outbounds.vue'), + }, + { + path: '/services', + name: 'pages.services', + component: () => import('@/views/Services.vue'), + }, + { + path: '/endpoints', + name: 'pages.endpoints', + component: () => import('@/views/Endpoints.vue'), + }, + { + path: '/rules', + name: 'pages.rules', + component: () => import('@/views/Rules.vue'), + }, + { + path: '/tls', + name: 'pages.tls', + component: () => import('@/views/Tls.vue'), + }, + { + path: '/basics', + name: 'pages.basics', + component: () => import('@/views/Basics.vue'), + }, + { + path: '/dns', + name: 'pages.dns', + component: () => import('@/views/Dns.vue'), + }, + { + path: '/admins', + name: 'pages.admins', + component: () => import('@/views/Admins.vue'), + }, + { + path: '/settings', + name: 'pages.settings', + component: () => import('@/views/Settings.vue'), + }, + ], + }, +] + +const router = createRouter({ + history: createWebHistory((window as any).BASE_URL), + routes, +}) + +const DEFAULT_TITLE = 'S-UI' +let intervalId:any + +// Navigation guard to check authentication state +router.beforeEach((to) => { + // Check the session cookie + const sessionCookie = document.cookie.split(';').find(cookie => cookie.trim().startsWith('s-ui=')) + const isAuthenticated = !!sessionCookie + + // If the route requires authentication and the user is not authenticated, redirect to /login + if (to.meta.requiresAuth && !isAuthenticated) { + return '/login' + } + if (to.path === '/login' && isAuthenticated) { + // If already authenticated and visiting /login, redirect to '/' + return '/' + } + + // Load default data + if (to.path !== '/login') { + loadDataInterval() + } else { + if (intervalId) { + clearInterval(intervalId) + intervalId = undefined + } + } +}) + +const loadDataInterval = () => { + if (intervalId) return + Data().loadData() + intervalId = setInterval(() => { + Data().loadData() + }, 10000) +} + +export default router diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts new file mode 100644 index 0000000..7b565e2 --- /dev/null +++ b/frontend/src/store/index.ts @@ -0,0 +1,5 @@ +import { createPinia } from 'pinia' + +const pinia = createPinia() + +export default pinia \ No newline at end of file diff --git a/frontend/src/store/modules/data.ts b/frontend/src/store/modules/data.ts new file mode 100644 index 0000000..0853565 --- /dev/null +++ b/frontend/src/store/modules/data.ts @@ -0,0 +1,143 @@ +import HttpUtils from '@/plugins/httputil' +import { defineStore } from 'pinia' +import { push } from 'notivue' +import { i18n } from '@/locales' +import { Inbound } from '@/types/inbounds' +import { Client } from '@/types/clients' + +const Data = defineStore('Data', { + state: () => ({ + lastLoad: 0, + reloadItems: localStorage.getItem("reloadItems")?.split(',')?? [], + subURI: "", + enableTraffic: false, + onlines: {inbound: [], outbound: [], user: []}, + config: {}, + inbounds: [], + outbounds: [], + services: [], + endpoints: [], + clients: [], + tlsConfigs: [], + }), + actions: { + async loadData() { + const msg = await HttpUtils.get('api/load', this.lastLoad >0 ? {lu: this.lastLoad} : {} ) + if(msg.success) { + this.onlines = msg.obj.onlines + if (msg.obj.lastLog) { + push.error({ + title: i18n.global.t('error.core'), + duration: 5000, + message: msg.obj.lastLog + }) + } + + if (msg.obj.config) { + this.setNewData(msg.obj) + } + } + }, + setNewData(data: any) { + this.lastLoad = Math.floor((new Date()).getTime()/1000) + if (data.subURI) this.subURI = data.subURI + if (data.enableTraffic) this.enableTraffic = data.enableTraffic + if (data.config) this.config = data.config + if (Object.hasOwn(data, 'clients')) this.clients = data.clients ?? [] + if (Object.hasOwn(data, 'inbounds')) this.inbounds = data.inbounds ?? [] + if (Object.hasOwn(data, 'outbounds')) this.outbounds = data.outbounds ?? [] + if (Object.hasOwn(data, 'services')) this.services = data.services ?? [] + if (Object.hasOwn(data, 'endpoints')) this.endpoints = data.endpoints ?? [] + if (Object.hasOwn(data, 'tls')) this.tlsConfigs = data.tls ?? [] + }, + async loadInbounds(ids: number[]): Promise { + const options = ids.length > 0 ? {id: ids.join(",")} : {} + const msg = await HttpUtils.get('api/inbounds', options) + if(msg.success) { + return msg.obj.inbounds + } + return [] + }, + async loadClients(id: number): Promise { + const options = id > 0 ? {id: id} : {} + const msg = await HttpUtils.get('api/clients', options) + if(msg.success) { + return msg.obj.clients[0]??{} + } + return {} + }, + async save (object: string, action: string, data: any, initUsers?: number[]): Promise { + let postData = { + object: object, + action: action, + data: JSON.stringify(data, null, 2), + initUsers: initUsers?.join(',') ?? undefined + } + const msg = await HttpUtils.post('api/save', postData) + if (msg.success) { + const objectName = ['tls', 'config'].includes(object) ? object : object.substring(0, object.length - 1) + push.success({ + title: i18n.global.t('success'), + duration: 5000, + message: i18n.global.t('actions.' + action) + " " + i18n.global.t('objects.' + objectName) + }) + this.setNewData(msg.obj) + } + return msg.success + }, + // Check duplicate client name + checkClientName (id: number, newName: string): boolean { + const oldName = id > 0 ? this.clients.findLast((i: any) => i.id == id)?.name : null + if (newName != oldName && this.clients.findIndex((c: any) => c.name == newName) != -1) { + push.error({ + message: i18n.global.t('error.dplData') + ": " + i18n.global.t('client.name') + }) + return true + } + return false + }, + // Check bulk client names + checkBulkClientNames (names: string[]): boolean { + const newNames = new Set(names) + const oldNames = new Set(this.clients.map((c: any) => c.name)) + const allNames = new Set([...oldNames, ...newNames]) + if (newNames.size != names.length || oldNames.size + newNames.size != allNames.size) { + push.error({ + message: i18n.global.t('error.dplData') + ": " + i18n.global.t('client.name') + }) + return true + } + return false + }, + // check duplicate tag + checkTag (object: string, id: number, tag: string): boolean { + let objects = [] + switch (object) { + case 'inbound': + objects = this.inbounds + break + case 'outbound': + objects = this.outbounds + break + case 'service': + objects = this.services + break + case 'endpoint': + objects = this.endpoints + break + default: + return false + } + const oldObject = id > 0 ? objects.findLast((i: any) => i.id == id) : null + if (tag != oldObject?.tag && objects.findIndex((i: any) => i.tag == tag) != -1) { + push.error({ + message: i18n.global.t('error.dplData') + ": " + i18n.global.t('objects.tag') + }) + return true + } + return false + }, + } +}) + +export default Data \ No newline at end of file diff --git a/frontend/src/styles/settings.scss b/frontend/src/styles/settings.scss new file mode 100644 index 0000000..ef640dd --- /dev/null +++ b/frontend/src/styles/settings.scss @@ -0,0 +1,39 @@ +/** + * src/styles/settings.scss + * + * Configures SASS variables and Vuetify overwrites + */ + +// https://vuetifyjs.com/features/sass-variables/` +// @use 'vuetify/settings' with ( +// $color-pack: false +// ); +@font-face { + font-display: swap; + font-family: 'Vazirmatn'; + font-style: normal; + font-weight: 400; + src: url('@/assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2'); + unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039; +} +$body-font-family: "Vazirmatn"; + +$typoOptions: text-h1, text-sm-h1, text-md-h1, text-lg-h1, text-h2, text-sm-h2, + text-md-h2, text-lg-h2, text-h3, text-sm-h3, text-md-h3, text-lg-h3, text-h4, + text-sm-h4, text-md-h4, text-lg-h4, text-h5, text-sm-h5, text-md-h5, + text-lg-h5, text-h6, text-sm-h6, text-md-h6, text-lg-h6, headline, title, + subtitle-1, subtitle-2, text-body-1, text-sm-body-1, text-md-body-1, + text-lg-body-1, text-body-2, text-sm-body-2, text-md-body-2, text-lg-body-2, + text-caption; +body { + font-family: -apple-system, BlinkMacSystemFont, $body-font-family, sans-serif !important; + @each $typoOption in $typoOptions { + .#{$typoOption} { + font-family: -apple-system, BlinkMacSystemFont, $body-font-family, sans-serif !important; + } + } +} + +.v-btn { + letter-spacing: 0; +} \ No newline at end of file diff --git a/frontend/src/types/brutal.ts b/frontend/src/types/brutal.ts new file mode 100644 index 0000000..8ba9b3e --- /dev/null +++ b/frontend/src/types/brutal.ts @@ -0,0 +1,5 @@ +export interface Brutal { + enabled: boolean + up_mbps: number + down_mbps: number +} \ No newline at end of file diff --git a/frontend/src/types/clients.ts b/frontend/src/types/clients.ts new file mode 100644 index 0000000..0d4ea42 --- /dev/null +++ b/frontend/src/types/clients.ts @@ -0,0 +1,185 @@ +import RandomUtil from "@/plugins/randomUtil" + +export interface Link { + type: "local" | "external" | "sub" + remark?: string + uri: string +} + +export interface Client { + id?: number + enable: boolean + name: string + config?: Config + inbounds: number[] + links?: Link[] + volume: number + expiry: number + up: number + down: number + desc: string + group: string + delayStart?: boolean + autoReset?: boolean + resetDays?: number + nextReset?: number + totalUp?: number + totalDown?: number +} + +const defaultClient: Client = { + enable: true, + name: "", + config: {}, + inbounds: [], + links: [], + volume: 0, + expiry: 0, + up: 0, + down: 0, + desc: "", + group: "", + delayStart: false, + autoReset: false, + resetDays: 0, + nextReset: 0, + totalUp: 0, + totalDown: 0, +} + +type Config = { + [key: string]: { + name?: string + username?: string + [key: string]: any + } +} + +export function updateConfigs(configs: Config, newUserName: string): Config { + for (const key in configs) { + if (configs.hasOwnProperty(key)) { + const config = configs[key] + if (config.hasOwnProperty("name")) { + config.name = newUserName + } else if (config.hasOwnProperty("username")) { + config.username = newUserName + } + } + } + return configs +} + +export function shuffleConfigs(configs: Config, key?: string) { + const keys = key ? [key] : Object.keys(configs) + keys.forEach(k => { + switch (k) { + case "mixed": + case "socks": + case "http": + case "anytls": + case "trojan": + case "naive": + case "hysteria2": + configs[k].password = RandomUtil.randomSeq(10) + break + case "shadowsocks": + configs[k].password = RandomUtil.randomShadowsocksPassword(32) + break + case "shadowsocks16": + configs[k].password = RandomUtil.randomShadowsocksPassword(16) + break + case "shadowtls": + configs[k].password = RandomUtil.randomShadowsocksPassword(32) + break + case "hysteria": + configs[k].auth_str = RandomUtil.randomSeq(10) + break + case "tuic": + configs[k].password = RandomUtil.randomSeq(10) + configs[k].uuid = RandomUtil.randomUUID() + break + case "vmess": + case "vless": + configs[k].uuid = RandomUtil.randomUUID() + break + } + }) +} + +export function randomConfigs(user: string): Config { + const mixedPassword = RandomUtil.randomSeq(10) + const ssPassword16 = RandomUtil.randomShadowsocksPassword(16) + const ssPassword32 = RandomUtil.randomShadowsocksPassword(32) + const uuid = RandomUtil.randomUUID() + return { + mixed: { + username: user, + password: mixedPassword, + }, + socks: { + username: user, + password: mixedPassword, + }, + http: { + username: user, + password: mixedPassword, + }, + shadowsocks: { + name: user, + password: ssPassword32, + }, + shadowsocks16: { + name: user, + password: ssPassword16, + }, + shadowtls: { + name: user, + password: ssPassword32, + }, + vmess: { + name: user, + uuid: uuid, + alterId: 0, + }, + vless: { + name: user, + uuid: uuid, + flow: "xtls-rprx-vision", + }, + anytls: { + name: user, + password: mixedPassword, + }, + trojan: { + name: user, + password: mixedPassword, + }, + naive: { + username: user, + password: mixedPassword, + }, + hysteria: { + name: user, + auth_str: mixedPassword, + }, + tuic: { + name: user, + uuid: uuid, + password: mixedPassword, + }, + hysteria2: { + name: user, + password: mixedPassword, + }, + } +} + +export function createClient(json?: Partial): Client { + defaultClient.name = RandomUtil.randomSeq(8) + const defaultObject: Client = { ...defaultClient, ...(json || {}) } + + // Add missing config + defaultObject.config = { ...randomConfigs(defaultObject.name), ...defaultObject.config } + + return defaultObject +} diff --git a/frontend/src/types/config.ts b/frontend/src/types/config.ts new file mode 100644 index 0000000..581a5eb --- /dev/null +++ b/frontend/src/types/config.ts @@ -0,0 +1,116 @@ +import { Inbound } from './inbounds' +import { Outbound } from './outbounds' +import { Dns } from './dns' +import { Dial } from './dial' + +interface Log { + disabled?: boolean + level?: string + output?: string + timestamp?: boolean +} + +export interface Ntp extends Dial{ + enabled?: boolean + server: string + server_port?: number + interval?: string +} + +interface Route { + rules: RouteRule[] | RouteRuleLogical[] + rule_set: RouteRuleSet[] + final?: string, + auto_detect_interface?: boolean + default_interface?: string + default_mark?: number + default_domain_resolver: string +} + +interface RouteRule { + inbound?: string[] | string + ip_version?: 4 | 6, + network?: "tcp" | "udp" | "icmp" + auth_user?: string[] + protocol?: string[] | string + domain?: string[] | string + domain_suffix?: string[] | string + domain_keyword?: string[] | string + domain_regex?: string[] | string + source_ip_cidr?: string[] | string + source_ip_is_private?: boolean + ip_cidr?: string[] | string + ip_is_private?: boolean + source_port?: number[] | number + source_port_range?: string[] | string + port?: number[] | number + port_range?: string[] | string + clash_mode?: string + rule_set?: string[] | string + invert?: boolean + outbound: string +} + +interface RouteRuleLogical { + type: "logical" + mode: "and" | "or" + rules: RouteRule[] + invert?: boolean + outbound: string +} + +interface RouteRuleSet { + type: string + tag: string + format: string + path?: string + url?: string + download_detour?: string + update_interval?: string +} + +interface Experimental { + cache_file?: CacheFile + clash_api?: ClashApi + v2ray_api?: V2rayApi +} + +interface CacheFile { + enabled?: boolean + path?: string + cache_id?: string + store_fakeip?: boolean +} + +interface V2rayApi { + listen: string + stats: V2rayApiStats +} + +export interface V2rayApiStats { + enabled: boolean + inbounds: string[] + outbounds: string[] + users: string[] +} + +interface ClashApi { + external_controller?: string + external_ui?: string + external_ui_download_url?: string + external_ui_download_detour?: string + secret?: string + default_mode?: string + access_control_allow_origin?: string[] + access_control_allow_private_network?: boolean +} + +export interface Config { + log: Log + dns: Dns + ntp?: Ntp + inbounds: Inbound[] + outbounds: Outbound[] + route: Route + experimental: Experimental +} \ No newline at end of file diff --git a/frontend/src/types/dial.ts b/frontend/src/types/dial.ts new file mode 100644 index 0000000..9b624c9 --- /dev/null +++ b/frontend/src/types/dial.ts @@ -0,0 +1,18 @@ +export interface Dial { + detour?: string + bind_interface?: string + inet4_bind_address?: string + inet6_bind_address?: string + bind_address_no_port?: boolean + routing_mark?: number + reuse_addr?: boolean + connect_timeout?: string + tcp_fast_open?: boolean + tcp_multi_path?: boolean + udp_fragment?: boolean + fallback_delay?: string + domain_resolver?: string | any + disable_tcp_keep_alive?: boolean + tcp_keep_alive?: string + tcp_keep_alive_interval?: string +} \ No newline at end of file diff --git a/frontend/src/types/dns.ts b/frontend/src/types/dns.ts new file mode 100644 index 0000000..de60dd0 --- /dev/null +++ b/frontend/src/types/dns.ts @@ -0,0 +1,127 @@ +export interface Dns { + servers: DnsServer[] + rules: dnsRule[] + final?: string + strategy?: string + disable_cache?: boolean, + disable_expire?: boolean, + independent_cache?: boolean, + cache_capacity?: number, + reverse_mapping?: boolean, + client_subnet?: string, +} + +export const DnsTypes = { + Local: 'local', + Hosts: 'hosts', + TCP: 'tcp', + UDP: 'udp', + TLS: 'tls', + QUIC: 'quic', + HTTPS: 'https', + HTTP3: 'h3', + DHCP: 'dhcp', + FakeIP: 'fakeip', + Tailscale: 'tailscale', + Resolved: 'resolved', +} + +export type DnsType = typeof DnsTypes[keyof typeof DnsTypes] + +type InterfaceMap = { + [Key in keyof typeof DnsTypes]: { + type: string + [otherProperties: string]: any + } +} + +export type DnsServer = InterfaceMap[keyof InterfaceMap] + +const defaultValues: Record = { + local: { type: 'local' }, + hosts: { type: 'hosts', path: ['/etc/hosts'] }, + tcp: { type: 'tcp', server_port: 53 }, + udp: { type: 'udp', server_port: 53 }, + tls: { type: 'tls', server_port: 853, tls: {} }, + quic: { type: 'quic', server_port: 853, tls: {} }, + https: { type: 'https', server_port: 443, tls: {}, headers: {} }, + h3: { type: 'h3', server_port: 443, tls: {}, headers: {} }, + predefined: { type: 'predefined', rcode: 'NOERROR' }, + dhcp: { type: 'dhcp' }, + fakeip: { type: 'fakeip', inet4_range: '198.18.0.0/15', inet6_range: 'fc00::/18' }, + tailscale: { type: 'tailscale' }, + resolved: { type: 'resolved' }, +} +export function createDnsServer(type: string, json?: Partial): DnsServer { + const defaultObject: DnsServer = { ...defaultValues[type], ...(json || {}) } + return defaultObject +} + +interface generalDnsRule { + invert: boolean + action: 'route' | 'route-options' | 'reject' | 'predefined' + server?: string + strategy?: string + disable_cache?: boolean + rewrite_ttl?: number + client_subnet?: string + method?: string + no_drop?: boolean + rcode?: string + answer?: string[] + ns?: string[] + extra?: string[] +} + +export const actionDnsRuleKeys = [ + 'invert', + 'action', + 'server', + 'strategy', + 'disable_cache', + 'rewrite_ttl', + 'client_subnet', + 'method', + 'no_drop', + 'rcode', + 'answer', + 'ns', + 'extra', +] +export interface logicalDnsRule extends generalDnsRule { + type: 'logical' | 'simple' + mode: 'and' | 'or' + rules: dnsRule[] +} + +export interface dnsRule extends generalDnsRule { + inbound?: string[] + ip_version?: 4 | 6 + query_type?: string + network?: string[] + auth_user?: string[] + protocol?: string[] + domain?: string[] + domain_suffix?: string[] + domain_keyword?: string[] + domain_regex?: string[] + source_ip_cidr?: string[] + source_ip_is_private?: boolean + ip_cidr?: string[] + ip_is_private: boolean + ip_accept_any: boolean + source_port?: number[] + source_port_range?: string[] + port?: number[] + port_range?: string[] + process_name?: string[] + process_path?: string[] + process_path_regex?: string[] + package_name?: string[] + user?: string[] + user_id?: number[] + clash_mode?: string + rule_set?: string[] + rule_set_ip_cidr_match_source?: boolean + rule_set_ip_cidr_accept_empty?: boolean +} diff --git a/frontend/src/types/endpoints.ts b/frontend/src/types/endpoints.ts new file mode 100644 index 0000000..56540e7 --- /dev/null +++ b/frontend/src/types/endpoints.ts @@ -0,0 +1,82 @@ +import { Dial } from "./dial" + +export const EpTypes = { + Wireguard: 'wireguard', + Warp: 'warp', + Tailscale: 'tailscale', +} + +type EpType = typeof EpTypes[keyof typeof EpTypes] + +interface EndpointBasics { + id: number + type: EpType + tag: string +} + +export interface WgPeer { + address: string + port: number + public_key: string + pre_shared_key?: string + allowed_ips?: string[] + persistent_keepalive_interval?: number + reserved?: number[] +} + +export interface WireGuard extends EndpointBasics, Dial { + system?: boolean + name?: string + mtu?: number + address: string[] + private_key: string + listen_port: number + peers: WgPeer[] + udp_timeout?: string + workers?: number + ext: any +} + +export interface Warp extends WireGuard {} + +export interface Tailscale extends EndpointBasics, Dial { + state_directory?: string + auth_key?: string + control_url?: string + ephemeral?: boolean + hostname?: string + accept_routes?: boolean + exit_node?: string + exit_node_allow_lan_access?: boolean + advertise_routes?: string[] + advertise_exit_node?: boolean + relay_server_port?: number + relay_server_static_endpoints?: string[] + system_interface?: boolean + system_interface_name?: string + system_interface_mtu?: number + udp_timeout?: string +} + +// Create interfaces dynamically based on EpTypes keys +type InterfaceMap = { + [Key in keyof typeof EpTypes]: { + type: string + [otherProperties: string]: any // You can add other properties as needed + } +} + +// Create union type from InterfaceMap +export type Endpoint = InterfaceMap[keyof InterfaceMap] + +// Create defaultValues object dynamically +const defaultValues: Record = { + wireguard: { type: EpTypes.Wireguard, address: ['10.0.0.2/32','fe80::2/128'], private_key: '', listen_port: 0 }, + warp: { type: EpTypes.Warp, address: [], private_key: '', listen_port: 0, mtu: 1420, peers: [{ address: '', port: 0, public_key: ''}] }, + tailscale: { type: EpTypes.Tailscale, domain_resolver: 'local' }, +} + +export function createEndpoint(type: string,json?: Partial): Endpoint { + const defaultObject: Endpoint = { ...defaultValues[type], ...(json || {}) } + return defaultObject +} \ No newline at end of file diff --git a/frontend/src/types/inbounds.ts b/frontend/src/types/inbounds.ts new file mode 100644 index 0000000..841c07a --- /dev/null +++ b/frontend/src/types/inbounds.ts @@ -0,0 +1,240 @@ +import { iMultiplex } from "./multiplex" +import { iTls } from "./tls" +import { Dial } from "./dial" +import { Transport } from "./transport" + +export const InTypes = { + Direct: 'direct', + Mixed: 'mixed', + SOCKS: 'socks', + HTTP: 'http', + Shadowsocks: 'shadowsocks', + VMess: 'vmess', + Trojan: 'trojan', + Naive: 'naive', + Hysteria: 'hysteria', + ShadowTLS: 'shadowtls', + TUIC: 'tuic', + Hysteria2: 'hysteria2', + VLESS: 'vless', + AnyTls: 'anytls', + Tun: 'tun', + Redirect: 'redirect', + TProxy: 'tproxy', +} + +type InType = typeof InTypes[keyof typeof InTypes] + +export interface Addr { + server: string + server_port: number + tls?: boolean + insecure?: boolean + server_name?: string + remark?: string +} + +export interface Listen { + listen: string + listen_port: number + tcp_fast_open?: boolean + tcp_multi_path?: boolean + udp_fragment?: boolean + udp_timeout?: string + detour?: string + disable_tcp_keep_alive?: boolean + tcp_keep_alive?: string + tcp_keep_alive_interval?: string +} + +interface InboundBasics extends Listen { + id: number + type: InType + tag: string + tls_id: number + addrs?: Addr[] + out_json?: any +} + +interface ShadowTLSHandShake extends Dial { + server: string + server_port: number +} + +export interface Direct extends InboundBasics { + network?: "udp" | "tcp" + override_address?: string + override_port?: number +} +export interface Mixed extends InboundBasics {} +export interface SOCKS extends InboundBasics {} +export interface HTTP extends InboundBasics {} +export interface Shadowsocks extends InboundBasics { + method: string + password: string + network?: "udp" | "tcp" + multiplex?: iMultiplex + managed?: boolean +} +export interface VMess extends InboundBasics { + tls: iTls + multiplex?: iMultiplex + transport?: Transport +} +export interface Trojan extends InboundBasics { + tls: iTls + fallback?: { + server: string + server_port: number + } + multiplex?: iMultiplex + transport?: Transport +} +export interface Naive extends InboundBasics { + tls: iTls, + quic_congestion_control?: "" | "bbr" | "bbr2" | "cubic" | "reno" +} +export interface Hysteria extends InboundBasics { + up_mbps: number + down_mbps: number + obfs?: string + recv_window_conn?: number + recv_window_client?: number + max_conn_client?: number + disable_mtu_discovery?: boolean +} +export interface ShadowTLS extends InboundBasics { + version: 1|2|3 + password?: string + handshake: ShadowTLSHandShake + handshake_for_server_name?: { + [server_name: string]: ShadowTLSHandShake + } + strict_mode?: boolean + wildcard_sni?: string +} +export interface VLESS extends InboundBasics { + multiplex?: iMultiplex + transport?: Transport + tls: iTls +} + +export interface AnyTls extends InboundBasics { + padding_scheme: string[] + tls: iTls +} +export interface TUIC extends InboundBasics { + congestion_control: ""|"cubic"|"new_reno"|"bbr" + auth_timeout?: string + zero_rtt_handshake?: boolean + heartbeat?: string +} +export interface Hysteria2 extends InboundBasics { + up_mbps?: number + down_mbps?: number + obfs?: { + type?: "salamander" + password: string + } + ignore_client_bandwidth?: boolean + masquerade?: string | { + type: string + directory?: string + url?: string + rewrite_host?: boolean + status_code?: number + headers?: Headers[] + content?: string + } + brutal_debug?: boolean +} +export interface Tun extends InboundBasics { + interface_name?: string + address?: string[] + mtu?: number + endpoint_independent_nat?: boolean + udp_timeout?: string + stack?: string + auto_route?: boolean + strict_route?: boolean + auto_redirect?: boolean + exclude_mptcp?: boolean + auto_redirect_iproute2_fallback_rule_index?: number + // auto_redirect_input_mark?: string + // auto_redirect_output_mark?: string + // route_address?: string[] + // route_exclude_address?: string[] + // include_interface?: string[] + // exclude_interface?: string[] + // include_uid?: string[] + // include_uid_range?: string[] + // exclude_uid?: number[] + // exclude_uid_range?: string[] + // include_android_user?: number[] + // include_package?: string[] + // exclude_package?: string[] +} +export interface Redirect extends InboundBasics {} +export interface TProxy extends InboundBasics { + network?: "udp" | "tcp" +} + +// Create interfaces dynamically based on InTypes keys +type InterfaceMap = { + direct: Direct + mixed: Mixed + socks: SOCKS + http: SOCKS + shadowsocks: Shadowsocks + vmess: VMess + trojan: Trojan + naive: Naive + hysteria: Hysteria + shadowtls: ShadowTLS + tuic: TUIC + hysteria2: Hysteria2 + vless: VLESS + anytls: AnyTls + tun: Tun + redirect: Redirect + tproxy: TProxy +} + +// Create union type from InterfaceMap +export type Inbound = InterfaceMap[keyof InterfaceMap] + +// Create defaultValues object dynamically +const defaultValues: Record = { + direct: { type: InTypes.Direct }, + mixed: { type: InTypes.Mixed }, + socks: { type: InTypes.SOCKS }, + http: { type: InTypes.HTTP, tls_id: 0 }, + shadowsocks: { type: InTypes.Shadowsocks, method: 'none' }, + vmess: { type: InTypes.VMess, tls_id: 0, transport: {} }, + trojan: { type: InTypes.Trojan, tls_id: 0, transport: {} }, + naive: { type: InTypes.Naive, tls_id: 0 }, + hysteria: { type: InTypes.Hysteria, up_mbps: 100, down_mbps: 100, tls_id: 0 }, + shadowtls: { type: InTypes.ShadowTLS, version: 3, handshake: {}, handshake_for_server_name: {} }, + tuic: { type: InTypes.TUIC, congestion_control: "cubic", tls_id: 0 }, + hysteria2: { type: InTypes.Hysteria2, tls_id: 0 }, + vless: { type: InTypes.VLESS, tls_id: 0, transport: {} }, + anytls: { type: InTypes.AnyTls, tls_id: 0, padding_scheme: [ + "stop=8", + "0=30-30", + "1=100-400", + "2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000", + "3=9-9,500-1000", + "4=500-1000", + "5=500-1000", + "6=500-1000", + "7=500-1000" + ]}, + tun: { type: InTypes.Tun, mtu: 9000, stack: 'system', udp_timeout: '5m', auto_route: false }, + redirect: { type: InTypes.Redirect }, + tproxy: { type: InTypes.TProxy }, +} + +export function createInbound(type: InType,json?: Partial): Inbound { + const defaultObject: Inbound = { ...defaultValues[type] ?? {}, ...(json ?? {}) } + return defaultObject +} \ No newline at end of file diff --git a/frontend/src/types/multiplex.ts b/frontend/src/types/multiplex.ts new file mode 100644 index 0000000..ab8c0a2 --- /dev/null +++ b/frontend/src/types/multiplex.ts @@ -0,0 +1,14 @@ +import { Brutal } from "./brutal" + +export interface iMultiplex{ + enabled: boolean + padding?: boolean + brutal?: Brutal +} + +export interface oMultiplex extends iMultiplex{ + protocol?: "smux" | "yamux" | "h2mux" + max_connections?: number + min_streams?: number + max_streams?: number +} \ No newline at end of file diff --git a/frontend/src/types/outbounds.ts b/frontend/src/types/outbounds.ts new file mode 100644 index 0000000..4f9dacf --- /dev/null +++ b/frontend/src/types/outbounds.ts @@ -0,0 +1,268 @@ +import { oTls } from "./tls" +import { oMultiplex } from "./multiplex" +import { Transport } from "./transport" +import { Dial } from "./dial" + +export const OutTypes = { + Direct: 'direct', + SOCKS: 'socks', + HTTP: 'http', + Shadowsocks: 'shadowsocks', + VMess: 'vmess', + Trojan: 'trojan', + Naive: 'naive', + Hysteria: 'hysteria', + VLESS: 'vless', + ShadowTLS: 'shadowtls', + TUIC: 'tuic', + Hysteria2: 'hysteria2', + AnyTls: 'anytls', + Tor: 'tor', + SSH: 'ssh', + Selector: 'selector', + URLTest: 'urltest', +} + +type OutType = typeof OutTypes[keyof typeof OutTypes] + +interface OutboundBasics { + id: number + type: OutType + tag: string +} + +export interface WgPeer { + server: string + server_port: number + public_key: string + pre_shared_key?: string + allowed_ips?: string[] + reserved?: number[] +} + +export interface Direct extends OutboundBasics, Dial {} + +export interface SOCKS extends OutboundBasics, Dial { + server: string + server_port: number + version?: "4" | "4a" | "5" + username?: string + password?: string + network?: "udp" | "tcp" + udp_over_tcp?: false | { + enabled: true + version?: number + } +} + +export interface HTTP extends OutboundBasics, Dial { + server: string + server_port: number + username?: string + password?: string + path?: string + headers?: { + [key: string]: string + } + tls?: oTls +} + +export interface Shadowsocks extends OutboundBasics, Dial { + server: string + server_port: number + method: string + password: string + network?: "udp" | "tcp" + udp_over_tcp?: false | { + enabled: true + version?: number + } + multiplex?: oMultiplex +} + +export interface VMESS extends OutboundBasics, Dial { + server: string + server_port: number + uuid: string + security?: string + alter_id: 0 + global_padding?: boolean + authenticated_length?: boolean + network?: "udp" | "tcp" + packet_encoding?: string + tls?: oTls + multiplex?: oMultiplex + transport?: Transport +} + +export interface Trojan extends OutboundBasics, Dial { + server: string + server_port: number + password: string + network?: "udp" | "tcp" + tls?: oTls + multiplex?: oMultiplex + transport?: Transport +} + +export interface Naive extends OutboundBasics, Dial { + server: string + server_port: number + username?: string + password?: string + insecure_concurrency?: number + extra_headers?: { [key: string]: string } + udp_over_tcp?: false | { enabled?: boolean; version?: number } + quic?: boolean + quic_congestion_control?: "" | "bbr" | "bbr2" | "cubic" | "reno" + tls: oTls +} + +export interface Hysteria extends OutboundBasics, Dial { + server: string + server_port: number + up_mbps: number + down_mbps: number + obfs?: string + auth_str?: string + recv_window_conn?: number + recv_window?: number + disable_mtu_discovery?: boolean + network?: "udp" | "tcp" + tls: oTls +} + +export interface ShadowTLS extends OutboundBasics, Dial { + server: string + server_port: number + version: 1|2|3 + password?: string + tls: oTls +} + +export interface VLESS extends OutboundBasics, Dial { + server: string + server_port: number + uuid: string + flow?: string + network?: "udp" | "tcp" + packet_encoding?: string + tls?: oTls + multiplex?: oMultiplex + transport?: Transport +} + +export interface TUIC extends OutboundBasics, Dial { + server: string + server_port: number + uuid: string + password?: string + congestion_control?: "cubic"|"new_reno"|"bbr" + udp_relay_mode?: "native" | "quic" + udp_over_stream?: boolean + zero_rtt_handshake?: boolean + heartbeat?: string + network?: "udp" | "tcp" + tls: oTls +} + +export interface Hysteria2 extends OutboundBasics, Dial { + server: string + server_port: number + server_ports?: string[] + hop_interval: string + up_mbps?: number + down_mbps?: number + obfs?: { + type?: "salamander" + password: string + } + password?: string + network?: "udp" | "tcp" + tls: oTls + brutal_debug?: boolean +} + +export interface AnyTls extends OutboundBasics, Dial { + server: string + server_port: number + password: string + idle_session_check_interval: string + idle_session_timeout: string + min_idle_session: number + tls: oTls +} + +export interface Tor extends OutboundBasics, Dial { + executable_path?: string + extra_args?: string[] + data_directory: string + torrc?: { + [options: string]: string + } +} + +export interface SSH extends OutboundBasics, Dial { + server: string + server_port?: number + user?: string + password?: string + private_key?: string + private_key_path?: string + private_key_passphrase?: string + host_key?: string[] + host_key_algorithms?: string[] + client_version?: string +} + +export interface Selector extends OutboundBasics { + outbounds: string[] + url?: string + interval?: string + tolerance?: number + idle_timeout?: string + interrupt_exist_connections?: boolean +} + +export interface URLTest extends OutboundBasics { + outbounds: string[] + default?: string + interrupt_exist_connections?: boolean +} + +// Create interfaces dynamically based on OutTypes keys +type InterfaceMap = { + [Key in keyof typeof OutTypes]: { + type: string + [otherProperties: string]: any // You can add other properties as needed + } +} + +// Create union type from InterfaceMap +export type Outbound = InterfaceMap[keyof InterfaceMap] + +// Create defaultValues object dynamically +const defaultValues: Record = { + direct: { type: OutTypes.Direct }, + socks: { type: OutTypes.SOCKS, version: "5" }, + http: { type: OutTypes.HTTP, tls: {} }, + shadowsocks: { type: OutTypes.Shadowsocks, method: 'none', multiplex: {} }, + vmess: { type: OutTypes.VMess, tls: {}, multiplex: {}, transport: {}, security: 'auto', global_padding: false }, + trojan: { type: OutTypes.Trojan, tls: {}, multiplex: {}, transport: {} }, + naive: { type: OutTypes.Naive, tls: { enabled: true } }, + hysteria: { type: OutTypes.Hysteria, up_mbps: 100, down_mbps: 100, tls: { enabled: true } }, + shadowtls: { type: OutTypes.ShadowTLS, version: 3, tls: { enabled: true } }, + vless: { type: OutTypes.VLESS, tls: {}, multiplex: {}, transport: {} }, + tuic: { type: OutTypes.TUIC, congestion_control: 'cubic', tls: { enabled: true } }, + hysteria2: { type: OutTypes.Hysteria2, tls: { enabled: true } }, + anytls: { type: OutTypes.AnyTls, tls: { enabled: true } }, + tor: { type: OutTypes.Tor, executable_path: './tor', data_directory: '$HOME/.cache/tor', torrc: { ClientOnly: '1' } }, + ssh: { type: OutTypes.SSH }, + selector: { type: OutTypes.Selector }, + urltest: { type: OutTypes.URLTest }, +} + +export function createOutbound(type: string,json?: Partial): Outbound { + const defaultObject: Outbound = { ...defaultValues[type], ...(json || {}) } + return defaultObject +} \ No newline at end of file diff --git a/frontend/src/types/rules.ts b/frontend/src/types/rules.ts new file mode 100644 index 0000000..967c1c7 --- /dev/null +++ b/frontend/src/types/rules.ts @@ -0,0 +1,81 @@ +interface generalRule { + invert: boolean + action: 'route' | 'route-options' | 'reject' | 'hijack-dns' | 'sniff' | 'resolve' | 'bypass' + outbound?: string + override_address?: string + override_port?: number + udp_disable_domain_unmapping?: boolean + udp_connect?: boolean + udp_timeout?: string + method?: string + no_drop?: boolean + sniffer: string[] + timeout: string + strategy: string + server: string +} + +export const actionKeys = [ + 'invert', + 'action', + 'outbound', + 'override_address', + 'override_port', + 'udp_disable_domain_unmapping', + 'udp_connect', + 'udp_timeout', + 'method', + 'no_drop', + 'sniffer', + 'timeout', + 'strategy', + 'server' +] +export interface logicalRule extends generalRule { + type: 'logical' | 'simple' + mode: 'and' | 'or' + rules: rule[] +} + +export interface rule extends generalRule { + inbound?: string[] + ip_version?: 4 | 6 + network?: string[] + auth_user?: string[] + protocol?: string[] + domain?: string[] + domain_suffix?: string[] + domain_keyword?: string[] + domain_regex?: string[] + source_ip_cidr?: string[] + source_ip_is_private?: boolean + ip_cidr?: string[] + ip_is_private?: boolean + source_port?: number[] + source_port_range?: string[] + port?: number[] + port_range?: string[] + process_name?: string[] + process_path?: string[] + process_path_regex?: string[] + package_name?: string[] + user?: string[] + user_id?: number[] + clash_mode?: string + rule_set?: string[] + rule_set_ip_cidr_match_source?: boolean + preferred_by?: string[] + interface_address?: string[] + network_interface_address?: string[] + default_interface_address?: string[] +} + +export interface ruleset { + type: 'local' | 'remote' + tag: string + format: 'source' | 'binary' + path?: string + url?: string + download_detour?: string + update_interval?: string +} \ No newline at end of file diff --git a/frontend/src/types/services.ts b/frontend/src/types/services.ts new file mode 100644 index 0000000..942f66c --- /dev/null +++ b/frontend/src/types/services.ts @@ -0,0 +1,77 @@ +import { Listen } from "./inbounds" +import { iTls } from "./tls" + +export const SrvTypes = { + DERP: 'derp', + Resolved: 'resolved', + SSMAPI: 'ssm-api', + OCM: 'ocm', + CCM: 'ccm', +} + +type SrvType = typeof SrvTypes[keyof typeof SrvTypes] + +interface SrvBasics extends Listen { + id: number + type: SrvType + tag: string + tls_id: number +} + +export interface DERP extends SrvBasics { + tls: iTls + config_path: string + verify_client_endpoint?: string[] + verify_client_url?: any[] + home?: string + mesh_with?: any[] + mesh_psk?: string + mesh_psk_file?: string + stun?: any +} + +export interface Resolved extends SrvBasics {} + +export interface SSMAPI extends SrvBasics { + servers: any + tls?: iTls +} + +export interface OCM extends SrvBasics { + credential_path?: string + usages_path?: string + users?: { name: string; token: string }[] + headers?: { [key: string]: string } + detour?: string +} + +export interface CCM extends SrvBasics { + credential_path?: string + usages_path?: string + users?: { name: string; token: string }[] + headers?: { [key: string]: string } + detour?: string +} + +type InterfaceMap = { + derp: DERP + resolved: Resolved + 'ssm-api': SSMAPI + ocm: OCM + ccm: CCM +} + +export type Srv = InterfaceMap[keyof InterfaceMap] + +const defaultValues: Record = { + derp: { type: 'derp', config_path: '', tls_id:0 }, + resolved: { type: 'resolved', listen: '::', listen_port: 53 }, + 'ssm-api': { type: 'ssm-api', tls_id: 0, servers: {} }, + ocm: { type: 'ocm', id: 0, tag: '', listen: '::', listen_port: 8080, tls_id: 0, users: [] } as OCM, + ccm: { type: 'ccm', id: 0, tag: '', listen: '::', listen_port: 8080, tls_id: 0, users: [] } as CCM, +} + +export function createSrv(type: string, json?: Partial): Srv { + const defaultObject: Srv = { ...defaultValues[type], ...(json || {}) } + return defaultObject +} \ No newline at end of file diff --git a/frontend/src/types/tls.ts b/frontend/src/types/tls.ts new file mode 100644 index 0000000..919d32b --- /dev/null +++ b/frontend/src/types/tls.ts @@ -0,0 +1,139 @@ +import { Dial } from "./dial" + +export interface tls { + id: number + name: string + server: iTls + client: oTls +} + +export interface iTls { + enabled?: boolean + server_name?: string + alpn?: string[] + min_version?: string + max_version?: string + cipher_suites?: string[] + curve_preferences?: string[] + certificate?: string[] + certificate_path?: string + key?: string[] + key_path?: string + client_authentication?: string + client_certificate?: string[] + client_certificate_path?: string[] + client_certificate_public_key_sha256?: string[] + acme?: acme + ech?: ech + reality?: reality + store?: 'mozilla' | 'chrome' + kernel_tx?: boolean + kernel_rx?: boolean +} + +export interface acme { + domain: string[] + data_directory?: string + default_server_name?: string + email?: string + provider?: string + disable_http_challenge?: boolean + disable_tls_alpn_challenge?: boolean + alternative_http_port?: number + alternative_tls_port?: number + external_account?: { + key_id: string + mac_key: string + } + dns01_challenge?: { + provider: string + [key: string]: string + } +} + +export interface ech { + enabled: boolean + key?: string[] + key_path?: string +} + +interface realityHanshake extends Dial { + server: string + server_port: number +} + +export interface reality { + enabled: boolean + handshake: realityHanshake + private_key: string + short_id: string[] + max_time_difference?: string +} + +export const defaultInTls: iTls = { + alpn: ['h3', 'h2', 'http/1.1'], + min_version: "1.2", + max_version: "1.3", + cipher_suites: [], +} + +export interface oTls { + enabled?: boolean + disable_sni?: boolean + server_name?: string + insecure?: boolean + alpn?: string[] + min_version?: string + max_version?: string + cipher_suites?: string[] + curve_preferences?: string[] + certificate?: string + certificate_path?: string + certificate_public_key_sha256?: string[] + client_certificate?: string[] + client_certificate_path?: string + client_key?: string[] + client_key_path?: string + fragment?: boolean + fragment_fallback_delay?: string + record_fragment?: boolean + ech?: { + enabled: boolean + pq_signature_schemes_enabled?: boolean + dynamic_record_sizing_disabled?: boolean + config?: string[] + config_path?: string + query_server_name?: string + }, + utls?: { + enabled: boolean + fingerprint: string + }, + reality?: { + enabled: boolean + public_key: string + short_id: string + } +} + +export const defaultOutTls: oTls = { + alpn: ['h3', 'h2', 'http/1.1'], + min_version: "1.2", + max_version: "1.3", + cipher_suites: [], + utls: { + enabled: true, + fingerprint: "chrome", + }, + reality: { + enabled: true, + public_key: "", + short_id: "", + }, + ech: { + enabled: true, + pq_signature_schemes_enabled: false, + dynamic_record_sizing_disabled: false, + config_path: "", + } +} \ No newline at end of file diff --git a/frontend/src/types/transport.ts b/frontend/src/types/transport.ts new file mode 100644 index 0000000..4336dc2 --- /dev/null +++ b/frontend/src/types/transport.ts @@ -0,0 +1,48 @@ +export const TrspTypes = { + HTTP: 'http', + WebSocket: 'ws', + QUIC: 'quic', + gRPC: 'grpc', + HTTPUpgrade: "httpupgrade" +} + +export type TrspType = typeof TrspTypes[keyof typeof TrspTypes] + +export type Transport = HTTP|WebSocket|QUIC|gRPC|HTTPUpgrade + +interface TransportBasics { + type: TrspType +} + +export interface HTTP extends TransportBasics { + host?: string[] + path?: string + method?: string + headers?: {} + idle_timeout?: string + ping_timeout?: string +} + +export interface WebSocket extends TransportBasics { + path: string + headers?: { + Host: string + } + max_early_data?: number + early_data_header_name?: string +} + +export interface QUIC extends TransportBasics {} + +export interface gRPC extends TransportBasics { + service_name?: string + idle_timeout?: string + ping_timeout?: string + permit_without_stream?: boolean +} + +export interface HTTPUpgrade extends TransportBasics { + host?: string + path?: string + headers?: {} +} diff --git a/frontend/src/views/Admins.vue b/frontend/src/views/Admins.vue new file mode 100644 index 0000000..9b2178f --- /dev/null +++ b/frontend/src/views/Admins.vue @@ -0,0 +1,161 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Basics.vue b/frontend/src/views/Basics.vue new file mode 100644 index 0000000..1b98349 --- /dev/null +++ b/frontend/src/views/Basics.vue @@ -0,0 +1,324 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Clients.vue b/frontend/src/views/Clients.vue new file mode 100644 index 0000000..ce53b35 --- /dev/null +++ b/frontend/src/views/Clients.vue @@ -0,0 +1,424 @@ + + + + \ No newline at end of file diff --git a/frontend/src/views/Dns.vue b/frontend/src/views/Dns.vue new file mode 100644 index 0000000..b7f62b2 --- /dev/null +++ b/frontend/src/views/Dns.vue @@ -0,0 +1,372 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Endpoints.vue b/frontend/src/views/Endpoints.vue new file mode 100644 index 0000000..6a3d84b --- /dev/null +++ b/frontend/src/views/Endpoints.vue @@ -0,0 +1,176 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..b5e57fe --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/views/Inbounds.vue b/frontend/src/views/Inbounds.vue new file mode 100644 index 0000000..eecb546 --- /dev/null +++ b/frontend/src/views/Inbounds.vue @@ -0,0 +1,190 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..b488d95 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,111 @@ + + + + \ No newline at end of file diff --git a/frontend/src/views/Outbounds.vue b/frontend/src/views/Outbounds.vue new file mode 100644 index 0000000..227017c --- /dev/null +++ b/frontend/src/views/Outbounds.vue @@ -0,0 +1,250 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Rules.vue b/frontend/src/views/Rules.vue new file mode 100644 index 0000000..ad3ffe7 --- /dev/null +++ b/frontend/src/views/Rules.vue @@ -0,0 +1,319 @@ + + + diff --git a/frontend/src/views/Services.vue b/frontend/src/views/Services.vue new file mode 100644 index 0000000..9e4d874 --- /dev/null +++ b/frontend/src/views/Services.vue @@ -0,0 +1,131 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue new file mode 100644 index 0000000..aa3b989 --- /dev/null +++ b/frontend/src/views/Settings.vue @@ -0,0 +1,284 @@ + + + diff --git a/frontend/src/views/Tls.vue b/frontend/src/views/Tls.vue new file mode 100644 index 0000000..0bda504 --- /dev/null +++ b/frontend/src/views/Tls.vue @@ -0,0 +1,141 @@ + + + diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..7a17990 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,12 @@ +/// + +declare module 'moment/locale/ru' +declare module 'moment/locale/vi' +declare module 'moment/locale/zh-cn' +declare module 'moment/locale/zh-tw' + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..275a67c --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "skipLibCheck": true, + "noEmit": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }], + "exclude": ["node_modules"], +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..5aeca0e --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.mts"], + "exclude": [] +} diff --git a/frontend/vite.config.mts b/frontend/vite.config.mts new file mode 100644 index 0000000..d406000 --- /dev/null +++ b/frontend/vite.config.mts @@ -0,0 +1,64 @@ +// Plugins +import vue from '@vitejs/plugin-vue' +import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' + +// Utilities +import { defineConfig } from 'vite' +import { fileURLToPath, URL } from 'node:url' +import { randomBytes } from 'crypto' + +function getUniqueFileName(template) { + if (template.includes('.js') || template.includes('.css')) { + const hash = randomBytes(8).toString('hex') + return template.replace('[name]', hash) + } + return template +} + +export default defineConfig({ + base: '', + plugins: [ + vue({ + template: { transformAssetUrls }, + }), + vuetify({ + autoImport: true, + styles: { + configFile: 'src/styles/settings.scss', + }, + }) + ], + build: { + manifest: false, + outDir: 'dist', + chunkSizeWarningLimit: 2000, + rollupOptions: { + output: { + codeSplitting: false, + entryFileNames: getUniqueFileName('assets/[name].js'), + chunkFileNames: getUniqueFileName('assets/[name].js'), + assetFileNames: (assetInfo) => { + if (assetInfo.names.some(name => name.endsWith('.css'))) + return getUniqueFileName('assets/[name].css') + return 'assets/' + assetInfo.names[0] + }, + }, + } + }, + define: { 'process.env': {} }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'], + }, + server: { + port: 3000, + proxy: { + '/app/api': { + target: 'http://localhost:2095', + changeOrigin: true, + }, + }, + } +}) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2c6ced3 --- /dev/null +++ b/go.mod @@ -0,0 +1,199 @@ +module github.com/alireza0/s-ui + +go 1.25.7 + +require ( + github.com/gin-contrib/gzip v1.2.5 + github.com/gin-contrib/sessions v1.0.4 + github.com/gin-gonic/gin v1.12.0 + github.com/gofrs/uuid/v5 v5.4.0 + github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 + github.com/robfig/cron/v3 v3.0.1 + github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde + github.com/sagernet/sing-box v1.13.4 + github.com/shirou/gopsutil/v4 v4.26.2 + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + filippo.io/edwards25519 v1.2.0 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/akutz/memconn v0.1.0 // indirect + github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect + github.com/anytls/sing-anytls v0.0.11 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/caddyserver/certmagic v0.25.2 // indirect + github.com/caddyserver/zerossl v0.1.5 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/coder/websocket v1.8.14 // indirect + github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect + github.com/cretz/bine v0.2.0 // indirect + github.com/database64128/netx-go v0.1.1 // indirect + github.com/database64128/tfo-go/v2 v2.3.2 // indirect + github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gaissmai/bart v0.18.0 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-chi/chi/v5 v5.2.5 // indirect + github.com/go-chi/render v1.0.3 // indirect + github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jsimonetti/rtnetlink v1.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/keybase/go-keychain v0.0.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/libdns/acmedns v0.5.0 // indirect + github.com/libdns/alidns v1.0.6 // indirect + github.com/libdns/cloudflare v0.2.2 // indirect + github.com/libdns/libdns v1.1.1 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.30 // indirect + github.com/mdlayher/netlink v1.9.0 // indirect + github.com/mdlayher/socket v0.5.1 // indirect + github.com/metacubex/utls v1.8.4 // indirect + github.com/mholt/acmez/v3 v3.1.6 // indirect + github.com/miekg/dns v1.1.72 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/openai/openai-go/v3 v3.26.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pires/go-proxyproto v0.8.1 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus-community/pro-bing v0.4.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/safchain/ethtool v0.3.0 // indirect + github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect + github.com/sagernet/cors v1.2.1 // indirect + github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9 // indirect + github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/fswatch v0.1.1 // indirect + github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 // indirect + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect + github.com/sagernet/nftables v0.3.0-beta.4 // indirect + github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 // indirect + github.com/sagernet/sing-mux v0.3.4 // indirect + github.com/sagernet/sing-quic v0.6.0 // indirect + github.com/sagernet/sing-shadowsocks v0.2.8 // indirect + github.com/sagernet/sing-shadowsocks2 v0.2.1 // indirect + github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect + github.com/sagernet/sing-tun v0.8.6 // indirect + github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect + github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 // indirect + github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c // indirect + github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect + github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect + github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect + github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect + github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect + github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect + github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/vishvananda/netns v0.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.42.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + golang.zx2c4.com/wireguard/windows v0.5.3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect + lukechampine.com/blake3 v1.4.1 // indirect +) + +replace github.com/quic-go/quic-go => github.com/quic-go/quic-go v0.57.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5d3e1bc --- /dev/null +++ b/go.sum @@ -0,0 +1,493 @@ +code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= +code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= +github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= +github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc= +github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= +github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= +github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= +github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= +github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= +github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM= +github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc= +github.com/database64128/tfo-go/v2 v2.3.2 h1:UhZMKiMq3swZGUiETkLBDzQnZBPSAeBMClpJGlnJ5Fw= +github.com/database64128/tfo-go/v2 v2.3.2/go.mod h1:GC3uB5oa4beGpCUbRb2ZOWP73bJJFmMyAVgQSO7r724= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE= +github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= +github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY= +github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= +github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= +github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= +github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= +github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= +github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 h1:u9i04mGE3iliBh0EFuWaKsmcwrLacqGmq1G3XoaM7gY= +github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo= +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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= +github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= +github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= +github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U= +github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ= +github.com/libdns/acmedns v0.5.0 h1:5pRtmUj4Lb/QkNJSl1xgOGBUJTWW7RjpNaIhjpDXjPE= +github.com/libdns/acmedns v0.5.0/go.mod h1:X7UAFP1Ep9NpTwWpVlrZzJLR7epynAy0wrIxSPFgKjQ= +github.com/libdns/alidns v1.0.6 h1:/Ii428ty6WHFJmE24rZxq2taq++gh7rf9jhgLfp8PmM= +github.com/libdns/alidns v1.0.6/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec= +github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI= +github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY= +github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco= +github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg= +github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= +github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= +github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +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/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= +github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= +github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= +github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= +github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= +github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= +github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= +github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= +github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9 h1:xq5Yr10jXEppD3cnGjE3WENaB6D0YsZu6KptZ8d3054= +github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9 h1:uxQyy6Y/boOuecVA66tf79JgtoRGfeDJcfYZZLKVA5E= +github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:Xm6cCvs0/twozC1JYNq0sVlOVmcSGzV7YON1XGcD97w= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 h1:Y7lWrZwEhC/HX8Pb5C92CrQihuaE7hrHmWB2ykst3iQ= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:3Ggy5wiyjA6t+aVVPnXlSEIVj9zkxd4ybH3NsvsNefs= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:DuFTCnZloblY+7olXiZoRdueWfxi34EV5UheTFKM2rA= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:x/6T2gjpLw9yNdCVR6xBlzMUzED9fxNFNt6U6A6SOh8= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Lx9PExM70rg8aNxPm0JPeSr5SWC3yFiCz4wIq86ugx8= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:BTEpw7/vKR9BNBsHebfpiGHDCPpjVJ3vLIbHNU3VUfM= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:hdEph9nQXRnKwc/lIDwo15rmzbC6znXF5jJWHPN1Fiw= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Iq++oYV7dtRJHTpu8yclHJdn+1oj2t1e84/YpdXYWW8= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 h1:Y43fuLL8cgwRHpEKwxh0O3vYp7g/SZGvbkJj3cQ6USA= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:bX2GJmF0VCC+tBrVAa49YEsmJ4A9dLmwoA6DJUxRtCY= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:gQTR/2azUCInE0r3kmesZT9xu+x801+BmtDY0d0Tw9Y= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 h1:X4mP3jlYvxgrKpZLOKMmc/O8T5/zP83/23pgfQOc3tY= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:c6xj2nXr/65EDiRFddUKQIBQ/b/lAPoH8WFYlgadaPc= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:ahbl7yjOvGVVNUwk9TcQk+xejVfoYAYFRlhWnby0/YM= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 h1:JC5Zv5+J85da6g5G56VhdaK53fmo6Os2q/wWi5QlxOw= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 h1:4bt7Go588BoM4VjNYMxx0MrvbwlFQn3DdRDCM7BmkRo= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:E1z0BeLUh8EZfCjIyS9BrfCocZrt+0KPS0bzop3Sxf4= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 h1:d8ejxRHO7Vi9JqR/6DxR7RyI/swA2JfDWATR4T7otBw= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 h1:iUDVEVu3RxL5ArPIY72BesbuX5zQ1la/ZFwKpQcGc5c= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 h1:xB6ikOC/R3n3hjy68EJ0sbZhH4vwEhd6JM9jZ1U2SVY= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 h1:mBOuLCPOOMMq8N1+dUM5FqZclqga1+u6fAbPqQcbIhc= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:cwPyDfj+ZNFE7kvcWbayQJyeC/KQA16HTXOxgHphL0w= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Zk9zG8kt3mXAboclUXQlvvxKQuhnI8u5NdDEl8uotNY= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:Lu05srGqddQRMnl1MZtGAReln2yJljeGx9b1IadlMJ8= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Tk9bDywUmOtc0iMjjCVIwMlAQNsxCy+bK+bTNA0OaBE= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:tQqDQw3tEHdQpt7NTdAwF3UvZ3CjNIj/IJKMRFmm388= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:biUIbI2YxUrcQikEfS/bwPA8NsHp/WO+VZUG4morUmE= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= +github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/GejpNh+z30d5H1hwaB0nD9eY3o= +github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1/go.mod h1:NJKBtm9nVEK3iyOYWsUlrDQuoGh4zJ4KOPhSYVidvQ4= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= +github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= +github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= +github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= +github.com/sagernet/sing v0.8.2 h1:kX1IH9SWJv4S0T9M8O+HNahWgbOuY1VauxbF7NU5lOg= +github.com/sagernet/sing v0.8.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c= +github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-box v1.13.3 h1:aaJz3LxpqEO2oHsynSaD4947CtVHtj5B75ChQKeEM3U= +github.com/sagernet/sing-box v1.13.3/go.mod h1:XxKnCcvh3OYE7KCqDoZ9D2U+wSh1GKm/O9TqqNkI1Hc= +github.com/sagernet/sing-box v1.13.4 h1:XfDZ4lvIFUuKS4SJDa2LjWnLxYwJfy5OF4jgI8lWUi4= +github.com/sagernet/sing-box v1.13.4/go.mod h1:ZlRKCQgJCu9ht00xse/BtsLPWYy+901l5clVvKEfJ+Y= +github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= +github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= +github.com/sagernet/sing-quic v0.6.0 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM= +github.com/sagernet/sing-quic v0.6.0/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= +github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= +github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= +github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= +github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= +github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= +github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= +github.com/sagernet/sing-tun v0.8.3 h1:mozxmuIoRhFdVHnheenLpBaammVj7bZPcnkApaYKDPY= +github.com/sagernet/sing-tun v0.8.3/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.6 h1:NydXFikSXhiKqhahHKtuZ90HQPZFzlOFVRONmkr4C7I= +github.com/sagernet/sing-tun v0.8.6/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= +github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= +github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= +github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e h1:Sv1qUhJIidjSTc24XEknovDZnbmVSlAXj8wNVgIfgGo= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 h1:8zc1Aph1+ElqF9/7aSPkO0o4vTd+AfQC+CO324mLWGg= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= +github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg= +github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= +github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= +github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= +github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= +github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..b36188e --- /dev/null +++ b/install.sh @@ -0,0 +1,188 @@ +#!/bin/bash + +red='\033[0;31m' +green='\033[0;32m' +yellow='\033[0;33m' +plain='\033[0m' + +cur_dir=$(pwd) + +# check root +[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1 + +# Check OS and set release variable +if [[ -f /etc/os-release ]]; then + source /etc/os-release + release=$ID +elif [[ -f /usr/lib/os-release ]]; then + source /usr/lib/os-release + release=$ID +else + echo "Failed to check the system OS, please contact the author!" >&2 + exit 1 +fi +echo "The OS release is: $release" + +arch() { + case "$(uname -m)" in + x86_64 | x64 | amd64) echo 'amd64' ;; + i*86 | x86) echo '386' ;; + armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;; + armv7* | armv7 | arm) echo 'armv7' ;; + armv6* | armv6) echo 'armv6' ;; + armv5* | armv5) echo 'armv5' ;; + s390x) echo 's390x' ;; + *) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;; + esac +} + +echo "arch: $(arch)" + +install_base() { + case "${release}" in + centos | almalinux | rocky | oracle) + yum -y update && yum install -y -q wget curl tar tzdata + ;; + fedora) + dnf -y update && dnf install -y -q wget curl tar tzdata + ;; + arch | manjaro | parch) + pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata + ;; + opensuse-tumbleweed) + zypper refresh && zypper -q install -y wget curl tar timezone + ;; + *) + apt-get update && apt-get install -y -q wget curl tar tzdata + ;; + esac +} + +config_after_install() { + echo -e "${yellow}Migration... ${plain}" + /usr/local/s-ui/sui migrate + + echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" + read -p "Do you want to continue with the modification [y/n]? ": config_confirm + if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then + echo -e "Enter the ${yellow}panel port${plain} (leave blank for existing/default value):" + read config_port + echo -e "Enter the ${yellow}panel path${plain} (leave blank for existing/default value):" + read config_path + + # Sub configuration + echo -e "Enter the ${yellow}subscription port${plain} (leave blank for existing/default value):" + read config_subPort + echo -e "Enter the ${yellow}subscription path${plain} (leave blank for existing/default value):" + read config_subPath + + # Set configs + echo -e "${yellow}Initializing, please wait...${plain}" + params="" + [ -z "$config_port" ] || params="$params -port $config_port" + [ -z "$config_path" ] || params="$params -path $config_path" + [ -z "$config_subPort" ] || params="$params -subPort $config_subPort" + [ -z "$config_subPath" ] || params="$params -subPath $config_subPath" + /usr/local/s-ui/sui setting ${params} + + read -p "Do you want to change admin credentials [y/n]? ": admin_confirm + if [[ "${admin_confirm}" == "y" || "${admin_confirm}" == "Y" ]]; then + # First admin credentials + read -p "Please set up your username:" config_account + read -p "Please set up your password:" config_password + + # Set credentials + echo -e "${yellow}Initializing, please wait...${plain}" + /usr/local/s-ui/sui admin -username ${config_account} -password ${config_password} + else + echo -e "${yellow}Your current admin credentials: ${plain}" + /usr/local/s-ui/sui admin -show + fi + else + echo -e "${red}cancel...${plain}" + if [[ ! -f "/usr/local/s-ui/db/s-ui.db" ]]; then + local usernameTemp=$(head -c 6 /dev/urandom | base64) + local passwordTemp=$(head -c 6 /dev/urandom | base64) + echo -e "this is a fresh installation,will generate random login info for security concerns:" + echo -e "###############################################" + echo -e "${green}username:${usernameTemp}${plain}" + echo -e "${green}password:${passwordTemp}${plain}" + echo -e "###############################################" + echo -e "${red}if you forgot your login info,you can type ${green}s-ui${red} for configuration menu${plain}" + /usr/local/s-ui/sui admin -username ${usernameTemp} -password ${passwordTemp} + else + echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type ${green}s-ui${red} for configuration menu${plain}" + fi + fi +} + +prepare_services() { + if [[ -f "/etc/systemd/system/sing-box.service" ]]; then + echo -e "${yellow}Stopping sing-box service... ${plain}" + systemctl stop sing-box + rm -f /usr/local/s-ui/bin/sing-box /usr/local/s-ui/bin/runSingbox.sh /usr/local/s-ui/bin/signal + fi + if [[ -e "/usr/local/s-ui/bin" ]]; then + echo -e "###############################################################" + echo -e "${green}/usr/local/s-ui/bin${red} directory exists yet!" + echo -e "Please check the content and delete it manually after migration ${plain}" + echo -e "###############################################################" + fi + systemctl daemon-reload +} + +install_s-ui() { + cd /tmp/ + + if [ $# == 0 ]; then + last_version=$(curl -Ls "https://api.github.com/repos/alireza0/s-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + if [[ ! -n "$last_version" ]]; then + echo -e "${red}Failed to fetch s-ui version, it maybe due to Github API restrictions, please try it later${plain}" + exit 1 + fi + echo -e "Got s-ui latest version: ${last_version}, beginning the installation..." + wget -N --no-check-certificate -O /tmp/s-ui-linux-$(arch).tar.gz https://github.com/alireza0/s-ui/releases/download/${last_version}/s-ui-linux-$(arch).tar.gz + if [[ $? -ne 0 ]]; then + echo -e "${red}Downloading s-ui failed, please be sure that your server can access Github ${plain}" + exit 1 + fi + else + last_version=$1 + url="https://github.com/alireza0/s-ui/releases/download/${last_version}/s-ui-linux-$(arch).tar.gz" + echo -e "Beginning the install s-ui v$1" + wget -N --no-check-certificate -O /tmp/s-ui-linux-$(arch).tar.gz ${url} + if [[ $? -ne 0 ]]; then + echo -e "${red}download s-ui v$1 failed,please check the version exists${plain}" + exit 1 + fi + fi + + if [[ -e /usr/local/s-ui/ ]]; then + systemctl stop s-ui + fi + + tar zxvf s-ui-linux-$(arch).tar.gz + rm s-ui-linux-$(arch).tar.gz -f + + chmod +x s-ui/sui s-ui/s-ui.sh + cp s-ui/s-ui.sh /usr/bin/s-ui + cp -rf s-ui /usr/local/ + cp -f s-ui/*.service /etc/systemd/system/ + rm -rf s-ui + + config_after_install + prepare_services + + systemctl enable s-ui --now + + echo -e "${green}s-ui v${last_version}${plain} installation finished, it is up and running now..." + echo -e "You may access the Panel with following URL(s):${green}" + /usr/local/s-ui/sui uri + echo -e "${plain}" + echo -e "" + s-ui help +} + +echo -e "${green}Executing...${plain}" +install_base +install_s-ui $1 diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..1f47d14 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,128 @@ +package logger + +import ( + "fmt" + "os" + "time" + + "github.com/op/go-logging" +) + +var ( + logger *logging.Logger + logBuffer []struct { + time string + level logging.Level + log string + } +) + +func InitLogger(level logging.Level) { + newLogger := logging.MustGetLogger("s-ui") + var err error + var backend logging.Backend + var format logging.Formatter + + _, inContainer := os.LookupEnv("container") + if !inContainer { + if _, statErr := os.Stat("/.dockerenv"); statErr == nil { + inContainer = true + } + } + if inContainer { + backend = logging.NewLogBackend(os.Stderr, "", 0) + format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`) + } else { + backend, err = logging.NewSyslogBackend("") + if err != nil { + fmt.Println("Unable to use syslog: " + err.Error()) + backend = logging.NewLogBackend(os.Stderr, "", 0) + } + if err != nil { + format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`) + } else { + format = logging.MustStringFormatter(`%{level} - %{message}`) + } + } + + backendFormatter := logging.NewBackendFormatter(backend, format) + backendLeveled := logging.AddModuleLevel(backendFormatter) + backendLeveled.SetLevel(level, "s-ui") + newLogger.SetBackend(backendLeveled) + + logger = newLogger +} + +func GetLogger() *logging.Logger { + return logger +} + +func Debug(args ...interface{}) { + logger.Debug(args...) + addToBuffer("DEBUG", fmt.Sprint(args...)) +} + +func Debugf(format string, args ...interface{}) { + logger.Debugf(format, args...) + addToBuffer("DEBUG", fmt.Sprintf(format, args...)) +} + +func Info(args ...interface{}) { + logger.Info(args...) + addToBuffer("INFO", fmt.Sprint(args...)) +} + +func Infof(format string, args ...interface{}) { + logger.Infof(format, args...) + addToBuffer("INFO", fmt.Sprintf(format, args...)) +} + +func Warning(args ...interface{}) { + logger.Warning(args...) + addToBuffer("WARNING", fmt.Sprint(args...)) +} + +func Warningf(format string, args ...interface{}) { + logger.Warningf(format, args...) + addToBuffer("WARNING", fmt.Sprintf(format, args...)) +} + +func Error(args ...interface{}) { + logger.Error(args...) + addToBuffer("ERROR", fmt.Sprint(args...)) +} + +func Errorf(format string, args ...interface{}) { + logger.Errorf(format, args...) + addToBuffer("ERROR", fmt.Sprintf(format, args...)) +} + +func addToBuffer(level string, newLog string) { + t := time.Now() + if len(logBuffer) >= 10240 { + logBuffer = logBuffer[1:] + } + + logLevel, _ := logging.LogLevel(level) + logBuffer = append(logBuffer, struct { + time string + level logging.Level + log string + }{ + time: t.Format("2006/01/02 15:04:05"), + level: logLevel, + log: newLog, + }) +} + +func GetLogs(c int, level string) []string { + var output []string + logLevel, _ := logging.LogLevel(level) + + for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- { + if logBuffer[i].level <= logLevel { + output = append(output, fmt.Sprintf("%s %s - %s", logBuffer[i].time, logBuffer[i].level, logBuffer[i].log)) + } + } + return output +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2d54dd2 --- /dev/null +++ b/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + + "github.com/alireza0/s-ui/app" + "github.com/alireza0/s-ui/cmd" +) + +func runApp() { + app := app.NewApp() + + err := app.Init() + if err != nil { + log.Fatal(err) + } + + err = app.Start() + if err != nil { + log.Fatal(err) + } + + sigCh := make(chan os.Signal, 1) + // Trap shutdown signals + signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM) + for { + sig := <-sigCh + + switch sig { + case syscall.SIGHUP: + app.RestartApp() + default: + app.Stop() + return + } + } +} + +func main() { + if len(os.Args) < 2 { + runApp() + return + } else { + cmd.ParseCmd() + } +} diff --git a/middleware/domainValidator.go b/middleware/domainValidator.go new file mode 100644 index 0000000..44f0de5 --- /dev/null +++ b/middleware/domainValidator.go @@ -0,0 +1,25 @@ +package middleware + +import ( + "net" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func DomainValidator(domain string) gin.HandlerFunc { + return func(c *gin.Context) { + host := c.Request.Host + if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 { + host, _, _ = net.SplitHostPort(c.Request.Host) + } + + if host != domain { + c.AbortWithStatus(http.StatusForbidden) + return + } + + c.Next() + } +} diff --git a/network/auto_https_conn.go b/network/auto_https_conn.go new file mode 100644 index 0000000..f0b700b --- /dev/null +++ b/network/auto_https_conn.go @@ -0,0 +1,67 @@ +package network + +import ( + "bufio" + "bytes" + "fmt" + "net" + "net/http" + "sync" +) + +type AutoHttpsConn struct { + net.Conn + + firstBuf []byte + bufStart int + + readRequestOnce sync.Once +} + +func NewAutoHttpsConn(conn net.Conn) net.Conn { + return &AutoHttpsConn{ + Conn: conn, + } +} + +func (c *AutoHttpsConn) readRequest() bool { + c.firstBuf = make([]byte, 2048) + n, err := c.Conn.Read(c.firstBuf) + c.firstBuf = c.firstBuf[:n] + if err != nil { + return false + } + reader := bytes.NewReader(c.firstBuf) + bufReader := bufio.NewReader(reader) + request, err := http.ReadRequest(bufReader) + if err != nil { + return false + } + resp := http.Response{ + Header: http.Header{}, + } + resp.StatusCode = http.StatusTemporaryRedirect + location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI) + resp.Header.Set("Location", location) + resp.Write(c.Conn) + c.Close() + c.firstBuf = nil + return true +} + +func (c *AutoHttpsConn) Read(buf []byte) (int, error) { + c.readRequestOnce.Do(func() { + c.readRequest() + }) + + if c.firstBuf != nil { + n := copy(buf, c.firstBuf[c.bufStart:]) + c.bufStart += n + if c.bufStart >= len(c.firstBuf) { + c.firstBuf = nil + } + return n, nil + } + + return c.Conn.Read(buf) +} diff --git a/network/auto_https_listener.go b/network/auto_https_listener.go new file mode 100644 index 0000000..87fee83 --- /dev/null +++ b/network/auto_https_listener.go @@ -0,0 +1,21 @@ +package network + +import "net" + +type AutoHttpsListener struct { + net.Listener +} + +func NewAutoHttpsListener(listener net.Listener) net.Listener { + return &AutoHttpsListener{ + Listener: listener, + } +} + +func (l *AutoHttpsListener) Accept() (net.Conn, error) { + conn, err := l.Listener.Accept() + if err != nil { + return nil, err + } + return NewAutoHttpsConn(conn), nil +} diff --git a/runSUI.sh b/runSUI.sh new file mode 100644 index 0000000..3b8526c --- /dev/null +++ b/runSUI.sh @@ -0,0 +1,2 @@ +./build.sh +SUI_DB_FOLDER="db" SUI_DEBUG=true ./sui \ No newline at end of file diff --git a/s-ui.service b/s-ui.service new file mode 100644 index 0000000..cac1b08 --- /dev/null +++ b/s-ui.service @@ -0,0 +1,14 @@ +[Unit] +Description=s-ui Service +After=network.target +Wants=network.target + +[Service] +Type=simple +WorkingDirectory=/usr/local/s-ui/ +ExecStart=/usr/local/s-ui/sui +Restart=on-failure +RestartSec=10s + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/s-ui.sh b/s-ui.sh new file mode 100644 index 0000000..2526292 --- /dev/null +++ b/s-ui.sh @@ -0,0 +1,934 @@ +#!/bin/bash + +red='\033[0;31m' +green='\033[0;32m' +yellow='\033[0;33m' +plain='\033[0m' + +function LOGD() { + echo -e "${yellow}[DEG] $* ${plain}" +} + +function LOGE() { + echo -e "${red}[ERR] $* ${plain}" +} + +function LOGI() { + echo -e "${green}[INF] $* ${plain}" +} + +[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1 + +if [[ -f /etc/os-release ]]; then + source /etc/os-release + release=$ID +elif [[ -f /usr/lib/os-release ]]; then + source /usr/lib/os-release + release=$ID +else + echo "Failed to check the system OS, please contact the author!" >&2 + exit 1 +fi + +echo "The OS release is: $release" + +confirm() { + if [[ $# > 1 ]]; then + echo && read -p "$1 [Default$2]: " temp + if [[ x"${temp}" == x"" ]]; then + temp=$2 + fi + else + read -p "$1 [y/n]: " temp + fi + if [[ x"${temp}" == x"y" || x"${temp}" == x"Y" ]]; then + return 0 + else + return 1 + fi +} + +confirm_restart() { + confirm "Restart the ${1} service" "y" + if [[ $? == 0 ]]; then + restart + else + show_menu + fi +} + +before_show_menu() { + echo && echo -n -e "${yellow}Press enter to return to the main menu: ${plain}" && read temp + show_menu +} + +install() { + bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/main/install.sh) + if [[ $? == 0 ]]; then + if [[ $# == 0 ]]; then + start + else + start 0 + fi + fi +} + +update() { + confirm "This function will forcefully reinstall the latest version, and the data will not be lost. Do you want to continue?" "n" + if [[ $? != 0 ]]; then + LOGE "Cancelled" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 0 + fi + bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/main/install.sh) + if [[ $? == 0 ]]; then + LOGI "Update is complete, Panel has automatically restarted " + exit 0 + fi +} + +custom_version() { + echo "Enter the panel version (like 0.0.1):" + read panel_version + + if [ -z "$panel_version" ]; then + echo "Panel version cannot be empty. Exiting." + exit 1 + fi + + download_link="https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh" + + install_command="bash <(curl -Ls $download_link) $panel_version" + + echo "Downloading and installing panel version $panel_version..." + eval $install_command +} + +uninstall() { + confirm "Are you sure you want to uninstall the panel?" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + systemctl stop s-ui + systemctl disable s-ui + rm /etc/systemd/system/s-ui.service -f + systemctl daemon-reload + systemctl reset-failed + rm /etc/s-ui/ -rf + rm /usr/local/s-ui/ -rf + + echo "" + echo -e "Uninstalled Successfully, If you want to remove this script, then after exiting the script run ${green}rm /usr/local/s-ui -f${plain} to delete it." + echo "" + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +reset_admin() { + echo "It is not recommended to set admin's credentials to default!" + confirm "Are you sure you want to reset admin's credentials to default ?" "n" + if [[ $? == 0 ]]; then + /usr/local/s-ui/sui admin -reset + fi + before_show_menu +} + +set_admin() { + echo "It is not recommended to set admin's credentials to a complex text." + read -p "Please set up your username:" config_account + read -p "Please set up your password:" config_password + /usr/local/s-ui/sui admin -username ${config_account} -password ${config_password} + before_show_menu +} + +view_admin() { + /usr/local/s-ui/sui admin -show + before_show_menu +} + +reset_setting() { + confirm "Are you sure you want to reset settings to default ?" "n" + if [[ $? == 0 ]]; then + /usr/local/s-ui/sui setting -reset + fi + before_show_menu +} + +set_setting() { + echo -e "Enter the ${yellow}panel port${plain} (leave blank for existing/default value):" + read config_port + echo -e "Enter the ${yellow}panel path${plain} (leave blank for existing/default value):" + read config_path + + echo -e "Enter the ${yellow}subscription port${plain} (leave blank for existing/default value):" + read config_subPort + echo -e "Enter the ${yellow}subscription path${plain} (leave blank for existing/default value):" + read config_subPath + + echo -e "${yellow}Initializing, please wait...${plain}" + params="" + [ -z "$config_port" ] || params="$params -port $config_port" + [ -z "$config_path" ] || params="$params -path $config_path" + [ -z "$config_subPort" ] || params="$params -subPort $config_subPort" + [ -z "$config_subPath" ] || params="$params -subPath $config_subPath" + /usr/local/s-ui/sui setting ${params} + before_show_menu +} + +view_setting() { + /usr/local/s-ui/sui setting -show + view_uri + before_show_menu +} + +view_uri() { + info=$(/usr/local/s-ui/sui uri) + if [[ $? != 0 ]]; then + LOGE "Get current uri error" + before_show_menu + fi + LOGI "You may access the Panel with following URL(s):" + echo -e "${green}${info}${plain}" +} + +start() { + check_status $1 + if [[ $? == 0 ]]; then + echo "" + LOGI -e "${1} is running, No need to start again, If you need to restart, please select restart" + else + systemctl start $1 + sleep 2 + check_status $1 + if [[ $? == 0 ]]; then + LOGI "${1} Started Successfully" + else + LOGE "Failed to start ${1}, Probably because it takes longer than two seconds to start, Please check the log information later" + fi + fi + + if [[ $# == 1 ]]; then + before_show_menu + fi +} + +stop() { + check_status $1 + if [[ $? == 1 ]]; then + echo "" + LOGI "${1} stopped, No need to stop again!" + else + systemctl stop $1 + sleep 2 + check_status + if [[ $? == 1 ]]; then + LOGI "${1} stopped successfully" + else + LOGE "Failed to stop ${1}, Probably because the stop time exceeds two seconds, Please check the log information later" + fi + fi + + if [[ $# == 1 ]]; then + before_show_menu + fi +} + +restart() { + systemctl restart $1 + sleep 2 + check_status $1 + if [[ $? == 0 ]]; then + LOGI "${1} Restarted successfully" + else + LOGE "Failed to restart ${1}, Probably because it takes longer than two seconds to start, Please check the log information later" + fi + if [[ $# == 1 ]]; then + before_show_menu + fi +} + +status() { + systemctl status s-ui -l + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +enable() { + systemctl enable $1 + if [[ $? == 0 ]]; then + LOGI "Set ${1} to boot automatically on startup successfully" + else + LOGE "Failed to set ${1} Autostart" + fi + + if [[ $# == 1 ]]; then + before_show_menu + fi +} + +disable() { + systemctl disable $1 + if [[ $? == 0 ]]; then + LOGI "Autostart ${1} Cancelled successfully" + else + LOGE "Failed to cancel ${1} autostart" + fi + + if [[ $# == 1 ]]; then + before_show_menu + fi +} + +show_log() { + journalctl -u $1.service -e --no-pager -f + if [[ $# == 1 ]]; then + before_show_menu + fi +} + +update_shell() { + wget -O /usr/bin/s-ui -N --no-check-certificate https://github.com/alireza0/s-ui/raw/main/s-ui.sh + if [[ $? != 0 ]]; then + echo "" + LOGE "Failed to download script, Please check whether the machine can connect Github" + before_show_menu + else + chmod +x /usr/bin/s-ui + LOGI "Upgrade script succeeded, Please rerun the script" && exit 0 + fi +} + +check_status() { + if [[ ! -f "/etc/systemd/system/$1.service" ]]; then + return 2 + fi + temp=$(systemctl status "$1" | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) + if [[ x"${temp}" == x"running" ]]; then + return 0 + else + return 1 + fi +} + +check_enabled() { + temp=$(systemctl is-enabled $1) + if [[ x"${temp}" == x"enabled" ]]; then + return 0 + else + return 1 + fi +} + +check_uninstall() { + check_status s-ui + if [[ $? != 2 ]]; then + echo "" + LOGE "Panel is already installed, Please do not reinstall" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +check_install() { + check_status s-ui + if [[ $? == 2 ]]; then + echo "" + LOGE "Please install the panel first" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +show_status() { + check_status $1 + case $? in + 0) + echo -e "${1} state: ${green}Running${plain}" + show_enable_status $1 + ;; + 1) + echo -e "${1} state: ${yellow}Not Running${plain}" + show_enable_status $1 + ;; + 2) + echo -e "${1} state: ${red}Not Installed${plain}" + ;; + esac +} + +show_enable_status() { + check_enabled $1 + if [[ $? == 0 ]]; then + echo -e "Start ${1} automatically: ${green}Yes${plain}" + else + echo -e "Start ${1} automatically: ${red}No${plain}" + fi +} + +check_s-ui_status() { + count=$(ps -ef | grep "sui" | grep -v "grep" | wc -l) + if [[ count -ne 0 ]]; then + return 0 + else + return 1 + fi +} + +show_s-ui_status() { + check_s-ui_status + if [[ $? == 0 ]]; then + echo -e "s-ui state: ${green}Running${plain}" + else + echo -e "s-ui state: ${red}Not Running${plain}" + fi +} + +bbr_menu() { + echo -e "${green}\t1.${plain} Enable BBR" + echo -e "${green}\t2.${plain} Disable BBR" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -p "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + enable_bbr + ;; + 2) + disable_bbr + ;; + *) echo "Invalid choice" ;; + esac +} + +disable_bbr() { + if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then + echo -e "${yellow}BBR is not currently enabled.${plain}" + exit 0 + fi + sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf + sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf + sysctl -p + if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then + echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}" + else + echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}" + fi +} + +enable_bbr() { + if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then + echo -e "${green}BBR is already enabled!${plain}" + exit 0 + fi + case "${release}" in + ubuntu | debian | armbian) + apt-get update && apt-get install -yqq --no-install-recommends ca-certificates + ;; + centos | almalinux | rocky | oracle) + yum -y update && yum -y install ca-certificates + ;; + fedora) + dnf -y update && dnf -y install ca-certificates + ;; + arch | manjaro | parch) + pacman -Sy --noconfirm ca-certificates + ;; + *) + echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" + exit 1 + ;; + esac + echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf + echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf + sysctl -p + if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then + echo -e "${green}BBR has been enabled successfully.${plain}" + else + echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}" + fi +} + +install_acme() { + cd ~ + LOGI "install acme..." + curl https://get.acme.sh | sh + if [ $? -ne 0 ]; then + LOGE "install acme failed" + return 1 + else + LOGI "install acme succeed" + fi + return 0 +} + +ssl_cert_issue_main() { + echo -e "${green}\t1.${plain} Get SSL" + echo -e "${green}\t2.${plain} Revoke" + echo -e "${green}\t3.${plain} Force Renew" + echo -e "${green}\t4.${plain} Self-signed Certificate" + read -p "Choose an option: " choice + case "$choice" in + 1) ssl_cert_issue ;; + 2) + local domain="" + read -p "Please enter your domain name to revoke the certificate: " domain + ~/.acme.sh/acme.sh --revoke -d ${domain} + LOGI "Certificate revoked" + ;; + 3) + local domain="" + read -p "Please enter your domain name to forcefully renew an SSL certificate: " domain + ~/.acme.sh/acme.sh --renew -d ${domain} --force ;; + 4) + generate_self_signed_cert + ;; + *) echo "Invalid choice" ;; + esac +} + +ssl_cert_issue() { + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. we will install it" + install_acme + if [ $? -ne 0 ]; then + LOGE "install acme failed, please check logs" + exit 1 + fi + fi + case "${release}" in + ubuntu | debian | armbian) + apt update && apt install socat -y + ;; + centos | almalinux | rocky | oracle) + yum -y update && yum -y install socat + ;; + fedora) + dnf -y update && dnf -y install socat + ;; + arch | manjaro | parch) + pacman -Sy --noconfirm socat + ;; + *) + echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" + exit 1 + ;; + esac + if [ $? -ne 0 ]; then + LOGE "install socat failed, please check logs" + exit 1 + else + LOGI "install socat succeed..." + fi + + local domain="" + read -p "Please enter your domain name:" domain + LOGD "your domain is:${domain},check it..." + local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') + + if [ ${currentCert} == ${domain} ]; then + local certInfo=$(~/.acme.sh/acme.sh --list) + LOGE "system already has certs here,can not issue again,current certs details:" + LOGI "$certInfo" + exit 1 + else + LOGI "your domain is ready for issuing cert now..." + fi + + certPath="/root/cert/${domain}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + else + rm -rf "$certPath" + mkdir -p "$certPath" + fi + + local WebPort=80 + read -p "please choose which port do you use,default will be 80 port:" WebPort + if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then + LOGE "your input ${WebPort} is invalid,will use default port" + fi + LOGI "will use port:${WebPort} to issue certs,please make sure this port is open..." + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue -d ${domain} --standalone --httpport ${WebPort} + if [ $? -ne 0 ]; then + LOGE "issue certs failed,please check logs" + rm -rf ~/.acme.sh/${domain} + exit 1 + else + LOGE "issue certs succeed,installing certs..." + fi + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem + + if [ $? -ne 0 ]; then + LOGE "install certs failed,exit" + rm -rf ~/.acme.sh/${domain} + exit 1 + else + LOGI "install certs succeed,enable auto renew..." + fi + + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + LOGE "auto renew failed, certs details:" + ls -lah cert/* + chmod 755 $certPath/* + exit 1 + else + LOGI "auto renew succeed, certs details:" + ls -lah cert/* + chmod 755 $certPath/* + fi +} + +ssl_cert_issue_CF() { + echo -E "" + LOGD "******Instructions for use******" + echo "1) New certificate from Cloudflare" + echo "2) Force renew existing Certificates" + echo "3) Back to Menu" + read -p "Enter your choice [1-3]: " choice + + certPath="/root/cert-CF" + + case $choice in + 1|2) + force_flag="" + if [ "$choice" -eq 2 ]; then + force_flag="--force" + echo "Forcing SSL certificate reissuance..." + else + echo "Starting SSL certificate issuance..." + fi + + LOGD "******Instructions for use******" + LOGI "This Acme script requires the following data:" + LOGI "1.Cloudflare Registered e-mail" + LOGI "2.Cloudflare Global API Key" + LOGI "3.The domain name that has been resolved DNS to the current server by Cloudflare" + LOGI "4.The script applies for a certificate. The default installation path is /root/cert " + confirm "Confirmed?[y/n]" "y" + if [ $? -eq 0 ]; then + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. Installing..." + install_acme + if [ $? -ne 0 ]; then + LOGE "Install acme failed, please check logs" + exit 1 + fi + fi + + CF_Domain="" + if [ ! -d "$certPath" ]; then + mkdir -p $certPath + else + rm -rf $certPath + mkdir -p $certPath + fi + + LOGD "Please set a domain name:" + read -p "Input your domain here: " CF_Domain + LOGD "Your domain name is set to: ${CF_Domain}" + + CF_GlobalKey="" + CF_AccountEmail="" + LOGD "Please set the API key:" + read -p "Input your key here: " CF_GlobalKey + LOGD "Your API key is: ${CF_GlobalKey}" + + LOGD "Please set up registered email:" + read -p "Input your email here: " CF_AccountEmail + LOGD "Your registered email address is: ${CF_AccountEmail}" + + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + if [ $? -ne 0 ]; then + LOGE "Default CA, Let's Encrypt failed, script exiting..." + exit 1 + fi + + export CF_Key="${CF_GlobalKey}" + export CF_Email="${CF_AccountEmail}" + + ~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} $force_flag --log + if [ $? -ne 0 ]; then + LOGE "Certificate issuance failed, script exiting..." + exit 1 + else + LOGI "Certificate issued Successfully, Installing..." + fi + + mkdir -p ${certPath}/${CF_Domain} + if [ $? -ne 0 ]; then + LOGE "Failed to create directory: ${certPath}/${CF_Domain}" + exit 1 + fi + + ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \ + --fullchain-file ${certPath}/${CF_Domain}/fullchain.pem \ + --key-file ${certPath}/${CF_Domain}/privkey.pem + + if [ $? -ne 0 ]; then + LOGE "Certificate installation failed, script exiting..." + exit 1 + else + LOGI "Certificate installed Successfully, Turning on automatic updates..." + fi + + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + LOGE "Auto update setup failed, script exiting..." + exit 1 + else + LOGI "The certificate is installed and auto-renewal is turned on." + ls -lah ${certPath}/${CF_Domain} + chmod 755 ${certPath}/${CF_Domain} + fi + fi + show_menu + ;; + 3) + echo "Exiting..." + show_menu + ;; + *) + echo "Invalid choice, please select again." + show_menu + ;; + esac +} + +generate_self_signed_cert() { + cert_dir="/etc/sing-box" + mkdir -p "$cert_dir" + LOGI "Choose certificate type:" + echo -e "${green}\t1.${plain} Ed25519 (*recommended*)" + echo -e "${green}\t2.${plain} RSA 2048" + echo -e "${green}\t3.${plain} RSA 4096" + echo -e "${green}\t4.${plain} ECDSA prime256v1" + echo -e "${green}\t5.${plain} ECDSA secp384r1" + read -p "Enter your choice [1-5, default 1]: " cert_type + cert_type=${cert_type:-1} + + case "$cert_type" in + 1) + algo="ed25519" + key_opt="-newkey ed25519" + ;; + 2) + algo="rsa" + key_opt="-newkey rsa:2048" + ;; + 3) + algo="rsa" + key_opt="-newkey rsa:4096" + ;; + 4) + algo="ecdsa" + key_opt="-newkey ec -pkeyopt ec_paramgen_curve:prime256v1" + ;; + 5) + algo="ecdsa" + key_opt="-newkey ec -pkeyopt ec_paramgen_curve:secp384r1" + ;; + *) + algo="ed25519" + key_opt="-newkey ed25519" + ;; + esac + + LOGI "Generating self-signed certificate ($algo)..." + sudo openssl req -x509 -nodes -days 3650 $key_opt \ + -keyout "${cert_dir}/self.key" \ + -out "${cert_dir}/self.crt" \ + -subj "/CN=myserver" + if [[ $? -eq 0 ]]; then + sudo chmod 600 "${cert_dir}/self."* + LOGI "Self-signed certificate generated successfully!" + LOGI "Certificate path: ${cert_dir}/self.crt" + LOGI "Key path: ${cert_dir}/self.key" + else + LOGE "Failed to generate self-signed certificate." + fi + before_show_menu +} + +show_usage() { + echo -e "S-UI Control Menu Usage" + echo -e "------------------------------------------" + echo -e "SUBCOMMANDS:" + echo -e "s-ui - Admin Management Script" + echo -e "s-ui start - Start s-ui" + echo -e "s-ui stop - Stop s-ui" + echo -e "s-ui restart - Restart s-ui" + echo -e "s-ui status - Current Status of s-ui" + echo -e "s-ui enable - Enable Autostart on OS Startup" + echo -e "s-ui disable - Disable Autostart on OS Startup" + echo -e "s-ui log - Check s-ui Logs" + echo -e "s-ui update - Update" + echo -e "s-ui install - Install" + echo -e "s-ui uninstall - Uninstall" + echo -e "s-ui help - Control Menu Usage" + echo -e "------------------------------------------" +} + +show_menu() { + echo -e " + ${green}S-UI Admin Management Script ${plain} +———————————————————————————————— + ${green}0.${plain} Exit +———————————————————————————————— + ${green}1.${plain} Install + ${green}2.${plain} Update + ${green}3.${plain} Custom Version + ${green}4.${plain} Uninstall +———————————————————————————————— + ${green}5.${plain} Reset admin credentials to default + ${green}6.${plain} Set admin credentials + ${green}7.${plain} View admin credentials +———————————————————————————————— + ${green}8.${plain} Reset Panel Settings + ${green}9.${plain} Set Panel settings + ${green}10.${plain} View Panel Settings +———————————————————————————————— + ${green}11.${plain} S-UI Start + ${green}12.${plain} S-UI Stop + ${green}13.${plain} S-UI Restart + ${green}14.${plain} S-UI Check State + ${green}15.${plain} S-UI Check Logs + ${green}16.${plain} S-UI Enable Autostart + ${green}17.${plain} S-UI Disable Autostart +———————————————————————————————— + ${green}18.${plain} Enable or Disable BBR + ${green}19.${plain} SSL Certificate Management + ${green}20.${plain} Cloudflare SSL Certificate +———————————————————————————————— + " + show_status s-ui + echo && read -p "Please enter your selection [0-20]: " num + + case "${num}" in + 0) + exit 0 + ;; + 1) + check_uninstall && install + ;; + 2) + check_install && update + ;; + 3) + check_install && custom_version + ;; + 4) + check_install && uninstall + ;; + 5) + check_install && reset_admin + ;; + 6) + check_install && set_admin + ;; + 7) + check_install && view_admin + ;; + 8) + check_install && reset_setting + ;; + 9) + check_install && set_setting + ;; + 10) + check_install && view_setting + ;; + 11) + check_install && start s-ui + ;; + 12) + check_install && stop s-ui + ;; + 13) + check_install && restart s-ui + ;; + 14) + check_install && status s-ui + ;; + 15) + check_install && show_log s-ui + ;; + 16) + check_install && enable s-ui + ;; + 17) + check_install && disable s-ui + ;; + 18) + bbr_menu + ;; + 19) + ssl_cert_issue_main + ;; + 20) + ssl_cert_issue_CF + ;; + *) + LOGE "Please enter the correct number [0-20]" + ;; + esac +} + +if [[ $# > 0 ]]; then + case $1 in + "start") + check_install 0 && start s-ui 0 + ;; + "stop") + check_install 0 && stop s-ui 0 + ;; + "restart") + check_install 0 && restart s-ui 0 + ;; + "status") + check_install 0 && status 0 + ;; + "enable") + check_install 0 && enable s-ui 0 + ;; + "disable") + check_install 0 && disable s-ui 0 + ;; + "log") + check_install 0 && show_log s-ui 0 + ;; + "update") + check_install 0 && update 0 + ;; + "install") + check_uninstall 0 && install 0 + ;; + "uninstall") + check_install 0 && uninstall 0 + ;; + *) show_usage ;; + esac +else + show_menu +fi diff --git a/service/client.go b/service/client.go new file mode 100644 index 0000000..3bece6b --- /dev/null +++ b/service/client.go @@ -0,0 +1,548 @@ +package service + +import ( + "bytes" + "encoding/json" + "strings" + "time" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util" + "github.com/alireza0/s-ui/util/common" + + "gorm.io/gorm" +) + +type ClientService struct{} + +func (s *ClientService) Get(id string) (*[]model.Client, error) { + if id == "" { + return s.GetAll() + } + return s.getById(id) +} + +func (s *ClientService) getById(id string) (*[]model.Client, error) { + db := database.GetDB() + var client []model.Client + err := db.Model(model.Client{}).Where("id in ?", strings.Split(id, ",")).Scan(&client).Error + if err != nil { + return nil, err + } + + return &client, nil +} + +func (s *ClientService) GetAll() (*[]model.Client, error) { + db := database.GetDB() + var clients []model.Client + err := db.Model(model.Client{}). + Select("`id`, `enable`, `name`, `desc`, `group`, `inbounds`, `up`, `down`, `volume`, `expiry`"). + Scan(&clients).Error + if err != nil { + return nil, err + } + return &clients, nil +} + +func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) ([]uint, error) { + var err error + var inboundIds []uint + + switch act { + case "new", "edit": + var client model.Client + err = json.Unmarshal(data, &client) + if err != nil { + return nil, err + } + err = s.updateLinksWithFixedInbounds(tx, []*model.Client{&client}, hostname) + if err != nil { + return nil, err + } + if act == "edit" { + // Find changed inbounds + inboundIds, err = s.findInboundsChanges(tx, &client, false) + if err != nil { + return nil, err + } + } else { + err = json.Unmarshal(client.Inbounds, &inboundIds) + if err != nil { + return nil, err + } + } + err = tx.Save(&client).Error + if err != nil { + return nil, err + } + case "addbulk": + var clients []*model.Client + err = json.Unmarshal(data, &clients) + if err != nil { + return nil, err + } + err = json.Unmarshal(clients[0].Inbounds, &inboundIds) + if err != nil { + return nil, err + } + err = s.updateLinksWithFixedInbounds(tx, clients, hostname) + if err != nil { + return nil, err + } + err = tx.Save(clients).Error + if err != nil { + return nil, err + } + case "editbulk": + var clients []*model.Client + err = json.Unmarshal(data, &clients) + if err != nil { + return nil, err + } + for _, client := range clients { + changedInboundIds, err := s.findInboundsChanges(tx, client, true) + if err != nil { + return nil, err + } + if len(changedInboundIds) > 0 { + inboundIds = common.UnionUintArray(inboundIds, changedInboundIds) + } + } + if len(inboundIds) > 0 { + err = s.updateLinksWithFixedInbounds(tx, clients, hostname) + if err != nil { + return nil, err + } + } + err = tx.Save(clients).Error + if err != nil { + return nil, err + } + case "delbulk": + var ids []uint + err = json.Unmarshal(data, &ids) + if err != nil { + return nil, err + } + for _, id := range ids { + var client model.Client + err = tx.Where("id = ?", id).First(&client).Error + if err != nil { + return nil, err + } + var clientInbounds []uint + err = json.Unmarshal(client.Inbounds, &clientInbounds) + if err != nil { + return nil, err + } + inboundIds = common.UnionUintArray(inboundIds, clientInbounds) + } + err = tx.Where("id in ?", ids).Delete(model.Client{}).Error + if err != nil { + return nil, err + } + case "del": + var id uint + err = json.Unmarshal(data, &id) + if err != nil { + return nil, err + } + var client model.Client + err = tx.Where("id = ?", id).First(&client).Error + if err != nil { + return nil, err + } + err = json.Unmarshal(client.Inbounds, &inboundIds) + if err != nil { + return nil, err + } + err = tx.Where("id = ?", id).Delete(model.Client{}).Error + if err != nil { + return nil, err + } + default: + return nil, common.NewErrorf("unknown action: %s", act) + } + + return inboundIds, nil +} + +func (s *ClientService) updateLinksWithFixedInbounds(tx *gorm.DB, clients []*model.Client, hostname string) error { + var err error + var inbounds []model.Inbound + var inboundIds []uint + + err = json.Unmarshal(clients[0].Inbounds, &inboundIds) + if err != nil { + return err + } + + // Zero inbounds means removing local links only + if len(inboundIds) > 0 { + err = tx.Model(model.Inbound{}).Preload("Tls").Where("id in ? and type in ?", inboundIds, util.InboundTypeWithLink).Find(&inbounds).Error + if err != nil { + return err + } + } + for index, client := range clients { + var clientLinks []map[string]string + err = json.Unmarshal(client.Links, &clientLinks) + if err != nil { + return err + } + + newClientLinks := []map[string]string{} + for _, inbound := range inbounds { + newLinks := util.LinkGenerator(client.Config, &inbound, hostname) + for _, newLink := range newLinks { + newClientLinks = append(newClientLinks, map[string]string{ + "remark": inbound.Tag, + "type": "local", + "uri": newLink, + }) + } + } + + // Add non local links + for _, clientLink := range clientLinks { + if clientLink["type"] != "local" { + newClientLinks = append(newClientLinks, clientLink) + } + } + + clients[index].Links, err = json.MarshalIndent(newClientLinks, "", " ") + if err != nil { + return err + } + } + return nil +} + +func (s *ClientService) UpdateClientsOnInboundAdd(tx *gorm.DB, initIds string, inboundId uint, hostname string) error { + clientIds := strings.Split(initIds, ",") + var clients []model.Client + err := tx.Model(model.Client{}).Where("id in ?", clientIds).Find(&clients).Error + if err != nil { + return err + } + var inbound model.Inbound + err = tx.Model(model.Inbound{}).Preload("Tls").Where("id = ?", inboundId).Find(&inbound).Error + if err != nil { + return err + } + for _, client := range clients { + // Add inbounds + var clientInbounds []uint + json.Unmarshal(client.Inbounds, &clientInbounds) + clientInbounds = append(clientInbounds, inboundId) + client.Inbounds, err = json.MarshalIndent(clientInbounds, "", " ") + if err != nil { + return err + } + // Add links + var clientLinks, newClientLinks []map[string]string + json.Unmarshal(client.Links, &clientLinks) + newLinks := util.LinkGenerator(client.Config, &inbound, hostname) + for _, newLink := range newLinks { + newClientLinks = append(newClientLinks, map[string]string{ + "remark": inbound.Tag, + "type": "local", + "uri": newLink, + }) + } + for _, clientLink := range clientLinks { + if clientLink["remark"] != inbound.Tag { + newClientLinks = append(newClientLinks, clientLink) + } + } + + client.Links, err = json.MarshalIndent(newClientLinks, "", " ") + if err != nil { + return err + } + err = tx.Save(&client).Error + if err != nil { + return err + } + } + return nil +} + +func (s *ClientService) UpdateClientsOnInboundDelete(tx *gorm.DB, id uint, tag string) error { + var clientIds []uint + err := tx.Raw("SELECT clients.id FROM clients, json_each(clients.inbounds) AS je WHERE je.value = ?", id).Scan(&clientIds).Error + if err != nil { + return err + } + if len(clientIds) == 0 { + return nil + } + var clients []model.Client + err = tx.Model(model.Client{}).Where("id IN ?", clientIds).Find(&clients).Error + if err != nil { + return err + } + for _, client := range clients { + // Delete inbounds + var clientInbounds, newClientInbounds []uint + json.Unmarshal(client.Inbounds, &clientInbounds) + for _, clientInbound := range clientInbounds { + if clientInbound != id { + newClientInbounds = append(newClientInbounds, clientInbound) + } + } + client.Inbounds, err = json.MarshalIndent(newClientInbounds, "", " ") + if err != nil { + return err + } + // Delete links + var clientLinks, newClientLinks []map[string]string + json.Unmarshal(client.Links, &clientLinks) + for _, clientLink := range clientLinks { + if clientLink["remark"] != tag { + newClientLinks = append(newClientLinks, clientLink) + } + } + client.Links, err = json.MarshalIndent(newClientLinks, "", " ") + if err != nil { + return err + } + err = tx.Save(&client).Error + if err != nil { + return err + } + } + return nil +} + +func (s *ClientService) UpdateLinksByInboundChange(tx *gorm.DB, inbounds *[]model.Inbound, hostname string, oldTag string) error { + var err error + for _, inbound := range *inbounds { + var clientIds []uint + err = tx.Raw("SELECT clients.id FROM clients, json_each(clients.inbounds) AS je WHERE je.value = ?", inbound.Id).Scan(&clientIds).Error + if err != nil { + return err + } + if len(clientIds) == 0 { + continue + } + var clients []model.Client + err = tx.Model(model.Client{}).Where("id IN ?", clientIds).Find(&clients).Error + if err != nil { + return err + } + for _, client := range clients { + var clientLinks, newClientLinks []map[string]string + json.Unmarshal(client.Links, &clientLinks) + newLinks := util.LinkGenerator(client.Config, &inbound, hostname) + for _, newLink := range newLinks { + newClientLinks = append(newClientLinks, map[string]string{ + "remark": inbound.Tag, + "type": "local", + "uri": newLink, + }) + } + for _, clientLink := range clientLinks { + if clientLink["type"] != "local" || (clientLink["remark"] != inbound.Tag && clientLink["remark"] != oldTag) { + newClientLinks = append(newClientLinks, clientLink) + } + } + + client.Links, err = json.MarshalIndent(newClientLinks, "", " ") + if err != nil { + return err + } + err = tx.Save(&client).Error + if err != nil { + return err + } + } + } + return nil +} + +func (s *ClientService) DepleteClients() ([]uint, error) { + var err error + var clients []model.Client + var changes []model.Changes + var users []string + var inboundIds []uint + + dt := time.Now().Unix() + db := database.GetDB() + + tx := db.Begin() + defer func() { + if err == nil { + tx.Commit() + if err1 := db.Exec("PRAGMA wal_checkpoint(FULL)").Error; err1 != nil { + logger.Error("Error checkpointing WAL: ", err1.Error()) + } + } else { + tx.Rollback() + } + }() + + // Reset clients + inboundIds, err = s.ResetClients(tx, dt) + if err != nil { + return nil, err + } + + // Deplete clients + err = tx.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", dt).Scan(&clients).Error + if err != nil { + return nil, err + } + + for _, client := range clients { + logger.Debug("Client ", client.Name, " is going to be disabled") + users = append(users, client.Name) + var userInbounds []uint + json.Unmarshal(client.Inbounds, &userInbounds) + // Find changed inbounds + inboundIds = common.UnionUintArray(inboundIds, userInbounds) + changes = append(changes, model.Changes{ + DateTime: dt, + Actor: "DepleteJob", + Key: "clients", + Action: "disable", + Obj: json.RawMessage("\"" + client.Name + "\""), + }) + } + + // Save changes + if len(changes) > 0 { + err = tx.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", dt).Update("enable", false).Error + if err != nil { + return nil, err + } + err = tx.Model(model.Changes{}).Create(&changes).Error + if err != nil { + return nil, err + } + LastUpdate = dt + } + + return inboundIds, nil +} + +func (s *ClientService) ResetClients(tx *gorm.DB, dt int64) ([]uint, error) { + var err error + var resetClients, allClients []*model.Client + var changes []model.Changes + var inboundIds []uint + // Set delay start without periodic reset + err = tx.Model(model.Client{}). + Where("enable = true AND delay_start = true AND auto_reset = false AND (Up + Down) > 0").Find(&resetClients).Error + if err != nil { + return nil, err + } + for _, client := range resetClients { + client.Expiry = dt + (int64(client.ResetDays) * 86400) + client.DelayStart = false + changes = append(changes, model.Changes{ + DateTime: dt, + Actor: "ResetJob", + Key: "clients", + Action: "reset", + Obj: json.RawMessage("\"" + client.Name + "\""), + }) + } + allClients = append(allClients, resetClients...) + + // Set delay start with periodic reset + err = tx.Model(model.Client{}). + Where("enable = true AND delay_start = true AND auto_reset = true AND (Up + Down) > 0").Find(&resetClients).Error + if err != nil { + return nil, err + } + for _, client := range resetClients { + client.NextReset = dt + (int64(client.ResetDays) * 86400) + client.DelayStart = false + changes = append(changes, model.Changes{ + DateTime: dt, + Actor: "ResetJob", + Key: "clients", + Action: "reset", + Obj: json.RawMessage("\"" + client.Name + "\""), + }) + } + allClients = append(allClients, resetClients...) + + // Set periodic reset + err = tx.Model(model.Client{}). + Where("delay_start = false AND auto_reset = true AND next_reset < ?", dt).Find(&resetClients).Error + if err != nil { + return nil, err + } + for _, client := range resetClients { + client.NextReset = dt + (int64(client.ResetDays) * 86400) + client.TotalUp += client.Up + client.TotalDown += client.Down + client.Up = 0 + client.Down = 0 + if !client.Enable { + client.Enable = true + var clientInboundIds []uint + json.Unmarshal(client.Inbounds, &clientInboundIds) + inboundIds = common.UnionUintArray(inboundIds, clientInboundIds) + } + } + allClients = append(allClients, resetClients...) + + // Save clients + if len(allClients) > 0 { + err = tx.Save(allClients).Error + if err != nil { + return nil, err + } + } + + // Save changes + if len(changes) > 0 { + err = tx.Model(model.Changes{}).Create(&changes).Error + if err != nil { + return nil, err + } + LastUpdate = dt + } + return inboundIds, nil +} + +func (s *ClientService) findInboundsChanges(tx *gorm.DB, client *model.Client, fillOmitted bool) ([]uint, error) { + var err error + var oldClient model.Client + var oldInboundIds, newInboundIds []uint + err = tx.Model(model.Client{}).Where("id = ?", client.Id).First(&oldClient).Error + if err != nil { + return nil, err + } + if fillOmitted { + client.Links = oldClient.Links + client.Config = oldClient.Config + } + err = json.Unmarshal(oldClient.Inbounds, &oldInboundIds) + if err != nil { + return nil, err + } + err = json.Unmarshal(client.Inbounds, &newInboundIds) + if err != nil { + return nil, err + } + + // Check client.Config changes + if !bytes.Equal(oldClient.Config, client.Config) || + oldClient.Name != client.Name || + oldClient.Enable != client.Enable { + return common.UnionUintArray(oldInboundIds, newInboundIds), nil + } + + // Check client.Inbounds changes + diffInbounds := common.DiffUintArray(oldInboundIds, newInboundIds) + + return diffInbounds, nil +} diff --git a/service/config.go b/service/config.go new file mode 100644 index 0000000..5908f76 --- /dev/null +++ b/service/config.go @@ -0,0 +1,297 @@ +package service + +import ( + "encoding/json" + "strconv" + "sync" + "time" + + "github.com/alireza0/s-ui/core" + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util/common" +) + +var ( + LastUpdate int64 + corePtr *core.Core + startCoreMu sync.Mutex + startCoreInProgress bool + lastStartFailTime time.Time + startCooldown = 15 * time.Second +) + +type ConfigService struct { + ClientService + TlsService + SettingService + InboundService + OutboundService + ServicesService + EndpointService +} + +type SingBoxConfig struct { + Log json.RawMessage `json:"log"` + Dns json.RawMessage `json:"dns"` + Ntp json.RawMessage `json:"ntp"` + Inbounds []json.RawMessage `json:"inbounds"` + Outbounds []json.RawMessage `json:"outbounds"` + Services []json.RawMessage `json:"services"` + Endpoints []json.RawMessage `json:"endpoints"` + Route json.RawMessage `json:"route"` + Experimental json.RawMessage `json:"experimental"` +} + +func NewConfigService(core *core.Core) *ConfigService { + corePtr = core + return &ConfigService{} +} + +func (s *ConfigService) GetConfig(data string) (*[]byte, error) { + var err error + if len(data) == 0 { + data, err = s.SettingService.GetConfig() + if err != nil { + return nil, err + } + } + singboxConfig := SingBoxConfig{} + err = json.Unmarshal([]byte(data), &singboxConfig) + if err != nil { + return nil, err + } + + singboxConfig.Inbounds, err = s.InboundService.GetAllConfig(database.GetDB()) + if err != nil { + return nil, err + } + singboxConfig.Outbounds, err = s.OutboundService.GetAllConfig(database.GetDB()) + if err != nil { + return nil, err + } + singboxConfig.Services, err = s.ServicesService.GetAllConfig(database.GetDB()) + if err != nil { + return nil, err + } + singboxConfig.Endpoints, err = s.EndpointService.GetAllConfig(database.GetDB()) + if err != nil { + return nil, err + } + rawConfig, err := json.MarshalIndent(singboxConfig, "", " ") + if err != nil { + return nil, err + } + return &rawConfig, nil +} + +func (s *ConfigService) StartCore() error { + if corePtr.IsRunning() { + return nil + } + startCoreMu.Lock() + if startCoreInProgress { + startCoreMu.Unlock() + return nil + } + if time.Since(lastStartFailTime) < startCooldown { + logger.Info("start core cooldown ", startCooldown/time.Second, " seconds") + startCoreMu.Unlock() + return nil + } + startCoreInProgress = true + startCoreMu.Unlock() + defer func() { + startCoreMu.Lock() + startCoreInProgress = false + startCoreMu.Unlock() + }() + + logger.Info("starting core") + rawConfig, err := s.GetConfig("") + if err != nil { + return err + } + err = corePtr.Start(*rawConfig) + if err != nil { + startCoreMu.Lock() + lastStartFailTime = time.Now() + startCoreMu.Unlock() + logger.Error("start sing-box err:", err.Error()) + return err + } + logger.Info("sing-box started") + return nil +} + +func (s *ConfigService) RestartCore() error { + err := s.StopCore() + if err != nil { + return err + } + return s.StartCore() +} + +func (s *ConfigService) restartCoreWithConfig(config json.RawMessage) error { + startCoreMu.Lock() + if startCoreInProgress { + startCoreMu.Unlock() + return nil + } + startCoreInProgress = true + startCoreMu.Unlock() + defer func() { + startCoreMu.Lock() + startCoreInProgress = false + startCoreMu.Unlock() + }() + + if corePtr.IsRunning() { + if err := corePtr.Stop(); err != nil { + logger.Error("restart sing-box err (stop):", err.Error()) + return err + } + } + rawConfig, err := s.GetConfig(string(config)) + if err != nil { + logger.Error("restart sing-box err (get config):", err.Error()) + return err + } + if err := corePtr.Start(*rawConfig); err != nil { + logger.Error("restart sing-box err (start):", err.Error()) + return err + } + logger.Info("sing-box restarted with new config") + return nil +} + +func (s *ConfigService) StopCore() error { + err := corePtr.Stop() + if err != nil { + return err + } + logger.Info("sing-box stopped") + return nil +} + +func (s *ConfigService) CheckOutbound(tag string, link string) core.CheckOutboundResult { + if tag == "" { + return core.CheckOutboundResult{Error: "missing query parameter: tag"} + } + if corePtr == nil || !corePtr.IsRunning() { + return core.CheckOutboundResult{Error: "core not running"} + } + return core.CheckOutbound(corePtr.GetCtx(), tag, link) +} + +func (s *ConfigService) Save(obj string, act string, data json.RawMessage, initUsers string, loginUser string, hostname string) ([]string, error) { + var err error + var objs []string = []string{obj} + + db := database.GetDB() + tx := db.Begin() + defer func() { + if err == nil { + tx.Commit() + // Try to start core if it is not running + if !corePtr.IsRunning() { + s.StartCore() + } + } else { + tx.Rollback() + } + }() + + switch obj { + case "clients": + var inboundIds []uint + inboundIds, err = s.ClientService.Save(tx, act, data, hostname) + if err == nil && len(inboundIds) > 0 { + objs = append(objs, "inbounds") + err = s.InboundService.RestartInbounds(tx, inboundIds) + if err != nil { + return nil, common.NewErrorf("failed to update users for inbounds: %v", err) + } + } + case "tls": + err = s.TlsService.Save(tx, act, data, hostname) + objs = append(objs, "clients", "inbounds") + case "inbounds": + err = s.InboundService.Save(tx, act, data, initUsers, hostname) + objs = append(objs, "clients") + case "outbounds": + err = s.OutboundService.Save(tx, act, data) + case "services": + err = s.ServicesService.Save(tx, act, data) + case "endpoints": + err = s.EndpointService.Save(tx, act, data) + case "config": + err = s.SettingService.SaveConfig(tx, data) + if err != nil { + return nil, err + } + configData := make(json.RawMessage, len(data)) + copy(configData, data) + go func() { _ = s.restartCoreWithConfig(configData) }() + case "settings": + err = s.SettingService.Save(tx, data) + default: + return nil, common.NewError("unknown object: ", obj) + } + if err != nil { + return nil, err + } + + dt := time.Now().Unix() + err = tx.Create(&model.Changes{ + DateTime: dt, + Actor: loginUser, + Key: obj, + Action: act, + Obj: data, + }).Error + if err != nil { + return nil, err + } + + LastUpdate = time.Now().Unix() + + return objs, nil +} + +func (s *ConfigService) CheckChanges(lu string) (bool, error) { + if lu == "" { + return true, nil + } + if LastUpdate == 0 { + db := database.GetDB() + var count int64 + err := db.Model(model.Changes{}).Where("date_time > " + lu).Count(&count).Error + if err == nil { + LastUpdate = time.Now().Unix() + } + return count > 0, err + } else { + intLu, err := strconv.ParseInt(lu, 10, 64) + return LastUpdate > intLu, err + } +} + +func (s *ConfigService) GetChanges(actor string, chngKey string, count string) []model.Changes { + c, _ := strconv.Atoi(count) + whereString := "`id`>0" + if len(actor) > 0 { + whereString += " and `actor`='" + actor + "'" + } + if len(chngKey) > 0 { + whereString += " and `key`='" + chngKey + "'" + } + db := database.GetDB() + var chngs []model.Changes + err := db.Model(model.Changes{}).Where(whereString).Order("`id` desc").Limit(c).Scan(&chngs).Error + if err != nil { + logger.Warning(err) + } + return chngs +} diff --git a/service/endpoints.go b/service/endpoints.go new file mode 100644 index 0000000..146f2bc --- /dev/null +++ b/service/endpoints.go @@ -0,0 +1,140 @@ +package service + +import ( + "encoding/json" + "os" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/util/common" + + "gorm.io/gorm" +) + +type EndpointService struct { + WarpService +} + +func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) { + db := database.GetDB() + endpoints := []*model.Endpoint{} + err := db.Model(model.Endpoint{}).Scan(&endpoints).Error + if err != nil { + return nil, err + } + var data []map[string]interface{} + for _, endpoint := range endpoints { + epData := map[string]interface{}{ + "id": endpoint.Id, + "type": endpoint.Type, + "tag": endpoint.Tag, + "ext": endpoint.Ext, + } + if endpoint.Options != nil { + var restFields map[string]json.RawMessage + if err := json.Unmarshal(endpoint.Options, &restFields); err != nil { + return nil, err + } + for k, v := range restFields { + epData[k] = v + } + } + data = append(data, epData) + } + return &data, nil +} + +func (o *EndpointService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) { + var endpointsJson []json.RawMessage + var endpoints []*model.Endpoint + err := db.Model(model.Endpoint{}).Scan(&endpoints).Error + if err != nil { + return nil, err + } + for _, endpoint := range endpoints { + endpointJson, err := endpoint.MarshalJSON() + if err != nil { + return nil, err + } + endpointsJson = append(endpointsJson, endpointJson) + } + return endpointsJson, nil +} + +func (s *EndpointService) Save(tx *gorm.DB, act string, data json.RawMessage) error { + var err error + + switch act { + case "new", "edit": + var endpoint model.Endpoint + err = endpoint.UnmarshalJSON(data) + if err != nil { + return err + } + + if endpoint.Type == "warp" { + if act == "new" { + err = s.WarpService.RegisterWarp(&endpoint) + if err != nil { + return err + } + } else { + var old_license string + err = tx.Model(model.Endpoint{}).Select("json_extract(ext, '$.license_key')").Where("id = ?", endpoint.Id).Find(&old_license).Error + if err != nil { + return err + } + err = s.WarpService.SetWarpLicense(old_license, &endpoint) + if err != nil { + return err + } + } + } + + if corePtr.IsRunning() { + configData, err := endpoint.MarshalJSON() + if err != nil { + return err + } + if act == "edit" { + var oldTag string + err = tx.Model(model.Endpoint{}).Select("tag").Where("id = ?", endpoint.Id).Find(&oldTag).Error + if err != nil { + return err + } + err = corePtr.RemoveEndpoint(oldTag) + if err != nil && err != os.ErrInvalid { + return err + } + } + err = corePtr.AddEndpoint(configData) + if err != nil { + return err + } + } + + err = tx.Save(&endpoint).Error + if err != nil { + return err + } + case "del": + var tag string + err = json.Unmarshal(data, &tag) + if err != nil { + return err + } + if corePtr.IsRunning() { + err = corePtr.RemoveEndpoint(tag) + if err != nil && err != os.ErrInvalid { + return err + } + } + err = tx.Where("tag = ?", tag).Delete(model.Endpoint{}).Error + if err != nil { + return err + } + default: + return common.NewErrorf("unknown action: %s", act) + } + return nil +} diff --git a/service/inbounds.go b/service/inbounds.go new file mode 100644 index 0000000..d9686f2 --- /dev/null +++ b/service/inbounds.go @@ -0,0 +1,362 @@ +package service + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/util" + "github.com/alireza0/s-ui/util/common" + + "gorm.io/gorm" +) + +type InboundService struct { + ClientService +} + +func (s *InboundService) Get(ids string) (*[]map[string]interface{}, error) { + if ids == "" { + return s.GetAll() + } + return s.getById(ids) +} + +func (s *InboundService) getById(ids string) (*[]map[string]interface{}, error) { + var inbound []model.Inbound + var result []map[string]interface{} + db := database.GetDB() + err := db.Model(model.Inbound{}).Where("id in ?", strings.Split(ids, ",")).Scan(&inbound).Error + if err != nil { + return nil, err + } + for _, inb := range inbound { + inbData, err := inb.MarshalFull() + if err != nil { + return nil, err + } + result = append(result, *inbData) + } + return &result, nil +} + +func (s *InboundService) GetAll() (*[]map[string]interface{}, error) { + db := database.GetDB() + inbounds := []model.Inbound{} + err := db.Model(model.Inbound{}).Scan(&inbounds).Error + if err != nil { + return nil, err + } + var data []map[string]interface{} + for _, inbound := range inbounds { + var shadowtls_version uint + ss_managed := false + inbData := map[string]interface{}{ + "id": inbound.Id, + "type": inbound.Type, + "tag": inbound.Tag, + "tls_id": inbound.TlsId, + } + if inbound.Options != nil { + var restFields map[string]json.RawMessage + if err := json.Unmarshal(inbound.Options, &restFields); err != nil { + return nil, err + } + inbData["listen"] = restFields["listen"] + inbData["listen_port"] = restFields["listen_port"] + if inbound.Type == "shadowtls" { + json.Unmarshal(restFields["version"], &shadowtls_version) + } + if inbound.Type == "shadowsocks" { + json.Unmarshal(restFields["managed"], &ss_managed) + } + } + if s.hasUser(inbound.Type) && + !(inbound.Type == "shadowtls" && shadowtls_version < 3) && + !(inbound.Type == "shadowsocks" && ss_managed) { + users := []string{} + err = db.Raw("SELECT clients.name FROM clients, json_each(clients.inbounds) as je WHERE je.value = ?", inbound.Id).Scan(&users).Error + if err != nil { + return nil, err + } + inbData["users"] = users + } + + data = append(data, inbData) + } + return &data, nil +} + +func (s *InboundService) FromIds(ids []uint) ([]*model.Inbound, error) { + db := database.GetDB() + inbounds := []*model.Inbound{} + err := db.Model(model.Inbound{}).Where("id in ?", ids).Scan(&inbounds).Error + if err != nil { + return nil, err + } + return inbounds, nil +} + +func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, initUserIds string, hostname string) error { + var err error + + switch act { + case "new", "edit": + var inbound model.Inbound + err = inbound.UnmarshalJSON(data) + if err != nil { + return err + } + if inbound.TlsId > 0 { + err = tx.Model(model.Tls{}).Where("id = ?", inbound.TlsId).Find(&inbound.Tls).Error + if err != nil { + return err + } + } + var oldTag string + if act == "edit" { + err = tx.Model(model.Inbound{}).Select("tag").Where("id = ?", inbound.Id).Find(&oldTag).Error + if err != nil { + return err + } + } + + if corePtr.IsRunning() { + if act == "edit" { + err = corePtr.RemoveInbound(oldTag) + if err != nil && err != os.ErrInvalid { + return err + } + } + + inboundConfig, err := inbound.MarshalJSON() + if err != nil { + return err + } + + if act == "edit" { + inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type) + } else { + inboundConfig, err = s.initUsers(tx, inboundConfig, initUserIds, inbound.Type) + } + if err != nil { + return err + } + + err = corePtr.AddInbound(inboundConfig) + if err != nil { + return err + } + } + + err = util.FillOutJson(&inbound, hostname) + if err != nil { + return err + } + + err = tx.Save(&inbound).Error + if err != nil { + return err + } + switch act { + case "new": + err = s.ClientService.UpdateClientsOnInboundAdd(tx, initUserIds, inbound.Id, hostname) + case "edit": + err = s.ClientService.UpdateLinksByInboundChange(tx, &[]model.Inbound{inbound}, hostname, oldTag) + } + if err != nil { + return err + } + case "del": + var tag string + err = json.Unmarshal(data, &tag) + if err != nil { + return err + } + if corePtr.IsRunning() { + err = corePtr.RemoveInbound(tag) + if err != nil && err != os.ErrInvalid { + return err + } + } + var id uint + err = tx.Model(model.Inbound{}).Select("id").Where("tag = ?", tag).Scan(&id).Error + if err != nil { + return err + } + err = s.ClientService.UpdateClientsOnInboundDelete(tx, id, tag) + if err != nil { + return err + } + err = tx.Where("tag = ?", tag).Delete(model.Inbound{}).Error + if err != nil { + return err + } + default: + return common.NewErrorf("unknown action: %s", act) + } + return nil +} + +func (s *InboundService) UpdateOutJsons(tx *gorm.DB, inboundIds []uint, hostname string) error { + var inbounds []model.Inbound + err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ?", inboundIds).Find(&inbounds).Error + if err != nil { + return err + } + for _, inbound := range inbounds { + err = util.FillOutJson(&inbound, hostname) + if err != nil { + return err + } + err = tx.Model(model.Inbound{}).Where("tag = ?", inbound.Tag).Update("out_json", inbound.OutJson).Error + if err != nil { + return err + } + } + + return nil +} + +func (s *InboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) { + var inboundsJson []json.RawMessage + var inbounds []*model.Inbound + err := db.Model(model.Inbound{}).Preload("Tls").Find(&inbounds).Error + if err != nil { + return nil, err + } + for _, inbound := range inbounds { + inboundJson, err := inbound.MarshalJSON() + if err != nil { + return nil, err + } + inboundJson, err = s.addUsers(db, inboundJson, inbound.Id, inbound.Type) + if err != nil { + return nil, err + } + inboundsJson = append(inboundsJson, inboundJson) + } + return inboundsJson, nil +} + +func (s *InboundService) hasUser(inboundType string) bool { + switch inboundType { + case "mixed", "socks", "http", "shadowsocks", "vmess", "trojan", "naive", "hysteria", "shadowtls", "tuic", "hysteria2", "vless", "anytls": + return true + } + return false +} + +func (s *InboundService) fetchUsers(db *gorm.DB, inboundType string, condition string, inbound map[string]interface{}) ([]json.RawMessage, error) { + if inboundType == "shadowtls" { + version, _ := inbound["version"].(float64) + if int(version) < 3 { + return nil, nil + } + } + if inboundType == "shadowsocks" { + method, _ := inbound["method"].(string) + if method == "2022-blake3-aes-128-gcm" { + inboundType = "shadowsocks16" + } + } + + var users []string + + err := db.Raw( + fmt.Sprintf(`SELECT json_extract(clients.config, "$.%s") + FROM clients WHERE enable = true AND %s`, + inboundType, condition)).Scan(&users).Error + if err != nil { + return nil, err + } + var usersJson []json.RawMessage + for _, user := range users { + if inboundType == "vless" && inbound["tls"] == nil { + user = strings.Replace(user, "xtls-rprx-vision", "", -1) + } + usersJson = append(usersJson, json.RawMessage(user)) + } + return usersJson, nil +} + +func (s *InboundService) addUsers(db *gorm.DB, inboundJson []byte, inboundId uint, inboundType string) ([]byte, error) { + if !s.hasUser(inboundType) { + return inboundJson, nil + } + + var inbound map[string]interface{} + err := json.Unmarshal(inboundJson, &inbound) + if err != nil { + return nil, err + } + + condition := fmt.Sprintf("%d IN (SELECT json_each.value FROM json_each(clients.inbounds))", inboundId) + inbound["users"], err = s.fetchUsers(db, inboundType, condition, inbound) + if err != nil { + return nil, err + } + + return json.Marshal(inbound) +} + +func (s *InboundService) initUsers(db *gorm.DB, inboundJson []byte, clientIds string, inboundType string) ([]byte, error) { + ClientIds := strings.Split(clientIds, ",") + if len(ClientIds) == 0 { + return inboundJson, nil + } + + if !s.hasUser(inboundType) { + return inboundJson, nil + } + + var inbound map[string]interface{} + err := json.Unmarshal(inboundJson, &inbound) + if err != nil { + return nil, err + } + + condition := fmt.Sprintf("id IN (%s)", strings.Join(ClientIds, ",")) + inbound["users"], err = s.fetchUsers(db, inboundType, condition, inbound) + if err != nil { + return nil, err + } + + return json.Marshal(inbound) +} + +func (s *InboundService) RestartInbounds(tx *gorm.DB, ids []uint) error { + if !corePtr.IsRunning() { + return nil + } + var inbounds []*model.Inbound + err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ?", ids).Find(&inbounds).Error + if err != nil { + return err + } + for _, inbound := range inbounds { + err = corePtr.RemoveInbound(inbound.Tag) + if err != nil && err != os.ErrInvalid { + return err + } + // Close all existing connections + corePtr.GetInstance().ConnTracker().CloseConnByInbound(inbound.Tag) + + inboundConfig, err := inbound.MarshalJSON() + if err != nil { + return err + } + inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type) + if err != nil { + return err + } + err = corePtr.AddInbound(inboundConfig) + if err != nil { + return err + } + } + return nil +} diff --git a/service/outbounds.go b/service/outbounds.go new file mode 100644 index 0000000..5f463fe --- /dev/null +++ b/service/outbounds.go @@ -0,0 +1,118 @@ +package service + +import ( + "encoding/json" + "os" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/util/common" + + "gorm.io/gorm" +) + +type OutboundService struct{} + +func (o *OutboundService) GetAll() (*[]map[string]interface{}, error) { + db := database.GetDB() + outbounds := []*model.Outbound{} + err := db.Model(model.Outbound{}).Scan(&outbounds).Error + if err != nil { + return nil, err + } + var data []map[string]interface{} + for _, outbound := range outbounds { + outData := map[string]interface{}{ + "id": outbound.Id, + "type": outbound.Type, + "tag": outbound.Tag, + } + if outbound.Options != nil { + var restFields map[string]json.RawMessage + if err := json.Unmarshal(outbound.Options, &restFields); err != nil { + return nil, err + } + for k, v := range restFields { + outData[k] = v + } + } + data = append(data, outData) + } + return &data, nil +} + +func (o *OutboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) { + var outboundsJson []json.RawMessage + var outbounds []*model.Outbound + err := db.Model(model.Outbound{}).Scan(&outbounds).Error + if err != nil { + return nil, err + } + for _, outbound := range outbounds { + outboundJson, err := outbound.MarshalJSON() + if err != nil { + return nil, err + } + outboundsJson = append(outboundsJson, outboundJson) + } + return outboundsJson, nil +} + +func (s *OutboundService) Save(tx *gorm.DB, act string, data json.RawMessage) error { + var err error + + switch act { + case "new", "edit": + var outbound model.Outbound + err = outbound.UnmarshalJSON(data) + if err != nil { + return err + } + + if corePtr.IsRunning() { + configData, err := outbound.MarshalJSON() + if err != nil { + return err + } + if act == "edit" { + var oldTag string + err = tx.Model(model.Outbound{}).Select("tag").Where("id = ?", outbound.Id).Find(&oldTag).Error + if err != nil { + return err + } + err = corePtr.RemoveOutbound(oldTag) + if err != nil && err != os.ErrInvalid { + return err + } + } + err = corePtr.AddOutbound(configData) + if err != nil { + return err + } + } + + err = tx.Save(&outbound).Error + if err != nil { + return err + } + case "del": + var tag string + err = json.Unmarshal(data, &tag) + if err != nil { + return err + } + if corePtr.IsRunning() { + err = corePtr.RemoveOutbound(tag) + if err != nil && err != os.ErrInvalid { + return err + } + } + err = tx.Where("tag = ?", tag).Delete(model.Outbound{}).Error + if err != nil { + return err + } + default: + return common.NewErrorf("unknown action: %s", act) + } + return nil +} diff --git a/service/panel.go b/service/panel.go new file mode 100644 index 0000000..2dd5aa5 --- /dev/null +++ b/service/panel.go @@ -0,0 +1,32 @@ +package service + +import ( + "os" + "runtime" + "syscall" + "time" + + "github.com/alireza0/s-ui/logger" +) + +type PanelService struct { +} + +func (s *PanelService) RestartPanel(delay time.Duration) error { + p, err := os.FindProcess(syscall.Getpid()) + if err != nil { + return err + } + go func() { + time.Sleep(delay) + if runtime.GOOS == "windows" { + err = p.Kill() + } else { + err = p.Signal(syscall.SIGHUP) + } + if err != nil { + logger.Error("send signal SIGHUP failed:", err) + } + }() + return nil +} diff --git a/service/server.go b/service/server.go new file mode 100644 index 0000000..2ce1f0f --- /dev/null +++ b/service/server.go @@ -0,0 +1,283 @@ +package service + +import ( + "encoding/base64" + "os" + "runtime" + "strconv" + "strings" + "time" + + "github.com/alireza0/s-ui/config" + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/logger" + + "github.com/sagernet/sing-box/common/tls" + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/mem" + "github.com/shirou/gopsutil/v4/net" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +type ServerService struct{} + +func (s *ServerService) GetStatus(request string) *map[string]interface{} { + status := make(map[string]interface{}, 0) + requests := strings.Split(request, ",") + for _, req := range requests { + switch req { + case "cpu": + status["cpu"] = s.GetCpuPercent() + case "mem": + status["mem"] = s.GetMemInfo() + case "dsk": + status["dsk"] = s.GetDiskInfo() + case "dio": + status["dio"] = s.GetDiskIO() + case "swp": + status["swp"] = s.GetSwapInfo() + case "net": + status["net"] = s.GetNetInfo() + case "sys": + status["sys"] = s.GetSystemInfo() + case "sbd": + status["sbd"] = s.GetSingboxInfo() + case "db": + status["db"] = s.GetDatabaseInfo() + } + } + return &status +} + +func (s *ServerService) GetCpuPercent() float64 { + percents, err := cpu.Percent(0, false) + if err != nil { + logger.Warning("get cpu percent failed:", err) + return 0 + } else { + return percents[0] + } +} + +func (s *ServerService) GetMemInfo() map[string]interface{} { + info := make(map[string]interface{}, 0) + memInfo, err := mem.VirtualMemory() + if err != nil { + logger.Warning("get virtual memory failed:", err) + } else { + info["current"] = memInfo.Used + info["total"] = memInfo.Total + } + return info +} + +func (s *ServerService) GetDiskInfo() map[string]interface{} { + info := make(map[string]interface{}, 0) + diskInfo, err := disk.Usage("/") + if err != nil { + logger.Warning("get disk usage failed:", err) + } else { + info["current"] = diskInfo.Used + info["total"] = diskInfo.Total + } + return info +} + +func (s *ServerService) GetDiskIO() map[string]interface{} { + info := make(map[string]interface{}, 0) + ioStats, err := disk.IOCounters() + if err != nil { + logger.Warning("get disk io counters failed:", err) + } else if len(ioStats) > 0 { + infoR, infoW := uint64(0), uint64(0) + for _, ioStat := range ioStats { + infoR += ioStat.ReadBytes + infoW += ioStat.WriteBytes + } + info["read"] = infoR + info["write"] = infoW + } else { + logger.Warning("can not find disk io counters") + } + return info +} + +func (s *ServerService) GetSwapInfo() map[string]interface{} { + info := make(map[string]interface{}, 0) + swapInfo, err := mem.SwapMemory() + if err != nil { + logger.Warning("get swap memory failed:", err) + } else { + info["current"] = swapInfo.Used + info["total"] = swapInfo.Total + } + return info +} + +func (s *ServerService) GetNetInfo() map[string]interface{} { + info := make(map[string]interface{}, 0) + ioStats, err := net.IOCounters(false) + if err != nil { + logger.Warning("get io counters failed:", err) + } else if len(ioStats) > 0 { + ioStat := ioStats[0] + info["sent"] = ioStat.BytesSent + info["recv"] = ioStat.BytesRecv + info["psent"] = ioStat.PacketsSent + info["precv"] = ioStat.PacketsRecv + } else { + logger.Warning("can not find io counters") + } + return info +} + +func (s *ServerService) GetSingboxInfo() map[string]interface{} { + var rtm runtime.MemStats + runtime.ReadMemStats(&rtm) + isRunning := corePtr.IsRunning() + uptime := uint32(0) + if isRunning { + uptime = corePtr.GetInstance().Uptime() + } + return map[string]interface{}{ + "running": isRunning, + "stats": map[string]interface{}{ + "NumGoroutine": uint32(runtime.NumGoroutine()), + "Alloc": rtm.Alloc, + "Uptime": uptime, + }, + } +} + +func (s *ServerService) GetSystemInfo() map[string]interface{} { + info := make(map[string]interface{}, 0) + var rtm runtime.MemStats + runtime.ReadMemStats(&rtm) + + info["appMem"] = rtm.Sys + info["appThreads"] = uint32(runtime.NumGoroutine()) + cpuInfo, err := cpu.Info() + if err == nil { + info["cpuType"] = cpuInfo[0].ModelName + } + info["cpuCount"] = runtime.NumCPU() + info["hostName"], _ = os.Hostname() + info["appVersion"] = config.GetVersion() + ipv4 := make([]string, 0) + ipv6 := make([]string, 0) + // get ip address + netInterfaces, _ := net.Interfaces() + for i := 0; i < len(netInterfaces); i++ { + if len(netInterfaces[i].Flags) > 2 && netInterfaces[i].Flags[0] == "up" && netInterfaces[i].Flags[1] != "loopback" { + addrs := netInterfaces[i].Addrs + + for _, address := range addrs { + if strings.Contains(address.Addr, ".") { + ipv4 = append(ipv4, address.Addr) + } else if address.Addr[0:6] != "fe80::" { + ipv6 = append(ipv6, address.Addr) + } + } + } + } + info["ipv4"] = ipv4 + info["ipv6"] = ipv6 + info["bootTime"], _ = host.BootTime() + + return info +} + +func (s *ServerService) GetLogs(count string, level string) []string { + c, err := strconv.Atoi(count) + if err != nil { + c = 10 + } + return logger.GetLogs(c, level) +} + +func (s *ServerService) GenKeypair(keyType string, options string) []string { + if len(keyType) == 0 { + return []string{"No keypair to generate"} + } + + switch keyType { + case "ech": + return s.generateECHKeyPair(options) + case "tls": + return s.generateTLSKeyPair(options) + case "reality": + return s.generateRealityKeyPair() + case "wireguard": + return s.generateWireGuardKey(options) + } + + return []string{"Failed to generate keypair"} +} + +func (s *ServerService) generateECHKeyPair(serverName string) []string { + configPem, keyPem, err := tls.ECHKeygenDefault(serverName) + if err != nil { + return []string{"Failed to generate ECH keypair: ", err.Error()} + } + return append(strings.Split(configPem, "\n"), strings.Split(keyPem, "\n")...) +} + +func (s *ServerService) generateTLSKeyPair(serverName string) []string { + privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, 12, 0)) + if err != nil { + return []string{"Failed to generate TLS keypair: ", err.Error()} + } + return append(strings.Split(string(privateKeyPem), "\n"), strings.Split(string(publicKeyPem), "\n")...) +} + +func (s *ServerService) generateRealityKeyPair() []string { + privateKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return []string{"Failed to generate Reality keypair: ", err.Error()} + } + publicKey := privateKey.PublicKey() + return []string{"PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]), "PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:])} +} + +func (s *ServerService) generateWireGuardKey(pk string) []string { + if len(pk) > 0 { + key, _ := wgtypes.ParseKey(pk) + return []string{key.PublicKey().String()} + } + wgKeys, err := wgtypes.GeneratePrivateKey() + if err != nil { + return []string{"Failed to generate wireguard keypair: ", err.Error()} + } + return []string{"PrivateKey: " + wgKeys.String(), "PublicKey: " + wgKeys.PublicKey().String()} +} + +func (s *ServerService) GetDatabaseInfo() map[string]int64 { + info := make(map[string]int64, 0) + db := database.GetDB() + if db == nil { + return nil + } + + var clientsCount, inboundsCount, outboundsCount, servicesCount, endpointsCount, clientUp, clientDown int64 + + db.Model(&model.Client{}).Count(&clientsCount) + db.Model(&model.Inbound{}).Count(&inboundsCount) + db.Model(&model.Outbound{}).Count(&outboundsCount) + db.Model(&model.Service{}).Count(&servicesCount) + db.Model(&model.Endpoint{}).Count(&endpointsCount) + db.Model(&model.Client{}).Select("COALESCE(SUM(up+total_up),0)").Scan(&clientUp) + db.Model(&model.Client{}).Select("COALESCE(SUM(down+total_down),0)").Scan(&clientDown) + + info["clients"] = clientsCount + info["inbounds"] = inboundsCount + info["outbounds"] = outboundsCount + info["services"] = servicesCount + info["endpoints"] = endpointsCount + info["clientUp"] = clientUp + info["clientDown"] = clientDown + + return info +} diff --git a/service/services.go b/service/services.go new file mode 100644 index 0000000..102417d --- /dev/null +++ b/service/services.go @@ -0,0 +1,153 @@ +package service + +import ( + "encoding/json" + "os" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/util/common" + + "gorm.io/gorm" +) + +type ServicesService struct{} + +func (s *ServicesService) GetAll() (*[]map[string]interface{}, error) { + db := database.GetDB() + services := []model.Service{} + err := db.Model(model.Service{}).Scan(&services).Error + if err != nil { + return nil, err + } + var data []map[string]interface{} + for _, srv := range services { + srvData := map[string]interface{}{ + "id": srv.Id, + "type": srv.Type, + "tag": srv.Tag, + "tls_id": srv.TlsId, + } + if srv.Options != nil { + var restFields map[string]json.RawMessage + if err := json.Unmarshal(srv.Options, &restFields); err != nil { + return nil, err + } + for k, v := range restFields { + srvData[k] = v + } + } + + data = append(data, srvData) + } + return &data, nil +} + +func (s *ServicesService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) { + var servicesJson []json.RawMessage + var services []*model.Service + err := db.Model(model.Service{}).Preload("Tls").Find(&services).Error + if err != nil { + return nil, err + } + for _, srv := range services { + srvJson, err := srv.MarshalJSON() + if err != nil { + return nil, err + } + servicesJson = append(servicesJson, srvJson) + } + return servicesJson, nil +} + +func (s *ServicesService) Save(tx *gorm.DB, act string, data json.RawMessage) error { + var err error + + switch act { + case "new", "edit": + var srv model.Service + err = srv.UnmarshalJSON(data) + if err != nil { + return err + } + + if srv.TlsId > 0 { + err = tx.Model(model.Tls{}).Where("id = ?", srv.TlsId).Find(&srv.Tls).Error + if err != nil { + return err + } + } + + if corePtr.IsRunning() { + configData, err := srv.MarshalJSON() + if err != nil { + return err + } + if act == "edit" { + var oldTag string + err = tx.Model(model.Service{}).Select("tag").Where("id = ?", srv.Id).Find(&oldTag).Error + if err != nil { + return err + } + err = corePtr.RemoveService(oldTag) + if err != nil && err != os.ErrInvalid { + return err + } + } + err = corePtr.AddService(configData) + if err != nil { + return err + } + } + + err = tx.Save(&srv).Error + if err != nil { + return err + } + case "del": + var tag string + err = json.Unmarshal(data, &tag) + if err != nil { + return err + } + if corePtr.IsRunning() { + err = corePtr.RemoveService(tag) + if err != nil && err != os.ErrInvalid { + return err + } + } + err = tx.Where("tag = ?", tag).Delete(model.Service{}).Error + if err != nil { + return err + } + default: + return common.NewErrorf("unknown action: %s", act) + } + return nil +} + +func (s *ServicesService) RestartServices(tx *gorm.DB, ids []uint) error { + if !corePtr.IsRunning() { + return nil + } + var services []*model.Service + err := tx.Model(model.Service{}).Preload("Tls").Where("id in ?", ids).Find(&services).Error + if err != nil { + return err + } + for _, srv := range services { + err = corePtr.RemoveService(srv.Tag) + if err != nil && err != os.ErrInvalid { + return err + } + srvConfig, err := srv.MarshalJSON() + if err != nil { + return err + } + err = corePtr.AddService(srvConfig) + if err != nil { + return err + } + } + return nil +} diff --git a/service/setting.go b/service/setting.go new file mode 100644 index 0000000..b2687fe --- /dev/null +++ b/service/setting.go @@ -0,0 +1,421 @@ +package service + +import ( + "encoding/json" + "os" + "runtime" + "strconv" + "strings" + "time" + + "github.com/alireza0/s-ui/config" + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util/common" + + "gorm.io/gorm" +) + +var defaultConfig = `{ + "log": { + "level": "info" + }, + "dns": { + "servers": [], + "rules": [] + }, + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "protocol": [ + "dns" + ], + "action": "hijack-dns" + } + ] + }, + "experimental": {} +}` + +var defaultValueMap = map[string]string{ + "webListen": "", + "webDomain": "", + "webPort": "2095", + "secret": common.Random(32), + "webCertFile": "", + "webKeyFile": "", + "webPath": "/app/", + "webURI": "", + "sessionMaxAge": "0", + "trafficAge": "30", + "timeLocation": "Asia/Tehran", + "subListen": "", + "subPort": "2096", + "subPath": "/sub/", + "subDomain": "", + "subCertFile": "", + "subKeyFile": "", + "subUpdates": "12", + "subEncode": "true", + "subShowInfo": "false", + "subURI": "", + "subJsonExt": "", + "subClashExt": "", + "config": defaultConfig, + "version": config.GetVersion(), +} + +type SettingService struct { +} + +func (s *SettingService) GetAllSetting() (*map[string]string, error) { + db := database.GetDB() + settings := make([]*model.Setting, 0) + err := db.Model(model.Setting{}).Find(&settings).Error + if err != nil { + return nil, err + } + allSetting := map[string]string{} + + for _, setting := range settings { + allSetting[setting.Key] = setting.Value + } + + for key, defaultValue := range defaultValueMap { + if _, exists := allSetting[key]; !exists { + err = s.saveSetting(key, defaultValue) + if err != nil { + return nil, err + } + allSetting[key] = defaultValue + } + } + + // Due to security principles + delete(allSetting, "secret") + delete(allSetting, "config") + delete(allSetting, "version") + + return &allSetting, nil +} + +func (s *SettingService) ResetSettings() error { + db := database.GetDB() + return db.Where("1 = 1").Delete(model.Setting{}).Error +} + +func (s *SettingService) getSetting(key string) (*model.Setting, error) { + db := database.GetDB() + setting := &model.Setting{} + err := db.Model(model.Setting{}).Where("key = ?", key).First(setting).Error + if err != nil { + return nil, err + } + return setting, nil +} + +func (s *SettingService) getString(key string) (string, error) { + setting, err := s.getSetting(key) + if database.IsNotFound(err) { + value, ok := defaultValueMap[key] + if !ok { + return "", common.NewErrorf("key <%v> not in defaultValueMap", key) + } + return value, nil + } else if err != nil { + return "", err + } + return setting.Value, nil +} + +func (s *SettingService) saveSetting(key string, value string) error { + setting, err := s.getSetting(key) + db := database.GetDB() + if database.IsNotFound(err) { + return db.Create(&model.Setting{ + Key: key, + Value: value, + }).Error + } else if err != nil { + return err + } + setting.Key = key + setting.Value = value + return db.Save(setting).Error +} + +func (s *SettingService) setString(key string, value string) error { + return s.saveSetting(key, value) +} + +func (s *SettingService) getBool(key string) (bool, error) { + str, err := s.getString(key) + if err != nil { + return false, err + } + return strconv.ParseBool(str) +} + +// func (s *SettingService) setBool(key string, value bool) error { +// return s.setString(key, strconv.FormatBool(value)) +// } + +func (s *SettingService) getInt(key string) (int, error) { + str, err := s.getString(key) + if err != nil { + return 0, err + } + return strconv.Atoi(str) +} + +func (s *SettingService) setInt(key string, value int) error { + return s.setString(key, strconv.Itoa(value)) +} +func (s *SettingService) GetListen() (string, error) { + return s.getString("webListen") +} + +func (s *SettingService) GetWebDomain() (string, error) { + return s.getString("webDomain") +} + +func (s *SettingService) GetPort() (int, error) { + return s.getInt("webPort") +} + +func (s *SettingService) SetPort(port int) error { + return s.setInt("webPort", port) +} + +func (s *SettingService) GetCertFile() (string, error) { + return s.getString("webCertFile") +} + +func (s *SettingService) GetKeyFile() (string, error) { + return s.getString("webKeyFile") +} + +func (s *SettingService) GetWebPath() (string, error) { + webPath, err := s.getString("webPath") + if err != nil { + return "", err + } + if !strings.HasPrefix(webPath, "/") { + webPath = "/" + webPath + } + if !strings.HasSuffix(webPath, "/") { + webPath += "/" + } + return webPath, nil +} + +func (s *SettingService) SetWebPath(webPath string) error { + if !strings.HasPrefix(webPath, "/") { + webPath = "/" + webPath + } + if !strings.HasSuffix(webPath, "/") { + webPath += "/" + } + return s.setString("webPath", webPath) +} + +func (s *SettingService) GetSecret() ([]byte, error) { + secret, err := s.getString("secret") + if secret == defaultValueMap["secret"] { + err := s.saveSetting("secret", secret) + if err != nil { + logger.Warning("save secret failed:", err) + } + } + return []byte(secret), err +} + +func (s *SettingService) GetSessionMaxAge() (int, error) { + return s.getInt("sessionMaxAge") +} + +func (s *SettingService) GetTrafficAge() (int, error) { + return s.getInt("trafficAge") +} + +func (s *SettingService) GetTimeLocation() (*time.Location, error) { + l, err := s.getString("timeLocation") + if err != nil { + return nil, err + } + if runtime.GOOS == "windows" { + l = "Local" + } + location, err := time.LoadLocation(l) + if err != nil { + defaultLocation := defaultValueMap["timeLocation"] + logger.Errorf("location <%v> not exist, using default location: %v", l, defaultLocation) + return time.LoadLocation(defaultLocation) + } + return location, nil +} + +func (s *SettingService) GetSubListen() (string, error) { + return s.getString("subListen") +} + +func (s *SettingService) GetSubPort() (int, error) { + return s.getInt("subPort") +} + +func (s *SettingService) SetSubPort(subPort int) error { + return s.setInt("subPort", subPort) +} + +func (s *SettingService) GetSubPath() (string, error) { + subPath, err := s.getString("subPath") + if err != nil { + return "", err + } + if !strings.HasPrefix(subPath, "/") { + subPath = "/" + subPath + } + if !strings.HasSuffix(subPath, "/") { + subPath += "/" + } + return subPath, nil +} + +func (s *SettingService) SetSubPath(subPath string) error { + if !strings.HasPrefix(subPath, "/") { + subPath = "/" + subPath + } + if !strings.HasSuffix(subPath, "/") { + subPath += "/" + } + return s.setString("subPath", subPath) +} + +func (s *SettingService) GetSubDomain() (string, error) { + return s.getString("subDomain") +} + +func (s *SettingService) GetSubCertFile() (string, error) { + return s.getString("subCertFile") +} + +func (s *SettingService) GetSubKeyFile() (string, error) { + return s.getString("subKeyFile") +} + +func (s *SettingService) GetSubUpdates() (int, error) { + return s.getInt("subUpdates") +} + +func (s *SettingService) GetSubEncode() (bool, error) { + return s.getBool("subEncode") +} + +func (s *SettingService) GetSubShowInfo() (bool, error) { + return s.getBool("subShowInfo") +} + +func (s *SettingService) GetSubURI() (string, error) { + return s.getString("subURI") +} + +func (s *SettingService) GetFinalSubURI(host string) (string, error) { + allSetting, err := s.GetAllSetting() + if err != nil { + return "", err + } + SubURI := (*allSetting)["subURI"] + if SubURI != "" { + return SubURI, nil + } + protocol := "http" + if (*allSetting)["subKeyFile"] != "" && (*allSetting)["subCertFile"] != "" { + protocol = "https" + } + if (*allSetting)["subDomain"] != "" { + host = (*allSetting)["subDomain"] + } + port := ":" + (*allSetting)["subPort"] + if (port == "80" && protocol == "http") || (port == "443" && protocol == "https") { + port = "" + } + return protocol + "://" + host + port + (*allSetting)["subPath"], nil +} + +func (s *SettingService) GetConfig() (string, error) { + return s.getString("config") +} + +func (s *SettingService) SetConfig(config string) error { + return s.setString("config", config) +} + +func (s *SettingService) SaveConfig(tx *gorm.DB, config json.RawMessage) error { + configs, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + return tx.Model(model.Setting{}).Where("key = ?", "config").Update("value", string(configs)).Error +} + +func (s *SettingService) Save(tx *gorm.DB, data json.RawMessage) error { + var err error + var settings map[string]string + err = json.Unmarshal(data, &settings) + if err != nil { + return err + } + for key, obj := range settings { + // Secure file existence check + if obj != "" && (key == "webCertFile" || + key == "webKeyFile" || + key == "subCertFile" || + key == "subKeyFile") { + err = s.fileExists(obj) + if err != nil { + return common.NewError(" -> ", obj, " is not exists") + } + } + + // Correct Pathes start and ends with `/` + if key == "webPath" || + key == "subPath" { + if !strings.HasPrefix(obj, "/") { + obj = "/" + obj + } + if !strings.HasSuffix(obj, "/") { + obj += "/" + } + } + + // Delete all stats if it is set to 0 + if key == "trafficAge" && obj == "0" { + err = tx.Where("id > 0").Delete(model.Stats{}).Error + if err != nil { + return err + } + } + err = tx.Model(model.Setting{}).Where("key = ?", key).Update("value", obj).Error + if err != nil { + return err + } + } + return err +} + +func (s *SettingService) GetSubJsonExt() (string, error) { + return s.getString("subJsonExt") +} + +func (s *SettingService) GetSubClashExt() (string, error) { + return s.getString("subClashExt") +} + +func (s *SettingService) fileExists(path string) error { + _, err := os.Stat(path) + return err +} diff --git a/service/stats.go b/service/stats.go new file mode 100644 index 0000000..d122342 --- /dev/null +++ b/service/stats.go @@ -0,0 +1,162 @@ +package service + +import ( + "sort" + "time" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + + "gorm.io/gorm" +) + +type onlines struct { + Inbound []string `json:"inbound,omitempty"` + User []string `json:"user,omitempty"` + Outbound []string `json:"outbound,omitempty"` +} + +var onlineResources = &onlines{} + +type StatsService struct { +} + +func (s *StatsService) SaveStats(enableTraffic bool) error { + if corePtr == nil || !corePtr.IsRunning() { + return nil + } + box := corePtr.GetInstance() + if box == nil { + return nil + } + st := box.StatsTracker() + if st == nil { + return nil + } + stats := st.GetStats() + + // Reset onlines + onlineResources.Inbound = nil + onlineResources.Outbound = nil + onlineResources.User = nil + + if len(*stats) == 0 { + return nil + } + + var err error + db := database.GetDB() + tx := db.Begin() + defer func() { + if err == nil { + tx.Commit() + } else { + tx.Rollback() + } + }() + + for _, stat := range *stats { + if stat.Resource == "user" { + if stat.Direction { + err = tx.Model(model.Client{}).Where("name = ?", stat.Tag). + UpdateColumn("up", gorm.Expr("up + ?", stat.Traffic)).Error + } else { + err = tx.Model(model.Client{}).Where("name = ?", stat.Tag). + UpdateColumn("down", gorm.Expr("down + ?", stat.Traffic)).Error + } + if err != nil { + return err + } + } + if stat.Direction { + switch stat.Resource { + case "inbound": + onlineResources.Inbound = append(onlineResources.Inbound, stat.Tag) + case "outbound": + onlineResources.Outbound = append(onlineResources.Outbound, stat.Tag) + case "user": + onlineResources.User = append(onlineResources.User, stat.Tag) + } + } + } + + if !enableTraffic { + return nil + } + return tx.Create(&stats).Error +} + +func (s *StatsService) GetStats(resource string, tag string, limit int) ([]model.Stats, error) { + var err error + var result []model.Stats + + currentTime := time.Now().Unix() + timeDiff := currentTime - (int64(limit) * 3600) + + db := database.GetDB() + resources := []string{resource} + if resource == "endpoint" { + resources = []string{"inbound", "outbound"} + } + err = db.Model(model.Stats{}).Where("resource in ? AND tag = ? AND date_time > ?", resources, tag, timeDiff).Scan(&result).Error + if err != nil { + return nil, err + } + + result = s.downsampleStats(result, 60) // 60 rows for 30 buckets + return result, nil +} + +// downsampleStats reduces stats to maxRows rows. +// Each bucket outputs two rows (direction false and true) with average Traffic. +func (s *StatsService) downsampleStats(stats []model.Stats, maxRows int) []model.Stats { + if len(stats) <= maxRows { + return stats + } + numBuckets := int(maxRows / 2) + sort.Slice(stats, func(i, j int) bool { return stats[i].DateTime < stats[j].DateTime }) + timeMin, timeMax := stats[0].DateTime, stats[len(stats)-1].DateTime + bucketSpan := (timeMax - timeMin) / int64(numBuckets) + if bucketSpan == 0 { + bucketSpan = 1 + } + downsampled := make([]model.Stats, 0, maxRows) + for i := 0; i < numBuckets; i++ { + bucketStart := timeMin + int64(i)*bucketSpan + bucketEnd := timeMin + int64(i+1)*bucketSpan + if i == numBuckets-1 { + bucketEnd = timeMax + 1 + } + for _, dir := range []bool{false, true} { + var sum int64 + var count int + for _, r := range stats { + if r.DateTime >= bucketStart && r.DateTime < bucketEnd && r.Direction == dir { + sum += r.Traffic + count++ + } + } + avg := int64(0) + if count > 0 { + avg = sum / int64(count) + } + downsampled = append(downsampled, model.Stats{ + DateTime: bucketStart, + Resource: stats[0].Resource, + Tag: stats[0].Tag, + Direction: dir, + Traffic: avg, + }) + } + } + return downsampled +} + +func (s *StatsService) GetOnlines() (onlines, error) { + return *onlineResources, nil +} +func (s *StatsService) DelOldStats(days int) error { + oldTime := time.Now().AddDate(0, 0, -(days)).Unix() + db := database.GetDB() + return db.Where("date_time < ?", oldTime).Delete(model.Stats{}).Error +} diff --git a/service/tls.go b/service/tls.go new file mode 100644 index 0000000..51eafa9 --- /dev/null +++ b/service/tls.go @@ -0,0 +1,105 @@ +package service + +import ( + "encoding/json" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/util/common" + + "gorm.io/gorm" +) + +type TlsService struct { + InboundService + ServicesService +} + +func (s *TlsService) GetAll() ([]model.Tls, error) { + db := database.GetDB() + tlsConfig := []model.Tls{} + err := db.Model(model.Tls{}).Scan(&tlsConfig).Error + if err != nil { + return nil, err + } + + return tlsConfig, nil +} + +func (s *TlsService) Save(tx *gorm.DB, action string, data json.RawMessage, hostname string) error { + var err error + + switch action { + case "new", "edit": + var tls model.Tls + err = json.Unmarshal(data, &tls) + if err != nil { + return err + } + err = tx.Save(&tls).Error + if err != nil { + return err + } + if action == "edit" { + var inbounds []model.Inbound + err = tx.Model(model.Inbound{}).Preload("Tls").Where("tls_id = ?", tls.Id).Find(&inbounds).Error + if err != nil { + return err + } + if len(inbounds) > 0 { + err = s.ClientService.UpdateLinksByInboundChange(tx, &inbounds, hostname, "") + if err != nil { + return err + } + var inboundIds []uint + for _, inbound := range inbounds { + inboundIds = append(inboundIds, inbound.Id) + } + err = s.InboundService.UpdateOutJsons(tx, inboundIds, hostname) + if err != nil { + return common.NewError("unable to update out_json of inbounds: ", err.Error()) + } + err = s.InboundService.RestartInbounds(tx, inboundIds) + if err != nil { + return err + } + } + var serviceIds []uint + err = tx.Model(model.Service{}).Where("tls_id = ?", tls.Id).Scan(&serviceIds).Error + if err != nil { + return err + } + if len(serviceIds) > 0 { + err = s.ServicesService.RestartServices(tx, serviceIds) + if err != nil { + return err + } + } + } + case "del": + var id uint + err = json.Unmarshal(data, &id) + if err != nil { + return err + } + var inboundCount int64 + err = tx.Model(model.Inbound{}).Where("tls_id = ?", id).Count(&inboundCount).Error + if err != nil { + return err + } + var serviceCount int64 + err = tx.Model(model.Service{}).Where("tls_id = ?", id).Count(&serviceCount).Error + if err != nil { + return err + } + if inboundCount > 0 || serviceCount > 0 { + return common.NewError("tls in use") + } + err = tx.Where("id = ?", id).Delete(model.Tls{}).Error + if err != nil { + return err + } + } + + return nil +} diff --git a/service/user.go b/service/user.go new file mode 100644 index 0000000..46043b4 --- /dev/null +++ b/service/user.go @@ -0,0 +1,161 @@ +package service + +import ( + "encoding/json" + "time" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util/common" +) + +type UserService struct { +} + +func (s *UserService) GetFirstUser() (*model.User, error) { + db := database.GetDB() + + user := &model.User{} + err := db.Model(model.User{}). + First(user). + Error + if err != nil { + return nil, err + } + return user, nil +} + +func (s *UserService) UpdateFirstUser(username string, password string) error { + if username == "" { + return common.NewError("username can not be empty") + } else if password == "" { + return common.NewError("password can not be empty") + } + db := database.GetDB() + user := &model.User{} + err := db.Model(model.User{}).First(user).Error + if database.IsNotFound(err) { + user.Username = username + user.Password = password + return db.Model(model.User{}).Create(user).Error + } else if err != nil { + return err + } + user.Username = username + user.Password = password + return db.Save(user).Error +} + +func (s *UserService) Login(username string, password string, remoteIP string) (string, error) { + user := s.CheckUser(username, password, remoteIP) + if user == nil { + return "", common.NewError("wrong user or password! IP: ", remoteIP) + } + return user.Username, nil +} + +func (s *UserService) CheckUser(username string, password string, remoteIP string) *model.User { + db := database.GetDB() + + user := &model.User{} + err := db.Model(model.User{}). + Where("username = ? and password = ?", username, password). + First(user). + Error + if database.IsNotFound(err) { + return nil + } else if err != nil { + logger.Warning("check user err:", err, " IP: ", remoteIP) + return nil + } + + lastLoginTxt := time.Now().Format("2006-01-02 15:04:05") + " " + remoteIP + err = db.Model(model.User{}). + Where("username = ?", username). + Update("last_logins", &lastLoginTxt).Error + if err != nil { + logger.Warning("unable to log login data", err) + } + return user +} + +func (s *UserService) GetUsers() (*[]model.User, error) { + var users []model.User + db := database.GetDB() + err := db.Model(model.User{}).Select("id,username,last_logins").Scan(&users).Error + if err != nil { + return nil, err + } + return &users, nil +} + +func (s *UserService) ChangePass(id string, oldPass string, newUser string, newPass string) error { + db := database.GetDB() + user := &model.User{} + err := db.Model(model.User{}).Where("id = ? AND password = ?", id, oldPass).First(user).Error + if err != nil || database.IsNotFound(err) { + return err + } + user.Username = newUser + user.Password = newPass + return db.Save(user).Error +} + +func (s *UserService) LoadTokens() ([]byte, error) { + db := database.GetDB() + var tokens []model.Tokens + err := db.Model(model.Tokens{}).Preload("User").Where("expiry == 0 or expiry > ?", time.Now().Unix()).Find(&tokens).Error + if err != nil { + return nil, err + } + var result []map[string]interface{} + for _, t := range tokens { + result = append(result, map[string]interface{}{ + "token": t.Token, + "expiry": t.Expiry, + "username": t.User.Username, + }) + } + jsonResult, _ := json.MarshalIndent(result, "", " ") + return jsonResult, nil +} + +func (s *UserService) GetUserTokens(username string) (*[]model.Tokens, error) { + db := database.GetDB() + var token []model.Tokens + err := db.Model(model.Tokens{}).Select("id,desc,'****' as token,expiry,user_id").Where("user_id = (select id from users where username = ?)", username).Find(&token).Error + if err != nil && !database.IsNotFound(err) { + println(err.Error()) + return nil, err + } + return &token, nil +} + +func (s *UserService) AddToken(username string, expiry int64, desc string) (string, error) { + db := database.GetDB() + var userId uint + err := db.Model(model.User{}).Where("username = ?", username).Select("id").Scan(&userId).Error + if err != nil { + return "", err + } + if expiry > 0 { + expiry = expiry*86400 + time.Now().Unix() + } + token := &model.Tokens{ + Token: common.Random(32), + Desc: desc, + Expiry: expiry, + UserId: userId, + } + err = db.Create(token).Error + if err != nil { + return "", err + } + return token.Token, nil +} + +func (s *UserService) DeleteToken(id string) error { + db := database.GetDB() + return db.Model(model.Tokens{}).Where("id = ?", id).Delete(&model.Tokens{}).Error +} diff --git a/service/warp.go b/service/warp.go new file mode 100644 index 0000000..0bf96a1 --- /dev/null +++ b/service/warp.go @@ -0,0 +1,224 @@ +package service + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "strconv" + "time" + + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util/common" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +type WarpService struct{} + +func (s *WarpService) getWarpInfo(deviceId string, accessToken string) ([]byte, error) { + url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", deviceId) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+accessToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 200 { + return nil, err + } + defer resp.Body.Close() + buffer := bytes.NewBuffer(make([]byte, 8192)) + buffer.Reset() + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} + +func (s *WarpService) RegisterWarp(ep *model.Endpoint) error { + tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z") + privateKey, _ := wgtypes.GenerateKey() + publicKey := privateKey.PublicKey().String() + hostName, _ := os.Hostname() + + data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "s-ui", "name": "%s"}`, publicKey, tos, hostName) + url := "https://api.cloudflareclient.com/v0a2158/reg" + + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data))) + if err != nil { + return err + } + + req.Header.Add("CF-Client-Version", "a-7.21-0721") + req.Header.Add("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 200 { + return err + } + defer resp.Body.Close() + buffer := bytes.NewBuffer(make([]byte, 8192)) + buffer.Reset() + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return err + } + + var rspData map[string]interface{} + err = json.Unmarshal(buffer.Bytes(), &rspData) + if err != nil { + return err + } + + deviceId := rspData["id"].(string) + token := rspData["token"].(string) + license, ok := rspData["account"].(map[string]interface{})["license"].(string) + if !ok { + logger.Debug("Error accessing license value.") + return err + } + + warpInfo, err := s.getWarpInfo(deviceId, token) + if err != nil { + return err + } + + var warpDetails map[string]interface{} + err = json.Unmarshal(warpInfo, &warpDetails) + if err != nil { + return err + } + + warpConfig, _ := warpDetails["config"].(map[string]interface{}) + clientId, _ := warpConfig["client_id"].(string) + reserved := s.getReserved(clientId) + interfaceConfig, _ := warpConfig["interface"].(map[string]interface{}) + addresses, _ := interfaceConfig["addresses"].(map[string]interface{}) + v4, _ := addresses["v4"].(string) + v6, _ := addresses["v6"].(string) + peer, _ := warpConfig["peers"].([]interface{})[0].(map[string]interface{}) + peerEndpoint, _ := peer["endpoint"].(map[string]interface{})["host"].(string) + peerEpAddress, peerEpPort, err := net.SplitHostPort(peerEndpoint) + if err != nil { + return err + } + peerPublicKey, _ := peer["public_key"].(string) + peerPort, _ := strconv.Atoi(peerEpPort) + + peers := []map[string]interface{}{ + { + "address": peerEpAddress, + "port": peerPort, + "public_key": peerPublicKey, + "allowed_ips": []string{"0.0.0.0/0", "::/0"}, + "reserved": reserved, + }, + } + + warpData := map[string]interface{}{ + "access_token": token, + "device_id": deviceId, + "license_key": license, + } + + ep.Ext, err = json.MarshalIndent(warpData, "", " ") + if err != nil { + return err + } + + var epOptions map[string]interface{} + err = json.Unmarshal(ep.Options, &epOptions) + if err != nil { + return err + } + epOptions["private_key"] = privateKey.String() + epOptions["address"] = []string{fmt.Sprintf("%s/32", v4), fmt.Sprintf("%s/128", v6)} + epOptions["listen_port"] = 0 + epOptions["peers"] = peers + + ep.Options, err = json.MarshalIndent(epOptions, "", " ") + return err +} + +func (s *WarpService) getReserved(clientID string) []int { + var reserved []int + decoded, err := base64.StdEncoding.DecodeString(clientID) + if err != nil { + return nil + } + + hexString := "" + for _, char := range decoded { + hex := fmt.Sprintf("%02x", char) + hexString += hex + } + + for i := 0; i < len(hexString); i += 2 { + hexByte := hexString[i : i+2] + decValue, err := strconv.ParseInt(hexByte, 16, 32) + if err != nil { + return nil + } + reserved = append(reserved, int(decValue)) + } + + return reserved +} + +func (s *WarpService) SetWarpLicense(old_license string, ep *model.Endpoint) error { + var warpData map[string]string + err := json.Unmarshal(ep.Ext, &warpData) + if err != nil { + return err + } + + if warpData["license_key"] == old_license { + return nil + } + + url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"]) + data := fmt.Sprintf(`{"license": "%s"}`, warpData["license_key"]) + + req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data))) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+warpData["access_token"]) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + buffer := bytes.NewBuffer(make([]byte, 8192)) + buffer.Reset() + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return err + } + var response map[string]interface{} + err = json.Unmarshal(buffer.Bytes(), &response) + if err != nil { + return err + } + + if success, ok := response["success"].(bool); ok && success == false { + errorArr, _ := response["errors"].([]interface{}) + errorObj := errorArr[0].(map[string]interface{}) + return common.NewError(errorObj["code"], errorObj["message"]) + } + + return nil +} diff --git a/sub/clashService.go b/sub/clashService.go new file mode 100644 index 0000000..a7cb81f --- /dev/null +++ b/sub/clashService.go @@ -0,0 +1,393 @@ +package sub + +import ( + "strings" + + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/service" + "github.com/alireza0/s-ui/util" + + "gopkg.in/yaml.v3" +) + +type ClashService struct { + service.SettingService + JsonService + LinkService +} + +const basicClashConfig = `mixed-port: 7890 +allow-lan: false +mode: rule +log-level: info +external-controller: 127.0.0.1:9090 +tun: + enable: true + stack: system + auto-route: true + auto-detect-interface: true + dns-hijack: + - any:53 +dns: + enable: true + ipv6: false + enhanced-mode: fake-ip + fake-ip-range: 198.18.0.1/16 + default-nameserver: + - 8.8.8.8 + - 1.1.1.1 + nameserver: + - https://doh.pub/dns-query + - https://1.0.0.1/dns-query + fallback: + - tcp://9.9.9.9:53 + fake-ip-filter: + - "*.lan" + - localhost + - "*.local" +rules: + - GEOIP,Private,DIRECT + - MATCH,Proxy +` + +const ProxyGroups = `- name: Proxy + type: select + proxies: [] +- name: Auto + type: url-test + proxies: [] + url: http://www.gstatic.com/generate_204 + interval: 300 + tolerance: 50 +` + +func (s *ClashService) GetClash(subId string) (*string, []string, error) { + + client, inDatas, err := s.getData(subId) + if err != nil { + return nil, nil, err + } + + outbounds, outTags, err := s.getOutbounds(client.Config, inDatas) + if err != nil { + return nil, nil, err + } + + links := s.LinkService.GetLinks(&client.Links, "external", "") + tagNumEnable := 0 + if len(links) > 1 { + tagNumEnable = 1 + } + for index, link := range links { + json, tag, err := util.GetOutbound(link, (index+1)*tagNumEnable) + if err == nil && len(tag) > 0 { + *outbounds = append(*outbounds, *json) + *outTags = append(*outTags, tag) + } + } + + basicConfig, err := s.getClashConfig() + if err != nil || len(basicConfig) == 0 { + basicConfig = basicClashConfig + } + + resultStr, err := s.ConvertToClashMeta(outbounds, basicConfig) + if err != nil { + return nil, nil, err + } + + updateInterval, _ := s.SettingService.GetSubUpdates() + headers := util.GetHeaders(client, updateInterval) + + return &resultStr, headers, nil +} + +func (s *ClashService) getClashConfig() (string, error) { + subClashExt, err := s.SettingService.GetSubClashExt() + if err != nil { + return "", err + } + + return subClashExt, nil +} + +func (s *ClashService) ConvertToClashMeta(outbounds *[]map[string]interface{}, basicConfig string) (string, error) { + var proxies []interface{} + proxyTags := make([]string, 0) + for _, obMap := range *outbounds { + + t, _ := obMap["type"].(string) + if t == "selector" || t == "urltest" || t == "direct" { + continue + } + + proxy := make(map[string]interface{}) + proxy["name"] = obMap["tag"] + proxy["type"] = t + + server, _ := obMap["server"].(string) + if len(server) > 0 && strings.Contains(server, ":") && !strings.Contains(server, ".") && !(strings.HasPrefix(server, "[") && strings.HasSuffix(server, "]")) { + server = "'[" + server + "]'" + } + proxy["server"] = server + + proxy["port"] = obMap["server_port"] + + switch t { + case "vmess", "vless", "tuic": + proxy["uuid"] = obMap["uuid"] + if t == "vmess" { + if alterId, ok := obMap["alter_id"].(float64); ok { + proxy["alterId"] = int(alterId) + } else { + proxy["alterId"] = 0 + } + proxy["cipher"] = "auto" + } + if t == "vless" { + if flow, ok := obMap["flow"].(string); ok { + proxy["flow"] = flow + } + } + if t == "tuic" { + proxy["password"] = obMap["password"] + if congestion_control, ok := obMap["congestion_control"].(string); ok { + proxy["congestion-controller"] = congestion_control + } + } + case "trojan": + proxy["password"] = obMap["password"] + case "socks", "http": + if t == "socks" { + proxy["type"] = "socks5" + } + proxy["username"] = obMap["username"] + proxy["password"] = obMap["password"] + case "hysteria", "hysteria2": + if _, ok := obMap["up_mbps"].(float64); ok { + proxy["up"] = obMap["up_mbps"] + } + if _, ok := obMap["down_mbps"].(float64); ok { + proxy["down"] = obMap["down_mbps"] + } + if t == "hysteria" { + proxy["auth-str"] = obMap["auth_str"] + if obfs, ok := obMap["obfs"].(string); ok { + proxy["obfs"] = obfs + } + } else { + proxy["password"] = obMap["password"] + if obfs, ok := obMap["obfs"].(map[string]interface{}); ok { + proxy["obfs"] = obfs["type"] + proxy["obfs-password"] = obfs["password"] + } + } + + if portLists, ok := obMap["server_ports"].([]interface{}); ok { + var ports []string + for _, portList := range portLists { + portRange, _ := portList.(string) + ports = append(ports, strings.ReplaceAll(portRange, ":", "-")) + } + proxy["ports"] = strings.Join(ports, ",") + } + case "anytls": + proxy["password"] = obMap["password"] + if tls, ok := obMap["tls"].(map[string]interface{}); ok { + proxy["sni"] = tls["server_name"] + proxy["skip-cert-verify"] = tls["insecure"] + } + case "shadowsocks": + proxy["type"] = "ss" + proxy["cipher"] = obMap["method"] + proxy["password"] = obMap["password"] + if network, ok := obMap["network"].(string); ok && network != "tcp" { + proxy["udp"] = true + } + if uot, ok := obMap["udp_over_tcp"].(bool); ok && uot { + proxy["udp-over-tcp"] = true + } + default: + continue + } + + // TLS params + tls, isTls := obMap["tls"].(map[string]interface{}) + if isTls { + tlsEnabled, ok := tls["enabled"].(bool) + if ok && !tlsEnabled { + isTls = false + } + } + if isTls { + proxy["tls"] = tls["enabled"] + + // ALPN if exists + if alpn, ok := tls["alpn"].([]interface{}); ok { + proxy["alpn"] = alpn + } + + // Add reality if exists + if reality, ok := tls["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) { + reality_opts := make(map[string]interface{}) + if pbk, ok := reality["public_key"].(string); ok { + reality_opts["public-key"] = pbk + } + if sid, ok := reality["short_id"].(string); ok { + reality_opts["short-id"] = sid + } + proxy["reality-opts"] = reality_opts + } + if utls, ok := tls["utls"].(map[string]interface{}); ok { + if enabled, ok := utls["enabled"].(bool); ok && enabled { + if fp, ok := utls["fingerprint"].(string); ok { + proxy["client-fingerprint"] = fp + } + } + } + if sni, ok := tls["server_name"].(string); ok { + if t == "vless" || t == "vmess" { + proxy["servername"] = sni + } else { + proxy["sni"] = sni + } + } + if insecure, ok := tls["insecure"].(bool); ok && insecure { + proxy["skip-cert-verify"] = insecure + } + // ech outbounds + if ech, ok := tls["ech"].(map[string]interface{}); ok && ech["enabled"].(bool) { + ech_config, _ := ech["config"].([]interface{}) + ech_string := "" + for i := 1; i < len(ech_config)-1; i++ { + ech_string += ech_config[i].(string) + } + proxy["ech-opts"] = map[string]interface{}{ + "enable": true, + "config": ech_string, + } + } + } + + // Transport if exist + if transport, ok := obMap["transport"].(map[string]interface{}); ok { + tt, _ := transport["type"].(string) + switch tt { + case "http": + httpOpts := make(map[string]interface{}) + if path, ok := transport["path"].([]interface{}); ok { + httpOpts["path"] = path[0] + } else if path, ok := transport["path"].(string); ok { + httpOpts["path"] = path + } + if host, ok := transport["host"].([]interface{}); ok { + httpOpts["host"] = host[0] + } + if isTls { + proxy["network"] = "h2" + proxy["h2-opts"] = httpOpts + } else { + proxy["network"] = "http" + proxy["http-opts"] = map[string]interface{}{"path": []interface{}{httpOpts["path"]}, "host": httpOpts["host"]} + } + case "ws", "httpupgrade": + proxy["network"] = "ws" + wsOpts := make(map[string]interface{}) + if path, ok := transport["path"].(string); ok { + wsOpts["path"] = path + } + if headers, ok := transport["headers"].([]interface{}); ok { + wsOpts["headers"] = headers + } + if ed, ok := transport["early_data_header_name"].(string); ok { + wsOpts["early-data-header-name"] = ed + } + if tt == "httpupgrade" { + wsOpts["v2ray-http-upgrade"] = true + } + proxy["ws-opts"] = wsOpts + case "grpc": + proxy["network"] = "grpc" + grpcOpts := make(map[string]interface{}) + if service_name, ok := transport["service_name"].(string); ok { + grpcOpts["grpc-service-name"] = service_name + } + proxy["grpc-opts"] = grpcOpts + } + } + + // Multiplex + if mux, ok := obMap["multiplex"].(map[string]interface{}); ok { + if enabled, ok := mux["enabled"].(bool); ok && enabled { + smux := make(map[string]interface{}) + smux["enabled"] = true + if protocol, ok := mux["protocol"].(string); ok { + smux["protocol"] = protocol + } + if _, ok := mux["max_connections"].(float64); ok { + smux["max-connections"] = mux["max_connections"] + } + if _, ok := mux["min_streams"].(float64); ok { + smux["min-streams"] = mux["min_streams"] + } + if _, ok := mux["max_streams"].(float64); ok { + smux["max-streams"] = mux["max_streams"] + } + if _, ok := mux["padding"].(bool); ok { + smux["padding"] = mux["padding"] + } + if brutal, ok := mux["brutal"].(map[string]interface{}); ok { + if enabled, ok := brutal["enabled"].(bool); ok && enabled { + brutalOpts := make(map[string]interface{}) + brutalOpts["enabled"] = true + if _, ok := brutal["up_mbps"].(float64); ok { + brutalOpts["up"] = brutal["up_mbps"] + } + if _, ok := brutal["down_mbps"].(float64); ok { + brutalOpts["down"] = brutal["down_mbps"] + } + smux["brutal-opts"] = brutalOpts + } + } + proxy["smux"] = smux + } + } + + proxies = append(proxies, proxy) + proxyTags = append(proxyTags, obMap["tag"].(string)) + } + + var proxyGroups []map[string]interface{} + err := yaml.Unmarshal([]byte(ProxyGroups), &proxyGroups) + if err != nil { + logger.Error(err.Error()) + } + + proxyGroups[1]["proxies"] = proxyTags + proxyGroups[0]["proxies"] = append([]string{proxyGroups[1]["name"].(string)}, proxyTags...) + + // Merge proxies and proxy groups if exist + var output map[string]interface{} + err = yaml.Unmarshal([]byte(basicConfig), &output) + if err != nil { + logger.Error(err.Error()) + } + + if p, ok := output["proxies"].([]interface{}); ok { + output["proxies"] = append(p, proxies...) + } else { + output["proxies"] = proxies + } + + if pg, ok := output["proxy-groups"].([]interface{}); ok { + output["proxy-groups"] = append(pg, proxyGroups[0], proxyGroups[1]) + } else { + output["proxy-groups"] = proxyGroups + } + + result, err := yaml.Marshal(output) + if err != nil { + return "", err + } + return string(result), nil +} diff --git a/sub/jsonService.go b/sub/jsonService.go new file mode 100644 index 0000000..61141aa --- /dev/null +++ b/sub/jsonService.go @@ -0,0 +1,326 @@ +package sub + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/service" + "github.com/alireza0/s-ui/util" +) + +const defaultJson = ` +{ + "inbounds": [ + { + "type": "tun", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "mtu": 9000, + "auto_route": true, + "strict_route": false, + "endpoint_independent_nat": false, + "stack": "system", + "platform": { + "http_proxy": { + "enabled": true, + "server": "127.0.0.1", + "server_port": 2080 + } + } + }, + { + "type": "mixed", + "listen": "127.0.0.1", + "listen_port": 2080, + "users": [] + } + ] +} +` + +type JsonService struct { + service.SettingService + LinkService +} + +func (j *JsonService) GetJson(subId string, format string) (*string, []string, error) { + var jsonConfig map[string]interface{} + + client, inDatas, err := j.getData(subId) + if err != nil { + return nil, nil, err + } + + outbounds, outTags, err := j.getOutbounds(client.Config, inDatas) + if err != nil { + return nil, nil, err + } + + links := j.LinkService.GetLinks(&client.Links, "external", "") + tagNumEnable := 0 + if len(links) > 1 { + tagNumEnable = 1 + } + for index, link := range links { + json, tag, err := util.GetOutbound(link, (index+1)*tagNumEnable) + if err == nil && len(tag) > 0 { + *outbounds = append(*outbounds, *json) + *outTags = append(*outTags, tag) + } + } + + j.addDefaultOutbounds(outbounds, outTags) + + err = json.Unmarshal([]byte(defaultJson), &jsonConfig) + if err != nil { + return nil, nil, err + } + + jsonConfig["outbounds"] = outbounds + + // Add other objects from settings + j.addOthers(&jsonConfig) + + result, _ := json.MarshalIndent(jsonConfig, "", " ") + resultStr := string(result) + + updateInterval, _ := j.SettingService.GetSubUpdates() + headers := util.GetHeaders(client, updateInterval) + + return &resultStr, headers, nil +} + +func (j *JsonService) getData(subId string) (*model.Client, []*model.Inbound, error) { + db := database.GetDB() + client := &model.Client{} + err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error + if err != nil { + return nil, nil, err + } + var clientInbounds []uint + err = json.Unmarshal(client.Inbounds, &clientInbounds) + if err != nil { + return nil, nil, err + } + var inbounds []*model.Inbound + err = db.Model(model.Inbound{}).Preload("Tls").Where("id in ?", clientInbounds).Find(&inbounds).Error + if err != nil { + return nil, nil, err + } + return client, inbounds, nil +} + +func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inbounds []*model.Inbound) (*[]map[string]interface{}, *[]string, error) { + var outbounds []map[string]interface{} + var configs map[string]interface{} + var outTags []string + + err := json.Unmarshal(clientConfig, &configs) + if err != nil { + return nil, nil, err + } + for _, inData := range inbounds { + if len(inData.OutJson) < 5 { + continue + } + var outbound map[string]interface{} + err = json.Unmarshal(inData.OutJson, &outbound) + if err != nil { + return nil, nil, err + } + protocol, _ := outbound["type"].(string) + + // Shadowsocks + if protocol == "shadowsocks" { + var userPass []string + var inbOptions map[string]interface{} + err = json.Unmarshal(inData.Options, &inbOptions) + if err != nil { + return nil, nil, err + } + method, _ := inbOptions["method"].(string) + if strings.HasPrefix(method, "2022") { + inbPass, _ := inbOptions["password"].(string) + userPass = append(userPass, inbPass) + } + var pass string + if method == "2022-blake3-aes-128-gcm" { + pass, _ = configs["shadowsocks16"].(map[string]interface{})["password"].(string) + } else { + pass, _ = configs["shadowsocks"].(map[string]interface{})["password"].(string) + } + userPass = append(userPass, pass) + outbound["password"] = strings.Join(userPass, ":") + } else { // Other protocols + config, _ := configs[protocol].(map[string]interface{}) + for key, value := range config { + if key == "name" || key == "alterId" || (key == "flow" && inData.TlsId == 0) { + continue + } + outbound[key] = value + } + } + + var addrs []map[string]interface{} + err = json.Unmarshal(inData.Addrs, &addrs) + if err != nil { + return nil, nil, err + } + tag, _ := outbound["tag"].(string) + if len(addrs) == 0 { + // For mixed protocol, use separated socks and http + if protocol == "mixed" { + outbound["tag"] = tag + j.pushMixed(&outbounds, &outTags, outbound) + } else { + outTags = append(outTags, tag) + outbounds = append(outbounds, outbound) + } + } else { + for index, addr := range addrs { + // Copy original config + newOut := make(map[string]interface{}, len(outbound)) + for key, value := range outbound { + newOut[key] = value + } + // Change and push copied config + newOut["server"], _ = addr["server"].(string) + port, _ := addr["server_port"].(float64) + newOut["server_port"] = int(port) + + // Override TLS + if addrTls, ok := addr["tls"].(map[string]interface{}); ok { + outTls, _ := newOut["tls"].(map[string]interface{}) + if outTls == nil { + outTls = make(map[string]interface{}) + } + for key, value := range addrTls { + outTls[key] = value + } + newOut["tls"] = outTls + } + + remark, _ := addr["remark"].(string) + newTag := fmt.Sprintf("%d.%s%s", index+1, tag, remark) + newOut["tag"] = newTag + // For mixed protocol, use separated socks and http + if protocol == "mixed" { + j.pushMixed(&outbounds, &outTags, newOut) + } else { + outTags = append(outTags, newTag) + outbounds = append(outbounds, newOut) + } + } + } + } + return &outbounds, &outTags, nil +} + +func (j *JsonService) addDefaultOutbounds(outbounds *[]map[string]interface{}, outTags *[]string) { + outbound := []map[string]interface{}{ + { + "outbounds": append([]string{"auto", "direct"}, *outTags...), + "tag": "proxy", + "type": "selector", + }, + { + "tag": "auto", + "type": "urltest", + "outbounds": outTags, + "url": "http://www.gstatic.com/generate_204", + "interval": "10m", + "tolerance": 50, + }, + { + "type": "direct", + "tag": "direct", + }, + } + *outbounds = append(outbound, *outbounds...) +} + +func (j *JsonService) addOthers(jsonConfig *map[string]interface{}) error { + rules_start := []interface{}{ + map[string]interface{}{ + "action": "sniff", + }, + map[string]interface{}{ + "clash_mode": "Direct", + "action": "route", + "outbound": "direct", + }, + } + rules_end := []interface{}{ + map[string]interface{}{ + "clash_mode": "Global", + "action": "route", + "outbound": "proxy", + }, + } + route := map[string]interface{}{ + "auto_detect_interface": true, + "final": "proxy", + "rules": rules_start, + } + + othersStr, err := j.SettingService.GetSubJsonExt() + if err != nil { + return err + } + if len(othersStr) == 0 { + (*jsonConfig)["route"] = route + return nil + } + var othersJson map[string]interface{} + err = json.Unmarshal([]byte(othersStr), &othersJson) + if err != nil { + return err + } + if _, ok := othersJson["log"]; ok { + (*jsonConfig)["log"] = othersJson["log"] + } + if _, ok := othersJson["dns"]; ok { + (*jsonConfig)["dns"] = othersJson["dns"] + } + if _, ok := othersJson["inbounds"]; ok { + (*jsonConfig)["inbounds"] = othersJson["inbounds"] + } + if _, ok := othersJson["experimental"]; ok { + (*jsonConfig)["experimental"] = othersJson["experimental"] + } + if _, ok := othersJson["rule_set"]; ok { + route["rule_set"] = othersJson["rule_set"] + } + if settingRules, ok := othersJson["rules"].([]interface{}); ok { + rules := append(rules_start, settingRules...) + route["rules"] = append(rules, rules_end...) + } + if defaultDomainResolver, ok := othersJson["default_domain_resolver"].(string); ok { + route["default_domain_resolver"] = defaultDomainResolver + } + (*jsonConfig)["route"] = route + + return nil +} + +func (j *JsonService) pushMixed(outbounds *[]map[string]interface{}, outTags *[]string, out map[string]interface{}) { + socksOut := make(map[string]interface{}, 1) + httpOut := make(map[string]interface{}, 1) + for key, value := range out { + socksOut[key] = value + httpOut[key] = value + } + socksTag := fmt.Sprintf("%s-socks", out["tag"]) + httpTag := fmt.Sprintf("%s-http", out["tag"]) + socksOut["type"] = "socks" + httpOut["type"] = "http" + socksOut["tag"] = socksTag + httpOut["tag"] = httpTag + *outbounds = append(*outbounds, socksOut, httpOut) + *outTags = append(*outTags, socksTag, httpTag) +} diff --git a/sub/linkService.go b/sub/linkService.go new file mode 100644 index 0000000..67c29e7 --- /dev/null +++ b/sub/linkService.go @@ -0,0 +1,74 @@ +package sub + +import ( + "encoding/json" + "strings" + + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util" +) + +type Link struct { + Type string `json:"type"` + Remark string `json:"remark"` + Uri string `json:"uri"` +} + +type LinkService struct { +} + +func (s *LinkService) GetLinks(linkJson *json.RawMessage, types string, clientInfo string) []string { + links := []Link{} + var result []string + err := json.Unmarshal(*linkJson, &links) + if err != nil { + return nil + } + for _, link := range links { + switch link.Type { + case "external": + result = append(result, link.Uri) + case "sub": + subLinks := util.GetExternalLink(link.Uri) + result = append(result, strings.Split(subLinks, "\n")...) + case "local": + if types == "all" { + result = append(result, s.addClientInfo(link.Uri, clientInfo)) + } + } + } + return result +} + +func (s *LinkService) addClientInfo(uri string, clientInfo string) string { + if len(clientInfo) == 0 { + return uri + } + protocol := strings.Split(uri, "://") + if len(protocol) < 2 { + return uri + } + switch protocol[0] { + case "vmess": + var vmessJson map[string]interface{} + config, err := util.B64StrToByte(protocol[1]) + if err != nil { + logger.Warning("sub: Error decoding vmess content:", err) + return uri + } + err = json.Unmarshal(config, &vmessJson) + if err != nil { + logger.Warning("sub: Error decoding vmess content:", err) + return uri + } + vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo + result, err := json.MarshalIndent(vmessJson, "", " ") + if err != nil { + logger.Warning("sub: Error decoding vmess + clientInfo content:", err) + return uri + } + return "vmess://" + util.ByteToB64Str(result) + default: + return uri + clientInfo + } +} diff --git a/sub/sub.go b/sub/sub.go new file mode 100644 index 0000000..11276d2 --- /dev/null +++ b/sub/sub.go @@ -0,0 +1,162 @@ +package sub + +import ( + "context" + "crypto/tls" + "io" + "net" + "net/http" + "strconv" + "time" + + "github.com/alireza0/s-ui/config" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/middleware" + "github.com/alireza0/s-ui/network" + "github.com/alireza0/s-ui/service" + + "github.com/gin-gonic/gin" +) + +type Server struct { + httpServer *http.Server + listener net.Listener + ctx context.Context + cancel context.CancelFunc + + service.SettingService +} + +func NewServer() *Server { + ctx, cancel := context.WithCancel(context.Background()) + return &Server{ + ctx: ctx, + cancel: cancel, + } +} + +func (s *Server) initRouter() (*gin.Engine, error) { + if config.IsDebug() { + gin.SetMode(gin.DebugMode) + } else { + gin.DefaultWriter = io.Discard + gin.DefaultErrorWriter = io.Discard + gin.SetMode(gin.ReleaseMode) + } + + engine := gin.Default() + + subPath, err := s.SettingService.GetSubPath() + if err != nil { + return nil, err + } + + subDomain, err := s.SettingService.GetSubDomain() + if err != nil { + return nil, err + } + + if subDomain != "" { + engine.Use(middleware.DomainValidator(subDomain)) + } + + g := engine.Group(subPath) + NewSubHandler(g) + + return engine, nil +} + +func (s *Server) Start() (err error) { + //This is an anonymous function, no function name + defer func() { + if err != nil { + s.Stop() + } + }() + + engine, err := s.initRouter() + if err != nil { + return err + } + + certFile, err := s.SettingService.GetSubCertFile() + if err != nil { + return err + } + keyFile, err := s.SettingService.GetSubKeyFile() + if err != nil { + return err + } + listen, err := s.SettingService.GetSubListen() + if err != nil { + return err + } + port, err := s.SettingService.GetSubPort() + if err != nil { + return err + } + + listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) + listener, err := net.Listen("tcp", listenAddr) + if err != nil { + return err + } + + if certFile != "" || keyFile != "" { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + listener.Close() + return err + } + c := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + listener = network.NewAutoHttpsListener(listener) + listener = tls.NewListener(listener, c) + } + + if certFile != "" || keyFile != "" { + logger.Info("Sub server run https on", listener.Addr()) + } else { + logger.Info("Sub server run http on", listener.Addr()) + } + s.listener = listener + + s.httpServer = &http.Server{ + Handler: engine, + } + + go func() { + s.httpServer.Serve(listener) + }() + + return nil +} + +func (s *Server) Stop() error { + var err error + if s.httpServer != nil { + shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 30*time.Second) + err = s.httpServer.Shutdown(shutdownCtx) + cancelShutdown() + if err != nil { + s.cancel() + if s.listener != nil { + _ = s.listener.Close() + } + return err + } + } else if s.listener != nil { + err = s.listener.Close() + if err != nil { + s.cancel() + return err + } + } + s.cancel() + return nil +} + +func (s *Server) GetCtx() context.Context { + return s.ctx +} diff --git a/sub/subHandler.go b/sub/subHandler.go new file mode 100644 index 0000000..14c7962 --- /dev/null +++ b/sub/subHandler.go @@ -0,0 +1,78 @@ +package sub + +import ( + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/service" + + "github.com/gin-gonic/gin" +) + +type SubHandler struct { + service.SettingService + SubService + JsonService + ClashService +} + +func NewSubHandler(g *gin.RouterGroup) { + a := &SubHandler{} + a.initRouter(g) +} + +func (s *SubHandler) initRouter(g *gin.RouterGroup) { + g.GET("/:subid", s.subs) + g.HEAD("/:subid", s.subHeaders) +} + +func (s *SubHandler) subs(c *gin.Context) { + var headers []string + var result *string + var err error + subId := c.Param("subid") + format, isFormat := c.GetQuery("format") + if isFormat { + switch format { + case "json": + result, headers, err = s.JsonService.GetJson(subId, format) + case "clash": + result, headers, err = s.ClashService.GetClash(subId) + } + if err != nil || result == nil { + logger.Error(err) + c.String(400, "Error!") + return + } + } else { + result, headers, err = s.SubService.GetSubs(subId) + if err != nil || result == nil { + logger.Error(err) + c.String(400, "Error!") + return + } + } + + s.addHeaders(c, headers) + + c.String(200, *result) +} + +func (s *SubHandler) subHeaders(c *gin.Context) { + subId := c.Param("subid") + client, err := s.SubService.getClientBySubId(subId) + if err != nil { + logger.Error(err) + c.String(400, "Error!") + return + } + + headers := s.SubService.getClientHeaders(client) + s.addHeaders(c, headers) + + c.Status(200) +} + +func (s *SubHandler) addHeaders(c *gin.Context, headers []string) { + c.Writer.Header().Set("Subscription-Userinfo", headers[0]) + c.Writer.Header().Set("Profile-Update-Interval", headers[1]) + c.Writer.Header().Set("Profile-Title", headers[2]) +} diff --git a/sub/subService.go b/sub/subService.go new file mode 100644 index 0000000..93cce41 --- /dev/null +++ b/sub/subService.go @@ -0,0 +1,93 @@ +package sub + +import ( + "encoding/base64" + "fmt" + "strings" + "time" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/service" + "github.com/alireza0/s-ui/util" +) + +type SubService struct { + service.SettingService + LinkService +} + +func (s *SubService) GetSubs(subId string) (*string, []string, error) { + var err error + + client, err := s.getClientBySubId(subId) + if err != nil { + return nil, nil, err + } + + clientInfo := "" + subShowInfo, _ := s.SettingService.GetSubShowInfo() + if subShowInfo { + clientInfo = s.getClientInfo(client) + } + + linksArray := s.LinkService.GetLinks(&client.Links, "all", clientInfo) + result := strings.Join(linksArray, "\n") + + headers := s.getClientHeaders(client) + + subEncode, _ := s.SettingService.GetSubEncode() + if subEncode { + result = base64.StdEncoding.EncodeToString([]byte(result)) + } + + return &result, headers, nil +} + +func (j *SubService) getClientBySubId(subId string) (*model.Client, error) { + db := database.GetDB() + client := &model.Client{} + err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error + if err != nil { + return nil, err + } + return client, nil +} + +func (s *SubService) getClientHeaders(client *model.Client) []string { + updateInterval, _ := s.SettingService.GetSubUpdates() + return util.GetHeaders(client, updateInterval) +} + +func (s *SubService) getClientInfo(c *model.Client) string { + now := time.Now().Unix() + + var result []string + if vol := c.Volume - (c.Up + c.Down); vol > 0 { + result = append(result, fmt.Sprintf("%s%s", s.formatTraffic(vol), "📊")) + } + if c.Expiry > 0 { + result = append(result, fmt.Sprintf("%d%s⏳", (c.Expiry-now)/86400, "Days")) + } + if len(result) > 0 { + return " " + strings.Join(result, " ") + } else { + return " ♾" + } +} + +func (s *SubService) formatTraffic(trafficBytes int64) string { + if trafficBytes < 1024 { + return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1)) + } else if trafficBytes < (1024 * 1024) { + return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024)) + } else if trafficBytes < (1024 * 1024 * 1024) { + return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024)) + } else if trafficBytes < (1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024)) + } else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024)) + } else { + return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024)) + } +} diff --git a/util/base64.go b/util/base64.go new file mode 100644 index 0000000..13218c0 --- /dev/null +++ b/util/base64.go @@ -0,0 +1,20 @@ +package util + +import "encoding/base64" + +// Function to return decoded bytes if a string is Base64 encoded +func StrOrBase64Encoded(str string) string { + decoded, err := base64.StdEncoding.DecodeString(str) + if err == nil { + return string(decoded) + } + return str +} + +func B64StrToByte(str string) ([]byte, error) { + return base64.StdEncoding.DecodeString(str) +} + +func ByteToB64Str(b []byte) string { + return base64.StdEncoding.EncodeToString(b) +} diff --git a/util/common/array.go b/util/common/array.go new file mode 100644 index 0000000..43d3b79 --- /dev/null +++ b/util/common/array.go @@ -0,0 +1,45 @@ +package common + +// UnionUintArray returns a new unique slice that contains all elements from both input slices +func UnionUintArray(a []uint, b []uint) []uint { + m := make(map[uint]bool) + for _, v := range a { + m[v] = true + } + for _, v := range b { + m[v] = true + } + var res []uint + for k := range m { + res = append(res, k) + } + return res +} + +// Find different elements in two slices +// Returns elements in 'a' that are not in 'b' and elements in 'b' that are not in 'a' +func DiffUintArray(a []uint, b []uint) []uint { + different := []uint{} + set := make(map[uint]bool) + + for _, item := range a { + set[item] = true + } + for _, item := range b { + if !set[item] { + different = append(different, item) + } + } + + set = make(map[uint]bool) + for _, item := range b { + set[item] = true + } + for _, item := range a { + if !set[item] { + different = append(different, item) + } + } + + return different +} diff --git a/util/common/err.go b/util/common/err.go new file mode 100644 index 0000000..9edba83 --- /dev/null +++ b/util/common/err.go @@ -0,0 +1,28 @@ +package common + +import ( + "errors" + "fmt" + + "github.com/alireza0/s-ui/logger" +) + +func NewErrorf(format string, a ...interface{}) error { + msg := fmt.Sprintf(format, a...) + return errors.New(msg) +} + +func NewError(a ...interface{}) error { + msg := fmt.Sprintln(a...) + return errors.New(msg) +} + +func Recover(msg string) interface{} { + panicErr := recover() + if panicErr != nil { + if msg != "" { + logger.Error(msg, "panic:", panicErr) + } + } + return panicErr +} diff --git a/util/common/random.go b/util/common/random.go new file mode 100644 index 0000000..55145fd --- /dev/null +++ b/util/common/random.go @@ -0,0 +1,55 @@ +package common + +import ( + crand "crypto/rand" + "math/big" + mrand "math/rand" + "sync" + "time" +) + +var ( + allSeq []rune = []rune{ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + } + + fallbackRand = mrand.New(mrand.NewSource(time.Now().UnixNano())) + fallbackMu = sync.Mutex{} +) + +func Random(n int) string { + if n <= 0 || len(allSeq) == 0 { + return "" + } + result := make([]rune, n) + maxBig := big.NewInt(int64(len(allSeq))) + for i := 0; i < n; i++ { + num, err := crand.Int(crand.Reader, maxBig) + if err != nil { + // fallback + fallbackMu.Lock() + result[i] = allSeq[fallbackRand.Intn(len(allSeq))] + fallbackMu.Unlock() + continue + } + result[i] = allSeq[int(num.Int64())] + } + return string(result) +} + +func RandomInt(n int) int { + if n <= 0 { + return 0 + } + max := big.NewInt(int64(n)) + result, err := crand.Int(crand.Reader, max) + if err != nil { + // fallback + fallbackMu.Lock() + defer fallbackMu.Unlock() + return fallbackRand.Intn(n) + } + return int(result.Int64()) +} diff --git a/util/genLink.go b/util/genLink.go new file mode 100644 index 0000000..6187372 --- /dev/null +++ b/util/genLink.go @@ -0,0 +1,615 @@ +package util + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/alireza0/s-ui/database/model" + "github.com/alireza0/s-ui/util/common" +) + +var InboundTypeWithLink = []string{"socks", "http", "mixed", "shadowsocks", "naive", "hysteria", "hysteria2", "anytls", "tuic", "vless", "trojan", "vmess"} + +type LinkParam struct { + Key string + Value string +} + +func LinkGenerator(clientConfig json.RawMessage, i *model.Inbound, hostname string) []string { + inbound, err := i.MarshalFull() + if err != nil { + return []string{} + } + + var tls map[string]interface{} + if i.TlsId > 0 { + tls = prepareTls(i.Tls) + } + + var userConfig map[string]map[string]interface{} + if err := json.Unmarshal(clientConfig, &userConfig); err != nil { + return []string{} + } + + var Addrs []map[string]interface{} + if err := json.Unmarshal(i.Addrs, &Addrs); err != nil { + return []string{} + } + if len(Addrs) == 0 { + Addrs = append(Addrs, map[string]interface{}{ + "server": hostname, + "server_port": (*inbound)["listen_port"], + "remark": i.Tag, + }) + if i.TlsId > 0 { + Addrs[0]["tls"] = tls + } + } else { + for index, addr := range Addrs { + addrRemark, _ := addr["remark"].(string) + Addrs[index]["remark"] = i.Tag + addrRemark + if i.TlsId > 0 { + newTls := map[string]interface{}{} + for k, v := range tls { + newTls[k] = v + } + + // Override tls + if addrTls, ok := addr["tls"].(map[string]interface{}); ok { + for k, v := range addrTls { + newTls[k] = v + } + } + Addrs[index]["tls"] = newTls + } + } + } + + switch i.Type { + case "socks": + return socksLink(userConfig["socks"], Addrs) + case "http": + return httpLink(userConfig["http"], Addrs) + case "mixed": + return append( + socksLink(userConfig["socks"], Addrs), + httpLink(userConfig["http"], Addrs)..., + ) + case "shadowsocks": + return shadowsocksLink(userConfig, *inbound, Addrs) + case "naive": + return naiveLink(userConfig["naive"], *inbound, Addrs) + case "hysteria": + return hysteriaLink(userConfig["hysteria"], *inbound, Addrs) + case "hysteria2": + return hysteria2Link(userConfig["hysteria2"], *inbound, Addrs) + case "tuic": + return tuicLink(userConfig["tuic"], *inbound, Addrs) + case "vless": + return vlessLink(userConfig["vless"], *inbound, Addrs) + case "anytls": + return anytlsLink(userConfig["anytls"], Addrs) + case "trojan": + return trojanLink(userConfig["trojan"], *inbound, Addrs) + case "vmess": + return vmessLink(userConfig["vmess"], *inbound, Addrs) + } + + return []string{} +} + +func prepareTls(t *model.Tls) map[string]interface{} { + var iTls, oTls map[string]interface{} + if err := json.Unmarshal(t.Client, &oTls); err != nil { + return nil + } + if err := json.Unmarshal(t.Server, &iTls); err != nil { + return nil + } + + for k, v := range iTls { + switch k { + case "enabled", "server_name", "alpn": + oTls[k] = v + case "reality": + reality := v.(map[string]interface{}) + clientReality := oTls["reality"].(map[string]interface{}) + clientReality["enabled"] = reality["enabled"] + if shortIDs, hasSIds := reality["short_id"].([]interface{}); hasSIds && len(shortIDs) > 0 { + clientReality["short_id"] = shortIDs[common.RandomInt(len(shortIDs))] + } + oTls["reality"] = clientReality + } + } + return oTls +} + +func socksLink(userConfig map[string]interface{}, addrs []map[string]interface{}) []string { + var links []string + for _, addr := range addrs { + links = append(links, fmt.Sprintf("socks5://%s:%s@%s:%d", userConfig["username"], userConfig["password"], addr["server"].(string), uint(addr["server_port"].(float64)))) + } + return links +} + +func httpLink(userConfig map[string]interface{}, addrs []map[string]interface{}) []string { + var links []string + protocol := "http" + for _, addr := range addrs { + if addr["tls"] != nil { + protocol = "https" + } + links = append(links, fmt.Sprintf("%s://%s:%s@%s:%d", protocol, userConfig["username"], userConfig["password"], addr["server"].(string), uint(addr["server_port"].(float64)))) + } + return links +} + +func shadowsocksLink( + userConfig map[string]map[string]interface{}, + inbound map[string]interface{}, + addrs []map[string]interface{}) []string { + + var userPass []string + method, _ := inbound["method"].(string) + if strings.HasPrefix(method, "2022") { + inbPass, _ := inbound["password"].(string) + userPass = append(userPass, inbPass) + } + var pass string + if method == "2022-blake3-aes-128-gcm" { + pass, _ = userConfig["shadowsocks16"]["password"].(string) + } else { + pass, _ = userConfig["shadowsocks"]["password"].(string) + } + userPass = append(userPass, pass) + + uriBase := fmt.Sprintf("ss://%s", toBase64([]byte(fmt.Sprintf("%s:%s", method, strings.Join(userPass, ":"))))) + + var links []string + for _, addr := range addrs { + port, _ := addr["server_port"].(float64) + links = append(links, fmt.Sprintf("%s@%s:%.0f#%s", uriBase, addr["server"].(string), port, addr["remark"].(string))) + } + return links +} + +func naiveLink( + userConfig map[string]interface{}, + inbound map[string]interface{}, + addrs []map[string]interface{}) []string { + + password, _ := userConfig["password"].(string) + username, _ := userConfig["username"].(string) + + baseUri := "http2://" + var links []string + + for _, addr := range addrs { + var params []LinkParam + params = append(params, LinkParam{"padding", "1"}) + if tls, ok := addr["tls"].(map[string]interface{}); ok { + if sni, ok := tls["server_name"].(string); ok { + params = append(params, LinkParam{"peer", sni}) + } + if alpn, ok := tls["alpn"].([]interface{}); ok { + alpnList := make([]string, len(alpn)) + for i, v := range alpn { + alpnList[i] = v.(string) + } + params = append(params, LinkParam{"alpn", strings.Join(alpnList, ",")}) + } + if insecure, ok := tls["insecure"].(bool); ok && insecure { + params = append(params, LinkParam{"insecure", "1"}) + } + } + if tfo, ok := inbound["tcp_fast_open"].(bool); ok && tfo { + params = append(params, LinkParam{"tfo", "1"}) + } else { + params = append(params, LinkParam{"tfo", "0"}) + } + + port, _ := addr["server_port"].(float64) + uri := baseUri + toBase64([]byte(fmt.Sprintf("%s:%s@%s:%.0f", username, password, addr["server"].(string), port))) + links = append(links, addParams(uri, params, addr["remark"].(string))) + } + return links +} + +func hysteriaLink( + userConfig map[string]interface{}, + inbound map[string]interface{}, + addrs []map[string]interface{}) []string { + + baseUri := "hysteria://" + var links []string + + for _, addr := range addrs { + var params []LinkParam + if upmbps, ok := inbound["up_mbps"].(float64); ok { + params = append(params, LinkParam{"downmbps", fmt.Sprintf("%.0f", upmbps)}) + } + if downmbps, ok := inbound["down_mbps"].(float64); ok { + params = append(params, LinkParam{"upmbps", fmt.Sprintf("%.0f", downmbps)}) + } + if auth, ok := userConfig["auth_str"].(string); ok { + params = append(params, LinkParam{"auth", auth}) + } + if tls, ok := addr["tls"].(map[string]interface{}); ok { + getTlsParams(¶ms, tls, "insecure") + } + if obfs, ok := inbound["obfs"].(string); ok { + params = append(params, LinkParam{"obfs", obfs}) + } + if tfo, ok := inbound["tcp_fast_open"].(bool); ok && tfo { + params = append(params, LinkParam{"fastopen", "1"}) + } else { + params = append(params, LinkParam{"fastopen", "0"}) + } + var outJson map[string]interface{} + if err := json.Unmarshal(inbound["out_json"].(json.RawMessage), &outJson); err != nil { + return []string{} // Handle error + } + if mport, ok := outJson["server_ports"].([]interface{}); ok { + mportList := make([]string, len(mport)) + for i, v := range mport { + mportList[i] = v.(string) + } + params = append(params, LinkParam{"mport", strings.Join(mportList, ",")}) + } + + port, _ := addr["server_port"].(float64) + uri := fmt.Sprintf("%s%s:%.0f", baseUri, addr["server"].(string), port) + links = append(links, addParams(uri, params, addr["remark"].(string))) + } + + return links +} + +func hysteria2Link( + userConfig map[string]interface{}, + inbound map[string]interface{}, + addrs []map[string]interface{}) []string { + + password, _ := userConfig["password"].(string) + baseUri := fmt.Sprintf("%s%s@", "hysteria2://", password) + var links []string + + for _, addr := range addrs { + var params []LinkParam + if upmbps, ok := inbound["up_mbps"].(float64); ok { + params = append(params, LinkParam{"downmbps", fmt.Sprintf("%.0f", upmbps)}) + } + if downmbps, ok := inbound["down_mbps"].(float64); ok { + params = append(params, LinkParam{"upmbps", fmt.Sprintf("%.0f", downmbps)}) + } + if tls, ok := addr["tls"].(map[string]interface{}); ok { + getTlsParams(¶ms, tls, "insecure") + } + if obfs, ok := inbound["obfs"].(map[string]interface{}); ok { + if obfsType, ok := obfs["type"].(string); ok { + params = append(params, LinkParam{"obfs", obfsType}) + } + if obfsPassword, ok := obfs["password"].(string); ok { + params = append(params, LinkParam{"obfs-password", obfsPassword}) + } + } + if tfo, ok := inbound["tcp_fast_open"].(bool); ok && tfo { + params = append(params, LinkParam{"fastopen", "1"}) + } else { + params = append(params, LinkParam{"fastopen", "0"}) + } + var outJson map[string]interface{} + if err := json.Unmarshal(inbound["out_json"].(json.RawMessage), &outJson); err != nil { + return []string{} // Handle error + } + if mport, ok := outJson["server_ports"].([]interface{}); ok { + mportList := make([]string, len(mport)) + for i, v := range mport { + mportList[i] = v.(string) + } + params = append(params, LinkParam{"mport", strings.Join(mportList, ",")}) + } + + port, _ := addr["server_port"].(float64) + uri := fmt.Sprintf("%s%s:%.0f", baseUri, addr["server"].(string), port) + links = append(links, addParams(uri, params, addr["remark"].(string))) + } + + return links +} + +func anytlsLink( + userConfig map[string]interface{}, + addrs []map[string]interface{}) []string { + + password, _ := userConfig["password"].(string) + baseUri := fmt.Sprintf("%s%s@", "anytls://", password) + var links []string + + for _, addr := range addrs { + var params []LinkParam + if tls, ok := addr["tls"].(map[string]interface{}); ok { + getTlsParams(¶ms, tls, "insecure") + } + + port, _ := addr["server_port"].(float64) + uri := fmt.Sprintf("%s%s:%.0f", baseUri, addr["server"].(string), port) + links = append(links, addParams(uri, params, addr["remark"].(string))) + } + + return links +} + +func tuicLink( + userConfig map[string]interface{}, + inbound map[string]interface{}, + addrs []map[string]interface{}) []string { + + password, _ := userConfig["password"].(string) + uuid, _ := userConfig["uuid"].(string) + baseUri := fmt.Sprintf("%s%s:%s@", "tuic://", uuid, password) + var links []string + + for _, addr := range addrs { + var params []LinkParam + if tls, ok := addr["tls"].(map[string]interface{}); ok { + getTlsParams(¶ms, tls, "insecure") + } + if congestionControl, ok := inbound["congestion_control"].(string); ok { + params = append(params, LinkParam{"congestion_control", congestionControl}) + } + + port, _ := addr["server_port"].(float64) + uri := fmt.Sprintf("%s%s:%.0f", baseUri, addr["server"].(string), port) + links = append(links, addParams(uri, params, addr["remark"].(string))) + } + + return links +} + +func vlessLink( + userConfig map[string]interface{}, + inbound map[string]interface{}, + addrs []map[string]interface{}) []string { + + uuid, _ := userConfig["uuid"].(string) + baseParams := getTransportParams(inbound["transport"]) + var links []string + + for _, addr := range addrs { + params := make([]LinkParam, len(baseParams)) + copy(params, baseParams) + if tls, ok := addr["tls"].(map[string]interface{}); ok && tls["enabled"].(bool) { + getTlsParams(¶ms, tls, "allowInsecure") + if flow, ok := userConfig["flow"].(string); ok { + params = append(params, LinkParam{"flow", flow}) + } + } + port, _ := addr["server_port"].(float64) + uri := fmt.Sprintf("vless://%s@%s:%.0f", uuid, addr["server"].(string), port) + uri = addParams(uri, params, addr["remark"].(string)) + links = append(links, uri) + } + + return links +} + +func trojanLink( + userConfig map[string]interface{}, + inbound map[string]interface{}, + addrs []map[string]interface{}) []string { + password, _ := userConfig["password"].(string) + baseParams := getTransportParams(inbound["transport"]) + var links []string + + for _, addr := range addrs { + params := make([]LinkParam, len(baseParams)) + copy(params, baseParams) + if tls, ok := addr["tls"].(map[string]interface{}); ok && tls["enabled"].(bool) { + getTlsParams(¶ms, tls, "allowInsecure") + } + port, _ := addr["server_port"].(float64) + uri := fmt.Sprintf("trojan://%s@%s:%.0f", password, addr["server"].(string), port) + uri = addParams(uri, params, addr["remark"].(string)) + links = append(links, uri) + } + + return links +} + +func vmessLink( + userConfig map[string]interface{}, + inbound map[string]interface{}, + addrs []map[string]interface{}) []string { + + uuid, _ := userConfig["uuid"].(string) + transportParams := getTransportParams(inbound["transport"]) + var links []string + + baseParams := map[string]interface{}{ + "v": "2", + "id": uuid, + "aid": 0, + } + + var net, typ, host, path string + for _, p := range transportParams { + switch p.Key { + case "type": + net = p.Value + case "host": + host = p.Value + case "path": + path = p.Value + } + } + + if net == "http" || net == "tcp" { + baseParams["net"] = "tcp" + if net == "http" { + typ = "http" + } + } else { + baseParams["net"] = net + } + + for _, addr := range addrs { + obj := make(map[string]interface{}) + for k, v := range baseParams { + obj[k] = v + } + + obj["add"], _ = addr["server"].(string) + port, _ := addr["server_port"].(float64) + obj["port"] = fmt.Sprintf("%.0f", port) + obj["ps"], _ = addr["remark"].(string) + if typ != "" { + obj["type"] = typ + } + if host != "" { + obj["host"] = host + } + if path != "" { + obj["path"] = path + } + populateVmessTlsParams(obj, addr["tls"]) + + jsonStr, _ := json.Marshal(obj) + + uri := fmt.Sprintf("vmess://%s", toBase64(jsonStr)) + links = append(links, uri) + } + return links +} + +func populateVmessTlsParams(obj map[string]interface{}, tlsConfig interface{}) { + if tlsMap, ok := tlsConfig.(map[string]interface{}); ok && tlsMap["enabled"].(bool) { + obj["tls"] = "tls" + var tlsParams []LinkParam + getTlsParams(&tlsParams, tlsMap, "allowInsecure") + for _, p := range tlsParams { + switch p.Key { + case "security": + // ignore, as "tls" is already set + case "allowInsecure": + obj["allowInsecure"] = 1 + case "sni": + obj["sni"] = p.Value + case "fp": + obj["fp"] = p.Value + case "alpn": + obj["alpn"] = p.Value + } + } + } else { + obj["tls"] = "none" + } +} + +func toBase64(d []byte) string { + return base64.StdEncoding.EncodeToString(d) +} + +func addParams(uri string, params []LinkParam, remark string) string { + URL, _ := url.Parse(uri) + var q []string + for _, p := range params { + switch p.Key { + case "mport", "alpn": + q = append(q, fmt.Sprintf("%s=%s", p.Key, p.Value)) + default: + q = append(q, fmt.Sprintf("%s=%s", p.Key, url.QueryEscape(p.Value))) + } + } + URL.RawQuery = strings.Join(q, "&") + URL.Fragment = remark + return URL.String() +} + +func getTransportParams(t interface{}) []LinkParam { + var params []LinkParam + trasport, _ := t.(map[string]interface{}) + var transportType string + if tt, ok := trasport["type"].(string); ok { + transportType = tt + } else { + transportType = "tcp" + } + params = append(params, LinkParam{"type", transportType}) + if transportType == "tcp" { + return params + } + + switch transportType { + case "http": + if host, ok := trasport["host"].([]interface{}); ok { + var hosts []string + for _, v := range host { + hosts = append(hosts, v.(string)) + } + params = append(params, LinkParam{"host", strings.Join(hosts, ",")}) + } + if path, ok := trasport["path"].(string); ok { + params = append(params, LinkParam{"path", path}) + } + case "ws": + if path, ok := trasport["path"].(string); ok { + params = append(params, LinkParam{"path", path}) + } + if headers, ok := trasport["headers"].(map[string]interface{}); ok { + if host, ok := headers["Host"].(string); ok { + params = append(params, LinkParam{"host", host}) + } + } + case "grpc": + if serviceName, ok := trasport["service_name"].(string); ok { + params = append(params, LinkParam{"serviceName", serviceName}) + } + case "httpupgrade": + if host, ok := trasport["host"].(string); ok { + params = append(params, LinkParam{"host", host}) + } + if path, ok := trasport["path"].(string); ok { + params = append(params, LinkParam{"path", path}) + } + } + return params +} + +func getTlsParams(params *[]LinkParam, tls map[string]interface{}, insecureKey string) { + if reality, ok := tls["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) { + *params = append(*params, LinkParam{"security", "reality"}) + if pbk, ok := reality["public_key"].(string); ok { + *params = append(*params, LinkParam{"pbk", pbk}) + } + if sid, ok := reality["short_id"].(string); ok { + *params = append(*params, LinkParam{"sid", sid}) + } + } else { + *params = append(*params, LinkParam{"security", "tls"}) + if insecure, ok := tls["insecure"].(bool); ok && insecure { + *params = append(*params, LinkParam{insecureKey, "1"}) + } + if disableSni, ok := tls["disable_sni"].(bool); ok && disableSni { + *params = append(*params, LinkParam{"disable_sni", "1"}) + } + } + if utls, ok := tls["utls"].(map[string]interface{}); ok { + if fingerprint, ok := utls["fingerprint"].(string); ok { + *params = append(*params, LinkParam{"fp", fingerprint}) + } + } + if sni, ok := tls["server_name"].(string); ok { + *params = append(*params, LinkParam{"sni", sni}) + } + if alpn, ok := tls["alpn"].([]interface{}); ok { + alpnList := make([]string, len(alpn)) + for i, v := range alpn { + alpnList[i] = v.(string) + } + *params = append(*params, LinkParam{"alpn", strings.Join(alpnList, ",")}) + } +} diff --git a/util/linkToJson.go b/util/linkToJson.go new file mode 100644 index 0000000..068b540 --- /dev/null +++ b/util/linkToJson.go @@ -0,0 +1,581 @@ +package util + +import ( + "encoding/json" + "fmt" + "net" + "net/url" + "strconv" + "strings" + + "github.com/alireza0/s-ui/util/common" +) + +func GetOutbound(uri string, i int) (*map[string]interface{}, string, error) { + u, err := url.Parse(uri) + if err == nil { + switch u.Scheme { + case "vmess": + return vmess(u.Host, i) + case "vless": + return vless(u, i) + case "trojan": + return trojan(u, i) + case "hy", "hysteria": + return hy(u, i) + case "hy2", "hysteria2": + return hy2(u, i) + case "anytls": + return anytls(u, i) + case "tuic": + return tuic(u, i) + case "ss", "shadowsocks": + return ss(u, i) + case "naive+https", "naive+quic", "http2": + return parseNaiveLink(u, i) + } + } + return nil, "", common.NewError("Unsupported link format") +} + +func vmess(data string, i int) (*map[string]interface{}, string, error) { + dataByte, err := B64StrToByte(data) + if err != nil { + return nil, "", err + } + var dataJson map[string]interface{} + err = json.Unmarshal(dataByte, &dataJson) + if err != nil { + return nil, "", err + } + transport := map[string]interface{}{} + tp_net, _ := dataJson["net"].(string) + tp_type, _ := dataJson["type"].(string) + tp_host, _ := dataJson["host"].(string) + tp_path, _ := dataJson["path"].(string) + switch strings.ToLower(tp_net) { + case "tcp", "": + if tp_type == "http" { + transport["type"] = tp_type + if len(tp_host) > 0 { + transport["host"] = strings.Split(tp_host, ",") + } + transport["path"] = tp_path + } + case "http", "h2": + transport["type"] = "http" + if len(tp_host) > 0 { + transport["host"] = strings.Split(tp_host, ",") + } + transport["path"] = tp_path + case "ws": + transport["type"] = tp_net + transport["path"] = tp_path + transport["early_data_header_name"] = "Sec-WebSocket-Protocol" + if len(tp_host) > 0 { + transport["headers"] = map[string]interface{}{ + "Host": tp_host, + } + } + case "quic": + transport["type"] = tp_net + case "grpc": + transport["type"] = tp_net + transport["service_name"] = tp_path + case "httpupgrade": + transport["type"] = tp_net + transport["path"] = tp_path + transport["host"] = tp_host + default: + return nil, "", common.NewError("Invalid vmess") + } + tls := map[string]interface{}{} + vmess_tls, _ := dataJson["tls"].(string) + if vmess_tls == "tls" { + tls["enabled"] = true + tls_sni, _ := dataJson["sni"].(string) + tls_alpn, _ := dataJson["alpn"].(string) + _, tls_insecure := dataJson["allowInsecure"] + tls_fp, _ := dataJson["fp"].(string) + if len(tls_sni) > 0 { + tls["server_name"] = tls_sni + } + if len(tls_alpn) > 0 { + tls["alpn"] = strings.Split(tls_alpn, ",") + } + if tls_insecure { + tls["insecure"] = true + } + if len(tls_fp) > 0 { + tls["utls"] = map[string]interface{}{ + "enabled": true, + "fingerprint": tls_fp, + } + } + } + tag, _ := dataJson["ps"].(string) + if i > 0 { + tag = fmt.Sprintf("%d.%s", i, tag) + } + alter_id := 0 + if aid, ok := dataJson["aid"].(float64); ok { + alter_id = int(aid) + } + vmess := map[string]interface{}{ + "type": "vmess", + "tag": tag, + "server": dataJson["add"], + "server_port": dataJson["port"], + "uuid": dataJson["id"], + "security": "auto", + "alter_id": alter_id, + "tls": tls, + "transport": transport, + } + return &vmess, tag, err +} + +func vless(u *url.URL, i int) (*map[string]interface{}, string, error) { + query, _ := url.ParseQuery(u.RawQuery) + security := query.Get("security") + host, portStr, _ := net.SplitHostPort(u.Host) + port := 80 + if len(portStr) > 0 { + port, _ = strconv.Atoi(portStr) + } else { + if security == "tls" || security == "reality" { + port = 443 + } + } + tp_type := query.Get("type") + tag := u.Fragment + if i > 0 { + tag = fmt.Sprintf("%d.%s", i, u.Fragment) + } + vless := map[string]interface{}{ + "type": "vless", + "tag": tag, + "server": host, + "server_port": port, + "uuid": u.User.Username(), + "flow": query.Get("flow"), + "tls": getTls(security, &query), + "transport": getTransport(tp_type, &query), + } + return &vless, tag, nil +} + +func trojan(u *url.URL, i int) (*map[string]interface{}, string, error) { + query, _ := url.ParseQuery(u.RawQuery) + security := query.Get("security") + host, portStr, _ := net.SplitHostPort(u.Host) + port := 80 + if len(portStr) > 0 { + port, _ = strconv.Atoi(portStr) + } else { + if security == "tls" || security == "reality" { + port = 443 + } + } + tp_type := query.Get("type") + tag := u.Fragment + if i > 0 { + tag = fmt.Sprintf("%d.%s", i, u.Fragment) + } + trojan := map[string]interface{}{ + "type": "trojan", + "tag": tag, + "server": host, + "server_port": port, + "password": u.User.Username(), + "tls": getTls(security, &query), + "transport": getTransport(tp_type, &query), + } + return &trojan, tag, nil +} + +func hy(u *url.URL, i int) (*map[string]interface{}, string, error) { + query, _ := url.ParseQuery(u.RawQuery) + host, portStr, _ := net.SplitHostPort(u.Host) + port := 443 + if len(portStr) > 0 { + port, _ = strconv.Atoi(portStr) + } + + security := query.Get("security") + if len(security) == 0 { + security = "tls" + } + + tag := u.Fragment + if i > 0 { + tag = fmt.Sprintf("%d.%s", i, u.Fragment) + } + hy := map[string]interface{}{ + "type": "hysteria", + "tag": tag, + "server": host, + "server_port": port, + "obfs": query.Get("obfsParam"), + "auth_str": query.Get("auth"), + "tls": getTls(security, &query), + } + down, _ := strconv.Atoi(query.Get("downmbps")) + up, _ := strconv.Atoi(query.Get("upmbps")) + recv_window_conn, _ := strconv.Atoi(query.Get("recv_window_conn")) + recv_window, _ := strconv.Atoi(query.Get("recv_window")) + if down > 0 { + hy["down_mbps"] = down + } + if up > 0 { + hy["up_mbps"] = up + } + if recv_window_conn > 0 { + hy["recv_window_conn"] = recv_window_conn + } + if recv_window > 0 { + hy["recv_window"] = recv_window + } + return &hy, tag, nil +} + +func hy2(u *url.URL, i int) (*map[string]interface{}, string, error) { + query, _ := url.ParseQuery(u.RawQuery) + host, portStr, _ := net.SplitHostPort(u.Host) + port := 443 + if len(portStr) > 0 { + port, _ = strconv.Atoi(portStr) + } + + security := query.Get("security") + if len(security) == 0 { + security = "tls" + } + + tag := u.Fragment + if i > 0 { + tag = fmt.Sprintf("%d.%s", i, u.Fragment) + } + hy2 := map[string]interface{}{ + "type": "hysteria2", + "tag": tag, + "server": host, + "server_port": port, + "password": u.User.Username(), + "tls": getTls(security, &query), + } + down, _ := strconv.Atoi(query.Get("downmbps")) + up, _ := strconv.Atoi(query.Get("upmbps")) + obfs := query.Get("obfs") + mport := strings.ReplaceAll(query.Get("mport"), "-", ":") + fastopen := query.Get("fastopen") + if down > 0 { + hy2["down_mbps"] = down + } + if up > 0 { + hy2["up_mbps"] = up + } + if obfs == "salamander" { + hy2["obfs"] = map[string]interface{}{ + "type": "salamander", + "password": query.Get("obfs-password"), + } + } + if len(mport) > 0 { + hy2["server_ports"] = strings.Split(mport, ",") + } + if fastopen == "1" || fastopen == "true" { + hy2["fastopen"] = true + } + return &hy2, tag, nil +} + +func anytls(u *url.URL, i int) (*map[string]interface{}, string, error) { + query, _ := url.ParseQuery(u.RawQuery) + host, portStr, _ := net.SplitHostPort(u.Host) + port := 443 + if len(portStr) > 0 { + port, _ = strconv.Atoi(portStr) + } + + security := query.Get("security") + if len(security) == 0 { + security = "tls" + } + + tag := u.Fragment + if i > 0 { + tag = fmt.Sprintf("%d.%s", i, u.Fragment) + } + anytls := map[string]interface{}{ + "type": "anytls", + "tag": tag, + "server": host, + "server_port": port, + "password": u.User.Username(), + "tls": getTls(security, &query), + } + return &anytls, tag, nil +} + +func tuic(u *url.URL, i int) (*map[string]interface{}, string, error) { + query, _ := url.ParseQuery(u.RawQuery) + host, portStr, _ := net.SplitHostPort(u.Host) + port := 443 + if len(portStr) > 0 { + port, _ = strconv.Atoi(portStr) + } + + security := query.Get("security") + if len(security) == 0 { + security = "tls" + } + + tag := u.Fragment + if i > 0 { + tag = fmt.Sprintf("%d.%s", i, u.Fragment) + } + password, _ := u.User.Password() + tuic := map[string]interface{}{ + "type": "tuic", + "tag": tag, + "server": host, + "server_port": port, + "uuid": u.User.Username(), + "password": password, + "congestion_control": query.Get("congestion_control"), + "udp_relay_mode": query.Get("udp_relay_mode"), + "tls": getTls(security, &query), + } + return &tuic, tag, nil +} + +func ss(u *url.URL, i int) (*map[string]interface{}, string, error) { + query, _ := url.ParseQuery(u.RawQuery) + host, portStr, _ := net.SplitHostPort(u.Host) + port := 443 + if len(portStr) > 0 { + port, _ = strconv.Atoi(portStr) + } + method := u.User.Username() + password, ok := u.User.Password() + if !ok { + decrypted := StrOrBase64Encoded(method) + decrypted_arr := strings.Split(decrypted, ":") + if len(decrypted_arr) > 1 { + method = decrypted_arr[0] + password = strings.Join(decrypted_arr[1:], ":") + } else { + return nil, "", common.NewError("Unsupported shadowsocks") + } + } + + tag := u.Fragment + if i > 0 { + tag = fmt.Sprintf("%d.%s", i, u.Fragment) + } + ss := map[string]interface{}{ + "type": "shadowsocks", + "tag": tag, + "server": host, + "server_port": port, + "method": method, + "password": password, + } + + v2ray_type := query.Get("type") + if len(v2ray_type) > 0 { + pl_arr := []string{} + host_header := query.Get("host") + if query.Get("security") == "tls" { + pl_arr = append(pl_arr, "tls") + } + if v2ray_type == "quic" { + pl_arr = append(pl_arr, "mode=quic") + } + if len(host_header) > 0 { + pl_arr = append(pl_arr, "host="+host_header) + } + ss["plugin"] = "v2ray-plugin" + ss["plugin_opts"] = strings.Join(pl_arr, ";") + } + plugin := query.Get("plugin") + if len(plugin) > 0 { + pl_arr := strings.Split(plugin, ";") + if len(pl_arr) > 0 { + ss["plugin"] = pl_arr[0] + ss["plugin_opts"] = strings.Join(pl_arr[1:], ";") + } + } + return &ss, tag, nil +} + +func parseNaiveLink(u *url.URL, i int) (*map[string]interface{}, string, error) { + var host, portStr, username, password string + var port int + + switch u.Scheme { + case "http2": + decoded := StrOrBase64Encoded(u.Hostname()) + if idx := strings.Index(decoded, "@"); idx != -1 { + userInfo := decoded[:idx] + hostPort := decoded[idx+1:] + if idx2 := strings.Index(userInfo, ":"); idx2 != -1 { + username = userInfo[:idx2] + password = userInfo[idx2+1:] + } else { + username = userInfo + } + host, portStr, _ = net.SplitHostPort(hostPort) + if portStr != "" { + port, _ = strconv.Atoi(portStr) + } else { + port = 443 + } + } else { + return nil, "", common.NewError("Invalid naive link (http2)") + } + case "naive+https", "naive+quic": + host, portStr, _ = net.SplitHostPort(u.Host) + if portStr != "" { + port, _ = strconv.Atoi(portStr) + } else { + port = 443 + } + if u.User != nil { + username = u.User.Username() + password, _ = u.User.Password() + } + default: + return nil, "", common.NewError("Unsupported naive scheme") + } + + tag := u.Fragment + if i > 0 { + tag = fmt.Sprintf("%d.%s", i, u.Fragment) + } + if tag == "" { + tag = fmt.Sprintf("naive-%d", i) + } + + naive := map[string]interface{}{ + "type": "naive", + "tag": tag, + "server": host, + "server_port": port, + "username": username, + "password": password, + "tls": map[string]interface{}{"enabled": true}, + } + + query := u.Query() + if peer := query.Get("peer"); peer != "" { + if tls, ok := naive["tls"].(map[string]interface{}); ok { + tls["server_name"] = peer + } + } + if insecure := query.Get("insecure"); insecure == "1" || insecure == "true" { + if tls, ok := naive["tls"].(map[string]interface{}); ok { + tls["insecure"] = true + } + } + if alpn := query.Get("alpn"); alpn != "" { + if tls, ok := naive["tls"].(map[string]interface{}); ok { + tls["alpn"] = strings.Split(alpn, ",") + } + } + if u.Scheme == "naive+quic" { + naive["quic"] = true + } + + return &naive, tag, nil +} + +func getTransport(tp_type string, q *url.Values) map[string]interface{} { + transport := map[string]interface{}{} + tp_host := q.Get("host") + tp_path := q.Get("path") + switch strings.ToLower(tp_type) { + case "tcp", "": + if q.Get("headerType") == "http" { + transport["type"] = "http" + if len(tp_host) > 0 { + transport["host"] = strings.Split(tp_host, ",") + } + transport["path"] = tp_path + } + case "http", "h2": + transport["type"] = "http" + if len(tp_host) > 0 { + transport["host"] = strings.Split(tp_host, ",") + } + transport["path"] = tp_path + case "ws": + transport["type"] = "ws" + transport["path"] = tp_path + if len(tp_host) > 0 { + transport["headers"] = map[string]interface{}{ + "Host": tp_host, + } + } + case "quic": + transport["type"] = "quic" + case "grpc": + transport["type"] = "grpc" + transport["service_name"] = q.Get("serviceName") + case "httpupgrade": + transport["type"] = "httpupgrade" + transport["path"] = tp_path + transport["host"] = tp_host + } + return transport +} + +func getTls(security string, q *url.Values) map[string]interface{} { + tls := map[string]interface{}{} + tls_fp := q.Get("fp") + tls_sni := q.Get("sni") + tls_allow_insecure := q.Get("allowInsecure") + tls_insecure := q.Get("insecure") + tls_alpn := q.Get("alpn") + tls_ech := q.Get("ech") + disable_sni := q.Get("disable_sni") + switch security { + case "tls": + tls["enabled"] = true + case "reality": + tls["enabled"] = true + tls["reality"] = map[string]interface{}{ + "enabled": true, + "public_key": q.Get("pbk"), + "short_id": q.Get("sid"), + } + } + if len(tls_sni) > 0 { + tls["server_name"] = tls_sni + } + if len(tls_alpn) > 0 { + tls["alpn"] = strings.Split(tls_alpn, ",") + } + if tls_insecure == "1" || tls_insecure == "true" || tls_allow_insecure == "1" || tls_allow_insecure == "true" { + tls["insecure"] = true + } + if len(tls_fp) > 0 { + tls["utls"] = map[string]interface{}{ + "enabled": true, + "fingerprint": tls_fp, + } + } + if len(tls_ech) > 0 { + tls["ech"] = map[string]interface{}{ + "enabled": true, + "config": []string{ + tls_ech, + }, + } + } + if disable_sni == "1" || disable_sni == "true" { + tls["disable_sni"] = true + } + return tls +} diff --git a/util/outJson.go b/util/outJson.go new file mode 100644 index 0000000..9658f93 --- /dev/null +++ b/util/outJson.go @@ -0,0 +1,237 @@ +package util + +import ( + "encoding/json" + + "github.com/alireza0/s-ui/util/common" + + "github.com/alireza0/s-ui/database/model" +) + +// Fill Inbound's out_json +func FillOutJson(i *model.Inbound, hostname string) error { + switch i.Type { + case "direct", "tun", "redirect", "tproxy": + return nil + } + var outJson map[string]interface{} + err := json.Unmarshal(i.OutJson, &outJson) + if err != nil { + return err + } + + if outJson == nil { + outJson = make(map[string]interface{}) + } + + if i.TlsId > 0 { + addTls(&outJson, i.Tls) + } else { + delete(outJson, "tls") + } + + inbound, err := i.MarshalFull() + if err != nil { + return err + } + + outJson["type"] = i.Type + outJson["tag"] = i.Tag + outJson["server"] = hostname + outJson["server_port"] = (*inbound)["listen_port"] + + switch i.Type { + case "http", "socks", "mixed", "anytls": + case "naive": + naiveOut(&outJson, *inbound) + case "shadowsocks": + shadowsocksOut(&outJson, *inbound) + case "shadowtls": + shadowTlsOut(&outJson, *inbound) + case "hysteria": + hysteriaOut(&outJson, *inbound) + case "hysteria2": + hysteria2Out(&outJson, *inbound) + case "tuic": + tuicOut(&outJson, *inbound) + case "vless": + vlessOut(&outJson, *inbound) + case "trojan": + trojanOut(&outJson, *inbound) + case "vmess": + vmessOut(&outJson, *inbound) + default: + for key := range outJson { + delete(outJson, key) + } + } + + i.OutJson, err = json.MarshalIndent(outJson, "", " ") + if err != nil { + return err + } + + return nil +} + +// addTls function +func addTls(out *map[string]interface{}, tls *model.Tls) { + var tlsServer, tlsConfig map[string]interface{} + err := json.Unmarshal(tls.Server, &tlsServer) + if err != nil { + return + } + err = json.Unmarshal(tls.Client, &tlsConfig) + if err != nil { + return + } + + if enabled, ok := tlsServer["enabled"]; ok { + tlsConfig["enabled"] = enabled + } + if serverName, ok := tlsServer["server_name"]; ok { + tlsConfig["server_name"] = serverName + } + if alpn, ok := tlsServer["alpn"]; ok { + tlsConfig["alpn"] = alpn + } + if minVersion, ok := tlsServer["min_version"]; ok { + tlsConfig["min_version"] = minVersion + } + if maxVersion, ok := tlsServer["max_version"]; ok { + tlsConfig["max_version"] = maxVersion + } + if certificate, ok := tlsServer["certificate"]; ok { + tlsConfig["certificate"] = certificate + } + if cipherSuites, ok := tlsServer["cipher_suites"]; ok { + tlsConfig["cipher_suites"] = cipherSuites + } + if reality, ok := tlsServer["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) { + realityConfig := tlsConfig["reality"].(map[string]interface{}) + realityConfig["enabled"] = true + if shortIDs, ok := reality["short_id"].([]interface{}); ok && len(shortIDs) > 0 { + realityConfig["short_id"] = shortIDs[common.RandomInt(len(shortIDs))] + } + tlsConfig["reality"] = realityConfig + } + if ech, ok := tlsServer["ech"].(map[string]interface{}); ok && ech["enabled"].(bool) { + echConfig := tlsConfig["ech"].(map[string]interface{}) + echConfig["enabled"] = true + echConfig["pq_signature_schemes_enabled"] = ech["pq_signature_schemes_enabled"] + echConfig["dynamic_record_sizing_disabled"] = ech["dynamic_record_sizing_disabled"] + tlsConfig["ech"] = echConfig + } + + (*out)["tls"] = tlsConfig +} + +func naiveOut(out *map[string]interface{}, inbound map[string]interface{}) { + if quic_congestion_control, ok := inbound["quic_congestion_control"].(string); ok { + (*out)["quic"] = true + switch quic_congestion_control { + case "bbr_standard": + (*out)["quic_congestion_control"] = "bbr" + case "bbr2_variant": + (*out)["quic_congestion_control"] = "bbr2" + default: + (*out)["quic_congestion_control"] = quic_congestion_control + } + } + +} + +func shadowsocksOut(out *map[string]interface{}, inbound map[string]interface{}) { + if method, ok := inbound["method"].(string); ok { + (*out)["method"] = method + } +} + +func shadowTlsOut(out *map[string]interface{}, inbound map[string]interface{}) { + if version, ok := inbound["version"].(float64); ok && int(version) == 3 { + (*out)["version"] = 3 + } else { + for key := range *out { + delete(*out, key) + } + } + (*out)["tls"] = map[string]interface{}{"enabled": true} +} + +func hysteriaOut(out *map[string]interface{}, inbound map[string]interface{}) { + delete(*out, "down_mbps") + delete(*out, "up_mbps") + delete(*out, "obfs") + delete(*out, "recv_window_conn") + delete(*out, "disable_mtu_discovery") + + if upMbps, ok := inbound["down_mbps"]; ok { + (*out)["up_mbps"] = upMbps + } + if downMbps, ok := inbound["up_mbps"]; ok { + (*out)["down_mbps"] = downMbps + } + if obfs, ok := inbound["obfs"]; ok { + (*out)["obfs"] = obfs + } + if recvWindow, ok := inbound["recv_window_conn"]; ok { + (*out)["recv_window_conn"] = recvWindow + } + if disableMTU, ok := inbound["disable_mtu_discovery"]; ok { + (*out)["disable_mtu_discovery"] = disableMTU + } +} + +func hysteria2Out(out *map[string]interface{}, inbound map[string]interface{}) { + delete(*out, "down_mbps") + delete(*out, "up_mbps") + delete(*out, "obfs") + + if upMbps, ok := inbound["down_mbps"]; ok { + (*out)["up_mbps"] = upMbps + } + if downMbps, ok := inbound["up_mbps"]; ok { + (*out)["down_mbps"] = downMbps + } + if obfs, ok := inbound["obfs"]; ok { + (*out)["obfs"] = obfs + } +} + +func tuicOut(out *map[string]interface{}, inbound map[string]interface{}) { + delete(*out, "zero_rtt_handshake") + delete(*out, "heartbeat") + if congestionControl, ok := inbound["congestion_control"].(string); ok { + (*out)["congestion_control"] = congestionControl + } else { + (*out)["congestion_control"] = "cubic" + } + if zeroRTT, ok := inbound["zero_rtt_handshake"].(bool); ok { + (*out)["zero_rtt_handshake"] = zeroRTT + } + if heartbeat, ok := inbound["heartbeat"]; ok { + (*out)["heartbeat"] = heartbeat + } +} + +func vlessOut(out *map[string]interface{}, inbound map[string]interface{}) { + delete(*out, "transport") + if transport, ok := inbound["transport"]; ok { + (*out)["transport"] = transport + } +} + +func trojanOut(out *map[string]interface{}, inbound map[string]interface{}) { + delete(*out, "transport") + if transport, ok := inbound["transport"]; ok { + (*out)["transport"] = transport + } +} + +func vmessOut(out *map[string]interface{}, inbound map[string]interface{}) { + (*out)["alter_id"] = 0 + delete(*out, "transport") + if transport, ok := inbound["transport"]; ok { + (*out)["transport"] = transport + } +} diff --git a/util/subInfo.go b/util/subInfo.go new file mode 100644 index 0000000..25bf3ca --- /dev/null +++ b/util/subInfo.go @@ -0,0 +1,15 @@ +package util + +import ( + "fmt" + + "github.com/alireza0/s-ui/database/model" +) + +func GetHeaders(client *model.Client, updateInterval int) []string { + var headers []string + headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", client.Up, client.Down, client.Volume, client.Expiry)) + headers = append(headers, fmt.Sprintf("%d", updateInterval)) + headers = append(headers, client.Name) + return headers +} diff --git a/util/subToJson.go b/util/subToJson.go new file mode 100644 index 0000000..d6df648 --- /dev/null +++ b/util/subToJson.go @@ -0,0 +1,97 @@ +package util + +import ( + "crypto/tls" + "encoding/json" + "io" + "net/http" + "strings" + + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/util/common" +) + +func GetExternalLink(url string) string { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + + response, err := client.Get(url) + if err != nil { + logger.Warning("sub: Error making HTTP request:", err) + return "" + } + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + logger.Warning("sub: Error reading response body:", err) + return "" + } + + data := StrOrBase64Encoded(string(body)) + return data +} + +func GetExternalSub(url string) ([]map[string]interface{}, error) { + var err error + var result []map[string]interface{} + + if len(url) == 0 { + return nil, common.NewError("no url") + } + + data := GetExternalLink(url) + if len(data) == 0 { + return nil, common.NewError("no result") + } + + // if the data is a JSON object + if strings.HasPrefix(data, "{") && strings.HasSuffix(data, "}") { + var jsonData map[string]interface{} + err = json.Unmarshal([]byte(data), &jsonData) + if err != nil { + logger.Warning("sub: Error unmarshalling JSON:", err) + return nil, err + } + outbounds, ok := jsonData["outbounds"].([]any) + if !ok { + logger.Warning("sub: Error getting outbounds:", err) + return nil, err + } + for _, outbound := range outbounds { + outboundMap, ok := outbound.(map[string]interface{}) + if ok && len(outboundMap) > 0 { + oType, _ := outboundMap["type"].(string) + switch oType { + case "urltest": + case "direct": + case "selector": + case "block": + continue + default: + result = append(result, outboundMap) + } + } + } + if len(result) == 0 { + return nil, common.NewError("no result") + } + return result, nil + } else { + // if data is a text + links := strings.Split(data, "\n") + for _, link := range links { + linkToJson, _, err := GetOutbound(link, 0) + if err == nil { + result = append(result, *linkToJson) + } + } + } + if len(result) == 0 { + return nil, common.NewError("no result") + } + return result, nil +} diff --git a/web/web.go b/web/web.go new file mode 100644 index 0000000..24959d9 --- /dev/null +++ b/web/web.go @@ -0,0 +1,229 @@ +package web + +import ( + "context" + "crypto/tls" + "embed" + "html/template" + "io" + "io/fs" + "net" + "net/http" + "strconv" + "strings" + "time" + + "github.com/alireza0/s-ui/api" + "github.com/alireza0/s-ui/config" + "github.com/alireza0/s-ui/logger" + "github.com/alireza0/s-ui/middleware" + "github.com/alireza0/s-ui/network" + "github.com/alireza0/s-ui/service" + + "github.com/gin-contrib/gzip" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" +) + +//go:embed * +var content embed.FS + +type Server struct { + httpServer *http.Server + listener net.Listener + ctx context.Context + cancel context.CancelFunc + settingService service.SettingService +} + +func NewServer() *Server { + ctx, cancel := context.WithCancel(context.Background()) + return &Server{ + ctx: ctx, + cancel: cancel, + } +} + +func (s *Server) initRouter() (*gin.Engine, error) { + if config.IsDebug() { + gin.SetMode(gin.DebugMode) + } else { + gin.DefaultWriter = io.Discard + gin.DefaultErrorWriter = io.Discard + gin.SetMode(gin.ReleaseMode) + } + + engine := gin.Default() + + // Load the HTML template + t := template.New("").Funcs(engine.FuncMap) + template, err := t.ParseFS(content, "html/index.html") + if err != nil { + return nil, err + } + engine.SetHTMLTemplate(template) + + base_url, err := s.settingService.GetWebPath() + if err != nil { + return nil, err + } + + webDomain, err := s.settingService.GetWebDomain() + if err != nil { + return nil, err + } + + if webDomain != "" { + engine.Use(middleware.DomainValidator(webDomain)) + } + + secret, err := s.settingService.GetSecret() + if err != nil { + return nil, err + } + + engine.Use(gzip.Gzip(gzip.DefaultCompression)) + assetsBasePath := base_url + "assets/" + + store := cookie.NewStore(secret) + engine.Use(sessions.Sessions("s-ui", store)) + + engine.Use(func(c *gin.Context) { + uri := c.Request.RequestURI + if strings.HasPrefix(uri, assetsBasePath) { + c.Header("Cache-Control", "max-age=31536000") + } + }) + + // Serve the assets folder + assetsFS, err := fs.Sub(content, "html/assets") + if err != nil { + panic(err) + } + + engine.StaticFS(assetsBasePath, http.FS(assetsFS)) + + group_apiv2 := engine.Group(base_url + "apiv2") + apiv2 := api.NewAPIv2Handler(group_apiv2) + + group_api := engine.Group(base_url + "api") + api.NewAPIHandler(group_api, apiv2) + + // Serve index.html as the entry point + // Handle all other routes by serving index.html + engine.NoRoute(func(c *gin.Context) { + if c.Request.URL.Path == strings.TrimSuffix(base_url, "/") { + c.Redirect(http.StatusTemporaryRedirect, base_url) + return + } + if !strings.HasPrefix(c.Request.URL.Path, base_url) { + c.String(404, "") + return + } + if c.Request.URL.Path != base_url+"login" && !api.IsLogin(c) { + c.Redirect(http.StatusTemporaryRedirect, base_url+"login") + return + } + if c.Request.URL.Path == base_url+"login" && api.IsLogin(c) { + c.Redirect(http.StatusTemporaryRedirect, base_url) + return + } + c.HTML(http.StatusOK, "index.html", gin.H{"BASE_URL": base_url}) + }) + + return engine, nil +} + +func (s *Server) Start() (err error) { + //This is an anonymous function, no function name + defer func() { + if err != nil { + s.Stop() + } + }() + + engine, err := s.initRouter() + if err != nil { + return err + } + + certFile, err := s.settingService.GetCertFile() + if err != nil { + return err + } + keyFile, err := s.settingService.GetKeyFile() + if err != nil { + return err + } + listen, err := s.settingService.GetListen() + if err != nil { + return err + } + port, err := s.settingService.GetPort() + if err != nil { + return err + } + listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) + listener, err := net.Listen("tcp", listenAddr) + if err != nil { + return err + } + if certFile != "" || keyFile != "" { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + listener.Close() + return err + } + c := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + listener = network.NewAutoHttpsListener(listener) + listener = tls.NewListener(listener, c) + } + + if certFile != "" || keyFile != "" { + logger.Info("web server run https on", listener.Addr()) + } else { + logger.Info("web server run http on", listener.Addr()) + } + s.listener = listener + + s.httpServer = &http.Server{ + Handler: engine, + } + + go func() { + s.httpServer.Serve(listener) + }() + + return nil +} + +func (s *Server) Stop() error { + var err error + if s.httpServer != nil { + shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 30*time.Second) + err = s.httpServer.Shutdown(shutdownCtx) + cancelShutdown() + if err != nil { + s.cancel() + if s.listener != nil { + _ = s.listener.Close() + } + return err + } + } else if s.listener != nil { + err = s.listener.Close() + if err != nil { + s.cancel() + return err + } + } + s.cancel() + return nil +} + +func (s *Server) GetCtx() context.Context { + return s.ctx +} diff --git a/windows/README.md b/windows/README.md new file mode 100644 index 0000000..ab9ffba --- /dev/null +++ b/windows/README.md @@ -0,0 +1,23 @@ +# Windows Files + +This directory contains all Windows-specific files for S-UI. + +## Available Files: + +- **s-ui-windows.xml**: Windows Service configuration +- **install-windows.bat**: Installation script +- **s-ui-windows.bat**: Control panel +- **uninstall-windows.bat**: Uninstallation script +- **build-windows.bat**: Simple build script for CMD +- **build-windows.ps1**: Advanced build script for PowerShell + +## Usage: + +To install S-UI on Windows: +1. Run `install-windows.bat` as Administrator +2. Follow the installation wizard +3. Use `s-ui-windows.bat` for management + +To build from source: +- With CMD: `build-windows.bat` +- With PowerShell: `.\build-windows.ps1` diff --git a/windows/build-windows.bat b/windows/build-windows.bat new file mode 100644 index 0000000..9b3445d --- /dev/null +++ b/windows/build-windows.bat @@ -0,0 +1,73 @@ +@echo off +setlocal enabledelayedexpansion + +echo Building S-UI for Windows... + +cd /d "%~dp0" + +REM Check if Go is installed +go version >nul 2>&1 +if errorlevel 1 ( + echo Error: Go is not installed or not in PATH + echo Please install Go from https://golang.org/dl/ + pause + exit /b 1 +) + +REM Check if Node.js is installed +node --version >nul 2>&1 +if errorlevel 1 ( + echo Error: Node.js is not installed or not in PATH + echo Please install Node.js from https://nodejs.org/ + pause + exit /b 1 +) + +echo Building frontend... +cd frontend +call npm install +if errorlevel 1 ( + echo Error: Failed to install frontend dependencies + pause + exit /b 1 +) + +call npm run build +if errorlevel 1 ( + echo Error: Failed to build frontend + pause + exit /b 1 +) + +cd .. + +echo Creating web/html directory... +if not exist "web\html" mkdir "web\html" + +echo Copying frontend build files... +xcopy "frontend\dist\*" "web\html\" /E /Y /Q + +echo Building backend... +set CGO_ENABLED=1 +set GOOS=windows +set GOARCH=amd64 + +REM Try to build with CGO first +go build -ldflags "-w -s" -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_tailscale" -o sui.exe main.go +if errorlevel 1 ( + echo Warning: CGO build failed, trying without CGO... + set CGO_ENABLED=0 + go build -ldflags "-w -s" -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_tailscale" -o sui.exe main.go + if errorlevel 1 ( + echo Error: Failed to build backend + pause + exit /b 1 + ) + echo Built without CGO (some features may be limited) +) else ( + echo Built with CGO +) + +echo Build completed successfully! +echo Output: sui.exe +pause diff --git a/windows/build-windows.ps1 b/windows/build-windows.ps1 new file mode 100644 index 0000000..f50fb55 --- /dev/null +++ b/windows/build-windows.ps1 @@ -0,0 +1,138 @@ +# PowerShell script for building S-UI on Windows +param( + [string]$Architecture = "amd64", + [switch]$NoCGO, + [switch]$Help +) + +if ($Help) { + Write-Host "Usage: .\build-windows.ps1 [-Architecture ] [-NoCGO] [-Help]" + Write-Host "Architectures: amd64, 386, arm64" + Write-Host "Examples:" + Write-Host " .\build-windows.ps1 # Build for amd64 with CGO" + Write-Host " .\build-windows.ps1 -Architecture 386 # Build for 32-bit Windows" + Write-Host " .\build-windows.ps1 -NoCGO # Build without CGO" + exit 0 +} + +Write-Host "Building S-UI for Windows ($Architecture)..." -ForegroundColor Green + +# Check if Go is installed +try { + $goVersion = go version 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Go not found" + } + Write-Host "Go version: $goVersion" -ForegroundColor Green +} catch { + Write-Host "Error: Go is not installed or not in PATH" -ForegroundColor Red + Write-Host "Please install Go from https://golang.org/dl/" -ForegroundColor Yellow + Read-Host "Press Enter to exit" + exit 1 +} + +# Check if Node.js is installed +try { + $nodeVersion = node --version 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Node.js not found" + } + Write-Host "Node.js version: $nodeVersion" -ForegroundColor Green +} catch { + Write-Host "Error: Node.js is not installed or not in PATH" -ForegroundColor Red + Write-Host "Please install Node.js from https://nodejs.org/" -ForegroundColor Yellow + Read-Host "Press Enter to exit" + exit 1 +} + +# Build frontend +Write-Host "Building frontend..." -ForegroundColor Yellow +Push-Location frontend + +try { + Write-Host "Installing dependencies..." -ForegroundColor Cyan + npm install + if ($LASTEXITCODE -ne 0) { + throw "Failed to install frontend dependencies" + } + + Write-Host "Building frontend..." -ForegroundColor Cyan + npm run build + if ($LASTEXITCODE -ne 0) { + throw "Failed to build frontend" + } +} catch { + Write-Host "Error: $_" -ForegroundColor Red + Pop-Location + Read-Host "Press Enter to exit" + exit 1 +} + +Pop-Location + +# Create web/html directory +Write-Host "Creating web/html directory..." -ForegroundColor Yellow +if (!(Test-Path "web\html")) { + New-Item -ItemType Directory -Path "web\html" -Force | Out-Null +} + +# Copy frontend build files +Write-Host "Copying frontend build files..." -ForegroundColor Yellow +Copy-Item "frontend\dist\*" "web\html\" -Recurse -Force + +# Build backend +Write-Host "Building backend..." -ForegroundColor Yellow + +# Set environment variables +$env:GOOS = "windows" +$env:GOARCH = $Architecture + +if ($NoCGO) { + $env:CGO_ENABLED = "0" + Write-Host "Building without CGO..." -ForegroundColor Yellow +} else { + $env:CGO_ENABLED = "1" + Write-Host "Building with CGO..." -ForegroundColor Yellow +} + +# Build command +$buildCmd = "go build -ldflags `"-w -s`" -tags `"with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_tailscale`" -o sui.exe main.go" + +try { + Invoke-Expression $buildCmd + if ($LASTEXITCODE -ne 0) { + if (!$NoCGO) { + Write-Host "CGO build failed, trying without CGO..." -ForegroundColor Yellow + $env:CGO_ENABLED = "0" + Invoke-Expression $buildCmd + if ($LASTEXITCODE -ne 0) { + throw "Failed to build backend even without CGO" + } + Write-Host "Built without CGO (some features may be limited)" -ForegroundColor Yellow + } else { + throw "Failed to build backend" + } + } else { + if ($env:CGO_ENABLED -eq "1") { + Write-Host "Built successfully with CGO" -ForegroundColor Green + } else { + Write-Host "Built successfully without CGO" -ForegroundColor Green + } + } +} catch { + Write-Host "Error: $_" -ForegroundColor Red + Read-Host "Press Enter to exit" + exit 1 +} + +Write-Host "Build completed successfully!" -ForegroundColor Green +Write-Host "Output: sui.exe" -ForegroundColor Green + +# Show file info +if (Test-Path "sui.exe") { + $fileInfo = Get-Item "sui.exe" + Write-Host "File size: $([math]::Round($fileInfo.Length / 1MB, 2)) MB" -ForegroundColor Cyan + Write-Host "Created: $($fileInfo.CreationTime)" -ForegroundColor Cyan +} + +Read-Host "Press Enter to exit" diff --git a/windows/install-windows.bat b/windows/install-windows.bat new file mode 100644 index 0000000..32e7b98 --- /dev/null +++ b/windows/install-windows.bat @@ -0,0 +1,195 @@ +@echo off +setlocal enabledelayedexpansion + +echo ======================================== +echo S-UI Windows Installer +echo ======================================== + +REM Check if running as Administrator +net session >nul 2>&1 +if %errorLevel% neq 0 ( + echo Error: This script must be run as Administrator + echo Right-click on this file and select "Run as administrator" + pause + exit /b 1 +) + +cd /d "%~dp0" +REM Set installation directory +set "INSTALL_DIR=C:\Program Files\s-ui" +set "SERVICE_NAME=s-ui" + +echo Installing S-UI to: %INSTALL_DIR% + +REM Create installation directory +if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%" +if not exist "%INSTALL_DIR%\db" mkdir "%INSTALL_DIR%\db" +if not exist "%INSTALL_DIR%\logs" mkdir "%INSTALL_DIR%\logs" +if not exist "%INSTALL_DIR%\cert" mkdir "%INSTALL_DIR%\cert" + +REM Copy files +echo Copying files... +copy "sui.exe" "%INSTALL_DIR%\" >nul +copy "s-ui-windows.xml" "%INSTALL_DIR%\" >nul +copy "s-ui-windows.bat" "%INSTALL_DIR%\" >nul + +REM Check if WinSW is available +set "WINSW_PATH=%INSTALL_DIR%\winsw.exe" +if not exist "%WINSW_PATH%" ( + echo Downloading WinSW... + powershell -Command "& {Invoke-WebRequest -Uri 'https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW-x64.exe' -OutFile '%WINSW_PATH%'}" + if exist "%WINSW_PATH%" ( + echo WinSW downloaded successfully + ) else ( + echo Warning: Failed to download WinSW. Service installation will be skipped. + echo You can manually download WinSW from: https://github.com/winsw/winsw/releases + ) +) + +REM Install Windows Service +if exist "%WINSW_PATH%" ( + echo Installing Windows Service... + cd /d "%INSTALL_DIR%" + copy "winsw.exe" "s-ui-service.exe" >nul + copy "s-ui-windows.xml" "s-ui-service.xml" >nul + + REM Install service + s-ui-service.exe install + if %errorLevel% equ 0 ( + echo Service installed successfully + ) else ( + echo Warning: Failed to install service. You can install it manually later. + ) +) + +REM Run migration +echo Running database migration... +cd /d "%INSTALL_DIR%" +sui.exe migrate +if %errorLevel% equ 0 ( + echo Migration completed successfully +) else ( + echo Warning: Migration failed or database is new +) + +REM Get network configuration +echo. +echo ======================================== +echo Network Configuration +echo ======================================== + +REM Get local IP addresses +echo Available IP addresses: +for /f "tokens=2 delims=:" %%i in ('ipconfig ^| findstr /i "IPv4"') do ( + echo %%i +) + +REM Get panel configuration +echo. +set /p panel_port="Enter panel port (default: 2095): " +if "%panel_port%"=="" set "panel_port=2095" + +set /p panel_path="Enter panel path (default: /app/): " +if "%panel_path%"=="" set "panel_path=/app/" + +set /p sub_port="Enter subscription port (default: 2096): " +if "%sub_port%"=="" set "sub_port=2096" + +set /p sub_path="Enter subscription path (default: /sub/): " +if "%sub_path%"=="" set "sub_path=/sub/" + +REM Apply settings +echo. +echo Applying settings... +cd /d "%INSTALL_DIR%" +sui.exe setting -port %panel_port% -path "%panel_path%" -subPort %sub_port% -subPath "%sub_path%" + +REM Get admin credentials +echo. +echo ======================================== +echo Admin Configuration +echo ======================================== + +set /p admin_username="Enter admin username (default: admin): " +if "%admin_username%"=="" set "admin_username=admin" + +set /p admin_password="Enter admin password: " +if "%admin_password%"=="" ( + echo Error: Password cannot be empty + pause + exit /b 1 +) + +REM Set admin credentials +echo Setting admin credentials... +sui.exe admin -username "%admin_username%" -password "%admin_password%" + +REM Start service +echo Starting S-UI service... +net start %SERVICE_NAME% +if %errorLevel% equ 0 ( + echo Service started successfully +) else ( + echo Warning: Failed to start service. You can start it manually later. +) + +REM Create desktop shortcut +echo Creating desktop shortcut... +set "DESKTOP=%USERPROFILE%\Desktop" +if exist "%DESKTOP%" ( + powershell -Command "& {$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%DESKTOP%\S-UI.lnk'); $Shortcut.TargetPath = '%INSTALL_DIR%\s-ui-windows.bat'; $Shortcut.WorkingDirectory = '%INSTALL_DIR%'; $Shortcut.Description = 'S-UI Control Panel'; $Shortcut.Save()}" + echo Desktop shortcut created +) + +REM Create Start Menu shortcut +echo Creating Start Menu shortcut... +set "START_MENU=%APPDATA%\Microsoft\Windows\Start Menu\Programs" +if exist "%START_MENU%" ( + if not exist "%START_MENU%\S-UI" mkdir "%START_MENU%\S-UI" + powershell -Command "& {$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%START_MENU%\S-UI\S-UI Control Panel.lnk'); $Shortcut.TargetPath = '%INSTALL_DIR%\s-ui-windows.bat'; $Shortcut.WorkingDirectory = '%INSTALL_DIR%'; $Shortcut.Description = 'S-UI Control Panel'; $Shortcut.Save()}" + echo Start Menu shortcut created +) + +REM Set permissions +echo Setting permissions... +icacls "%INSTALL_DIR%" /grant "Users:(OI)(CI)RX" /T >nul +icacls "%INSTALL_DIR%\db" /grant "Users:(OI)(CI)F" /T >nul +icacls "%INSTALL_DIR%\logs" /grant "Users:(OI)(CI)F" /T >nul + +REM Create environment variable +echo Setting environment variable... +setx SUI_HOME "%INSTALL_DIR%" /M >nul + +REM Show final configuration +echo. +echo ======================================== +echo Installation completed successfully! +echo ======================================== +echo. +echo S-UI has been installed to: %INSTALL_DIR% +echo. +echo Configuration: +echo Panel Port: %panel_port% +echo Panel Path: %panel_path% +echo Subscription Port: %sub_port% +echo Subscription Path: %sub_path% +echo Admin Username: %admin_username% +echo. +echo Access URLs: +for /f "tokens=2 delims=:" %%i in ('ipconfig ^| findstr /i "IPv4"') do ( + set "ip=%%i" + set "ip=!ip: =!" + echo Panel: http://!ip!:%panel_port%%panel_path% + echo Subscription: http://!ip!:%sub_port%%sub_path% +) +echo. +echo Service name: %SERVICE_NAME% +echo. +echo Useful commands: +echo net start %SERVICE_NAME% - Start the service +echo net stop %SERVICE_NAME% - Stop the service +echo sc query %SERVICE_NAME% - Check service status +echo. +echo You can also use the desktop shortcut or Start Menu item. +echo. +pause diff --git a/windows/s-ui-windows.bat b/windows/s-ui-windows.bat new file mode 100644 index 0000000..bf1065e --- /dev/null +++ b/windows/s-ui-windows.bat @@ -0,0 +1,237 @@ +@echo off +setlocal enabledelayedexpansion + +REM S-UI Windows Control Script +REM This script provides a menu-driven interface for managing S-UI on Windows + +cd /d "%~dp0" +set "SERVICE_NAME=s-ui" +set "INSTALL_DIR=%SUI_HOME%" +if "%INSTALL_DIR%"=="" set "INSTALL_DIR=C:\Program Files\s-ui" + +:menu +cls +echo ======================================== +echo S-UI Windows Control Panel +echo ======================================== +echo. +echo Current directory: %INSTALL_DIR% +echo. +echo 1. Start S-UI Service +echo 2. Stop S-UI Service +echo 3. Restart S-UI Service +echo 4. Check Service Status +echo 5. View Service Logs +echo 6. Open Panel in Browser +echo 7. Run S-UI Manually +echo 8. Install/Uninstall Service +echo 9. Open Installation Directory +echo 10. Show Configuration +echo 11. Show Access URLs +echo 0. Exit +echo. +echo ======================================== + +set /p choice="Please select an option [0-11]: " + +if "%choice%"=="1" goto start_service +if "%choice%"=="2" goto stop_service +if "%choice%"=="3" goto restart_service +if "%choice%"=="4" goto check_status +if "%choice%"=="5" goto view_logs +if "%choice%"=="6" goto open_panel +if "%choice%"=="7" goto run_manual +if "%choice%"=="8" goto service_management +if "%choice%"=="9" goto open_directory +if "%choice%"=="10" goto show_config +if "%choice%"=="11" goto show_urls +if "%choice%"=="0" goto exit +goto invalid_choice + +:start_service +echo Starting S-UI service... +net start %SERVICE_NAME% +if %errorLevel% equ 0 ( + echo Service started successfully! +) else ( + echo Failed to start service. Error code: %errorLevel% +) +pause +goto menu + +:stop_service +echo Stopping S-UI service... +net stop %SERVICE_NAME% +if %errorLevel% equ 0 ( + echo Service stopped successfully! +) else ( + echo Failed to stop service. Error code: %errorLevel% +) +pause +goto menu + +:restart_service +echo Restarting S-UI service... +net stop %SERVICE_NAME% >nul 2>&1 +timeout /t 2 /nobreak >nul +net start %SERVICE_NAME% +if %errorLevel% equ 0 ( + echo Service restarted successfully! +) else ( + echo Failed to restart service. Error code: %errorLevel% +) +pause +goto menu + +:check_status +echo Checking S-UI service status... +sc query %SERVICE_NAME% +echo. +echo Service status details: +for /f "tokens=3 delims=: " %%i in ('sc query %SERVICE_NAME% ^| find "STATE"') do ( + echo Current state: %%i +) +pause +goto menu + +:view_logs +echo Opening S-UI logs... +if exist "%INSTALL_DIR%\logs" ( + start "" "%INSTALL_DIR%\logs" +) else ( + echo Logs directory not found: %INSTALL_DIR%\logs +) +pause +goto menu + +:open_panel +echo Opening S-UI panel in browser... +start http://localhost:2095 +echo Panel opened in default browser. +pause +goto menu + +:run_manual +echo Running S-UI manually... +if exist "%INSTALL_DIR%\sui.exe" ( + cd /d "%INSTALL_DIR%" + echo Starting S-UI in current window... + echo Press Ctrl+C to stop + echo. + sui.exe +) else ( + echo S-UI executable not found: %INSTALL_DIR%\sui.exe + echo Please run the installer first. +) +pause +goto menu + +:service_management +cls +echo ======================================== +echo Service Management +echo ======================================== +echo. +echo 1. Install Windows Service +echo 2. Uninstall Windows Service +echo 3. Back to Main Menu +echo. +set /p service_choice="Select option [1-3]: " + +if "%service_choice%"=="1" goto install_service +if "%service_choice%"=="2" goto uninstall_service +if "%service_choice%"=="3" goto menu +goto invalid_choice + +:install_service +echo Installing Windows Service... +if exist "%INSTALL_DIR%\s-ui-service.exe" ( + cd /d "%INSTALL_DIR%" + s-ui-service.exe install + if %errorLevel% equ 0 ( + echo Service installed successfully! + echo Starting service... + net start %SERVICE_NAME% + ) else ( + echo Failed to install service. Error code: %errorLevel% + ) +) else ( + echo Service wrapper not found. Please run the installer first. +) +pause +goto service_management + +:uninstall_service +echo Uninstalling Windows Service... +if exist "%INSTALL_DIR%\s-ui-service.exe" ( + cd /d "%INSTALL_DIR%" + net stop %SERVICE_NAME% >nul 2>&1 + s-ui-service.exe uninstall + if %errorLevel% equ 0 ( + echo Service uninstalled successfully! + ) else ( + echo Failed to uninstall service. Error code: %errorLevel% + ) +) else ( + echo Service wrapper not found. +) +pause +goto service_management + +:open_directory +echo Opening installation directory... +if exist "%INSTALL_DIR%" ( + start "" "%INSTALL_DIR%" +) else ( + echo Installation directory not found: %INSTALL_DIR% +) +pause +goto menu + +:show_config +echo. +echo ======================================== +echo S-UI Configuration +echo ======================================== +if exist "%INSTALL_DIR%\sui.exe" ( + cd /d "%INSTALL_DIR%" + echo Current settings: + sui.exe setting -show + echo. + echo Admin credentials: + sui.exe admin -show +) else ( + echo S-UI executable not found. Please run the installer first. +) +pause +goto menu + +:show_urls +echo. +echo ======================================== +echo Access URLs +echo ======================================== +echo. +echo Local access: +echo Panel: http://localhost:2095 +echo Subscription: http://localhost:2096 +echo. +echo Network access: +for /f "tokens=2 delims=:" %%i in ('ipconfig ^| findstr /i "IPv4"') do ( + set "ip=%%i" + set "ip=!ip: =!" + echo Panel: http://!ip!:2095 + echo Subscription: http://!ip!:2096 +) +echo. +pause +goto menu + +:invalid_choice +echo Invalid choice. Please select a valid option. +pause +goto menu + +:exit +echo Thank you for using S-UI Windows Control Panel! +exit /b 0 diff --git a/windows/s-ui-windows.xml b/windows/s-ui-windows.xml new file mode 100644 index 0000000..e526d93 --- /dev/null +++ b/windows/s-ui-windows.xml @@ -0,0 +1,22 @@ + + + s-ui + S-UI Proxy Panel + S-UI is a proxy panel for managing proxy services + %BASE%\sui.exe + + rotate + %BASE%\logs + + + %BASE% + + + + + + 1 hour + Automatic + tcpip + netman + diff --git a/windows/uninstall-windows.bat b/windows/uninstall-windows.bat new file mode 100644 index 0000000..c620001 --- /dev/null +++ b/windows/uninstall-windows.bat @@ -0,0 +1,102 @@ +@echo off +setlocal enabledelayedexpansion + +echo ======================================== +echo S-UI Windows Uninstaller +echo ======================================== + +REM Check if running as Administrator +net session >nul 2>&1 +if %errorLevel% neq 0 ( + echo Error: This script must be run as Administrator + echo Right-click on this file and select "Run as administrator" + pause + exit /b 1 +) + +REM Set installation directory +set "INSTALL_DIR=C:\Program Files\s-ui" +set "SERVICE_NAME=s-ui" + +echo Uninstalling S-UI from: %INSTALL_DIR% + +REM Stop and remove Windows Service +if exist "%INSTALL_DIR%\s-ui-service.exe" ( + echo Stopping and removing Windows Service... + net stop %SERVICE_NAME% >nul 2>&1 + cd /d "%INSTALL_DIR%" + s-ui-service.exe uninstall >nul 2>&1 + if %errorLevel% equ 0 ( + echo Service removed successfully + ) else ( + echo Warning: Failed to remove service or service was not installed + ) +) + +REM Remove desktop shortcut +echo Removing desktop shortcut... +set "DESKTOP=%USERPROFILE%\Desktop" +if exist "%DESKTOP%\S-UI.lnk" ( + del "%DESKTOP%\S-UI.lnk" >nul 2>&1 + echo Desktop shortcut removed +) + +REM Remove Start Menu shortcut +echo Removing Start Menu shortcut... +set "START_MENU=%APPDATA%\Microsoft\Windows\Start Menu\Programs\S-UI" +if exist "%START_MENU%" ( + rmdir /s /q "%START_MENU%" >nul 2>&1 + echo Start Menu shortcut removed +) + +REM Remove environment variable +echo Removing environment variable... +reg delete "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v SUI_HOME /f >nul 2>&1 + +REM Ask user if they want to keep data +echo. +set /p keep_data="Do you want to keep your data (database, logs, certificates)? [y/n]: " +if /i "%keep_data%"=="y" ( + echo Keeping data files... + REM Remove only executable and service files + if exist "%INSTALL_DIR%\sui.exe" del "%INSTALL_DIR%\sui.exe" >nul 2>&1 + if exist "%INSTALL_DIR%\s-ui-service.exe" del "%INSTALL_DIR%\s-ui-service.exe" >nul 2>&1 + if exist "%INSTALL_DIR%\s-ui-service.xml" del "%INSTALL_DIR%\s-ui-service.xml" >nul 2>&1 + if exist "%INSTALL_DIR%\winsw.exe" del "%INSTALL_DIR%\winsw.exe" >nul 2>&1 + if exist "%INSTALL_DIR%\*.bat" del "%INSTALL_DIR%\*.bat" >nul 2>&1 + if exist "%INSTALL_DIR%\*.xml" del "%INSTALL_DIR%\*.xml" >nul 2>&1 + if exist "%INSTALL_DIR%\*.md" del "%INSTALL_DIR%\*.md" >nul 2>&1 + echo Data files preserved in: %INSTALL_DIR% +) else ( + echo Removing all files... + REM Remove entire installation directory + if exist "%INSTALL_DIR%" ( + rmdir /s /q "%INSTALL_DIR%" >nul 2>&1 + if exist "%INSTALL_DIR%" ( + echo Warning: Some files could not be removed. Please manually delete: %INSTALL_DIR% + ) else ( + echo All files removed successfully + ) + ) +) + +REM Remove firewall rules +echo Removing firewall rules... +netsh advfirewall firewall delete rule name="S-UI Panel" >nul 2>&1 +netsh advfirewall firewall delete rule name="S-UI Subscription" >nul 2>&1 + +echo. +echo ======================================== +echo Uninstallation completed! +echo ======================================== +echo. +echo S-UI has been uninstalled from your system. +echo. +if /i "%keep_data%"=="y" ( + echo Your data has been preserved in: %INSTALL_DIR% + echo You can safely delete this directory if you no longer need the data. +) +echo. +echo Thank you for using S-UI! +echo. +pause