diff --git a/.drone.yaml b/.drone.yaml new file mode 100644 index 0000000..22c41ed --- /dev/null +++ b/.drone.yaml @@ -0,0 +1,145 @@ +kind: pipeline +name: build-and-publish + +steps: + - name: download modules + image: golang:1.21-alpine + commands: + - sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories + - apk update --no-cache && apk add --no-cache tzdata git + - CGO_ENABLED=0 GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct go mod download + - mkdir -p etc + - cp /etc/ssl/certs/ca-certificates.crt ./etc/ca-certificates.crt + - cp /usr/share/zoneinfo/Asia/Shanghai ./etc/Shanghai + volumes: + - name: gocache + path: /go/pkg/mod + - name: build + path: /tmp/app + when: + event: + - pull_request + - promote + - rollback + - name: build frontend + image: node:18-alpine + commands: + - cd www + - sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories + - apk update --no-cache && apk add --no-cache tzdata git openssh + - npm config set registry https://registry.npmmirror.com + # - npm install -g pnpm + - npm install || (sleep 3 && rm -rf node_modules && npm install ) + - npm run build || (sleep 3 && rm -rf node_modules && npm install && npm run build) + volumes: + - name: nodecache + path: /drone/src/www/node_modules + when: + event: + - pull_request + - promote + - rollback + - name: build - amd64 + image: golang:1.21-alpine + commands: + - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct go build -ldflags="-s -w" -o frp-panel-amd64 cmd/*.go + volumes: + - name: gocache + path: /go/pkg/mod + - name: build + path: /tmp/app + depends_on: + - build frontend + - download modules + when: + event: + - pull_request + - promote + - rollback + - name: build - arm64 + image: golang:1.21-alpine + commands: + - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct go build -ldflags="-s -w" -o frp-panel-arm64 cmd/*.go + volumes: + - name: gocache + path: /go/pkg/mod + - name: build + path: /tmp/app + depends_on: + - build frontend + - download modules + when: + event: + - pull_request + - promote + - rollback + + - name: publish - amd64 + image: thegeeklab/drone-docker-buildx:24 + privileged: true + settings: + mirror: https://dockerproxy.com + buildkit_config: | + [registry."docker.io"] + mirrors = ["dockerproxy.com"] + debug: true + platforms: + - linux/amd64 + build_args: + - ARCH=amd64 + repo: vaalacat/frp-panel + tags: + - amd64 + registry: + from_secret: DOCKER_REGISTRY + username: + from_secret: DOCKER_USERNAME + password: + from_secret: DOCKER_PASSWORD + depends_on: + - build - amd64 + when: + event: + - promote + - rollback + target: + - production + - name: publish - arm64 + image: thegeeklab/drone-docker-buildx:24 + privileged: true + settings: + mirror: https://dockerproxy.com + buildkit_config: | + [registry."docker.io"] + mirrors = ["dockerproxy.com"] + debug: false + platforms: + - linux/arm64 + build_args: + - ARCH=arm64 + repo: vaalacat/frp-panel + tags: + - arm64 + registry: + from_secret: DOCKER_REGISTRY + username: + from_secret: DOCKER_USERNAME + password: + from_secret: DOCKER_PASSWORD + depends_on: + - build - arm64 + when: + event: + - promote + - rollback + target: + - production +volumes: + - name: build + temp: {} + - name: gocache + host: + path: /tmp/drone/frp-panel/gocache + - name: nodecache + host: + path: /tmp/drone/frp-panel/nodecache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c16d5f0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM alpine + +ARG ARCH + +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \ + apk update --no-cache && apk --no-cache add curl bash sqlite + +ENV TZ Asia/Shanghai + +WORKDIR /app +COPY ./frp-panel-${ARCH} /app/frp-panel +COPY ./etc /app/etc + +RUN ln -sf /app/etc/Shanghai /etc/localtime && mv /app/etc/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +# web port +EXPOSE 9000 + +# rpc port +EXPOSE 9001 + +ENTRYPOINT [ "/app/frp-panel" ] + +CMD [ "master" ] \ No newline at end of file diff --git a/Dockerfile.standalone b/Dockerfile.standalone new file mode 100644 index 0000000..77528ef --- /dev/null +++ b/Dockerfile.standalone @@ -0,0 +1,46 @@ +# Stage 1: Building frontend +FROM node:18-alpine AS frontend +WORKDIR /app +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories +RUN apk update --no-cache && apk add --no-cache tzdata git openssh +RUN npm config set registry https://registry.npmmirror.com +COPY www/package*.json . +RUN npm install + +COPY www/api/ ./api +COPY www/components/ ./components +COPY www/lib/ ./lib +COPY www/out/ ./out +COPY www/pages/ ./pages +COPY www/public/ ./public +COPY www/store/ ./store +COPY www/styles/ ./styles +COPY www/types/ ./types + +COPY www/*.js ./ +COPY www/*.ts ./ +COPY www/*.yaml ./ +COPY www/*.json ./ + +RUN ls && npm run build + +# Stage 2: Building binary +FROM golang:1.21-alpine AS builder +WORKDIR /app +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories +RUN apk update --no-cache && apk add --no-cache tzdata git +COPY go.mod go.sum ./ +RUN CGO_ENABLED=0 GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct go mod download +COPY . . +RUN rm -rf /app/cmd/out +COPY --from=frontend /app/out ./cmd/out +RUN CGO_ENABLED=0 GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct go build -ldflags="-s -w" -o frp-panel cmd/*.go + +# Stage 3: Build image +FROM alpine:latest +WORKDIR /app +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories +RUN apk update --no-cache && apk add --no-cache tzdata git +COPY --from=builder /app/frp-panel . +ENTRYPOINT ["/app/frp-panel"] +CMD ["master"] \ No newline at end of file diff --git a/README.md b/README.md index 5092999..3c284e0 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,41 @@ -项目如下 +> 详细博客地址: [https://vaala.cat/2024/01/14/frp-panel-doc/](https://vaala.cat/2024/01/14/frp-panel-doc/) +> 使用说明可以看博客,也可以直接滑到最后 -项目包含三个角色 -1. Master: 控制节点,接受来自前端的请求并负责管理Client和Server -2. Server: 服务端,受控制节点控制,负责对客户端提供服务,包含frps和rpc(用于连接Master)服务 -3. Client: 客户端,受控制节点控制,包含frpc和rpc(用于连接Master)服务 +# FRP-Panel -启动方式: +我们的目标就是做一个: +- 客户端配置可中心化管理 +- 多服务端配置管理 +- 可视化配置界面 +- 简化运行所需要的配置 + +的更强更完善的frp! -- master: `go run cmd/*.go master` 或是 `frp-panel master` -- client: `go run cmd/*.go client -i -s ` 或是 `frp-panel client -i -s ` -- server: `go run cmd/*.go server -i -s ` 或是 `frp-panel server -i -s ` +- demo Video: [demo Video](doc/frp-panel-demo.mp4) -项目配置文件会默认读取当前文件夹下的.env文件,项目内置了样例配置文件,可以按照自己的需求进行修改 +![](./doc/frp-panel-demo.gif) +## 项目开发指南 + +### 平台架构设计 + +技术栈选好了,下一步就是要设计程序的架构。在刚刚背景里说的那样,frp本身有frpc和frps(客户端和服务端),这两个角色肯定是必不可少了。然后我们还要新增一个东西去管理它们,所以frp-panel新增了一个master角色。master会负责管理各种frpc和frps,中心化的存储配置文件和连接信息。 + +然后是frpc和frps。原版是需要在两边分别写配置文件的。那么既然原版已经支持了,就不用在走原版的路子,我们直接不支持配置文件,所有的配置都必须从master获取。 + +其次还要考虑到与原版的兼容问题,frp-panel的客户端/服务端都必须要能连上官方frpc/frps服务。这样的话就可以做到配置文件/不要配置文件都能完美工作了。 +总的说来架构还是很简单的。 + +![arch](doc/arch.png) + +### 开发 + +项目包含三个角色 +1. Master: 控制节点,接受来自前端的请求并负责管理Client和Server +2. Server: 服务端,受控制节点控制,负责对客户端提供服务,包含frps和rpc(用于连接Master)服务 +3. Client: 客户端,受控制节点控制,包含frpc和rpc(用于连接Master)服务 + +接下来给出一个项目中各个包的功能 ``` . |-- biz # 主要业务逻辑 @@ -54,8 +77,84 @@ |-- store |-- styles `-- types + ``` +### 调试启动方式: + +- master: `go run cmd/*.go master` +- client: `go run cmd/*.go client -i -s ` +- server: `go run cmd/*.go server -i -s ` + +项目配置文件会默认读取当前文件夹下的.env文件,项目内置了样例配置文件,可以按照自己的需求进行修改 + 详细架构调用图 -![structure](doc/callvis.svg) \ No newline at end of file +![structure](doc/callvis.svg) + +## 项目使用说明 +frp-panel可选docker和直接运行模式部署,直接部署请到release下载文件:[release](https://github.com/VaalaCat/frp-panel/releases) + +### docker + +- master + +``` +docker run -d -p 9000:9000 \ + -p 9001:9001 \ + -v /opt/frp-panel:/data \ + -e APP_GLOBAL_SECRET=your_secret \ # Master的secret注意不要泄漏,客户端和服务端的是通过Master生成的 + -e MASTER_RPC_HOST=0.0.0.0 \ + vaalacat/frp-panel + +``` +- client + +``` +docker run -d -p your_port:your_port \ + -e APP_GLOBAL_SECRET=your_secret \ # 客户端和服务端的Secret与Master不一样,但也最好不要泄漏 + -e MASTER_RPC_HOST=your_master \ # master节点的IP,端口是默认9000,修改配置请看最后 + vaalacat/frp-panel client -s xxx -i xxx # 在WebUI复制的参数 +``` +- server + +``` +docker run -d -p your_port:your_port \ + -e APP_GLOBAL_SECRET=your_secret \ # 客户端和服务端的Secret与Master不一样,但也最好不要泄漏 + -e MASTER_RPC_HOST=your_master \ # master节点的IP,端口是默认9000,修改配置请看最后 + vaalacat/frp-panel server -s xxx -i xxx # 在WebUI复制的参数 +``` +直接运行 +- master + +``` +APP_GLOBAL_SECRET=your_secret MASTER_RPC_HOST=0.0.0.0 frp-panel master +``` +- client + +``` +APP_GLOBAL_SECRET=your_secret MASTER_RPC_HOST=0.0.0.0 frp-panel client -s xxx -i xxx # 在WebUI复制的参数 +``` +- client + +``` +APP_GLOBAL_SECRET=your_secret MASTER_RPC_HOST=0.0.0.0 frp-panel server -s xxx -i xxx # 在WebUI复制的参数 +``` +### 配置说明 + +[settings.go](conf/settings.go) +这里有详细的配置参数解释,需要进一步修改配置请参考该文件 + +### 一些图片 + +![](doc/platform_info.png) +![](doc/login.png) +![](doc/register.png) +![](doc/clients_menu.png) +![](doc/server_menu.png) +![](doc/create_client.png) +![](doc/create_server.png) +![](doc/edit_client.png) +![](doc/edit_client_adv.png) +![](doc/edit_server.png) +![](doc/edit_server_adv.png) \ No newline at end of file diff --git a/biz/master/auth/register.go b/biz/master/auth/register.go index c2f5790..939ab16 100644 --- a/biz/master/auth/register.go +++ b/biz/master/auth/register.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/VaalaCat/frp-panel/conf" "github.com/VaalaCat/frp-panel/dao" "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/pb" @@ -22,6 +23,19 @@ func RegisterHandler(c context.Context, req *pb.RegisterRequest) (*pb.RegisterRe }, fmt.Errorf("invalid username or password or email") } + userCount, err := dao.AdminCountUsers() + if err != nil { + return &pb.RegisterResponse{ + Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}, + }, err + } + + if !conf.Get().App.EnableRegister && userCount > 0 { + return &pb.RegisterResponse{ + Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "register is disabled"}, + }, fmt.Errorf("register is disabled") + } + hashedPassword, err := utils.HashPassword(password) if err != nil { return &pb.RegisterResponse{ diff --git a/biz/master/user/get_platform_info.go b/biz/master/user/get_platform_info.go index 50f8af2..ace8862 100644 --- a/biz/master/user/get_platform_info.go +++ b/biz/master/user/get_platform_info.go @@ -2,6 +2,7 @@ package user import ( "github.com/VaalaCat/frp-panel/common" + "github.com/VaalaCat/frp-panel/conf" "github.com/VaalaCat/frp-panel/dao" "github.com/VaalaCat/frp-panel/pb" "github.com/gin-gonic/gin" @@ -58,5 +59,7 @@ func getPlatformInfo(c *gin.Context) (*pb.GetPlatformInfoResponse, error) { UnconfiguredServerCount: int32(unconfiguredServers), ConfiguredClientCount: int32(configuredClients), ConfiguredServerCount: int32(configuredServers), + GlobalSecret: conf.MasterDefaultSalt(), + MasterRpcHost: conf.Get().Master.RPCHost, }, nil } diff --git a/cmd/client.go b/cmd/client.go index 639d4cf..d359d05 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -2,14 +2,17 @@ package main import ( bizclient "github.com/VaalaCat/frp-panel/biz/client" + "github.com/VaalaCat/frp-panel/conf" "github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/services/rpcclient" "github.com/VaalaCat/frp-panel/watcher" + "github.com/fatedier/golib/crypto" "github.com/sirupsen/logrus" "github.com/sourcegraph/conc" ) func runClient(clientID, clientSecret string) { + crypto.DefaultSalt = conf.Get().App.GlobalSecret logrus.Infof("start to run client") if len(clientSecret) == 0 { logrus.Fatal("client secret cannot be empty") diff --git a/cmd/main.go b/cmd/main.go index a7b474b..3c3c299 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,8 +3,6 @@ package main import ( "github.com/VaalaCat/frp-panel/conf" "github.com/VaalaCat/frp-panel/rpc" - "github.com/VaalaCat/frp-panel/utils" - "github.com/fatedier/golib/crypto" ) func main() { @@ -13,6 +11,5 @@ func main() { conf.InitConfig() rpc.InitRPCClients() - crypto.DefaultSalt = utils.MD5(conf.Get().App.GlobalSecret) rootCmd.Execute() } diff --git a/cmd/master.go b/cmd/master.go index 130fa46..fa9a697 100644 --- a/cmd/master.go +++ b/cmd/master.go @@ -13,6 +13,7 @@ import ( "github.com/VaalaCat/frp-panel/services/server" "github.com/VaalaCat/frp-panel/utils" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/golib/crypto" "github.com/glebarez/sqlite" "github.com/sirupsen/logrus" "github.com/sourcegraph/conc" @@ -23,6 +24,7 @@ import ( var fs embed.FS func runMaster() { + crypto.DefaultSalt = conf.MasterDefaultSalt() master.MustInitMasterService() router := bizmaster.NewRouter(fs) diff --git a/cmd/server.go b/cmd/server.go index 07014b1..29c31a1 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -7,11 +7,13 @@ import ( "github.com/VaalaCat/frp-panel/services/api" "github.com/VaalaCat/frp-panel/services/rpcclient" "github.com/VaalaCat/frp-panel/watcher" + "github.com/fatedier/golib/crypto" "github.com/sirupsen/logrus" "github.com/sourcegraph/conc" ) func runServer(clientID, clientSecret string) { + crypto.DefaultSalt = conf.Get().App.GlobalSecret logrus.Infof("start to run server") if len(clientID) == 0 { diff --git a/conf/helper.go b/conf/helper.go index 928c77d..64cf1f0 100644 --- a/conf/helper.go +++ b/conf/helper.go @@ -11,6 +11,14 @@ import ( "github.com/sirupsen/logrus" ) +func MasterDefaultSalt() string { + cfg := Get() + return utils.MD5(fmt.Sprintf("salt_%s:%d:%s", + cfg.Master.InternalFRPServerHost, + cfg.Master.InternalFRPServerPort, + cfg.App.GlobalSecret)) +} + func RPCListenAddr() string { cfg := Get() return fmt.Sprintf(":%d", cfg.Master.RPCPort) diff --git a/conf/settings.go b/conf/settings.go index 398d699..0e351a6 100644 --- a/conf/settings.go +++ b/conf/settings.go @@ -15,6 +15,7 @@ type Config struct { CookieDomain string `env:"COOKIE_DOMAIN" env-default:"" env-description:"cookie domain"` CookieSecure bool `env:"COOKIE_SECURE" env-default:"false" env-description:"cookie secure"` CookieHTTPOnly bool `env:"COOKIE_HTTP_ONLY" env-default:"true" env-description:"cookie http only"` + EnableRegister bool `env:"ENABLE_REGISTER" env-default:"false" env-description:"enable register, only allow the first admin to register"` } `env-prefix:"APP_"` Master struct { APIPort int `env:"API_PORT" env-default:"9000" env-description:"master api port"` @@ -33,7 +34,7 @@ type Config struct { } `env-prefix:"SERVER_"` DB struct { Type string `env:"TYPE" env-default:"sqlite3" env-description:"db type, mysql or sqlite3 and so on"` - DSN string `env:"DSN" env-default:"data.db" env-description:"db dsn, for sqlite is path, other is dsn, look at https://github.com/go-sql-driver/mysql#dsn-data-source-name"` + DSN string `env:"DSN" env-default:"/data/data.db" env-description:"db dsn, for sqlite is path, other is dsn, look at https://github.com/go-sql-driver/mysql#dsn-data-source-name"` } `env-prefix:"DB_"` } diff --git a/dao/user.go b/dao/user.go index 5a0e766..62b16cb 100644 --- a/dao/user.go +++ b/dao/user.go @@ -21,6 +21,16 @@ func AdminGetAllUsers() ([]*models.UserEntity, error) { }), nil } +func AdminCountUsers() (int64, error) { + db := models.GetDBManager().GetDefaultDB() + var count int64 + err := db.Model(&models.User{}).Count(&count).Error + if err != nil { + return 0, err + } + return count, nil +} + func GetUserByUserID(userID int) (*models.UserEntity, error) { if userID == 0 { return nil, fmt.Errorf("invalid user id") diff --git a/doc/arch.png b/doc/arch.png new file mode 100644 index 0000000..09346ff Binary files /dev/null and b/doc/arch.png differ diff --git a/doc/arch.svg b/doc/arch.svg new file mode 100644 index 0000000..ca8535d --- /dev/null +++ b/doc/arch.svg @@ -0,0 +1,21 @@ + + + + + + + + Client (FRPC)Client (FRPC)Client (FRPC)Client (FRPC)MasterServer (FRPS)Client (FRPC)get config获取配置get config获取配置connect via config用配置信息连接frpsAdmin管理员。◕‿◕。visitor访问者 \ No newline at end of file diff --git a/doc/clients_menu.png b/doc/clients_menu.png new file mode 100644 index 0000000..70cc646 Binary files /dev/null and b/doc/clients_menu.png differ diff --git a/doc/create_client.png b/doc/create_client.png new file mode 100644 index 0000000..4d450c8 Binary files /dev/null and b/doc/create_client.png differ diff --git a/doc/create_server.png b/doc/create_server.png new file mode 100644 index 0000000..2a206d0 Binary files /dev/null and b/doc/create_server.png differ diff --git a/doc/edit_client.png b/doc/edit_client.png new file mode 100644 index 0000000..d7009e8 Binary files /dev/null and b/doc/edit_client.png differ diff --git a/doc/edit_client_adv.png b/doc/edit_client_adv.png new file mode 100644 index 0000000..7d7978d Binary files /dev/null and b/doc/edit_client_adv.png differ diff --git a/doc/edit_server.png b/doc/edit_server.png new file mode 100644 index 0000000..148e52f Binary files /dev/null and b/doc/edit_server.png differ diff --git a/doc/edit_server_adv.png b/doc/edit_server_adv.png new file mode 100644 index 0000000..35a91d4 Binary files /dev/null and b/doc/edit_server_adv.png differ diff --git a/doc/frp-panel-demo.gif b/doc/frp-panel-demo.gif new file mode 100644 index 0000000..11be3d4 Binary files /dev/null and b/doc/frp-panel-demo.gif differ diff --git a/doc/frp-panel-demo.mp4 b/doc/frp-panel-demo.mp4 new file mode 100644 index 0000000..71af009 Binary files /dev/null and b/doc/frp-panel-demo.mp4 differ diff --git a/doc/login.png b/doc/login.png new file mode 100644 index 0000000..7791013 Binary files /dev/null and b/doc/login.png differ diff --git a/doc/platform_info.png b/doc/platform_info.png new file mode 100644 index 0000000..b344ef8 Binary files /dev/null and b/doc/platform_info.png differ diff --git a/doc/register.png b/doc/register.png new file mode 100644 index 0000000..0367e14 Binary files /dev/null and b/doc/register.png differ diff --git a/doc/server_menu.png b/doc/server_menu.png new file mode 100644 index 0000000..1cc67af Binary files /dev/null and b/doc/server_menu.png differ diff --git a/idl/api_user.proto b/idl/api_user.proto index 5d65611..63fdec2 100644 --- a/idl/api_user.proto +++ b/idl/api_user.proto @@ -29,4 +29,6 @@ message GetPlatformInfoResponse { int32 unconfigured_server_count = 5; int32 configured_client_count = 6; int32 configured_server_count = 7; + string global_secret = 8; + string master_rpc_host = 9; } \ No newline at end of file diff --git a/pb/api_user.pb.go b/pb/api_user.pb.go index 108122f..44ff2b4 100644 --- a/pb/api_user.pb.go +++ b/pb/api_user.pb.go @@ -257,6 +257,8 @@ type GetPlatformInfoResponse struct { UnconfiguredServerCount int32 `protobuf:"varint,5,opt,name=unconfigured_server_count,json=unconfiguredServerCount,proto3" json:"unconfigured_server_count,omitempty"` ConfiguredClientCount int32 `protobuf:"varint,6,opt,name=configured_client_count,json=configuredClientCount,proto3" json:"configured_client_count,omitempty"` ConfiguredServerCount int32 `protobuf:"varint,7,opt,name=configured_server_count,json=configuredServerCount,proto3" json:"configured_server_count,omitempty"` + GlobalSecret string `protobuf:"bytes,8,opt,name=global_secret,json=globalSecret,proto3" json:"global_secret,omitempty"` + MasterRpcHost string `protobuf:"bytes,9,opt,name=master_rpc_host,json=masterRpcHost,proto3" json:"master_rpc_host,omitempty"` } func (x *GetPlatformInfoResponse) Reset() { @@ -340,6 +342,20 @@ func (x *GetPlatformInfoResponse) GetConfiguredServerCount() int32 { return 0 } +func (x *GetPlatformInfoResponse) GetGlobalSecret() string { + if x != nil { + return x.GlobalSecret + } + return "" +} + +func (x *GetPlatformInfoResponse) GetMasterRpcHost() string { + if x != nil { + return x.MasterRpcHost + } + return "" +} + var File_api_user_proto protoreflect.FileDescriptor var file_api_user_proto_rawDesc = []byte{ @@ -368,7 +384,7 @@ var file_api_user_proto_rawDesc = []byte{ 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x18, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x95, 0x03, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x49, + 0xe2, 0x03, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x06, 0x73, @@ -392,9 +408,14 @@ var file_api_user_proto_rawDesc = []byte{ 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, - 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x09, 0x0a, 0x07, - 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e, 0x2e, 0x2f, 0x70, 0x62, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, + 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x72, 0x70, 0x63, 0x5f, + 0x68, 0x6f, 0x73, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x73, 0x74, + 0x65, 0x72, 0x52, 0x70, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/www/components/client_item.tsx b/www/components/client_item.tsx index 9a66661..8bee58c 100644 --- a/www/components/client_item.tsx +++ b/www/components/client_item.tsx @@ -27,6 +27,8 @@ import { ExecCommandStr } from "@/lib/consts" import { useMutation, useQuery } from "@tanstack/react-query" import { deleteClient, listClient } from "@/api/client" import { useRouter } from "next/router" +import { useStore } from "@nanostores/react" +import { $platformInfo } from "@/store/user" export type ClientTableSchema = { id: string, @@ -76,13 +78,18 @@ export const columns: ColumnDef[] = [ export const ClientSecret = ({ client }: { client: ClientTableSchema }) => { const [showSecrect, setShowSecrect] = useState(false) const fakeSecret = Array.from({ length: client.secret.length }).map(() => '*').join('') + const platformInfo = useStore($platformInfo) const { toast } = useToast() return
setShowSecrect(true)} onMouseLeave={() => setShowSecrect(false)} onClick={() => { - navigator.clipboard.writeText(ExecCommandStr("client", client)); - toast({ description: "复制成功", }); + if (platformInfo) { + navigator.clipboard.writeText(ExecCommandStr("client", client, platformInfo)); + toast({ description: "复制成功", }); + } else { + toast({ description: "获取平台信息失败", }); + } }} className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono">{ showSecrect ? client.secret : fakeSecret @@ -97,6 +104,7 @@ export interface ClientItemProps { export const ClientActions: React.FC = ({ client, table }) => { const { toast } = useToast() const router = useRouter(); + const platformInfo = useStore($platformInfo) const fetchDataOptions = { pageIndex: table.getState().pagination.pageIndex, pageSize: table.getState().pagination.pageSize, @@ -135,8 +143,12 @@ export const ClientActions: React.FC = ({ client, table }) => { 操作 { - navigator.clipboard.writeText(ExecCommandStr("client", client)); - toast({ description: "复制成功", }); + if (platformInfo) { + navigator.clipboard.writeText(ExecCommandStr("client", client, platformInfo)); + toast({ description: "复制成功", }); + } else { + toast({ description: "获取平台信息失败", }); + } }} > 复制启动命令 diff --git a/www/components/platforminfo.tsx b/www/components/platforminfo.tsx index 49e480e..12fa31b 100644 --- a/www/components/platforminfo.tsx +++ b/www/components/platforminfo.tsx @@ -2,11 +2,16 @@ import { useQuery } from '@tanstack/react-query'; import { getPlatformInfo } from '@/api/user'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { TbDeviceHeartMonitor, TbEngine, TbEngineOff, TbServer2, TbServerBolt, TbServerOff } from 'react-icons/tb'; +import { useEffect } from 'react'; +import { $platformInfo } from '@/store/user'; export const PlatformInfo = () => { const platformInfo = useQuery({ queryKey: ['platformInfo'], queryFn: getPlatformInfo }) + useEffect(() => { + $platformInfo.set(platformInfo.data) + }, [platformInfo]) return (
diff --git a/www/components/server_item.tsx b/www/components/server_item.tsx index da005a7..d2ce1c1 100644 --- a/www/components/server_item.tsx +++ b/www/components/server_item.tsx @@ -26,6 +26,9 @@ import { ExecCommandStr } from "@/lib/consts" import { useMutation, useQuery } from "@tanstack/react-query" import { deleteServer, listServer } from "@/api/server" import { useRouter } from "next/router" +import { getUserInfo } from "@/api/user" +import { useStore } from "@nanostores/react" +import { $platformInfo } from "@/store/user" export type ServerTableSchema = { id: string, @@ -85,12 +88,18 @@ export const ServerSecret = ({ server }: { server: ServerTableSchema }) => { const [showSecrect, setShowSecrect] = useState(false) const fakeSecret = Array.from({ length: server.secret.length }).map(() => '*').join('') const { toast } = useToast() + const platformInfo = useStore($platformInfo) + return
setShowSecrect(true)} onMouseLeave={() => setShowSecrect(false)} onClick={() => { - navigator.clipboard.writeText(ExecCommandStr("server", server)); - toast({ description: "复制成功", }); + if (platformInfo) { + navigator.clipboard.writeText(ExecCommandStr("server", server, platformInfo)); + toast({ description: "复制成功", }); + } else { + toast({ description: "获取平台信息失败", }); + } }} className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono">{ showSecrect ? server.secret : fakeSecret @@ -105,6 +114,7 @@ export interface ServerItemProps { export const ServerActions: React.FC = ({ server, table }) => { const { toast } = useToast() const router = useRouter(); + const platformInfo = useStore($platformInfo) const fetchDataOptions = { pageIndex: table.getState().pagination.pageIndex, @@ -142,8 +152,12 @@ export const ServerActions: React.FC = ({ server, table }) => { 操作 { - navigator.clipboard.writeText(ExecCommandStr("server", server)); - toast({ description: "复制成功", }); + if (platformInfo) { + navigator.clipboard.writeText(ExecCommandStr("server", server, platformInfo)); + toast({ description: "复制成功", }); + } else { + toast({ description: "获取平台信息失败", }); + } }} > 复制启动命令 diff --git a/www/lib/consts.ts b/www/lib/consts.ts index b4926c2..fdda0aa 100644 --- a/www/lib/consts.ts +++ b/www/lib/consts.ts @@ -1,5 +1,6 @@ import * as z from "zod" import { Client, Server } from "./pb/common" +import { GetPlatformInfoResponse } from "./pb/api_user" export const API_PATH = '/api/v1' export const SET_TOKEN_HEADER = 'x-set-authorization' @@ -17,6 +18,6 @@ export const ZodEmailSchema = z.string() .email("是不是输错了邮箱地址呢?") // .refine((e) => e === "abcd@fg.com", "This email is not in our database") -export const ExecCommandStr = (type: string, item: T) => { - return `frp-panel ${type} -s ${item.secret} -i ${item.id}` +export const ExecCommandStr = (type: string, item: T, info: GetPlatformInfoResponse) => { + return `APP_GLOBAL_SECRET=${info.globalSecret} MASTER_RPC_HOST=${info.masterRpcHost} frp-panel ${type} -s ${item.secret} -i ${item.id}` } \ No newline at end of file diff --git a/www/lib/pb/api_user.ts b/www/lib/pb/api_user.ts index aaeb97a..87c9062 100644 --- a/www/lib/pb/api_user.ts +++ b/www/lib/pb/api_user.ts @@ -85,6 +85,14 @@ export interface GetPlatformInfoResponse { * @generated from protobuf field: int32 configured_server_count = 7; */ configuredServerCount: number; + /** + * @generated from protobuf field: string global_secret = 8; + */ + globalSecret: string; + /** + * @generated from protobuf field: string master_rpc_host = 9; + */ + masterRpcHost: string; } // @generated message type with reflection information, may provide speed optimized methods class GetUserInfoRequest$Type extends MessageType { @@ -291,7 +299,9 @@ class GetPlatformInfoResponse$Type extends MessageType { no: 4, name: "unconfigured_client_count", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, { no: 5, name: "unconfigured_server_count", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, { no: 6, name: "configured_client_count", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, - { no: 7, name: "configured_server_count", kind: "scalar", T: 5 /*ScalarType.INT32*/ } + { no: 7, name: "configured_server_count", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 8, name: "global_secret", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 9, name: "master_rpc_host", kind: "scalar", T: 9 /*ScalarType.STRING*/ } ]); } create(value?: PartialMessage): GetPlatformInfoResponse { @@ -302,6 +312,8 @@ class GetPlatformInfoResponse$Type extends MessageType message.unconfiguredServerCount = 0; message.configuredClientCount = 0; message.configuredServerCount = 0; + message.globalSecret = ""; + message.masterRpcHost = ""; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -332,6 +344,12 @@ class GetPlatformInfoResponse$Type extends MessageType case /* int32 configured_server_count */ 7: message.configuredServerCount = reader.int32(); break; + case /* string global_secret */ 8: + message.globalSecret = reader.string(); + break; + case /* string master_rpc_host */ 9: + message.masterRpcHost = reader.string(); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -365,6 +383,12 @@ class GetPlatformInfoResponse$Type extends MessageType /* int32 configured_server_count = 7; */ if (message.configuredServerCount !== 0) writer.tag(7, WireType.Varint).int32(message.configuredServerCount); + /* string global_secret = 8; */ + if (message.globalSecret !== "") + writer.tag(8, WireType.LengthDelimited).string(message.globalSecret); + /* string master_rpc_host = 9; */ + if (message.masterRpcHost !== "") + writer.tag(9, WireType.LengthDelimited).string(message.masterRpcHost); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); diff --git a/www/store/user.ts b/www/store/user.ts index 9ae14f1..c4e4112 100644 --- a/www/store/user.ts +++ b/www/store/user.ts @@ -1,6 +1,8 @@ +import { GetPlatformInfoResponse } from '@/lib/pb/api_user' import { User } from '@/lib/pb/common' import { atom } from 'nanostores' export const $userInfo = atom() export const $statusOnline = atom(false) -export const $token = atom() \ No newline at end of file +export const $token = atom() +export const $platformInfo = atom() \ No newline at end of file