diff --git a/.github/workflows/wiki.workflow.yml b/.github/workflows/wiki.workflow.yml new file mode 100644 index 0000000..c5d8ab4 --- /dev/null +++ b/.github/workflows/wiki.workflow.yml @@ -0,0 +1,88 @@ +name: Deploy VitePress site to Pages + +on: + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + # Step 1: Check out the repository + - name: Checkout + uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + # Step 2: Setup Node.js and pnpm + - name: Setup Node.js and pnpm + uses: actions/setup-node@v4 + with: + node-version: "20" # Use Node.js version 20 + cache: "pnpm" # Enable pnpm cache + cache-dependency-path: "docs" + + # Step 3: Install pnpm + - name: Install pnpm + run: npm install -g pnpm + + # Step 4: Restore cache for pnpm and VitePress build + - name: Restore cache + uses: actions/cache@v4 + with: + # Cache pnpm store and .vitepress cache + path: | + ~/.pnpm-store + docs/.vitepress/cache + # Generate a cache key based on lock file and source files + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # Fallback to a partial cache if lock file changes + restore-keys: | + ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}- + + # Step 5: Install dependencies using pnpm + - name: Install dependencies + working-directory: ./docs + run: pnpm install + + # Step 6: Build the VitePress site + - name: Build with VitePress + working-directory: ./docs + run: pnpm vitepress build + + # Step 7: Upload artifact for deployment + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + # Step 1: Deploy to GitHub Pages + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index a3996ca..a5b184a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,23 @@ frp-panel out cmd/out frp-panel* -dist/ \ No newline at end of file +dist/ + +/coverage +/src/client/shared.ts +/src/node/shared.ts +*.log +*.tgz +.DS_Store +.idea +.temp +.vite_opt_cache +.vscode +dist +cache +temp +examples-temp +node_modules +pnpm-global +TODOs.md +*.timestamp-*.mjs \ No newline at end of file diff --git a/README.md b/README.md index c870a3a..68d014f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -> Detailed blog post: [https://vaala.cat/2024/01/14/frp-panel-doc/](https://vaala.cat/2024/01/14/frp-panel-doc/) -> You can refer to the blog for instructions, or scroll down to the end. +> FRP-Panel WIKI:[FRP-Panel WiKi](https://vaala.cat/frp-panel) +> Development blog: [https://vaala.cat/posts/frp-panel-doc/](https://vaala.cat/posts/frp-panel-doc/) +> You can refer to the wiki for instructions, or scroll down read simple doc. # FRP-Panel @@ -12,9 +13,9 @@ Our goal is to create a more powerful and comprehensive frp that provides: - Visual configuration interface - Simplified configuration required for running -- demo Video: [demo Video](doc/frp-panel-demo.mp4) +- demo Video: [demo Video](docs/public/images/frp-panel-demo.mp4) -![](./doc/frp-panel-demo.gif) +![](./docs/public/images/frp-panel-demo.gif) ## Project Usage Instructions @@ -165,7 +166,7 @@ Next, we have frpc and frps. The original version requires writing configuration In addition, we also need to consider the compatibility with the original version. The client/server of frp-panel must be able to connect to the official frpc/frps service. In this way, both configuration file and non-configuration file modes can work perfectly. Overall, the architecture is quite simple. -![arch](doc/arch.png) +![arch](docs/public/images/arch.png) ### Development @@ -231,7 +232,7 @@ The project configuration file will read the .env file in the current folder by Detailed architecture call diagram: -![structure](doc/callvis.svg) +![structure](docs/public/images/callvis.svg) ### Core Configuration Explanation @@ -241,31 +242,31 @@ This file contains detailed explanations of the configuration parameters. Please ## Screenshots ### Index Page -![Index Page](doc/en_index.png) +![Index Page](docs/public/images/en_index.png) ### Server List -![Server List](doc/en_server_list.png) +![Server List](docs/public/images/en_server_list.png) ### Server Edit -![Server Edit](doc/en_server_edit.png) +![Server Edit](docs/public/images/en_server_edit.png) ### Server Edit Advanced -![Server Edit Advanced](doc/en_server_edit_adv.png) +![Server Edit Advanced](docs/public/images/en_server_edit_adv.png) ### Client List -![Client List](doc/en_client_list.png) +![Client List](docs/public/images/en_client_list.png) ### Client Edit -![Client Edit](doc/en_client_edit.png) +![Client Edit](docs/public/images/en_client_edit.png) ### Client Edit Advanced -![Client Edit Advanced](doc/en_client_edit_adv.png) +![Client Edit Advanced](docs/public/images/en_client_edit_adv.png) ### Client Stats -![Client Stats](doc/en_client_stats.png) +![Client Stats](docs/public/images/en_client_stats.png) ### Realtime Log -![Realtime Log](doc/en_realtime_log.png) +![Realtime Log](docs/public/images/en_realtime_log.png) ### Remote Console -![Remote Console](doc/en_remote_console.png) +![Remote Console](docs/public/images/en_remote_console.png) diff --git a/README_zh.md b/README_zh.md index 64b786f..166db11 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,5 +1,7 @@ -> 详细博客地址: [https://vaala.cat/2024/01/14/frp-panel-doc/](https://vaala.cat/2024/01/14/frp-panel-doc/) -> 使用说明可以看博客,也可以直接滑到最后 +> 详细使用文档:[FRP-Panel WiKi](https://vaala.cat/frp-panel) +> 博客开发记录: [https://vaala.cat/posts/frp-panel-doc/](https://vaala.cat/posts/frp-panel-doc/) + +QQ交流群: 830620423 # FRP-Panel @@ -14,9 +16,9 @@ 的更强更完善的 frp! -- demo Video: [demo Video](doc/frp-panel-demo.mp4) +- demo Video: [demo Video](docs/public/images/frp-panel-demo.mp4) -![](./doc/frp-panel-demo.gif) +![](./docs/public/images/frp-panel-demo.gif) ## 项目使用说明 @@ -199,7 +201,7 @@ C:/frpp/frpp.exe uninstall 其次还要考虑到与原版的兼容问题,frp-panel 的客户端/服务端都必须要能连上官方 frpc/frps 服务。这样的话就可以做到配置文件/不要配置文件都能完美工作了。 总的说来架构还是很简单的。 -![arch](doc/arch.png) +![arch](docs/public/images/arch.png) ### 开发 @@ -266,7 +268,7 @@ C:/frpp/frpp.exe uninstall 详细架构调用图 -![structure](doc/callvis.svg) +![structure](docs/public/images/callvis.svg) ### 本体配置说明 @@ -276,31 +278,31 @@ C:/frpp/frpp.exe uninstall ## 截图展示 ### 首页 -![首页](doc/cn_index.png) +![首页](docs/public/images/cn_index.png) ### 服务器列表 -![服务器列表](doc/cn_server_list.png) +![服务器列表](docs/public/images/cn_server_list.png) ### 服务器编辑 -![服务器编辑](doc/cn_server_edit.png) +![服务器编辑](docs/public/images/cn_server_edit.png) ### 服务器高级编辑 -![服务器高级编辑](doc/cn_server_edit_adv.png) +![服务器高级编辑](docs/public/images/cn_server_edit_adv.png) ### 客户端列表 -![客户端列表](doc/cn_client_list.png) +![客户端列表](docs/public/images/cn_client_list.png) ### 客户端编辑 -![客户端编辑](doc/cn_client_edit.png) +![客户端编辑](docs/public/images/cn_client_edit.png) ### 客户端高级编辑 -![客户端高级编辑](doc/cn_client_edit_adv.png) +![客户端高级编辑](docs/public/images/cn_client_edit_adv.png) ### 客户端统计 -![客户端统计](doc/cn_client_stats.png) +![客户端统计](docs/public/images/cn_client_stats.png) ### 实时日志 -![实时日志](doc/cn_realtime_log.png) +![实时日志](docs/public/images/cn_realtime_log.png) ### 远程控制台 -![远程控制台](doc/cn_remote_console.png) +![远程控制台](docs/public/images/cn_remote_console.png) diff --git a/build.sh b/build.sh index 85fc03b..b557757 100755 --- a/build.sh +++ b/build.sh @@ -151,7 +151,7 @@ done # Move to current directory if current enabled if [[ "$CURRENT" == "true" ]]; then - mv dist/frp* ./frp-panel + cp dist/frp* ./frp-panel fi -echo "Build Done!" \ No newline at end of file +echo "Build Done!" diff --git a/cmd/frpp/cmd.go b/cmd/frpp/cmd.go index 8d1af5b..585ed6c 100644 --- a/cmd/frpp/cmd.go +++ b/cmd/frpp/cmd.go @@ -41,6 +41,7 @@ func initCmdWithFlag() []*cobra.Command { clientSecret string clientID string rpcHost string + apiHost string appSecret string rpcPort int apiPort int @@ -49,11 +50,11 @@ func initCmdWithFlag() []*cobra.Command { ) clientCmd = &cobra.Command{ - Use: "client [-s client secret] [-i client id] [-a app secret] [-r rpc host] [-c rpc port] [-p api port]", + Use: "client [-s client secret] [-i client id] [-a app secret] [-t api host] [-r rpc host] [-c rpc port] [-p api port]", Short: "run managed frpc", Run: func(cmd *cobra.Command, args []string) { run := func() { - patchConfig(rpcHost, appSecret, + patchConfig(apiHost, rpcHost, appSecret, clientID, clientSecret, apiScheme, rpcPort, apiPort) runClient() @@ -71,7 +72,7 @@ func initCmdWithFlag() []*cobra.Command { Short: "run managed frps", Run: func(cmd *cobra.Command, args []string) { run := func() { - patchConfig(rpcHost, appSecret, + patchConfig(apiHost, rpcHost, appSecret, clientID, clientSecret, apiScheme, rpcPort, apiPort) runServer() @@ -88,7 +89,7 @@ func initCmdWithFlag() []*cobra.Command { Use: "join [-j join token] [-r rpc host] [-p api port] [-e api scheme]", Short: "join to master with token, save param to config", Run: func(cmd *cobra.Command, args []string) { - pullRunConfig(joinToken, appSecret, rpcHost, apiScheme, rpcPort, apiPort, clientID) + pullRunConfig(joinToken, appSecret, rpcHost, apiScheme, rpcPort, apiPort, clientID, apiHost) }, } @@ -98,6 +99,10 @@ func initCmdWithFlag() []*cobra.Command { serverCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id") clientCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "rpc host, canbe ip or domain") serverCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "rpc host, canbe ip or domain") + + clientCmd.Flags().StringVarP(&apiHost, "api", "t", "", "api host, canbe ip or domain") + serverCmd.Flags().StringVarP(&apiHost, "api", "t", "", "api host, canbe ip or domain") + clientCmd.Flags().StringVarP(&appSecret, "app", "a", "", "app secret") serverCmd.Flags().StringVarP(&appSecret, "app", "a", "", "app secret") @@ -114,6 +119,7 @@ func initCmdWithFlag() []*cobra.Command { joinCmd.Flags().StringVarP(&appSecret, "app", "a", "", "app secret") joinCmd.Flags().StringVarP(&joinToken, "join-token", "j", "", "your token from master") joinCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "rpc host, canbe ip or domain") + joinCmd.Flags().StringVarP(&apiHost, "api", "t", "", "api host, canbe ip or domain") joinCmd.Flags().StringVarP(&apiScheme, "api-scheme", "e", "", "api scheme, master api scheme, scheme is http/https") joinCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id") @@ -206,12 +212,17 @@ func initLogger() { logger.Instance().SetReportCaller(true) } -func patchConfig(host, secret, clientID, clientSecret, apiScheme string, rpcPort, apiPort int) { +func patchConfig(apiHost, rpcHost, secret, clientID, clientSecret, apiScheme string, rpcPort, apiPort int) { c := context.Background() - if len(host) != 0 { - conf.Get().Master.RPCHost = host - conf.Get().Master.APIHost = host + if len(rpcHost) != 0 { + conf.Get().Master.RPCHost = rpcHost + conf.Get().Master.APIHost = rpcHost } + + if len(apiHost) != 0 { + conf.Get().Master.APIHost = apiHost + } + if len(secret) != 0 { conf.Get().App.Secret = secret } @@ -244,9 +255,9 @@ func setMasterCommandIfNonePresent() { } } -func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, apiPort int, clientID string) { +func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, apiPort int, clientID, apiHost string) { c := context.Background() - if err := checkPullParams(joinToken, rpcHost, apiScheme, apiPort); err != nil { + if err := checkPullParams(joinToken, apiHost, apiScheme, apiPort); err != nil { logger.Logger(c).Errorf("check pull params failed: %s", err.Error()) return } @@ -261,7 +272,7 @@ func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, api } clientID = utils.MakeClientIDPermited(clientID) - patchConfig(rpcHost, appSecret, "", "", apiScheme, rpcPort, apiPort) + patchConfig(apiHost, rpcHost, appSecret, "", "", apiScheme, rpcPort, apiPort) initResp, err := rpc.InitClient(clientID, joinToken) if err != nil { @@ -309,7 +320,7 @@ func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, api envMap[common.EnvClientSecret] = client.GetSecret() envMap[common.EnvMasterRPCHost] = rpcHost envMap[common.EnvMasterRPCPort] = cast.ToString(rpcPort) - envMap[common.EnvMasterAPIHost] = rpcHost + envMap[common.EnvMasterAPIHost] = apiHost envMap[common.EnvMasterAPIPort] = cast.ToString(apiPort) envMap[common.EnvMasterAPIScheme] = apiScheme @@ -320,12 +331,12 @@ func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, api logger.Logger(c).Infof("config saved to env file: %s, you can use `frp-panel client` without args to run client,\n\nconfig is: [%v]", common.SysEnvPath, envMap) } -func checkPullParams(joinToken, rpcHost, apiScheme string, apiPort int) error { +func checkPullParams(joinToken, apiHost, apiScheme string, apiPort int) error { if len(joinToken) == 0 { return errors.New("join token is empty") } - if len(rpcHost) == 0 { - return errors.New("rpc host is empty") + if len(apiHost) == 0 { + return errors.New("api host is empty") } if len(apiScheme) == 0 { return errors.New("api scheme is empty") diff --git a/cmd/frpp/master.go b/cmd/frpp/master.go index 7d7ee2a..34a1706 100644 --- a/cmd/frpp/master.go +++ b/cmd/frpp/master.go @@ -18,8 +18,8 @@ import ( "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/rpc" - "github.com/VaalaCat/frp-panel/services/api" "github.com/VaalaCat/frp-panel/services/master" + "github.com/VaalaCat/frp-panel/services/mux" "github.com/VaalaCat/frp-panel/services/rpcclient" "github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/watcher" @@ -27,6 +27,8 @@ import ( "github.com/glebarez/sqlite" "github.com/sirupsen/logrus" "github.com/sourcegraph/conc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -46,28 +48,31 @@ func runMaster() { auth.InitAuth() creds := dao.InitCert(conf.GetCertTemplate()) - master.MustInitMasterService(creds) router := bizmaster.NewRouter(fs) - api.MustInitApiService(conf.MasterAPIListenAddr(), router) - logger.Logger(c).Infof("start to run master") - m := master.GetMasterSerivce() - a := api.GetAPIService() + lisOpt := conf.GetListener(c) - r, w := initDefaultInternalServer() - defer w.Stop() - defer r.Stop() + masterService := master.NewMasterService(credentials.NewTLS(creds)) + server := masterService.GetServer() + muxServer := mux.NewMux(server, router, lisOpt.MuxLis, creds) + + masterH2CService := master.NewMasterService(insecure.NewCredentials()) + serverH2c := masterH2CService.GetServer() + httpMuxServer := mux.NewMux(serverH2c, router, lisOpt.ApiLis, nil) tasks := watcher.NewClient() tasks.AddCronTask("0 0 3 * * *", proxy.CollectDailyStats) defer tasks.Stop() var wg conc.WaitGroup - wg.Go(w.Run) - wg.Go(r.Run) - wg.Go(m.Run) - wg.Go(a.Run) + + logger.Logger(c).Infof("start to run master") + + wg.Go(runDefaultInternalServer) + wg.Go(muxServer.Run) + wg.Go(httpMuxServer.Run) wg.Go(tasks.Run) + wg.Wait() } @@ -114,7 +119,7 @@ func initDatabase(c context.Context) { models.GetDBManager().Init() } -func initDefaultInternalServer() (rpcclient.ClientRPCHandler, watcher.Client) { +func runDefaultInternalServer() { dao.InitDefaultServer(conf.Get().Master.APIHost) defaultServer, err := dao.GetDefaultServer() if err != nil { @@ -143,5 +148,13 @@ func initDefaultInternalServer() (rpcclient.ClientRPCHandler, watcher.Client) { w.AddDurationTask(common.PushProxyInfoDuration, bizserver.PushProxyInfo, defaultServer.ServerID, defaultServer.ConnectSecret) go initServerOnce(defaultServer.ServerID, defaultServer.ConnectSecret) - return r, w + var wg conc.WaitGroup + + defer w.Stop() + defer r.Stop() + + wg.Go(w.Run) + wg.Go(r.Run) + + wg.Wait() } diff --git a/cmd/frpp/server.go b/cmd/frpp/server.go index 69c0cc6..3151353 100644 --- a/cmd/frpp/server.go +++ b/cmd/frpp/server.go @@ -2,6 +2,7 @@ package main import ( "context" + "net" bizserver "github.com/VaalaCat/frp-panel/biz/server" "github.com/VaalaCat/frp-panel/common" @@ -31,10 +32,13 @@ func runServer() { logrus.Fatal("client id cannot be empty") } - router := bizserver.NewRouter() - api.MustInitApiService(conf.ServerAPIListenAddr(), router) + l, err := net.Listen("tcp", conf.ServerAPIListenAddr()) + if err != nil { + logger.Logger(c).WithError(err).Fatalf("failed to listen addr: %v", conf.ServerAPIListenAddr()) + return + } - a := api.GetAPIService() + a := api.NewApiService(l, bizserver.NewRouter(), true) defer a.Stop() cred, err := utils.TLSClientCertNoValidate(rpc.GetClientCert(clientID, clientSecret, pb.ClientType_CLIENT_TYPE_FRPS)) diff --git a/conf/helper.go b/conf/helper.go index 3f43499..13b9dcb 100644 --- a/conf/helper.go +++ b/conf/helper.go @@ -112,10 +112,12 @@ func GetCertTemplate() *x509.Certificate { return &x509.Certificate{ SerialNumber: big.NewInt(now.Unix()), Subject: pkix.Name{ + CommonName: cfg.Master.APIHost, Country: []string{"CN"}, Organization: []string{"frp-panel"}, OrganizationalUnit: []string{"frp-panel"}, }, + SignatureAlgorithm: x509.SHA512WithRSA, DNSNames: []string{cfg.Master.APIHost}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, NotBefore: now, @@ -123,8 +125,43 @@ func GetCertTemplate() *x509.Certificate { SubjectKeyId: []byte{102, 114, 112, 45, 112, 97, 110, 101, 108}, BasicConstraintsValid: true, IsCA: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, KeyUsage: x509.KeyUsageKeyEncipherment | - x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageKeyAgreement | + x509.KeyUsageDataEncipherment, } } + +type LisOpt struct { + MuxLis net.Listener + ApiLis net.Listener + RunAPI bool +} + +func GetListener(c context.Context) LisOpt { + runAPI := RPCListenAddr() != MasterAPIListenAddr() + + muxLis, err := net.Listen("tcp", RPCListenAddr()) + if err != nil { + logger.Logger(c).WithError(err).Fatalf("failed to listen: %v", RPCListenAddr()) + } + + opt := LisOpt{ + MuxLis: muxLis, + RunAPI: runAPI, + } + + if runAPI { + apiLis, err := net.Listen("tcp", MasterAPIListenAddr()) + if err != nil { + logger.Logger(c).WithError(err).Warnf("failed to listen: %v, but mux server can handle http api", MasterAPIListenAddr()) + } + opt.ApiLis = apiLis + } + + if !runAPI { + opt.ApiLis = nil + } + + return opt +} diff --git a/conf/settings.go b/conf/settings.go index 739444f..49e9276 100644 --- a/conf/settings.go +++ b/conf/settings.go @@ -49,6 +49,7 @@ type Config struct { Client struct { ID string `env:"ID" env-description:"client id"` Secret string `env:"SECRET" env-description:"client secret"` + TLSRpc bool `env:"TLS_RPC" env-default:"true" env-description:"use tls for rpc connection"` } `env-prefix:"CLIENT_"` IsDebug bool `env:"IS_DEBUG" env-default:"false" env-description:"is debug mode"` } diff --git a/dao/cert.go b/dao/cert.go index 61e6bcd..66f582a 100644 --- a/dao/cert.go +++ b/dao/cert.go @@ -2,19 +2,18 @@ package dao import ( "bytes" - "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" + "crypto/rsa" + "crypto/tls" "crypto/x509" "encoding/pem" "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/utils" "github.com/sirupsen/logrus" - "google.golang.org/grpc/credentials" ) -func InitCert(template *x509.Certificate) credentials.TransportCredentials { +func InitCert(template *x509.Certificate) *tls.Config { var ( certPem []byte keyPem []byte @@ -52,7 +51,12 @@ func InitCert(template *x509.Certificate) credentials.TransportCredentials { func GenX509Info(template *x509.Certificate) (certPem []byte, keyPem []byte, err error) { - priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + // priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + // if err != nil { + // return nil, nil, err + // } + + priv, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, nil, err } diff --git a/doc/frp-panel-demo.gif b/doc/frp-panel-demo.gif deleted file mode 100644 index 11be3d4..0000000 Binary files a/doc/frp-panel-demo.gif and /dev/null differ diff --git a/doc/frp-panel-demo.mp4 b/doc/frp-panel-demo.mp4 deleted file mode 100644 index 71af009..0000000 Binary files a/doc/frp-panel-demo.mp4 and /dev/null differ diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..7e37664 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vitepress' +import { zhConfig } from './config/zh' +import { enConfig } from './config/en' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + base: '/frp-panel/', + locales: { + root: { + label: '简体中文', + lang: 'zh', + ...zhConfig + }, + en: { + label: 'English', + lang: 'en', + ...enConfig + } + }, + title: "Frp-Panel WIKI", + description: "Wiki of vaalacat's wonderful frp-panel", + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + } +}) diff --git a/docs/.vitepress/config/en.ts b/docs/.vitepress/config/en.ts new file mode 100644 index 0000000..4da1f30 --- /dev/null +++ b/docs/.vitepress/config/en.ts @@ -0,0 +1,6 @@ +import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress' + +export const enConfig: LocaleSpecificConfig = { + themeConfig: { + } +} \ No newline at end of file diff --git a/docs/.vitepress/config/zh.ts b/docs/.vitepress/config/zh.ts new file mode 100644 index 0000000..68721ed --- /dev/null +++ b/docs/.vitepress/config/zh.ts @@ -0,0 +1,27 @@ +import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress' + +export const zhConfig: LocaleSpecificConfig = { + themeConfig: { + nav: [ + { text: '首页', link: '/' }, + { text: '源码', link: 'https://github.com/vaalacat/frp-panel' } + ], + + sidebar: [ + { + text: '快速开始', + collapsed: true, + link: '/zh/quick-start', + items: [ + { text: 'Master 部署', link: '/zh/deploy-master' }, + { text: 'Server 部署', link: '/zh/deploy-server' }, + { text: 'Client 部署', link: '/zh/deploy-client' }, + ] + } + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/vaalacat/frp-panel' } + ] + } +} \ No newline at end of file diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 0000000..16c2d2a --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,22 @@ +--- +layout: home + +hero: + name: "Frp-Panel Docs" + text: "Multi-Node, Centralized & Visual FRP Management Platform" + tagline: Secure, modern open-source alternative to Cloudflare Tunnel/Tailscale Funnel/Ngrok platforms and proxies
Cloudflare Tunnel/Tailscale Funnel/Ngrok platform and agent open source alternative + actions: + - theme: alt + text: Quick Start + link: /en + +features: + - title: 🚀 One-Click Deployment + details: Rapid deployment via Docker Compose or single-line commands, supports multiple architectures (x86/ARM), with built-in batch node startup commands and auto-config generation + - title: 📦 One-Click Installation + details: Fully automated install script with system dependency detection, supports Debian/Ubuntu/CentOS Linux distributions / Windows 10+ / macOS + - title: 🌐 Centralized Web Management + details: Visual dashboard for centralized frpc/frps node management, real-time traffic metrics, use HTTPS/GRPC/WSS protocol, with live log analysis and web-based interactive terminal + - title: 🔒 Enterprise-Grade Security + details: RPC port enforces TLS encryption by default. Both API and RPC ports support TLS via reverse proxy configurations +--- \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c71c318 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,27 @@ +--- +layout: home + +hero: + name: "Frp-Panel 文档" + text: "多节点、中心化、可视化 的 frp 管理平台" + tagline: Cloudflare Tunnel / Tailscale Funnel / Ngork / nps 平台和代理的安全、现代化开源替代品 + actions: + - theme: alt + text: 快速开始 + link: /zh/quick-start + - theme: alt + text: Quick Start + link: /en + +features: + - title: 🚀 一键启动 + details: 通过 Docker Compose 或单行命令快速部署,支持多种架构 (x86/ARM),内置批量节点启动命令和自动配置生成功能 + - title: 📦 一键安装 + details: 提供全自动安装脚本,自动检测系统依赖,支持 Debian/Ubuntu/CentOS 等主流 Linux 发行版 / Windows10 以上 / MacOS 等 + - title: 🌐 中心化网页管理 + details: 可视化仪表盘支持多节点集中管控frpc/frps,实时流量统计,支持 HTTPS/WSS 协议,提供实时日志分析和实时网页交互终端 + - title: 🔒 企业级安全 + details: RPC端口默认开启 TLS 加密通信,API 可使用反向代理支持 TLS。RPC 端口和 API 均支持以反向代理支持 TLS +--- + +QQ交流群: `830620423` \ No newline at end of file diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..d4a7f6d --- /dev/null +++ b/docs/package.json @@ -0,0 +1,10 @@ +{ + "devDependencies": { + "vitepress": "^1.6.3" + }, + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + } +} \ No newline at end of file diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 0000000..0d8e956 --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,1561 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + vitepress: + specifier: ^1.6.3 + version: 1.6.3(@algolia/client-search@5.23.3)(postcss@8.5.3)(search-insights@2.17.3) + +packages: + + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.23.3': + resolution: {integrity: sha512-yHI0hBwYcNPc+nJoHPTmmlP8pG6nstCEhpHaZQCDwLZhdMtNhd1hliZMCtLgNnvd1yKEgTt/ZDnTSdZLehfKdA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.23.3': + resolution: {integrity: sha512-/70Ey+nZm4bRr2DcNrGU251YIn9lDu0g8xeP4jTCyunGRNFZ/d8hQAw9El34pcTpO1QDojJWAi6ywKIrUaks9w==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.23.3': + resolution: {integrity: sha512-fkpbPclIvaiyw3ADKRBCxMZhrNx/8//6DClfWGxeEiTJ0HEEYtHlqE6GjAkEJubz4v1ioCQkhZwMoFfFct2/vQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.23.3': + resolution: {integrity: sha512-TXc5Ve6QOCihWCTWY9N56CZxF1iovzpBWBUhQhy6JSiUfX3MXceV3saV+sXHQ1NVt2NKkyUfEspYHBsTrYzIDg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.23.3': + resolution: {integrity: sha512-JlReruxxiw9LB53jF/BmvVV+c0thiWQUHRdgtbVIEusvRaiX1IdpWJSPQExEtBQ7VFg89nP8niCzWtA34ktKSA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.23.3': + resolution: {integrity: sha512-GDEExFMXwx0ScE0AZUA4F6ssztdJvGcXUkdWmWyt2hbYz43ukqmlVJqPaYgGmWdjJjvTx+dNF/hcinwWuXbCug==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.23.3': + resolution: {integrity: sha512-mwofV6tGo0oHt4BPi+S5eLC3wnhOa4A1OVgPxetTxZuetod+2W4cxKavUW2v/Ma5CABXPLooXX+g9E67umELZw==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.23.3': + resolution: {integrity: sha512-Zxgmi7Hk4lI52YFphzzJekUqWxYxVjY2GrCpOxV+QiojvUi8Ru+knq6REcwLHFSwpwaDh2Th5pOefMpn4EkQCw==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.23.3': + resolution: {integrity: sha512-zi/IqvsmFW4E5gMaovAE4KRbXQ+LDYpPGG1nHtfuD5u3SSuQ31fT1vX2zqb6PbPTlgJMEmMk91Mbb7fIKmbQUw==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.23.3': + resolution: {integrity: sha512-C9TwbT1zGwULLXGSUSB+G7o/30djacPmQcsTHepvT47PVfPr2ISK/5QVtUnjMU84LEP8uNjuPUeM4ZeWVJ2iuQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.23.3': + resolution: {integrity: sha512-/7oYeUhYzY0lls7WtkAURM6wy21/Wwmq9GdujW1MpoYVC0ATXXxwCiAfOpYL9xdWxLV0R3wjyD+yZEni+nboKg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.23.3': + resolution: {integrity: sha512-r/4fKz4t+bSU1KdjRq+swdNvuGfJ0spV8aFTHPtcsF+1ZaN/VqmdXrTe5NkaZLSztFeMqKwZlJIVvE7VuGlFtw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.23.3': + resolution: {integrity: sha512-UZiTNmUBQFPl3tUKuXaDd8BxEC0t0ny86wwW6XgwfM9IQf4PrzuMpvuOGIJMcCGlrNolZDEI0mcbz/tqRdKW7A==} + engines: {node: '>= 14.0.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.27.0': + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + engines: {node: '>=6.9.0'} + + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} + + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} + + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@iconify-json/simple-icons@1.2.31': + resolution: {integrity: sha512-xBUPtvkcSAiXs9DfVtudhLddQtQYin3I3Ph/W5FNYA0oE6r2hmLB8TgOog9OjOt1Sxn3IB5+4n5+64DMf2xNmQ==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@rollup/rollup-android-arm-eabi@4.40.0': + resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.40.0': + resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.40.0': + resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.40.0': + resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.40.0': + resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.40.0': + resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.40.0': + resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.40.0': + resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.40.0': + resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.40.0': + resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.40.0': + resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.40.0': + resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-win32-arm64-msvc@4.40.0': + resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.40.0': + resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.40.0': + resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} + cpu: [x64] + os: [win32] + + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} + + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} + + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} + + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} + + '@shikijs/transformers@2.5.0': + resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} + + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-vue@5.2.3': + resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/devtools-api@7.7.2': + resolution: {integrity: sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==} + + '@vue/devtools-kit@7.7.2': + resolution: {integrity: sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==} + + '@vue/devtools-shared@7.7.2': + resolution: {integrity: sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==} + + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/integrations@12.8.2': + resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + algoliasearch@5.23.3: + resolution: {integrity: sha512-0JlUaY/hl3LrKvbidI5FysEi2ggAlcTHM8AHV2UsrJUXnNo8/lWBfhzc1b7o8bK3YZNiU26JtLyT9exoj5VBgA==} + engines: {node: '>= 14.0.0'} + + birpc@0.2.19: + resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + focus-trap@7.6.4: + resolution: {integrity: sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + minisearch@7.1.2: + resolution: {integrity: sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.26.5: + resolution: {integrity: sha512-fmpDkgfGU6JYux9teDWLhj9mKN55tyepwYbxHgQuIxbWQzgFg5vk7Mrrtfx7xRxq798ynkY4DDDxZr235Kk+4w==} + + property-information@7.0.0: + resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.40.0: + resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@5.4.18: + resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitepress@1.6.3: + resolution: {integrity: sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + + vue@3.5.13: + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3) + '@algolia/client-search': 5.23.3 + algoliasearch: 5.23.3 + + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3)': + dependencies: + '@algolia/client-search': 5.23.3 + algoliasearch: 5.23.3 + + '@algolia/client-abtesting@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + '@algolia/client-analytics@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + '@algolia/client-common@5.23.3': {} + + '@algolia/client-insights@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + '@algolia/client-personalization@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + '@algolia/client-query-suggestions@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + '@algolia/client-search@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + '@algolia/ingestion@1.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + '@algolia/monitoring@1.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + '@algolia/recommend@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + '@algolia/requester-browser-xhr@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + + '@algolia/requester-fetch@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + + '@algolia/requester-node-http@5.23.3': + dependencies: + '@algolia/client-common': 5.23.3 + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/parser@7.27.0': + dependencies: + '@babel/types': 7.27.0 + + '@babel/types@7.27.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@docsearch/css@3.8.2': {} + + '@docsearch/js@3.8.2(@algolia/client-search@5.23.3)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.8.2(@algolia/client-search@5.23.3)(search-insights@2.17.3) + preact: 10.26.5 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@3.8.2(@algolia/client-search@5.23.3)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.23.3)(algoliasearch@5.23.3) + '@docsearch/css': 3.8.2 + algoliasearch: 5.23.3 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@iconify-json/simple-icons@1.2.31': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@rollup/rollup-android-arm-eabi@4.40.0': + optional: true + + '@rollup/rollup-android-arm64@4.40.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.40.0': + optional: true + + '@rollup/rollup-darwin-x64@4.40.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.40.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.40.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.40.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.40.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.40.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.40.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.40.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.40.0': + optional: true + + '@shikijs/core@2.5.0': + dependencies: + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 + + '@shikijs/engine-oniguruma@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/themes@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/transformers@2.5.0': + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 + + '@shikijs/types@2.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@types/estree@1.0.7': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-vue@5.2.3(vite@5.4.18)(vue@3.5.13)': + dependencies: + vite: 5.4.18 + vue: 3.5.13 + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.27.0 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.27.0 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.3 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/devtools-api@7.7.2': + dependencies: + '@vue/devtools-kit': 7.7.2 + + '@vue/devtools-kit@7.7.2': + dependencies: + '@vue/devtools-shared': 7.7.2 + birpc: 0.2.19 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.2': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.13(vue@3.5.13)': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13 + + '@vue/shared@3.5.13': {} + + '@vueuse/core@12.8.2': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.13 + transitivePeerDependencies: + - typescript + + '@vueuse/integrations@12.8.2(focus-trap@7.6.4)': + dependencies: + '@vueuse/core': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.13 + optionalDependencies: + focus-trap: 7.6.4 + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/shared@12.8.2': + dependencies: + vue: 3.5.13 + transitivePeerDependencies: + - typescript + + algoliasearch@5.23.3: + dependencies: + '@algolia/client-abtesting': 5.23.3 + '@algolia/client-analytics': 5.23.3 + '@algolia/client-common': 5.23.3 + '@algolia/client-insights': 5.23.3 + '@algolia/client-personalization': 5.23.3 + '@algolia/client-query-suggestions': 5.23.3 + '@algolia/client-search': 5.23.3 + '@algolia/ingestion': 1.23.3 + '@algolia/monitoring': 1.23.3 + '@algolia/recommend': 5.23.3 + '@algolia/requester-browser-xhr': 5.23.3 + '@algolia/requester-fetch': 5.23.3 + '@algolia/requester-node-http': 5.23.3 + + birpc@0.2.19: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + csstype@3.1.3: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + emoji-regex-xs@1.0.0: {} + + entities@4.5.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@2.0.2: {} + + focus-trap@7.6.4: + dependencies: + tabbable: 6.2.0 + + fsevents@2.3.3: + optional: true + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + + is-what@4.1.16: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + mark.js@8.11.1: {} + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + minisearch@7.1.2: {} + + mitt@3.0.1: {} + + nanoid@3.3.11: {} + + oniguruma-to-es@3.1.1: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.0.1 + regex-recursion: 6.0.2 + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.26.5: {} + + property-information@7.0.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + + rfdc@1.4.1: {} + + rollup@4.40.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.40.0 + '@rollup/rollup-android-arm64': 4.40.0 + '@rollup/rollup-darwin-arm64': 4.40.0 + '@rollup/rollup-darwin-x64': 4.40.0 + '@rollup/rollup-freebsd-arm64': 4.40.0 + '@rollup/rollup-freebsd-x64': 4.40.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 + '@rollup/rollup-linux-arm-musleabihf': 4.40.0 + '@rollup/rollup-linux-arm64-gnu': 4.40.0 + '@rollup/rollup-linux-arm64-musl': 4.40.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-musl': 4.40.0 + '@rollup/rollup-linux-s390x-gnu': 4.40.0 + '@rollup/rollup-linux-x64-gnu': 4.40.0 + '@rollup/rollup-linux-x64-musl': 4.40.0 + '@rollup/rollup-win32-arm64-msvc': 4.40.0 + '@rollup/rollup-win32-ia32-msvc': 4.40.0 + '@rollup/rollup-win32-x64-msvc': 4.40.0 + fsevents: 2.3.3 + + search-insights@2.17.3: {} + + shiki@2.5.0: + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + tabbable@6.2.0: {} + + trim-lines@3.0.1: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite@5.4.18: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.40.0 + optionalDependencies: + fsevents: 2.3.3 + + vitepress@1.6.3(@algolia/client-search@5.23.3)(postcss@8.5.3)(search-insights@2.17.3): + dependencies: + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.23.3)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.31 + '@shikijs/core': 2.5.0 + '@shikijs/transformers': 2.5.0 + '@shikijs/types': 2.5.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 5.2.3(vite@5.4.18)(vue@3.5.13) + '@vue/devtools-api': 7.7.2 + '@vue/shared': 3.5.13 + '@vueuse/core': 12.8.2 + '@vueuse/integrations': 12.8.2(focus-trap@7.6.4) + focus-trap: 7.6.4 + mark.js: 8.11.1 + minisearch: 7.1.2 + shiki: 2.5.0 + vite: 5.4.18 + vue: 3.5.13 + optionalDependencies: + postcss: 8.5.3 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + + vue@3.5.13: + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13) + '@vue/shared': 3.5.13 + + zwitch@2.0.4: {} diff --git a/doc/arch.png b/docs/public/images/arch.png similarity index 100% rename from doc/arch.png rename to docs/public/images/arch.png diff --git a/doc/arch.svg b/docs/public/images/arch.svg similarity index 100% rename from doc/arch.svg rename to docs/public/images/arch.svg diff --git a/doc/callvis.gv b/docs/public/images/callvis.gv similarity index 100% rename from doc/callvis.gv rename to docs/public/images/callvis.gv diff --git a/doc/callvis.svg b/docs/public/images/callvis.svg similarity index 100% rename from doc/callvis.svg rename to docs/public/images/callvis.svg diff --git a/doc/cn_client_edit.png b/docs/public/images/cn_client_edit.png similarity index 100% rename from doc/cn_client_edit.png rename to docs/public/images/cn_client_edit.png diff --git a/doc/cn_client_edit_adv.png b/docs/public/images/cn_client_edit_adv.png similarity index 100% rename from doc/cn_client_edit_adv.png rename to docs/public/images/cn_client_edit_adv.png diff --git a/doc/cn_client_list.png b/docs/public/images/cn_client_list.png similarity index 100% rename from doc/cn_client_list.png rename to docs/public/images/cn_client_list.png diff --git a/doc/cn_client_stats.png b/docs/public/images/cn_client_stats.png similarity index 100% rename from doc/cn_client_stats.png rename to docs/public/images/cn_client_stats.png diff --git a/doc/cn_index.png b/docs/public/images/cn_index.png similarity index 100% rename from doc/cn_index.png rename to docs/public/images/cn_index.png diff --git a/doc/cn_realtime_log.png b/docs/public/images/cn_realtime_log.png similarity index 100% rename from doc/cn_realtime_log.png rename to docs/public/images/cn_realtime_log.png diff --git a/doc/cn_remote_console.png b/docs/public/images/cn_remote_console.png similarity index 100% rename from doc/cn_remote_console.png rename to docs/public/images/cn_remote_console.png diff --git a/doc/cn_server_edit.png b/docs/public/images/cn_server_edit.png similarity index 100% rename from doc/cn_server_edit.png rename to docs/public/images/cn_server_edit.png diff --git a/doc/cn_server_edit_adv.png b/docs/public/images/cn_server_edit_adv.png similarity index 100% rename from doc/cn_server_edit_adv.png rename to docs/public/images/cn_server_edit_adv.png diff --git a/doc/cn_server_list.png b/docs/public/images/cn_server_list.png similarity index 100% rename from doc/cn_server_list.png rename to docs/public/images/cn_server_list.png diff --git a/doc/en_client_edit.png b/docs/public/images/en_client_edit.png similarity index 100% rename from doc/en_client_edit.png rename to docs/public/images/en_client_edit.png diff --git a/doc/en_client_edit_adv.png b/docs/public/images/en_client_edit_adv.png similarity index 100% rename from doc/en_client_edit_adv.png rename to docs/public/images/en_client_edit_adv.png diff --git a/doc/en_client_list.png b/docs/public/images/en_client_list.png similarity index 100% rename from doc/en_client_list.png rename to docs/public/images/en_client_list.png diff --git a/doc/en_client_stats.png b/docs/public/images/en_client_stats.png similarity index 100% rename from doc/en_client_stats.png rename to docs/public/images/en_client_stats.png diff --git a/doc/en_index.png b/docs/public/images/en_index.png similarity index 100% rename from doc/en_index.png rename to docs/public/images/en_index.png diff --git a/doc/en_realtime_log.png b/docs/public/images/en_realtime_log.png similarity index 100% rename from doc/en_realtime_log.png rename to docs/public/images/en_realtime_log.png diff --git a/doc/en_remote_console.png b/docs/public/images/en_remote_console.png similarity index 100% rename from doc/en_remote_console.png rename to docs/public/images/en_remote_console.png diff --git a/doc/en_server_edit.png b/docs/public/images/en_server_edit.png similarity index 100% rename from doc/en_server_edit.png rename to docs/public/images/en_server_edit.png diff --git a/doc/en_server_edit_adv.png b/docs/public/images/en_server_edit_adv.png similarity index 100% rename from doc/en_server_edit_adv.png rename to docs/public/images/en_server_edit_adv.png diff --git a/doc/en_server_list.png b/docs/public/images/en_server_list.png similarity index 100% rename from doc/en_server_list.png rename to docs/public/images/en_server_list.png diff --git a/docs/zh/deploy-client.md b/docs/zh/deploy-client.md new file mode 100644 index 0000000..4cc8ec3 --- /dev/null +++ b/docs/zh/deploy-client.md @@ -0,0 +1,60 @@ +# Client 部署 + +Client 推荐使用 docker 部署,直接部署在客户机中,可以通过远程终端直接在客户机以 root 权限执行命令,方便升级和管理。 + +## 准备 + +打开 Master 的 webui 并登录,如果没有账号,请直接注册,第一个用户即为管理员 + +在侧边栏跳转到 `服务端`,点击上方的 `新建` 并输入 服务端 的唯一识别ID和 服务端 能够被公网访问的 IP/域名,然后点击保存 + +![](../public/images/cn_client_list.png) + +刷新后,新的客户端会出现在列表中。 + +## 直接部署 + +点击对应客户端的 `ID (点击查看安装命令)` 一列,弹出不同系统的安装命令,粘贴到对应终端即可安装,这里以 Linux 为例 + +``` +curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http +``` + +如果你在国内,可以增加github加速到安装脚本前 + +``` +curl -sSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http +``` + +注意,如果你使用 反向代理 TLS,需要修改这行命令类似如下: + +```bash +curl -sSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- frp-panel client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https +``` + +## Docker Compose 部署 + +点击对应客户端的 `密钥 (点击查看启动命令)` 一列中的隐藏字段,复制类似的启动命令如下备用: + +```bash +frp-panel client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http +``` + +注意,如果你使用 反向代理 TLS,需要修改这行命令类似如下: + +```bash +frp-panel client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https +``` + +docker-compose.yaml + +```yaml +version: '3' +services: + frp-panel-client: + image: vaalacat/frp-panel + container_name: frp-panel-client + network_mode: host + restart: unless-stopped + command: client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https +``` \ No newline at end of file diff --git a/docs/zh/deploy-master.md b/docs/zh/deploy-master.md new file mode 100644 index 0000000..74c78f7 --- /dev/null +++ b/docs/zh/deploy-master.md @@ -0,0 +1,218 @@ +# Master 部署 + +Master 推荐使用 docker 部署!不推荐直接安装到服务器中 + +会给出三种部署方式,任选一种即可 + +部署后没有默认用户,注册的第一个用户即为管理员,为了安全,默认不开启多用户注册 + +## 前期准备 + +### 服务器开放公网端口: + +- **WEBUI 端口**: 默认 `TCP 9000` +- **RPC 端口**: 默认 `TCP 9001` +- **frps 的API端口**:没有默认,请随意预留,例子使用 `TCP/UDP 7000` +- **frps 对外开放的服务端口**:没有默认,请随意预留,例子使用 `TCP/UDP 26999-27050` + +如果使用反向代理,请忽略 WEBUI 和 RPC 端口,放行 80/443 即可 + +WEBUI 端口也可以处理 h2c 格式的 RPC 连接 + +RPC 端口也可以处理自签名 HTTPS 的 API 连接 + +二者都可使用反向代理服务器连接并提供TLS + +> 测试端口是否开放的方法(以8080为例),在服务器上运行: +> ```shell +> python3 -m http.server 8080 +> ``` +> 然后在另一台电脑/服务器上执行: +> ```shell +> curl http://服务器公网IP/域名:8080 -I +> ``` +> 成功的话,输出类似 +> ``` +> HTTP/1.0 200 OK +> Server: SimpleHTTP/0.6 Python/3.11.0 +> Date: Sat, 12 Apr 2025 17:12:15 GMT +> Content-type: text/html; charset=utf-8 +> Content-Length: 8225 +> ``` + +## 方式一:Docker Compose 部署 + +服务器需要安装docker和docker compose + +首先创建一个`docker-compose.yaml`文件,写入以下内容 + +```yaml +version: "3" + +services: + frpp-master: + image: vaalacat/frp-panel:latest + environment: + APP_GLOBAL_SECRET: your_secret + MASTER_RPC_HOST: 1.2.3.4 #服务器的外部IP或域名 + MASTER_RPC_PORT: 9001 + MASTER_API_HOST: 1.2.3.4 #服务器的外部IP或域名 + MASTER_API_PORT: 9000 + MASTER_API_SCHEME: http + volumes: + - ./data:/data + restart: unless-stopped + command: master +``` + +## 方式二:Docker 命令部署 + +服务器需要安装 docker,我们推荐使用 host 网络模式部署 `Master` + +```bash +# 推荐 +# MASTER_RPC_HOST要改成你服务器的外部IP +# APP_GLOBAL_SECRET注意不要泄漏,客户端和服务端的是通过Master生成的 +docker run -d \ + --network=host \ + --restart=unless-stopped \ + -v /opt/frp-panel:/data \ + -e APP_GLOBAL_SECRET=your_secret \ + -e MASTER_RPC_HOST=0.0.0.0 \ + vaalacat/frp-panel +``` + +如果你不想使用 host 网络模式,请参考使用下面的命令修改 + +```bash +# 或者 +# 运行时记得删除命令中的中文 +docker run -d -p 9000:9000 \ # API控制台端口 + -p 9001:9001 \ # rpc端口 + -p 7000:7000 \ # frps 端口 + -p 27000-27050:27000-27050 \ # 给frps预留的端口 + --restart=unless-stopped \ + -v /opt/frp-panel:/data \ # 数据存储位置 + -e APP_GLOBAL_SECRET=your_secret \ # Master的secret注意不要泄漏,客户端和服务端的是通过Master生成的 + -e MASTER_RPC_HOST=0.0.0.0 \ # 这里要改成你服务器的外部IP + vaalacat/frp-panel +``` + +## 方式三:使用 docker 反向代理 TLS 加密部署 + +这里我们以 [Traefik](https://traefik.io/traefik/) 为例 + +> `Traefik` 可以实时自动识别 Docker 容器的端口并热更新配置,非常适合 Docker 服务的反向代理 + +首先创建一个名为`traefik`的反向代理专用网络 +```bash +docker network create traefik +``` +然后启动反向代理和 Master 服务 +- `docker-compose.yaml` + +```yaml +version: '3' + +services: + traefk-reverse-proxy: + image: traefik:v3.3 + restart: unless-stopped + networks: + - traefik + command: + - --entryPoints.web.address=:80 + - --entryPoints.websecure.address=:443 + - --entryPoints.websecure.http2.maxConcurrentStreams=250 + - --providers.docker + - --providers.docker.network=traefik + - --api.insecure # 在生产环境请删除这一行 + # 这下面使用 80 端口做ACME HTTP DNS证书验证 + - --certificatesresolvers.le.acme.email=me@example.com + - --certificatesresolvers.le.acme.storage=/etc/traefik/conf/acme.json + - --certificatesresolvers.le.acme.httpchallenge=true + ports: + # 反向代理的 HTTP 端口 + - "80:80" + # 反向代理的 HTTPS 端口 + - "443:443" + # Traefik 的 Web UI (--api.insecure=true 会使用这个端口) + # 生产环境请删除这个端口 + - "8080:8080" + volumes: + # 挂载 docker.sock,这样 Traefik 可以自动识别主机上所有 docker 容器反向代理配置 + - /var/run/docker.sock:/var/run/docker.sock + # 保存 Traefik 申请的证书 + - ./conf:/etc/traefik/conf + + frpp-master: + image: vaalacat/frp-panel:latest # 这里换成你想使用的版本 + environment: + APP_GLOBAL_SECRET: your_secret + # 因为 api 和 rpc 使用的协议不一样 + # 我们需要对 api 和 rpc 使用两个域名 + # 以便反向代理正确识别需要转发的协议 + MASTER_RPC_HOST: frpp.example.com + MASTER_API_PORT: 443 + MASTER_API_HOST: frpp-rpc.example.com + MASTER_API_SCHEME: https + networks: + - traefik + volumes: + - ./data:/data + ports: + # 无需为 master 预留 api 和 rpc 端口 + # 预留frps api端口 + - 7000:7000 + - 7000:7000/udp + # 预留frps的业务端口 + # 26999 端口是留给 frps 的http代理端口 + - 26999-27050:26999-27050 + - 26999-27050:26999-27050/udp + restart: unless-stopped + command: master + labels: + # API + - traefik.http.routers.frp-panel-api.rule=Host(`frpp.example.com`) + - traefik.http.routers.frp-panel-api.tls=true + - traefik.http.routers.frp-panel-api.tls.certresolver=le + - traefik.http.routers.frp-panel-api.entrypoints=websecure + - traefik.http.routers.frp-panel-api.service=frp-panel-api + - traefik.http.services.frp-panel-api.loadbalancer.server.port=9000 + - traefik.http.services.frp-panel-api.loadbalancer.server.scheme=http + # RPC + - traefik.http.routers.frp-panel-rpc.rule=Host(`frpp-rpc.example.com`) + - traefik.http.routers.frp-panel-rpc.tls=true + - traefik.http.routers.frp-panel-rpc.tls.certresolver=le + - traefik.http.routers.frp-panel-rpc.entrypoints=websecure + - traefik.http.routers.frp-panel-rpc.service=frp-panel-rpc + - traefik.http.services.frp-panel-rpc.loadbalancer.server.port=9000 + - traefik.http.services.frp-panel-rpc.loadbalancer.server.scheme=h2c + # 下方如果你用不到 frps 的http代理,可以不要 + # 需要配置域名 *.frpp.example.com 泛解析到你服务器的公网IP + # 这样可以实现使用 .frpp.example.com 结束的域名,在 443 端口,转发多个服务到多个 frpc + - traefik.http.routers.frp-panel-tunnel.rule=HostRegexp(`.*.frpp.example.com`) + - traefik.http.routers.frp-panel-tunnel.tls.domains[0].sans=*.frpp.example.com + - traefik.http.routers.frp-panel-tunnel.tls=true + - traefik.http.routers.frp-panel-tunnel.tls.certresolver=le + - traefik.http.routers.frp-panel-tunnel.entrypoints=websecure + - traefik.http.routers.frp-panel-tunnel.service=frp-panel-tunnel + - traefik.http.services.frp-panel-tunnel.loadbalancer.server.port=26999 + - traefik.http.services.frp-panel-tunnel.loadbalancer.server.scheme=http +networks: + traefik: + external: true + name: traefik +``` + +上方的 `docker-compose.yaml` 部署完成后,可以访问 `服务器公网IP/域名:8080` 查看反向代理状态 + +随后配置 default server 即可实现 frp 子域名转发: + +| 配置项 | 值 | +|----|-----| +| FRPs 监听端口 | 7000 | +| FRPs 监听地址 | 0.0.0.0 | +| 代理监听地址 | 0.0.0.0 | +| HTTP 监听端口 | 26999 | +| 域名后缀 | frpp.example.com | diff --git a/docs/zh/deploy-server.md b/docs/zh/deploy-server.md new file mode 100644 index 0000000..61119e1 --- /dev/null +++ b/docs/zh/deploy-server.md @@ -0,0 +1,44 @@ +# Server 部署 + +Server 推荐使用 docker 部署!不推荐直接安装到服务器中 + +> 如果只有一台公网服务器需要管理,那么使用 `master` 自带的 `default server` 即可,无需单独部署 `server` + +## 准备 + +打开 Master 的 webui 并登录,如果没有账号,请直接注册,第一个用户即为管理员 + +在侧边栏跳转到 `服务端`,点击上方的 `新建` 并输入 服务端 的唯一识别ID和 服务端 能够被公网访问的 IP/域名,然后点击保存 + +![](../public/images/cn_server_list.png) + +刷新后,新的服务端会出现在列表中。点击对应服务端的`密钥 (点击查看启动命令)`一列中的隐藏字段,复制类似的启动命令如下备用: + +```bash +frp-panel server -s abc -i user.s.server1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http +``` + +注意,如果你使用 反向代理 TLS,需要修改这行命令类似如下: + +```bash +frp-panel server -s abc -i user.s.server1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https +``` + +## Docker Compose 部署 + +docker-compose.yaml + +```yaml +version: '3' +services: + frp-panel-server: + image: vaalacat/frp-panel + container_name: frp-panel-server + network_mode: host + restart: unless-stopped + command: server -s abc -i user.s.server1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https +``` + +## 直接部署 + +如果你想要直接部署,请参考 client 部署的步骤 \ No newline at end of file diff --git a/docs/zh/quick-start.md b/docs/zh/quick-start.md new file mode 100644 index 0000000..43a5182 --- /dev/null +++ b/docs/zh/quick-start.md @@ -0,0 +1,19 @@ +# 快速开始 + +## 开始前必读 + +`frp-panel` 有三个模块: + +1. `master`: 中控模块,负责分发配置文件和控制所有其他模块 +2. `server`: 对应`frps`,负责提供流量接入点 +3. `client`: 对应`frpc`,可以将本地的服务暴露给`server`上某一个接入点 + +> 部署`master`时,`master`会启动一个默认的`default server` 供`client`连接,因此`master`一般不会独立存在,但你可以选择不使用 + +部署时,我们一般从 `master` 开始。`master` 负责管理的 `server` 和 `client` 部署时,需要用到成功部署后的 `master` 控制页面中自动生成的内容。 + +对于 `frp-panel` 我们**推荐所有的组件都使用 `docker` 部署**,并且**使用 `host` 网络**模式,除非你需要远程终端控制远端的机器时才使用服务安装到客户机 + +## 架构图 + +![](../public/images/arch.svg) \ No newline at end of file diff --git a/go.mod b/go.mod index 045135c..0d7697b 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/tiendc/go-deepcopy v1.2.0 golang.org/x/crypto v0.31.0 + golang.org/x/net v0.33.0 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.35.2 gorm.io/driver/mysql v1.5.2 @@ -122,7 +123,6 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect diff --git a/rpc/master.go b/rpc/master.go index 83c2bee..8bde4b5 100644 --- a/rpc/master.go +++ b/rpc/master.go @@ -2,6 +2,7 @@ package rpc import ( "context" + "crypto/tls" "errors" "github.com/VaalaCat/frp-panel/common" @@ -10,6 +11,7 @@ import ( "github.com/imroc/req/v3" "github.com/sirupsen/logrus" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/proto" ) @@ -18,8 +20,16 @@ var ( ) func newMasterCli() { - conn, err := grpc.NewClient(conf.RPCCallAddr(), - grpc.WithTransportCredentials(conf.ClientCred)) + opt := []grpc.DialOption{} + if conf.Get().Client.TLSRpc { + logrus.Infof("use tls rpc") + opt = append(opt, grpc.WithTransportCredentials(conf.ClientCred)) + } else { + logrus.Infof("use insecure rpc") + opt = append(opt, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + conn, err := grpc.NewClient(conf.RPCCallAddr(), opt...) + if err != nil { logrus.Fatalf("did not connect: %v", err) } @@ -34,9 +44,16 @@ func MasterCli(c context.Context) (pb.MasterClient, error) { return masterCli, nil } +func httpCli() *req.Client { + c := req.C() + c.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + return c +} + func GetClientCert(clientID, clientSecret string, clientType pb.ClientType) []byte { apiEndpoint := conf.GetAPIURL() - c := req.C() + c := httpCli() + rawReq, err := proto.Marshal(&pb.GetClientCertRequest{ ClientId: clientID, ClientSecret: clientSecret, @@ -61,7 +78,9 @@ func GetClientCert(clientID, clientSecret string, clientType pb.ClientType) []by func InitClient(clientID, joinToken string) (*pb.InitClientResponse, error) { apiEndpoint := conf.GetAPIURL() - c := req.C() + + c := httpCli() + rawReq, err := proto.Marshal(&pb.InitClientRequest{ ClientId: &clientID, }) @@ -86,7 +105,8 @@ func InitClient(clientID, joinToken string) (*pb.InitClientResponse, error) { func GetClient(clientID, joinToken string) (*pb.GetClientResponse, error) { apiEndpoint := conf.GetAPIURL() - c := req.C() + c := httpCli() + rawReq, err := proto.Marshal(&pb.GetClientRequest{ ClientId: &clientID, }) diff --git a/services/api/service.go b/services/api/service.go index 1fa4bb1..1505f0f 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -1,6 +1,8 @@ package api import ( + "net" + "github.com/gin-gonic/gin" ) @@ -10,32 +12,29 @@ type ApiService interface { } type server struct { - srv *gin.Engine - addr string + srv *gin.Engine + addr net.Listener + enable bool } var ( - _ ApiService = (*server)(nil) - apiService *server + _ ApiService = (*server)(nil) ) -func NewApiService(listenAddr string, router *gin.Engine) *server { +func NewApiService(listen net.Listener, router *gin.Engine, enable bool) *server { return &server{ - srv: router, - addr: listenAddr, + srv: router, + addr: listen, + enable: enable, } } -func MustInitApiService(listenAddr string, router *gin.Engine) { - apiService = NewApiService(listenAddr, router) -} - -func GetAPIService() ApiService { - return apiService -} - func (s *server) Run() { - s.srv.Run(s.addr) + // 如果完全使用mux,可以不启动 + if !s.enable { + return + } + s.srv.RunListener(s.addr) } func (s *server) Stop() { diff --git a/services/master/grpc_server.go b/services/master/grpc_server.go index d4533e1..381792a 100644 --- a/services/master/grpc_server.go +++ b/services/master/grpc_server.go @@ -25,13 +25,15 @@ type server struct { pb.UnimplementedMasterServer } -func NewRpcServer(creds credentials.TransportCredentials) *grpc.Server { +func newRpcServer(creds credentials.TransportCredentials) *grpc.Server { + // s := grpc.NewServer(grpc.Creds(insecure.NewCredentials())) + // s := grpc.NewServer(grpc.Creds(creds)) s := grpc.NewServer(grpc.Creds(creds)) pb.RegisterMasterServer(s, &server{}) return s } -func RunRpcServer(s *grpc.Server) { +func runRpcServer(s *grpc.Server) { lis, err := net.Listen("tcp", conf.RPCListenAddr()) if err != nil { logrus.Fatalf("rpc server failed to listen: %v", err) diff --git a/services/master/grpc_service.go b/services/master/grpc_service.go index bdab2bd..3c5c531 100644 --- a/services/master/grpc_service.go +++ b/services/master/grpc_service.go @@ -1,54 +1,35 @@ package master import ( - "context" - - "github.com/VaalaCat/frp-panel/logger" - "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) -type MasterHandler interface { +type MasterService interface { Run() Stop() + GetServer() *grpc.Server } -type Master struct { +type master struct { grpcServer *grpc.Server } -var ( - cli *Master -) - -func MustInitMasterService(creds credentials.TransportCredentials) { - ctx := context.Background() - if cli != nil { - logger.Logger(ctx).Warn("server has been initialized") - return - } - cli = NewMasterHandler(creds) -} - -func GetMasterSerivce() MasterHandler { - if cli == nil { - logrus.Panic("server has not been initialized") - } - return cli -} - -func NewMasterHandler(creds credentials.TransportCredentials) *Master { - s := NewRpcServer(creds) - return &Master{ +func NewMasterService(creds credentials.TransportCredentials) MasterService { + s := newRpcServer(creds) + return &master{ grpcServer: s, } } -func (s *Master) Run() { - RunRpcServer(s.grpcServer) +func (s *master) Run() { + runRpcServer(s.grpcServer) } -func (s *Master) Stop() { +func (s *master) Stop() { s.grpcServer.Stop() } + +func (s *master) GetServer() *grpc.Server { + return s.grpcServer +} diff --git a/services/mux/mux_service.go b/services/mux/mux_service.go new file mode 100644 index 0000000..c758d13 --- /dev/null +++ b/services/mux/mux_service.go @@ -0,0 +1,60 @@ +package mux + +import ( + "crypto/tls" + "log" + "net" + "net/http" + "strings" + "time" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +type MuxServer interface { + Run() + Stop() +} + +type muxImpl struct { + srv *http.Server + lis net.Listener + tls bool +} + +func NewMux(grpcServer, apiServer http.Handler, lis net.Listener, creds *tls.Config) MuxServer { + tlsServer := grpcHandlerFunc(grpcServer, apiServer) + tlsServer.TLSConfig = creds + return &muxImpl{ + srv: tlsServer, + lis: lis, + tls: creds != nil, + } +} + +func (m *muxImpl) Run() { + if m.tls { + if err := m.srv.ServeTLS(m.lis, "", ""); err != nil { + log.Fatal(err) + } + } else { + if err := m.srv.Serve(m.lis); err != nil { + log.Fatal(err) + } + } +} + +func (m *muxImpl) Stop() { +} + +func grpcHandlerFunc(grpcServer http.Handler, httpHandler http.Handler) *http.Server { + return &http.Server{Handler: h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // fmt.Printf("proto major: %d, %s , %s\n", r.ProtoMajor, r.RequestURI, r.Header.Get("Content-Type")) + if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { + grpcServer.ServeHTTP(w, r) + } else { + httpHandler.ServeHTTP(w, r) + } + }), &http2.Server{}), ReadHeaderTimeout: time.Second * 30} +} diff --git a/utils/cert.go b/utils/cert.go index ba9fc30..b9a8a2d 100644 --- a/utils/cert.go +++ b/utils/cert.go @@ -37,7 +37,7 @@ func PemBlockForPrivKey(priv interface{}) *pem.Block { } } -func TLSServerCert(certPem, keyPem []byte) (credentials.TransportCredentials, error) { +func TLSServerCert(certPem, keyPem []byte) (*tls.Config, error) { cert, err := tls.X509KeyPair(certPem, keyPem) if err != nil { return nil, err @@ -46,7 +46,7 @@ func TLSServerCert(certPem, keyPem []byte) (credentials.TransportCredentials, er Certificates: []tls.Certificate{cert}, ClientAuth: tls.NoClientCert, } - return credentials.NewTLS(config), nil + return config, nil } func TLSClientCert(caPem []byte) (credentials.TransportCredentials, error) { @@ -62,6 +62,7 @@ func TLSClientCertNoValidate(caPem []byte) (credentials.TransportCredentials, er config := &tls.Config{ RootCAs: certpool, InsecureSkipVerify: true, + MinVersion: tls.VersionTLS12, } return credentials.NewTLS(config), nil