From e06b92b216d4839c3a212a2d144f4a698e5f14a6 Mon Sep 17 00:00:00 2001 From: VaalaCat Date: Mon, 28 Apr 2025 11:31:30 +0000 Subject: [PATCH] feat: rbac and temp node --- biz/master/auth/login.go | 28 +- biz/master/auth/register.go | 7 +- biz/master/client/create_client.go | 4 + biz/master/client/get_client.go | 14 +- biz/master/client/list_client.go | 7 +- biz/master/client/rpc_pull_config.go | 14 +- biz/master/client/update_tunnel.go | 12 +- biz/master/handler.go | 33 ++- biz/master/server/get_server.go | 2 +- biz/master/server/list_server.go | 2 +- biz/master/server/update_tunnel.go | 2 +- biz/master/user/sign_token.go | 45 +++ cmd/frpp/main.go | 11 +- cmd/frpp/{ => shared}/client.go | 2 +- cmd/frpp/{ => shared}/cmd.go | 150 ++++++---- cmd/frpp/{ => shared}/master.go | 3 +- cmd/frpp/{ => shared}/modules.go | 15 +- cmd/frpp/{ => shared}/providers.go | 122 ++++++-- cmd/frpp/{ => shared}/server.go | 2 +- cmd/frpp/{ => shared}/test.go | 2 +- cmd/frppc/client.go | 72 ----- cmd/frppc/cmd.go | 319 --------------------- cmd/frppc/main.go | 29 +- common/context.go | 25 ++ common/request.go | 2 +- common/response.go | 2 +- conf/helper.go | 29 +- defs/const.go | 32 +++ defs/error.go | 1 + defs/rbac_consts.go | 43 +++ docs/zh/deploy-client.md | 6 +- go.mod | 20 +- go.sum | 107 ++++++- idl/api_auth.proto | 15 + idl/api_client.proto | 3 +- idl/common.proto | 4 +- middleware/auth.go | 10 +- middleware/jwt.go | 52 ++-- middleware/rbac.go | 82 ++++++ models/cert.go | 2 +- models/client.go | 11 +- models/db.go | 89 +++--- models/enums.go | 5 +- models/server.go | 4 +- models/user.go | 9 +- models/user_group.go | 18 ++ pb/api_auth.pb.go | 209 +++++++++++++- pb/api_client.pb.go | 17 +- pb/common.pb.go | 30 +- services/app/app_impl.go | 34 +++ services/app/application.go | 7 + services/app/provider.go | 20 +- services/dao/client.go | 29 +- services/dao/user.go | 14 + services/dao/user_group.go | 45 +++ services/master/grpc_server.go | 6 + services/rbac/init.go | 43 +++ services/rbac/perm_manager.go | 82 ++++++ services/rbac/rbac_models.go | 17 ++ services/rpc/master.go | 11 +- utils/ctx.go | 2 +- utils/json.go | 11 + utils/jwt.go | 2 +- www/api/user.ts | 6 + www/components/frpc/client_item.tsx | 4 + www/components/frpc/client_join_button.tsx | 63 +++- www/config/notify.ts | 12 +- www/i18n/locales/en.json | 3 +- www/i18n/locales/zh.json | 3 +- www/lib/consts.ts | 2 +- www/lib/pb/api_auth.ts | 199 +++++++++++++ www/lib/pb/api_client.ts | 13 +- www/lib/pb/common.ts | 24 +- 73 files changed, 1664 insertions(+), 712 deletions(-) create mode 100644 biz/master/user/sign_token.go rename cmd/frpp/{ => shared}/client.go (99%) rename cmd/frpp/{ => shared}/cmd.go (78%) rename cmd/frpp/{ => shared}/master.go (97%) rename cmd/frpp/{ => shared}/modules.go (81%) rename cmd/frpp/{ => shared}/providers.go (74%) rename cmd/frpp/{ => shared}/server.go (99%) rename cmd/frpp/{ => shared}/test.go (97%) delete mode 100644 cmd/frppc/client.go delete mode 100644 cmd/frppc/cmd.go create mode 100644 defs/error.go create mode 100644 defs/rbac_consts.go create mode 100644 middleware/rbac.go create mode 100644 models/user_group.go create mode 100644 services/dao/user_group.go create mode 100644 services/rbac/init.go create mode 100644 services/rbac/perm_manager.go create mode 100644 services/rbac/rbac_models.go create mode 100644 utils/json.go diff --git a/biz/master/auth/login.go b/biz/master/auth/login.go index e96620a..738926e 100644 --- a/biz/master/auth/login.go +++ b/biz/master/auth/login.go @@ -1,13 +1,14 @@ package auth import ( - "fmt" - "github.com/VaalaCat/frp-panel/conf" + "github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/middleware" + "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/services/app" "github.com/VaalaCat/frp-panel/services/dao" + "github.com/VaalaCat/frp-panel/utils/logger" ) func LoginHandler(ctx *app.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) { @@ -24,7 +25,28 @@ func LoginHandler(ctx *app.Context, req *pb.LoginRequest) (*pb.LoginResponse, er }, nil } - tokenStr := conf.GetCommonJWT(ctx.GetApp().GetConfig(), fmt.Sprint(user.GetUserID())) + userCount, err := dao.NewQuery(ctx).AdminCountUsers() + if err != nil { + logger.Logger(ctx).WithError(err).Error("get user count failed") + } + + if userCount == 1 && user.GetSafeUserInfo().Role != defs.UserRole_Admin { + userEntity, ok := user.(models.User) + if !ok { + logger.Logger(ctx).Errorf("trans user entity failed, invalid user entity") + return &pb.LoginResponse{ + Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "invalid user"}, + }, nil + } + + userEntity.Role = defs.UserRole_Admin + + dao.NewQuery(ctx).AdminUpdateUser(&models.UserEntity{ + UserID: user.GetUserID(), + }, userEntity.UserEntity) + } + + tokenStr := conf.GetJWTWithAllPermission(ctx.GetApp().GetConfig(), user.GetUserID()) ginCtx := ctx.GetGinCtx() middleware.PushTokenStr(ginCtx, ctx.GetApp(), tokenStr) diff --git a/biz/master/auth/register.go b/biz/master/auth/register.go index 630cb9d..fc44ea9 100644 --- a/biz/master/auth/register.go +++ b/biz/master/auth/register.go @@ -3,6 +3,7 @@ package auth import ( "fmt" + "github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/services/app" @@ -47,10 +48,14 @@ func RegisterHandler(c *app.Context, req *pb.RegisterRequest) (*pb.RegisterRespo Password: hashedPassword, Email: email, Status: models.STATUS_NORMAL, - Role: models.ROLE_NORMAL, + Role: defs.UserRole_Normal, Token: uuid.New().String(), } + if userCount == 0 { + newUser.Role = defs.UserRole_Admin + } + err = dao.NewQuery(c).CreateUser(newUser) if err != nil { return &pb.RegisterResponse{ diff --git a/biz/master/client/create_client.go b/biz/master/client/create_client.go index 29a1a24..57c22d4 100644 --- a/biz/master/client/create_client.go +++ b/biz/master/client/create_client.go @@ -7,6 +7,7 @@ import ( "github.com/VaalaCat/frp-panel/services/app" "github.com/VaalaCat/frp-panel/services/dao" "github.com/VaalaCat/frp-panel/utils" + "github.com/VaalaCat/frp-panel/utils/logger" "github.com/google/uuid" ) @@ -28,6 +29,8 @@ func InitClientHandler(c *app.Context, req *pb.InitClientRequest) (*pb.InitClien globalClientID := app.GlobalClientID(userInfo.GetUserName(), "c", userClientID) + logger.Logger(c).Infof("start to init client, request:[%s], transformed global client id:[%s]", req.String(), globalClientID) + if err := dao.NewQuery(c).CreateClient(userInfo, &models.ClientEntity{ ClientID: globalClientID, @@ -35,6 +38,7 @@ func InitClientHandler(c *app.Context, req *pb.InitClientRequest) (*pb.InitClien UserID: userInfo.GetUserID(), ConnectSecret: uuid.New().String(), IsShadow: true, + Ephemeral: req.GetEphemeral(), }); err != nil { return &pb.InitClientResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}}, err } diff --git a/biz/master/client/get_client.go b/biz/master/client/get_client.go index 49b6242..686990e 100644 --- a/biz/master/client/get_client.go +++ b/biz/master/client/get_client.go @@ -1,6 +1,8 @@ package client import ( + "strings" + "github.com/VaalaCat/frp-panel/common" "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/pb" @@ -31,6 +33,10 @@ func GetClientHandler(ctx *app.Context, req *pb.GetClientRequest) (*pb.GetClient }, nil } + if !strings.Contains(clientID, ".") { + clientID = app.GlobalClientID(userInfo.GetUserName(), "c", clientID) + } + respCli := &pb.Client{} if len(serverID) == 0 { client, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID) @@ -50,6 +56,8 @@ func GetClientHandler(ctx *app.Context, req *pb.GetClientRequest) (*pb.GetClient Stopped: lo.ToPtr(client.Stopped), Comment: lo.ToPtr(client.Comment), ClientIds: clientIDs, + Ephemeral: &client.Ephemeral, + // LastSeenAt: lo.ToPtr(client.LastSeenAt.UnixMilli()), } } else { client, err := dao.NewQuery(ctx).GetClientByFilter(userInfo, &models.ClientEntity{ @@ -73,9 +81,13 @@ func GetClientHandler(ctx *app.Context, req *pb.GetClientRequest) (*pb.GetClient ServerId: lo.ToPtr(client.ServerID), Stopped: lo.ToPtr(client.Stopped), Comment: lo.ToPtr(client.Comment), - FrpsUrl: lo.ToPtr(client.FRPsUrl), + FrpsUrl: lo.ToPtr(client.FrpsUrl), + Ephemeral: &client.Ephemeral, ClientIds: nil, } + if client.LastSeenAt != nil { + respCli.LastSeenAt = lo.ToPtr(client.LastSeenAt.UnixMilli()) + } } return &pb.GetClientResponse{ diff --git a/biz/master/client/list_client.go b/biz/master/client/list_client.go index 1acf867..eea8973 100644 --- a/biz/master/client/list_client.go +++ b/biz/master/client/list_client.go @@ -59,7 +59,7 @@ func ListClientsHandler(ctx *app.Context, req *pb.ListClientsRequest) (*pb.ListC logger.Logger(ctx).Errorf("get client ids in shadow by client id error: %v", err) } - return &pb.Client{ + respCli := &pb.Client{ Id: lo.ToPtr(c.ClientID), Secret: lo.ToPtr(c.ConnectSecret), Config: lo.ToPtr(string(c.ConfigContent)), @@ -67,7 +67,12 @@ func ListClientsHandler(ctx *app.Context, req *pb.ListClientsRequest) (*pb.ListC Stopped: lo.ToPtr(c.Stopped), Comment: lo.ToPtr(c.Comment), ClientIds: clientIDs, + Ephemeral: lo.ToPtr(c.Ephemeral), } + if c.LastSeenAt != nil { + respCli.LastSeenAt = lo.ToPtr(c.LastSeenAt.UnixMilli()) + } + return respCli }) return &pb.ListClientsResponse{ diff --git a/biz/master/client/rpc_pull_config.go b/biz/master/client/rpc_pull_config.go index aef657b..2cef8c6 100644 --- a/biz/master/client/rpc_pull_config.go +++ b/biz/master/client/rpc_pull_config.go @@ -1,8 +1,6 @@ package client import ( - "fmt" - "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/services/app" @@ -23,6 +21,11 @@ func RPCPullConfig(ctx *app.Context, req *pb.PullClientConfigReq) (*pb.PullClien return nil, err } + if err := dao.NewQuery(ctx).AdminUpdateClientLastSeen(cli.ClientID); err != nil { + logger.Logger(ctx).WithError(err).Errorf("update client last_seen_at time error, req:[%s] clientId:[%s]", + req.String(), cli.ClientID) + } + if cli.IsShadow { proxies, err := dao.NewQuery(ctx).AdminListProxyConfigsWithFilters(&models.ProxyConfigEntity{ OriginClientID: cli.ClientID, @@ -34,7 +37,12 @@ func RPCPullConfig(ctx *app.Context, req *pb.PullClientConfigReq) (*pb.PullClien } if cli.Stopped && cli.IsShadow { - return nil, fmt.Errorf("client is stopped") + return &pb.PullClientConfigResp{ + Client: &pb.Client{ + Id: lo.ToPtr(cli.ClientID), + Stopped: lo.ToPtr(true), + }, + }, nil } return &pb.PullClientConfigResp{ diff --git a/biz/master/client/update_tunnel.go b/biz/master/client/update_tunnel.go index 0972e1c..9702e76 100644 --- a/biz/master/client/update_tunnel.go +++ b/biz/master/client/update_tunnel.go @@ -102,7 +102,7 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC cliCfg.ServerPort = srvConf.BindPort } - if len(req.GetFrpsUrl()) > 0 || len(cli.FRPsUrl) > 0 { + if len(req.GetFrpsUrl()) > 0 || len(cli.FrpsUrl) > 0 { // 有一个有就需要覆盖,优先请求的url var ( parsedFrpsUrl *url.URL @@ -120,21 +120,21 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC urlToParse = req.GetFrpsUrl() } - if len(cli.FRPsUrl) > 0 && parsedFrpsUrl == nil { - parsedFrpsUrl, err = ValidateFrpsUrl(cli.FRPsUrl) + if len(cli.FrpsUrl) > 0 && parsedFrpsUrl == nil { + parsedFrpsUrl, err = ValidateFrpsUrl(cli.FrpsUrl) if err != nil { - logger.Logger(c).WithError(err).Errorf("invalid old frps url, url: [%s]", cli.FRPsUrl) + logger.Logger(c).WithError(err).Errorf("invalid old frps url, url: [%s]", cli.FrpsUrl) return &pb.UpdateFRPCResponse{ Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}, }, err } - urlToParse = cli.FRPsUrl + urlToParse = cli.FrpsUrl } cliCfg.ServerAddr = parsedFrpsUrl.Hostname() cliCfg.ServerPort = cast.ToInt(parsedFrpsUrl.Port()) cliCfg.Transport.Protocol = parsedFrpsUrl.Scheme - cli.FRPsUrl = urlToParse + cli.FrpsUrl = urlToParse } cliCfg.User = userInfo.GetUserName() diff --git a/biz/master/handler.go b/biz/master/handler.go index 0f3f796..36a3100 100644 --- a/biz/master/handler.go +++ b/biz/master/handler.go @@ -27,52 +27,51 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) { router.POST("/auth", auth.MakeGinHandlerFunc(appInstance, auth.HandleLogin)) api := router.Group("/api") - v1 := api.Group("/v1") + api.POST("/v1/auth/cert", app.Wrapper(appInstance, auth.GetClientCert)) + api.POST("/v1/auth/login", app.Wrapper(appInstance, auth.LoginHandler)) + api.POST("/v1/auth/register", app.Wrapper(appInstance, auth.RegisterHandler)) + api.GET("/v1/auth/logout", auth.RemoveJWTHandler(appInstance)) + + v1 := api.Group("/v1", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), middleware.RBAC(appInstance)) { - authRouter := v1.Group("/auth") - { - authRouter.POST("/login", app.Wrapper(appInstance, auth.LoginHandler)) - authRouter.POST("/register", app.Wrapper(appInstance, auth.RegisterHandler)) - authRouter.GET("/logout", auth.RemoveJWTHandler(appInstance)) - authRouter.POST("/cert", app.Wrapper(appInstance, auth.GetClientCert)) - } - userRouter := v1.Group("/user", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance)) + userRouter := v1.Group("/user") { userRouter.POST("/get", app.Wrapper(appInstance, user.GetUserInfoHandler)) userRouter.POST("/update", app.Wrapper(appInstance, user.UpdateUserInfoHander)) + userRouter.POST("/sign-token", app.Wrapper(appInstance, user.SignTokenHandler)) } - platformRouter := v1.Group("/platform", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance)) + platformRouter := v1.Group("/platform") { platformRouter.GET("/baseinfo", platform.GetPlatformInfo(appInstance)) platformRouter.POST("/clientsstatus", app.Wrapper(appInstance, platform.GetClientsStatus)) } - clientRouter := v1.Group("/client", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance)) + clientRouter := v1.Group("/client") { clientRouter.POST("/get", app.Wrapper(appInstance, client.GetClientHandler)) clientRouter.POST("/init", app.Wrapper(appInstance, client.InitClientHandler)) clientRouter.POST("/delete", app.Wrapper(appInstance, client.DeleteClientHandler)) clientRouter.POST("/list", app.Wrapper(appInstance, client.ListClientsHandler)) } - serverRouter := v1.Group("/server", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance)) + serverRouter := v1.Group("/server") { serverRouter.POST("/get", app.Wrapper(appInstance, server.GetServerHandler)) serverRouter.POST("/init", app.Wrapper(appInstance, server.InitServerHandler)) serverRouter.POST("/delete", app.Wrapper(appInstance, server.DeleteServerHandler)) serverRouter.POST("/list", app.Wrapper(appInstance, server.ListServersHandler)) } - frpcRouter := v1.Group("/frpc", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance)) + frpcRouter := v1.Group("/frpc") { frpcRouter.POST("/update", app.Wrapper(appInstance, client.UpdateFrpcHander)) frpcRouter.POST("/delete", app.Wrapper(appInstance, client.RemoveFrpcHandler)) frpcRouter.POST("/stop", app.Wrapper(appInstance, client.StopFRPCHandler)) frpcRouter.POST("/start", app.Wrapper(appInstance, client.StartFRPCHandler)) } - frpsRouter := v1.Group("/frps", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance)) + frpsRouter := v1.Group("/frps") { frpsRouter.POST("/update", app.Wrapper(appInstance, server.UpdateFrpsHander)) frpsRouter.POST("/delete", app.Wrapper(appInstance, server.RemoveFrpsHandler)) } - proxyRouter := v1.Group("/proxy", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance)) + proxyRouter := v1.Group("/proxy") { proxyRouter.POST("/get_by_cid", app.Wrapper(appInstance, proxy.GetProxyStatsByClientID)) proxyRouter.POST("/get_by_sid", app.Wrapper(appInstance, proxy.GetProxyStatsByServerID)) @@ -82,7 +81,7 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) { proxyRouter.POST("/delete_config", app.Wrapper(appInstance, proxy.DeleteProxyConfig)) proxyRouter.POST("/get_config", app.Wrapper(appInstance, proxy.GetProxyConfig)) } - v1.GET("/pty/:clientID", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), shell.PTYHandler(appInstance)) - v1.GET("/log", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), streamlog.GetLogHandler(appInstance)) + v1.GET("/pty/:clientID", shell.PTYHandler(appInstance)) + v1.GET("/log", streamlog.GetLogHandler(appInstance)) } } diff --git a/biz/master/server/get_server.go b/biz/master/server/get_server.go index 96b61bf..7d49fdf 100644 --- a/biz/master/server/get_server.go +++ b/biz/master/server/get_server.go @@ -39,7 +39,7 @@ func GetServerHandler(c *app.Context, req *pb.GetServerRequest) (*pb.GetServerRe Secret: lo.ToPtr(serverEntity.ConnectSecret), Comment: lo.ToPtr(serverEntity.Comment), Ip: lo.ToPtr(serverEntity.ServerIP), - FrpsUrls: serverEntity.FRPsUrls, + FrpsUrls: serverEntity.FrpsUrls, }, }, nil } diff --git a/biz/master/server/list_server.go b/biz/master/server/list_server.go index 60a17f8..b1febd3 100644 --- a/biz/master/server/list_server.go +++ b/biz/master/server/list_server.go @@ -54,7 +54,7 @@ func ListServersHandler(c *app.Context, req *pb.ListServersRequest) (*pb.ListSer Secret: lo.ToPtr(c.ConnectSecret), Ip: lo.ToPtr(c.ServerIP), Comment: lo.ToPtr(c.Comment), - FrpsUrls: c.FRPsUrls, + FrpsUrls: c.FrpsUrls, } }), Total: lo.ToPtr(int32(serverCounts)), diff --git a/biz/master/server/update_tunnel.go b/biz/master/server/update_tunnel.go index c0b83db..e1bab14 100644 --- a/biz/master/server/update_tunnel.go +++ b/biz/master/server/update_tunnel.go @@ -53,7 +53,7 @@ func UpdateFrpsHander(c *app.Context, req *pb.UpdateFRPSRequest) (*pb.UpdateFRPS } if len(req.GetFrpsUrls()) > 0 { - srv.FRPsUrls = req.GetFrpsUrls() + srv.FrpsUrls = req.GetFrpsUrls() } if err := dao.NewQuery(c).UpdateServer(userInfo, srv); err != nil { diff --git a/biz/master/user/sign_token.go b/biz/master/user/sign_token.go new file mode 100644 index 0000000..96bfc32 --- /dev/null +++ b/biz/master/user/sign_token.go @@ -0,0 +1,45 @@ +package user + +import ( + "time" + + "github.com/VaalaCat/frp-panel/common" + "github.com/VaalaCat/frp-panel/conf" + "github.com/VaalaCat/frp-panel/defs" + "github.com/VaalaCat/frp-panel/pb" + "github.com/VaalaCat/frp-panel/services/app" + "github.com/VaalaCat/frp-panel/utils" + "github.com/VaalaCat/frp-panel/utils/logger" + "github.com/samber/lo" +) + +func SignTokenHandler(ctx *app.Context, req *pb.SignTokenRequest) (*pb.SignTokenResponse, error) { + var ( + userInfo = common.GetUserInfo(ctx) + permissions = req.GetPermissions() + expiresIn = req.GetExpiresIn() + cfg = ctx.GetApp().GetConfig() + ) + + token, err := utils.GetJwtTokenFromMap(conf.JWTSecret(cfg), + time.Now().Unix(), + int64(expiresIn), + map[string]interface{}{ + defs.UserIDKey: userInfo.GetUserID(), + defs.TokenPayloadKey_Permissions: permissions, + }) + if err != nil { + logger.Logger(ctx).WithError(err).Errorf("get jwt token failed, req: [%s]", req.String()) + return nil, err + } + + logger.Logger(ctx).Infof("get jwt token success, req: [%s]", req.String()) + + return &pb.SignTokenResponse{ + Token: lo.ToPtr(token), + Status: &pb.Status{ + Code: pb.RespCode_RESP_CODE_SUCCESS, + Message: "ok", + }, + }, nil +} diff --git a/cmd/frpp/main.go b/cmd/frpp/main.go index 8df19fe..bbbe23d 100644 --- a/cmd/frpp/main.go +++ b/cmd/frpp/main.go @@ -1,16 +1,23 @@ package main import ( + "embed" + + "github.com/VaalaCat/frp-panel/cmd/frpp/shared" "github.com/VaalaCat/frp-panel/utils/logger" "github.com/fatedier/golib/crypto" "github.com/spf13/cobra" ) +//go:embed all:out +var fs embed.FS + func main() { crypto.DefaultSalt = "frp" logger.InitLogger() cobra.MousetrapHelpText = "" - rootCmd := buildCommand() - setMasterCommandIfNonePresent(rootCmd) + + rootCmd := shared.BuildCommand(fs) + shared.SetMasterCommandIfNonePresent(rootCmd) rootCmd.Execute() } diff --git a/cmd/frpp/client.go b/cmd/frpp/shared/client.go similarity index 99% rename from cmd/frpp/client.go rename to cmd/frpp/shared/client.go index 11208b6..ddf3e2e 100644 --- a/cmd/frpp/client.go +++ b/cmd/frpp/shared/client.go @@ -1,4 +1,4 @@ -package main +package shared import ( "context" diff --git a/cmd/frpp/cmd.go b/cmd/frpp/shared/cmd.go similarity index 78% rename from cmd/frpp/cmd.go rename to cmd/frpp/shared/cmd.go index b6edee0..f5f72e5 100644 --- a/cmd/frpp/cmd.go +++ b/cmd/frpp/shared/cmd.go @@ -1,7 +1,8 @@ -package main +package shared import ( "context" + "embed" "errors" "fmt" "os" @@ -30,9 +31,10 @@ type CommonArgs struct { RpcPort *int ApiPort *int ApiScheme *string + JoinToken *string } -func buildCommand() *cobra.Command { +func BuildCommand(fs embed.FS) *cobra.Command { cfg := conf.NewConfig() logger.UpdateLoggerOpt( @@ -41,7 +43,7 @@ func buildCommand() *cobra.Command { ) return NewRootCmd( - NewMasterCmd(cfg), + NewMasterCmd(cfg, fs), NewClientCmd(cfg), NewServerCmd(cfg), NewJoinCmd(), @@ -59,6 +61,7 @@ func AddCommonFlags(commonCmd *cobra.Command) { commonCmd.Flags().StringP("id", "i", "", "client id") commonCmd.Flags().String("rpc-url", "", "rpc url, master rpc url, scheme can be grpc/ws/wss://hostname:port") commonCmd.Flags().String("api-url", "", "api url, master api url, scheme can be http/https://hostname:port") + commonCmd.Flags().StringP("join-token", "j", "", "your token from master, auto join with out webui") // deprecated start commonCmd.Flags().StringP("app", "a", "", "app secret") @@ -109,12 +112,11 @@ func GetCommonArgs(cmd *cobra.Command) CommonArgs { commonArgs.ApiScheme = &apiScheme } - return commonArgs -} + if joinToken, err := cmd.Flags().GetString("join-token"); err == nil { + commonArgs.JoinToken = &joinToken + } -type JoinArgs struct { - CommonArgs - JoinToken *string + return commonArgs } func NewJoinCmd() *cobra.Command { @@ -122,29 +124,25 @@ func NewJoinCmd() *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) { + ctx := context.Background() commonArgs := GetCommonArgs(cmd) warnDepParam(cmd) - joinArgs := &JoinArgs{ - CommonArgs: commonArgs, + cli, err := JoinMaster(conf.NewConfig(), commonArgs) + if err != nil { + logger.Logger(ctx).Fatalf("join master failed: %s", err.Error()) } - if joinToken, err := cmd.Flags().GetString("join-token"); err == nil { - joinArgs.JoinToken = &joinToken - } - - appInstance := app.NewApp() - pullRunConfig(appInstance, joinArgs) + saveConfig(ctx, cli, commonArgs) }, } - joinCmd.Flags().StringP("join-token", "j", "", "your token from master") AddCommonFlags(joinCmd) return joinCmd } -func NewMasterCmd(cfg conf.Config) *cobra.Command { +func NewMasterCmd(cfg conf.Config, fs embed.FS) *cobra.Command { return &cobra.Command{ Use: "master", Short: "run frp-panel manager", @@ -159,6 +157,8 @@ func NewMasterCmd(cfg conf.Config) *cobra.Command { fx.Supply( CommonArgs{}, fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)), + fs, + defs.AppRole_Master, ), fx.Provide(fx.Annotate(NewDefaultServerConfig, fx.ResultTags(`name:"defaultServerConfig"`))), fx.Invoke(NewConfigPrinter), @@ -202,6 +202,7 @@ func NewClientCmd(cfg conf.Config) *cobra.Command { fx.Supply( commonArgs, fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)), + defs.AppRole_Client, ), fx.Invoke(NewConfigPrinter), fx.Invoke(runClient), @@ -246,6 +247,7 @@ func NewServerCmd(cfg conf.Config) *cobra.Command { fx.Supply( commonArgs, fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)), + defs.AppRole_Server, ), fx.Invoke(runServer), } @@ -403,7 +405,7 @@ func warnDepParam(cmd *cobra.Command) { } } -func setMasterCommandIfNonePresent(rootCmd *cobra.Command) { +func SetMasterCommandIfNonePresent(rootCmd *cobra.Command) { cmd, _, err := rootCmd.Find(os.Args[1:]) if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp { args := append([]string{"master"}, os.Args[1:]...) @@ -411,96 +413,120 @@ func setMasterCommandIfNonePresent(rootCmd *cobra.Command) { } } -func pullRunConfig(appInstance app.Application, joinArgs *JoinArgs) { +func SetClientCommandIfNonePresent(rootCmd *cobra.Command) { + cmd, _, err := rootCmd.Find(os.Args[1:]) + if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp { + args := append([]string{"client"}, os.Args[1:]...) + rootCmd.SetArgs(args) + } +} + +func JoinMaster(cfg conf.Config, joinArgs CommonArgs) (*pb.Client, error) { c := context.Background() if err := checkPullParams(joinArgs); err != nil { logger.Logger(c).Errorf("check pull params failed: %s", err.Error()) - return - } - - if err := utils.EnsureDirectoryExists(defs.SysEnvPath); err != nil { - logger.Logger(c).Errorf("ensure directory failed: %s", err.Error()) - return + return nil, err } var clientID string if cliID := joinArgs.ClientID; cliID == nil || len(*cliID) == 0 { clientID = utils.GetHostnameWithIP() + } else { + clientID = *cliID } clientID = utils.MakeClientIDPermited(clientID) - patchConfig(appInstance, joinArgs.CommonArgs) - initResp, err := rpc.InitClient(appInstance, clientID, *joinArgs.JoinToken) - if err != nil { - logger.Logger(c).Errorf("init client failed: %s", err.Error()) - return - } - if initResp == nil { - logger.Logger(c).Errorf("init resp is nil") - return - } - if initResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS { - logger.Logger(c).Errorf("init client failed with status: %s", initResp.GetStatus().GetMessage()) - return + logger.Logger(c).Infof("join master with param, clientId:[%s] joinArgs:[%s]", clientID, utils.MarshalForJson(joinArgs)) + + // 检测是否存在已有的client + clientResp, err := rpc.GetClient(cfg, clientID, *joinArgs.JoinToken) + if err != nil || clientResp == nil || clientResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS { + logger.Logger(c).Infof("client [%s] not found, try to init client", clientID) + + // 创建短期client + initResp, err := rpc.InitClient(cfg, clientID, *joinArgs.JoinToken, true) + if err != nil { + logger.Logger(c).Errorf("init client failed: %s", err.Error()) + return nil, err + } + if initResp == nil { + logger.Logger(c).Errorf("init resp is nil") + return nil, err + } + if initResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS { + logger.Logger(c).Errorf("init client failed with status: %s", initResp.GetStatus().GetMessage()) + return nil, err + } + + clientID = initResp.GetClientId() + clientResp, err = rpc.GetClient(cfg, clientID, *joinArgs.JoinToken) + if err != nil { + logger.Logger(c).Errorf("get client failed: %s", err.Error()) + return nil, err + } } - clientID = initResp.GetClientId() - clientResp, err := rpc.GetClient(appInstance, clientID, *joinArgs.JoinToken) - if err != nil { - logger.Logger(c).Errorf("get client failed: %s", err.Error()) - return - } if clientResp == nil { logger.Logger(c).Errorf("client resp is nil") - return + return nil, err } if clientResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS { logger.Logger(c).Errorf("client resp code is not success: %s", clientResp.GetStatus().GetMessage()) - return + return nil, err } client := clientResp.GetClient() if client == nil { logger.Logger(c).Errorf("client is nil") + return nil, err + } + + return client, nil +} + +func saveConfig(ctx context.Context, cli *pb.Client, joinArgs CommonArgs) { + if err := utils.EnsureDirectoryExists(defs.SysEnvPath); err != nil { + logger.Logger(ctx).Errorf("ensure directory failed: %s", err.Error()) return } envMap, err := godotenv.Read(defs.SysEnvPath) if err != nil { envMap = make(map[string]string) - logger.Logger(c).Warnf("read env file failed, try to create: %s", err.Error()) + logger.Logger(ctx).Warnf("read env file failed, try to create: %s", err.Error()) } - envMap[defs.EnvClientID] = clientID - envMap[defs.EnvClientSecret] = client.GetSecret() + envMap[defs.EnvClientID] = cli.GetId() + envMap[defs.EnvClientSecret] = cli.GetSecret() envMap[defs.EnvClientAPIUrl] = *joinArgs.ApiUrl envMap[defs.EnvClientRPCUrl] = *joinArgs.RpcUrl if err = godotenv.Write(envMap, defs.SysEnvPath); err != nil { - logger.Logger(c).Errorf("write env file failed: %s", err.Error()) + logger.Logger(ctx).Errorf("write env file failed: %s", err.Error()) return } - 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]", defs.SysEnvPath, envMap) + logger.Logger(ctx).Infof("config saved to env file: %s, you can use `frp-panel client` without args to run client,\n\nconfig is: [%v]", + defs.SysEnvPath, envMap) } -func checkPullParams(joinArgs *JoinArgs) error { +func checkPullParams(joinArgs CommonArgs) error { if joinToken := joinArgs.JoinToken; joinToken != nil && len(*joinToken) == 0 { return errors.New("join token is empty") } - if apiUrl := joinArgs.ApiUrl; apiUrl == nil || len(*apiUrl) == 0 { - if apiHost := joinArgs.ApiHost; apiHost == nil || len(*apiHost) == 0 { - return errors.New("api host is empty") - } - if apiScheme := joinArgs.ApiScheme; apiScheme == nil || len(*apiScheme) == 0 { - return errors.New("api scheme is empty") - } + var ( + apiUrlAvaliable = joinArgs.ApiUrl != nil && len(*joinArgs.ApiUrl) > 0 + rpcUrlAvaliable = joinArgs.RpcUrl != nil && len(*joinArgs.RpcUrl) > 0 + ) + + if !apiUrlAvaliable { + return errors.New("api url is empty") } - if apiPort := joinArgs.ApiPort; apiPort == nil || *apiPort == 0 { - return errors.New("api port is empty") + if !rpcUrlAvaliable { + return errors.New("rpc url is empty") } return nil diff --git a/cmd/frpp/master.go b/cmd/frpp/shared/master.go similarity index 97% rename from cmd/frpp/master.go rename to cmd/frpp/shared/master.go index 21e969b..0109e1f 100644 --- a/cmd/frpp/master.go +++ b/cmd/frpp/shared/master.go @@ -1,4 +1,4 @@ -package main +package shared import ( "context" @@ -35,6 +35,7 @@ type runMasterParam struct { TaskManager watcher.Client `name:"masterTaskManager"` WsListener *wsgrpc.WSListener DefaultServerConfig conf.Config `name:"defaultServerConfig"` + PermManager app.PermissionManager } func runMaster(param runMasterParam) { diff --git a/cmd/frpp/modules.go b/cmd/frpp/shared/modules.go similarity index 81% rename from cmd/frpp/modules.go rename to cmd/frpp/shared/modules.go index 7bcaad6..458762e 100644 --- a/cmd/frpp/modules.go +++ b/cmd/frpp/shared/modules.go @@ -1,13 +1,14 @@ -package main +package shared import ( "go.uber.org/fx" ) var ( - clientMod = fx.Module("cmd.client", fx.Provide( - fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)), - )) + clientMod = fx.Module("cmd.client", + fx.Provide( + fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)), + )) serverMod = fx.Module("cmd.server", fx.Provide( fx.Annotate(NewServerAPI, fx.ResultTags(`name:"serverApiService"`)), @@ -16,12 +17,13 @@ var ( )) masterMod = fx.Module("cmd.master", fx.Provide( + NewPermissionManager, + NewEnforcer, NewListenerOptions, NewDBManager, NewWSListener, NewMasterTLSConfig, NewWSUpgrader, - NewFs, NewClientLogManager, fx.Annotate(NewWatcher, fx.ResultTags(`name:"masterTaskManager"`)), fx.Annotate(NewMasterRouter, fx.ResultTags(`name:"masterRouter"`)), @@ -34,11 +36,12 @@ var ( )) commonMod = fx.Module("common", fx.Provide( - NewPatchedConfig, NewLogHookManager, NewPTYManager, NewBaseApp, NewContext, NewClientsManager, + NewAutoJoin, + fx.Annotate(NewPatchedConfig, fx.ResultTags(`name:"argsPatchedConfig"`)), )) ) diff --git a/cmd/frpp/providers.go b/cmd/frpp/shared/providers.go similarity index 74% rename from cmd/frpp/providers.go rename to cmd/frpp/shared/providers.go index dce7c92..154f689 100644 --- a/cmd/frpp/providers.go +++ b/cmd/frpp/shared/providers.go @@ -1,4 +1,4 @@ -package main +package shared import ( "context" @@ -15,6 +15,7 @@ import ( "github.com/VaalaCat/frp-panel/biz/master/streamlog" bizserver "github.com/VaalaCat/frp-panel/biz/server" "github.com/VaalaCat/frp-panel/conf" + "github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/services/api" @@ -22,11 +23,13 @@ import ( "github.com/VaalaCat/frp-panel/services/dao" "github.com/VaalaCat/frp-panel/services/master" "github.com/VaalaCat/frp-panel/services/mux" + "github.com/VaalaCat/frp-panel/services/rbac" "github.com/VaalaCat/frp-panel/services/rpc" "github.com/VaalaCat/frp-panel/services/watcher" "github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/utils/logger" "github.com/VaalaCat/frp-panel/utils/wsgrpc" + "github.com/casbin/casbin/v2" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "github.com/gorilla/websocket" @@ -38,9 +41,6 @@ import ( "gorm.io/gorm" ) -//go:embed all:out -var fs embed.FS - func NewLogHookManager() app.StreamLogHookMgr { return &bizcommon.HookMgr{} } @@ -70,10 +70,6 @@ func NewClientsManager() app.ClientsManager { return rpc.NewClientsManager() } -func NewFs() embed.FS { - return fs -} - func NewPatchedConfig(param struct { fx.In @@ -96,7 +92,7 @@ func NewClientLogManager() app.ClientLogManager { func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager { logger.Logger(ctx).Infof("start to init database, type: %s", appInstance.GetConfig().DB.Type) - mgr := models.NewDBManager(nil, appInstance.GetConfig().DB.Type) + mgr := models.NewDBManager(appInstance.GetConfig().DB.Type) appInstance.SetDBManager(mgr) if appInstance.GetConfig().IsDebug { @@ -104,7 +100,7 @@ func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager { } switch appInstance.GetConfig().DB.Type { - case "sqlite3": + case defs.DBTypeSQLite3: if err := utils.EnsureDirectoryExists(appInstance.GetConfig().DB.DSN); err != nil { logger.Logger(ctx).WithError(err).Warnf("ensure directory failed, data location: [%s], keep data in current directory", appInstance.GetConfig().DB.DSN) @@ -117,27 +113,34 @@ func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager { if sqlitedb, err := gorm.Open(sqlite.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil { logger.Logger(ctx).Panic(err) } else { - appInstance.GetDBManager().SetDB("sqlite3", sqlitedb) + appInstance.GetDBManager().SetDB(defs.DBTypeSQLite3, defs.DBRoleDefault, sqlitedb) logger.Logger(ctx).Infof("init database success, data location: [%s]", appInstance.GetConfig().DB.DSN) } - case "mysql": + case defs.DBTypeMysql: if mysqlDB, err := gorm.Open(mysql.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil { logger.Logger(ctx).Panic(err) } else { - appInstance.GetDBManager().SetDB("mysql", mysqlDB) + appInstance.GetDBManager().SetDB(defs.DBTypeMysql, defs.DBRoleDefault, mysqlDB) logger.Logger(ctx).Infof("init database success, data type: [%s]", "mysql") } - case "postgres": + case defs.DBTypePostgres: if postgresDB, err := gorm.Open(postgres.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil { logger.Logger(ctx).Panic(err) } else { - appInstance.GetDBManager().SetDB("postgres", postgresDB) + appInstance.GetDBManager().SetDB(defs.DBTypePostgres, defs.DBRoleDefault, postgresDB) logger.Logger(ctx).Infof("init database success, data type: [%s]", "postgres") } default: logger.Logger(ctx).Panicf("currently unsupported database type: %s", appInstance.GetConfig().DB.Type) } + memoryDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + logger.Logger(ctx).Panic(err) + } + appInstance.GetDBManager().SetDB(defs.DBTypeSQLite3, defs.DBRoleRam, memoryDB) + logger.Logger(ctx).Infof("init memory database success") + appInstance.GetDBManager().Init() return mgr } @@ -279,7 +282,94 @@ func NewDefaultServerConfig(ctx *app.Context) conf.Config { const splitter = "\n--------------------------------------------\n" -func NewConfigPrinter(ctx *app.Context, config conf.Config) { +func NewConfigPrinter(param struct { + fx.In + + Ctx *app.Context + Config conf.Config +}) { + var ( + ctx = param.Ctx + config = param.Config + ) logger.Logger(ctx).Infof("%srunning config is: %s%s", splitter, config.PrintStr(), splitter) logger.Logger(ctx).Infof("%scurrent version: \n%s%s", splitter, conf.GetVersion().String(), splitter) } + +func NewAutoJoin(param struct { + fx.In + + Role defs.AppRole + Ctx *app.Context + Cfg conf.Config `name:"argsPatchedConfig"` + CommonArgs CommonArgs +}) conf.Config { + var ( + ctx = param.Ctx + clientID = param.Cfg.Client.ID + clientSecret = param.Cfg.Client.Secret + autoJoin = false + appInstance = param.Ctx.GetApp() + ) + + appInstance.SetConfig(param.Cfg) + + if param.Role != defs.AppRole_Client { + return param.Cfg + } + + // 用户不输入clientID和clientSecret时,使用autoJoin + if len(clientSecret) == 0 || len(clientID) == 0 { + if param.CommonArgs.JoinToken != nil && len(*param.CommonArgs.JoinToken) > 0 { + autoJoin = true + } else { + if len(clientSecret) == 0 { + logger.Logger(ctx).Fatal("client secret cannot be empty") + } + + if len(clientID) == 0 { + logger.Logger(ctx).Fatal("client id cannot be empty") + } + } + } + + if autoJoin { + logger.Logger(ctx).Infof("start to try join master, clientID: [%s], clientSecret: [%s]", clientID, clientSecret) + cli, err := JoinMaster(param.Cfg, param.CommonArgs) + if err != nil { + logger.Logger(ctx).Fatalf("join master failed: %s", err.Error()) + } + logger.Logger(ctx).Infof("join master success, clientID: [%s], clientInfo: [%s]", cli.GetId(), cli.String()) + tmpCfg := appInstance.GetConfig() + tmpCfg.Client.ID = cli.GetId() + tmpCfg.Client.Secret = cli.GetSecret() + appInstance.SetConfig(tmpCfg) + } + return appInstance.GetConfig() +} + +func NewPermissionManager(param struct { + fx.In + + Enforcer *casbin.Enforcer + AppInstance app.Application +}) app.PermissionManager { + permMgr := rbac.NewPermManager(param.Enforcer) + param.AppInstance.SetPermManager(permMgr) + return permMgr +} + +func NewEnforcer(param struct { + fx.In + + Ctx *app.Context + DBmanager app.DBManager + AppInstance app.Application +}) *casbin.Enforcer { + e, err := rbac.InitializeCasbin(param.Ctx, param.DBmanager.GetDefaultDB()) + if err != nil { + logger.Logger(param.Ctx).WithError(err).Fatal("initialize casbin failed") + } + param.AppInstance.SetEnforcer(e) + return e +} diff --git a/cmd/frpp/server.go b/cmd/frpp/shared/server.go similarity index 99% rename from cmd/frpp/server.go rename to cmd/frpp/shared/server.go index d1cc289..c7e20aa 100644 --- a/cmd/frpp/server.go +++ b/cmd/frpp/shared/server.go @@ -1,4 +1,4 @@ -package main +package shared import ( "context" diff --git a/cmd/frpp/test.go b/cmd/frpp/shared/test.go similarity index 97% rename from cmd/frpp/test.go rename to cmd/frpp/shared/test.go index 9f38051..51e2532 100644 --- a/cmd/frpp/test.go +++ b/cmd/frpp/shared/test.go @@ -1,4 +1,4 @@ -package main +package shared // func serverThings() { // time.Sleep(5 * time.Second) diff --git a/cmd/frppc/client.go b/cmd/frppc/client.go deleted file mode 100644 index b6aa307..0000000 --- a/cmd/frppc/client.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "context" - - bizclient "github.com/VaalaCat/frp-panel/biz/client" - "github.com/VaalaCat/frp-panel/defs" - "github.com/VaalaCat/frp-panel/pb" - "github.com/VaalaCat/frp-panel/services/app" - "github.com/VaalaCat/frp-panel/services/rpc" - "github.com/VaalaCat/frp-panel/services/rpcclient" - "github.com/VaalaCat/frp-panel/services/tunnel" - "github.com/VaalaCat/frp-panel/services/watcher" - "github.com/VaalaCat/frp-panel/utils" - "github.com/VaalaCat/frp-panel/utils/logger" - "github.com/sourcegraph/conc" -) - -func runClient(appInstance app.Application) { - var ( - c = context.Background() - clientID = appInstance.GetConfig().Client.ID - clientSecret = appInstance.GetConfig().Client.Secret - ctx = context.Background() - ) - logger.Logger(c).Infof("start to run client") - if len(clientSecret) == 0 { - logger.Logger(ctx).Fatal("client secret cannot be empty") - } - - if len(clientID) == 0 { - logger.Logger(ctx).Fatal("client id cannot be empty") - } - - cred, err := utils.TLSClientCertNoValidate(rpc.GetClientCert(appInstance, clientID, clientSecret, pb.ClientType_CLIENT_TYPE_FRPC)) - if err != nil { - logger.Logger(ctx).Fatal(err) - } - - appInstance.SetRPCCred(cred) - appInstance.SetMasterCli(rpc.NewMasterCli(appInstance)) - appInstance.SetClientController(tunnel.NewClientController()) - - r := rpcclient.NewClientRPCHandler( - appInstance, - clientID, - clientSecret, - pb.Event_EVENT_REGISTER_CLIENT, - bizclient.HandleServerMessage, - ) - appInstance.SetClientRPCHandler(r) - - w := watcher.NewClient() - w.AddDurationTask(defs.PullConfigDuration, bizclient.PullConfig, clientID, clientSecret) - - initClientOnce(appInstance, clientID, clientSecret) - - defer w.Stop() - defer r.Stop() - - var wg conc.WaitGroup - wg.Go(r.Run) - wg.Go(w.Run) - wg.Wait() -} - -func initClientOnce(appInstance app.Application, clientID, clientSecret string) { - err := bizclient.PullConfig(appInstance, clientID, clientSecret) - if err != nil { - logger.Logger(context.Background()).WithError(err).Errorf("cannot pull client config, wait for retry") - } -} diff --git a/cmd/frppc/cmd.go b/cmd/frppc/cmd.go deleted file mode 100644 index d2cf9d4..0000000 --- a/cmd/frppc/cmd.go +++ /dev/null @@ -1,319 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "os" - - "github.com/VaalaCat/frp-panel/conf" - "github.com/VaalaCat/frp-panel/defs" - "github.com/VaalaCat/frp-panel/pb" - "github.com/VaalaCat/frp-panel/services/app" - "github.com/VaalaCat/frp-panel/services/rpc" - "github.com/VaalaCat/frp-panel/utils" - "github.com/VaalaCat/frp-panel/utils/logger" - "github.com/joho/godotenv" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -var ( - clientCmd *cobra.Command - rootCmd *cobra.Command -) - -func initCommand(appInstance app.Application) { - rootCmd = &cobra.Command{ - Use: "frp-panel", - Short: "frp-panel is a frp panel QwQ", - } - CmdListWithFlag := initCmdWithFlag(appInstance) - CmdListWithoutFlag := initCmdWithoutFlag(appInstance) - rootCmd.AddCommand(CmdListWithFlag...) - rootCmd.AddCommand(CmdListWithoutFlag...) -} - -func initCmdWithFlag(appInstance app.Application) []*cobra.Command { - var ( - clientSecret string - clientID string - rpcHost string - apiHost string - rpcPort int - apiPort int - apiScheme string - joinToken string - rpcUrl string - apiUrl string - ) - - clientCmd = &cobra.Command{ - Use: "client [-s client secret] [-i client id] [-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(appInstance, - apiHost, rpcHost, - clientID, clientSecret, - apiScheme, rpcPort, apiPort, - apiUrl, rpcUrl) - runClient(appInstance) - } - if srv, err := utils.CreateSystemService(args, run); err != nil { - run() - } else { - srv.Run() - } - }, - } - - joinCmd := &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(appInstance, - joinToken, rpcHost, apiScheme, rpcPort, apiPort, clientID, apiHost, apiUrl, rpcUrl) - }, - } - - clientCmd.Flags().StringVarP(&clientSecret, "secret", "s", "", "client secret") - clientCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id") - - clientCmd.Flags().StringVar(&rpcUrl, "rpc-url", "", "rpc url, master rpc url, scheme can be grpc/ws/wss://hostname:port") - - clientCmd.Flags().StringVar(&apiUrl, "api-url", "", "api url, master api url, scheme can be http/https://hostname:port") - - // deprecated start - clientCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "deprecated, use --rpc-url instead, rpc host, canbe ip or domain") - clientCmd.Flags().StringVarP(&apiHost, "api", "t", "", "deprecated, use --api-url instead, api host, canbe ip or domain") - clientCmd.Flags().IntVarP(&rpcPort, "rpc-port", "c", 0, "deprecated, use --rpc-url instead, rpc port, master rpc port, scheme is grpc") - clientCmd.Flags().IntVarP(&apiPort, "api-port", "p", 0, "deprecated, use --api-url instead, api port, master api port, scheme is http/https") - clientCmd.Flags().StringVarP(&apiScheme, "api-scheme", "e", "", "deprecated, use --api-url instead, api scheme, master api scheme, scheme is http/https") - joinCmd.Flags().IntVarP(&rpcPort, "rpc-port", "c", 0, "deprecated, use --rpc-url instead, rpc port, master rpc port, scheme is grpc") - joinCmd.Flags().IntVarP(&apiPort, "api-port", "p", 0, "deprecated, use --api-url instead, api port, master api port, scheme is http/https") - joinCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "deprecated, use --rpc-url instead, rpc host, canbe ip or domain") - joinCmd.Flags().StringVarP(&apiHost, "api", "t", "", "deprecated, use --api-url instead, api host, canbe ip or domain") - joinCmd.Flags().StringVarP(&apiScheme, "api-scheme", "e", "", "deprecated, use --api-url instead, api scheme, master api scheme, scheme is http/https") - // deprecated end - - joinCmd.Flags().StringVarP(&joinToken, "join-token", "j", "", "your token from master") - joinCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id") - joinCmd.Flags().StringVar(&rpcUrl, "rpc-url", "", "rpc url, master rpc url, scheme can be grpc/ws/wss://hostname:port") - joinCmd.Flags().StringVar(&apiUrl, "api-url", "", "api url, master api url, scheme can be http/https://hostname:port") - - return []*cobra.Command{clientCmd, joinCmd} -} - -func initCmdWithoutFlag(appInstance app.Application) []*cobra.Command { - - installServiceCmd := &cobra.Command{ - Use: "install", - Short: "install frp-panel as service", - DisableFlagParsing: true, - DisableFlagsInUseLine: true, - Run: func(cmd *cobra.Command, args []string) { - utils.ControlSystemService(args, "install", func() {}) - }, - } - - uninstallServiceCmd := &cobra.Command{ - Use: "uninstall", - Short: "uninstall frp-panel service", - DisableFlagParsing: true, - DisableFlagsInUseLine: true, - Run: func(cmd *cobra.Command, args []string) { - utils.ControlSystemService(args, "uninstall", func() {}) - }, - } - - startServiceCmd := &cobra.Command{ - Use: "start", - Short: "start frp-panel service", - DisableFlagParsing: true, - DisableFlagsInUseLine: true, - Run: func(cmd *cobra.Command, args []string) { - utils.ControlSystemService(args, "start", func() {}) - }, - } - - stopServiceCmd := &cobra.Command{ - Use: "stop", - Short: "stop frp-panel service", - DisableFlagParsing: true, - DisableFlagsInUseLine: true, - Run: func(cmd *cobra.Command, args []string) { - utils.ControlSystemService(args, "stop", func() {}) - }, - } - - restartServiceCmd := &cobra.Command{ - Use: "restart", - Short: "restart frp-panel service", - DisableFlagParsing: true, - DisableFlagsInUseLine: true, - Run: func(cmd *cobra.Command, args []string) { - utils.ControlSystemService(args, "restart", func() {}) - }, - } - - versionCmd := &cobra.Command{ - Use: "version", - Short: "Print the version info of frp-panel", - Long: `All software has versions. This is frp-panel's`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println(conf.GetVersion().String()) - }, - } - return []*cobra.Command{ - installServiceCmd, - uninstallServiceCmd, - startServiceCmd, - stopServiceCmd, - restartServiceCmd, - versionCmd, - } -} - -func patchConfig(appInstance app.Application, apiHost, rpcHost, clientID, clientSecret, apiScheme string, rpcPort, apiPort int, apiUrl, rpcUrl string) { - c := context.Background() - tmpCfg := appInstance.GetConfig() - if len(rpcHost) != 0 { - tmpCfg.Master.RPCHost = rpcHost - tmpCfg.Master.APIHost = rpcHost - } - - if len(apiHost) != 0 { - tmpCfg.Master.APIHost = apiHost - } - - if rpcPort != 0 { - tmpCfg.Master.RPCPort = rpcPort - } - if apiPort != 0 { - tmpCfg.Master.APIPort = apiPort - } - if len(apiScheme) != 0 { - tmpCfg.Master.APIScheme = apiScheme - } - if len(clientID) != 0 { - tmpCfg.Client.ID = clientID - } - if len(clientSecret) != 0 { - tmpCfg.Client.Secret = clientSecret - } - - if len(apiUrl) != 0 { - tmpCfg.Client.APIUrl = apiUrl - } - if len(rpcUrl) != 0 { - tmpCfg.Client.RPCUrl = rpcUrl - } - - if rpcPort != 0 || apiPort != 0 || len(apiScheme) != 0 || len(rpcHost) != 0 || len(apiHost) != 0 { - logger.Logger(c).Warnf("deprecatedenv configs !!! rpc host: %s, rpc port: %d, api host: %s, api port: %d, api scheme: %s", - tmpCfg.Master.RPCHost, tmpCfg.Master.RPCPort, - tmpCfg.Master.APIHost, tmpCfg.Master.APIPort, - tmpCfg.Master.APIScheme) - } - logger.Logger(c).Infof("env config, api url: %s, rpc url: %s", tmpCfg.Client.APIUrl, tmpCfg.Client.RPCUrl) -} - -func setMasterCommandIfNonePresent() { - cmd, _, err := rootCmd.Find(os.Args[1:]) - if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp { - args := append([]string{"master"}, os.Args[1:]...) - rootCmd.SetArgs(args) - } -} - -func pullRunConfig(appInstance app.Application, joinToken, rpcHost, apiScheme string, rpcPort, apiPort int, clientID, apiHost string, apiUrl, rpcUrl string) { - c := context.Background() - if err := checkPullParams(joinToken, apiHost, apiScheme, apiPort, apiUrl); err != nil { - logger.Logger(c).Errorf("check pull params failed: %s", err.Error()) - return - } - - if err := utils.EnsureDirectoryExists(defs.SysEnvPath); err != nil { - logger.Logger(c).Errorf("ensure directory failed: %s", err.Error()) - return - } - - if len(clientID) == 0 { - clientID = utils.GetHostnameWithIP() - } - - clientID = utils.MakeClientIDPermited(clientID) - patchConfig(appInstance, apiHost, rpcHost, "", "", apiScheme, rpcPort, apiPort, apiUrl, rpcUrl) - - initResp, err := rpc.InitClient(appInstance, clientID, joinToken) - if err != nil { - logger.Logger(c).Errorf("init client failed: %s", err.Error()) - return - } - if initResp == nil { - logger.Logger(c).Errorf("init resp is nil") - return - } - if initResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS { - logger.Logger(c).Errorf("init client failed with status: %s", initResp.GetStatus().GetMessage()) - return - } - - clientID = initResp.GetClientId() - clientResp, err := rpc.GetClient(appInstance, clientID, joinToken) - if err != nil { - logger.Logger(c).Errorf("get client failed: %s", err.Error()) - return - } - if clientResp == nil { - logger.Logger(c).Errorf("client resp is nil") - return - } - if clientResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS { - logger.Logger(c).Errorf("client resp code is not success: %s", clientResp.GetStatus().GetMessage()) - return - } - - client := clientResp.GetClient() - if client == nil { - logger.Logger(c).Errorf("client is nil") - return - } - - envMap, err := godotenv.Read(defs.SysEnvPath) - if err != nil { - envMap = make(map[string]string) - logger.Logger(c).Warnf("read env file failed, try to create: %s", err.Error()) - } - - envMap[defs.EnvClientID] = clientID - envMap[defs.EnvClientSecret] = client.GetSecret() - envMap[defs.EnvClientAPIUrl] = apiUrl - envMap[defs.EnvClientRPCUrl] = rpcUrl - - if err = godotenv.Write(envMap, defs.SysEnvPath); err != nil { - logger.Logger(c).Errorf("write env file failed: %s", err.Error()) - return - } - 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]", defs.SysEnvPath, envMap) -} - -func checkPullParams(joinToken, apiHost, apiScheme string, apiPort int, apiUrl string) error { - if len(joinToken) == 0 { - return errors.New("join token is empty") - } - if len(apiUrl) == 0 { - if len(apiHost) == 0 { - return errors.New("api host is empty") - } - if len(apiScheme) == 0 { - return errors.New("api scheme is empty") - } - } - - if apiPort == 0 { - return errors.New("api port is empty") - } - return nil -} diff --git a/cmd/frppc/main.go b/cmd/frppc/main.go index 0ee52e2..94b1abf 100644 --- a/cmd/frppc/main.go +++ b/cmd/frppc/main.go @@ -1,13 +1,8 @@ package main import ( - "sync" - - "github.com/VaalaCat/frp-panel/biz/common" - "github.com/VaalaCat/frp-panel/biz/master/shell" + "github.com/VaalaCat/frp-panel/cmd/frpp/shared" "github.com/VaalaCat/frp-panel/conf" - "github.com/VaalaCat/frp-panel/services/app" - "github.com/VaalaCat/frp-panel/services/rpc" "github.com/VaalaCat/frp-panel/utils/logger" "github.com/fatedier/golib/crypto" "github.com/spf13/cobra" @@ -15,20 +10,20 @@ import ( func main() { crypto.DefaultSalt = "frp" - logger.InitLogger() cobra.MousetrapHelpText = "" - cfg := conf.NewConfig() - appInstance := app.NewApp() - appInstance.SetConfig(cfg) - appInstance.SetClientsManager(rpc.NewClientsManager()) - appInstance.SetStreamLogHookMgr(&common.HookMgr{}) - appInstance.SetShellPTYMgr(shell.NewPTYMgr()) - appInstance.SetClientRecvMap(&sync.Map{}) + rootCmd := shared.NewRootCmd( + shared.NewClientCmd(conf.NewConfig()), + shared.NewJoinCmd(), + shared.NewInstallServiceCmd(), + shared.NewUninstallServiceCmd(), + shared.NewStartServiceCmd(), + shared.NewStopServiceCmd(), + shared.NewRestartServiceCmd(), + shared.NewVersionCmd(), + ) - initCommand(appInstance) - - setMasterCommandIfNonePresent() + shared.SetClientCommandIfNonePresent(rootCmd) rootCmd.Execute() } diff --git a/common/context.go b/common/context.go index 554b0f7..ab3e847 100644 --- a/common/context.go +++ b/common/context.go @@ -2,6 +2,7 @@ package common import ( "context" + "encoding/json" "github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/models" @@ -20,3 +21,27 @@ func GetUserInfo(c context.Context) models.UserInfo { return u } + +func GetTokenPermission(c context.Context) ([]defs.APIPermission, error) { + val := c.Value(defs.TokenPayloadKey_Permissions) + if val == nil { + return nil, nil + } + + raw, err := json.Marshal(val) + if err != nil { + return nil, err + } + + perms := []defs.APIPermission{} + err = json.Unmarshal(raw, &perms) + if err != nil { + return nil, err + } + + return perms, nil +} + +func GetTokenString(c context.Context) string { + return c.Value(defs.TokenKey).(string) +} diff --git a/common/request.go b/common/request.go index 24aeae0..ec5dd31 100644 --- a/common/request.go +++ b/common/request.go @@ -24,7 +24,7 @@ type ReqType interface { pb.StartFRPCRequest | pb.StopFRPCRequest | pb.StartFRPSRequest | pb.StopFRPSRequest | pb.GetProxyStatsByClientIDRequest | pb.GetProxyStatsByServerIDRequest | pb.CreateProxyConfigRequest | pb.ListProxyConfigsRequest | pb.UpdateProxyConfigRequest | - pb.DeleteProxyConfigRequest | pb.GetProxyConfigRequest + pb.DeleteProxyConfigRequest | pb.GetProxyConfigRequest | pb.SignTokenRequest } func GetProtoRequest[T ReqType](c *gin.Context) (r *T, err error) { diff --git a/common/response.go b/common/response.go index 9e2588c..b01bdc2 100644 --- a/common/response.go +++ b/common/response.go @@ -25,7 +25,7 @@ type RespType interface { pb.StartFRPCResponse | pb.StopFRPCResponse | pb.StartFRPSResponse | pb.StopFRPSResponse | pb.GetProxyStatsByClientIDResponse | pb.GetProxyStatsByServerIDResponse | pb.CreateProxyConfigResponse | pb.ListProxyConfigsResponse | pb.UpdateProxyConfigResponse | - pb.DeleteProxyConfigResponse | pb.GetProxyConfigResponse + pb.DeleteProxyConfigResponse | pb.GetProxyConfigResponse | pb.SignTokenResponse } func OKResp[T RespType](c *gin.Context, origin *T) { diff --git a/conf/helper.go b/conf/helper.go index 892ce19..44b630a 100644 --- a/conf/helper.go +++ b/conf/helper.go @@ -58,19 +58,38 @@ func FRPsAuthOption(cfg Config, isDefault bool) v1.HTTPPluginOptions { } } -func GetCommonJWT(cfg Config, uid string) string { - token, _ := utils.GetJwtTokenFromMap(JWTSecret(cfg), +func GetJWTWithPayload(cfg Config, uid int, payload map[string]interface{}) (string, error) { + payload[defs.UserIDKey] = uid + return utils.GetJwtTokenFromMap(JWTSecret(cfg), time.Now().Unix(), int64(cfg.App.CookieAge), - map[string]string{defs.UserIDKey: uid}) + payload) +} + +func GetJWTWithAllPermission(cfg Config, uid int) string { + token, _ := GetJWTWithPayload(cfg, uid, map[string]interface{}{ + defs.TokenPayloadKey_Permissions: AllPermission(), + }) return token } -func GetCommonJWTWithExpireTime(cfg Config, uid string, expSec int) string { +func AllPermission() []defs.APIPermission { + return []defs.APIPermission{{ + Method: "*", + Path: "*", + }} +} + +func GetCommonJWT(cfg Config, uid int) string { + token, _ := GetJWTWithPayload(cfg, uid, map[string]interface{}{}) + return token +} + +func GetCommonJWTWithExpireTime(cfg Config, uid int64, expSec int) string { token, _ := utils.GetJwtTokenFromMap(JWTSecret(cfg), time.Now().Unix(), int64(expSec), - map[string]string{defs.UserIDKey: uid}) + map[string]interface{}{defs.UserIDKey: uid}) return token } diff --git a/defs/const.go b/defs/const.go index a55772b..bc97368 100644 --- a/defs/const.go +++ b/defs/const.go @@ -54,6 +54,14 @@ const ( CliTypeClient = "client" ) +type AppRole string + +const ( + AppRole_Client AppRole = CliTypeClient + AppRole_Server AppRole = CliTypeServer + AppRole_Master AppRole = "master" +) + const ( DefaultServerID = "default" DefaultAdminUserID = 1 @@ -77,3 +85,27 @@ const ( EnvClientAPIUrl = "CLIENT_API_URL" EnvClientRPCUrl = "CLIENT_RPC_URL" ) + +const ( + DBRoleDefault = "default" + DBRoleRam = "ram" +) + +const ( + DBTypeSQLite3 = "sqlite3" + DBTypeMysql = "mysql" + DBTypePostgres = "postgres" +) + +const ( + UserRole_Admin = "admin" + UserRole_Normal = "normal" +) + +type TokenStatus string + +const ( + TokenStatusActive TokenStatus = "active" + TokenStatusInactive TokenStatus = "inactive" + TokenStatusRevoked TokenStatus = "revoked" +) diff --git a/defs/error.go b/defs/error.go new file mode 100644 index 0000000..d2ca70b --- /dev/null +++ b/defs/error.go @@ -0,0 +1 @@ +package defs diff --git a/defs/rbac_consts.go b/defs/rbac_consts.go new file mode 100644 index 0000000..0c35f72 --- /dev/null +++ b/defs/rbac_consts.go @@ -0,0 +1,43 @@ +package defs + +type RBACAction string + +const ( + RBACActionCreate RBACAction = "create" + RBACActionRead RBACAction = "read" + RBACActionUpdate RBACAction = "update" + RBACActionDelete RBACAction = "delete" +) + +type RBACObj string + +const ( + RBACObjServer RBACObj = "server" + RBACObjClient RBACObj = "client" + RBACObjUser RBACObj = "user" + RBACObjGroup RBACObj = "group" + RBACObjAPI RBACObj = "api" +) + +type RBACSubject string + +const ( + RBACSubjectUser RBACSubject = "user" + RBACSubjectGroup RBACSubject = "group" + RBACSubjectToken RBACSubject = "token" +) + +type RBACDomain string + +const ( + RBACDomainTenant RBACDomain = "tenant" +) + +type APIPermission struct { + Method string `json:"method"` + Path string `json:"path"` +} + +const ( + TokenPayloadKey_Permissions = "permissions" +) diff --git a/docs/zh/deploy-client.md b/docs/zh/deploy-client.md index 31524d9..4b7d339 100644 --- a/docs/zh/deploy-client.md +++ b/docs/zh/deploy-client.md @@ -19,19 +19,19 @@ Client 推荐使用 docker 部署,直接部署在客户机中,可以通过 点击对应客户端的 `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 +curl -fSL 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 +curl -fSL 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 +curl -fSL 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 部署 diff --git a/go.mod b/go.mod index 1a7c049..eba84e1 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ toolchain go1.24.1 require ( github.com/UserExistsError/conpty v0.1.4 + github.com/casbin/casbin/v2 v2.105.0 + github.com/casbin/gorm-adapter/v3 v3.32.0 github.com/coocood/freecache v1.2.4 github.com/creack/pty v1.1.24 github.com/fatedier/frp v0.62.0 @@ -37,9 +39,9 @@ require ( golang.org/x/net v0.39.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.36.5 - gorm.io/driver/mysql v1.5.2 - gorm.io/driver/postgres v1.5.4 - gorm.io/gorm v1.25.5 + gorm.io/driver/mysql v1.5.7 + gorm.io/driver/postgres v1.5.9 + gorm.io/gorm v1.25.12 k8s.io/apimachinery v0.28.8 ) @@ -49,7 +51,9 @@ require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect + github.com/casbin/govaluate v1.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cloudflare/circl v1.6.1 // indirect @@ -68,6 +72,9 @@ require ( github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/mock v1.7.0-rc.1 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect github.com/gorilla/mux v1.8.1 // indirect @@ -77,7 +84,8 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.4.3 // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect @@ -88,6 +96,7 @@ require ( github.com/leodido/go-urn v1.2.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/microsoft/go-mssqldb v1.6.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/onsi/ginkgo/v2 v2.23.4 // indirect @@ -110,7 +119,6 @@ require ( github.com/refraction-networking/utls v1.6.7 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.10.0 // indirect @@ -145,6 +153,8 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/sqlserver v1.5.3 // indirect + gorm.io/plugin/dbresolver v1.5.3 // indirect gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect modernc.org/libc v1.22.5 // indirect diff --git a/go.sum b/go.sum index 5931b05..904a663 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,24 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= @@ -13,9 +31,17 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I= +github.com/casbin/casbin/v2 v2.105.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= +github.com/casbin/gorm-adapter/v3 v3.32.0 h1:Au+IOILBIE9clox5BJhI2nA3p9t7Ep1ePlupdGbGfus= +github.com/casbin/gorm-adapter/v3 v3.32.0/go.mod h1:Zre/H8p17mpv5U3EaWgPoxLILLdXO3gHW5aoQQpUDZI= +github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -38,6 +64,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= @@ -90,10 +118,20 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -117,10 +155,13 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4= github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,6 +169,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/iamacarpet/go-winpty v1.0.4 h1:r42xaLLRZcUqjX6vHZeHos2haACfWkooOJTnFdogBfI= @@ -142,10 +185,18 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= -github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/gateway v1.0.16 h1:mTBRuHSW8qviVqX7kXnxKevqlfS/OA01ys6k6fxSX7w= github.com/jackpal/gateway v1.0.16/go.mod h1:IOn1OUbso/cGYmnCBZbCEqhNCLSz0xxdtIpUpri5/nA= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -170,20 +221,28 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc= +github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= @@ -202,6 +261,8 @@ github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouAN github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -297,6 +358,7 @@ github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lL github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -318,10 +380,14 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= @@ -331,6 +397,7 @@ golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudw golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= @@ -341,10 +408,14 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= @@ -356,6 +427,7 @@ golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= @@ -369,7 +441,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -385,6 +460,7 @@ golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= @@ -393,10 +469,13 @@ golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= @@ -405,14 +484,18 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= @@ -442,6 +525,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -449,13 +533,18 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= -gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= -gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= -gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= -gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= -gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0= +gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU= +gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE= gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d h1:cCKla0V7sa6eixh74LtGQXakTu5QJEzkcX7DzNRhFOE= gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/idl/api_auth.proto b/idl/api_auth.proto index f78a8c2..1651a31 100644 --- a/idl/api_auth.proto +++ b/idl/api_auth.proto @@ -22,4 +22,19 @@ message RegisterRequest { message RegisterResponse { optional common.Status status = 1; +} + +message APIPermission { + optional string method = 1; + optional string path = 2; +} + +message SignTokenRequest { + optional int64 expires_in = 1; + repeated APIPermission permissions = 2; +} + +message SignTokenResponse { + optional common.Status status = 1; + optional string token = 2; } \ No newline at end of file diff --git a/idl/api_client.proto b/idl/api_client.proto index 96c67be..24d2769 100644 --- a/idl/api_client.proto +++ b/idl/api_client.proto @@ -7,6 +7,7 @@ option go_package="../pb"; message InitClientRequest { optional string client_id = 1; + optional bool ephemeral = 2; } message InitClientResponse { @@ -145,4 +146,4 @@ message GetProxyConfigResponse { optional common.Status status = 1; optional common.ProxyConfig proxy_config = 2; optional common.ProxyWorkingStatus working_status = 3; -} \ No newline at end of file +} diff --git a/idl/common.proto b/idl/common.proto index 17a352f..9aa88fb 100644 --- a/idl/common.proto +++ b/idl/common.proto @@ -43,6 +43,8 @@ message Client { repeated string client_ids = 8; // some client can connected to more than one server, make a shadow client to handle this optional string origin_client_id = 9; optional string frps_url = 10; // 客户端用于连接frps的url,解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000 + optional bool ephemeral = 11; // 是否临时节点 + optional int64 last_seen_at = 12; // 最后一次心跳时间戳 } message Server { @@ -93,4 +95,4 @@ message ProxyWorkingStatus { optional string status = 3; optional string err = 4; optional string remote_addr = 5; -} \ No newline at end of file +} diff --git a/middleware/auth.go b/middleware/auth.go index d557871..a4c3d91 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -6,9 +6,9 @@ import ( "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/services/app" "github.com/VaalaCat/frp-panel/services/dao" - "github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/utils/logger" "github.com/gin-gonic/gin" + "github.com/spf13/cast" ) func AuthCtx(appInstance app.Application) func(*gin.Context) { @@ -22,9 +22,9 @@ func AuthCtx(appInstance app.Application) func(*gin.Context) { logger.Logger(c).Info("finish auth user middleware") }() - userID, ok := utils.GetValue[int](c, defs.UserIDKey) - if !ok { - logger.Logger(c).Errorf("invalid user id") + userID, err := cast.ToIntE(c.Value(defs.UserIDKey)) + if err != nil { + logger.Logger(c).WithError(err).Errorf("invalid user id: %v", c.Value(defs.UserIDKey)) common.ErrUnAuthorized(c, "token invalid") c.Abort() return @@ -61,7 +61,7 @@ func AuthCtx(appInstance app.Application) func(*gin.Context) { func AuthAdmin(c *gin.Context) { u := common.GetUserInfo(c) - if u != nil && u.GetRole() == models.ROLE_ADMIN { + if u != nil && u.GetRole() == defs.UserRole_Admin { common.ErrUnAuthorized(c, "permission denied") c.Abort() return diff --git a/middleware/jwt.go b/middleware/jwt.go index ba31476..2ac1402 100644 --- a/middleware/jwt.go +++ b/middleware/jwt.go @@ -13,6 +13,7 @@ import ( "github.com/VaalaCat/frp-panel/utils/logger" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" + "github.com/spf13/cast" ) // JWTMAuth check if authed and set uid to context @@ -30,14 +31,14 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) { c.Set(k, v) } logger.Logger(c).Infof("query auth success") - if err = resignAndPatchCtxJWT(c, appInstance, t, tokenStr); err != nil { + if err = resignAndPatchCtxJWT(c, appInstance, cast.ToInt(t[defs.UserIDKey]), t, tokenStr); err != nil { logger.Logger(c).WithError(err).Errorf("resign jwt error") common.ErrUnAuthorized(c, "resign jwt error") c.Abort() return } c.Next() - SetToken(c, appInstance, utils.ToStr(t[defs.UserIDKey])) + SetToken(c, appInstance, cast.ToInt(t[defs.UserIDKey]), t) return } logger.Logger(c).Infof("query auth failed") @@ -50,7 +51,7 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) { c.Set(k, v) } logger.Logger(c).Infof("cookie auth success") - if err = resignAndPatchCtxJWT(c, appInstance, t, cookieToken); err != nil { + if err = resignAndPatchCtxJWT(c, appInstance, cast.ToInt(t[defs.UserIDKey]), t, cookieToken); err != nil { logger.Logger(c).WithError(err).Errorf("resign jwt error") common.ErrUnAuthorized(c, "resign jwt error") c.Abort() @@ -80,7 +81,7 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) { c.Set(k, v) } logger.Logger(c).Infof("header auth success") - if err = resignAndPatchCtxJWT(c, appInstance, t, tokenStr); err != nil { + if err = resignAndPatchCtxJWT(c, appInstance, cast.ToInt(t[defs.UserIDKey]), t, tokenStr); err != nil { logger.Logger(c).WithError(err).Errorf("resign jwt error") common.ErrUnAuthorized(c, "resign jwt error") c.Abort() @@ -94,7 +95,7 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) { } } -func resignAndPatchCtxJWT(c *gin.Context, appInstance app.Application, t jwt.MapClaims, tokenStr string) error { +func resignAndPatchCtxJWT(c *gin.Context, appInstance app.Application, userID int, t jwt.MapClaims, tokenStr string) error { tokenExpire, _ := t.GetExpirationTime() now := time.Now().Add(time.Duration(appInstance.GetConfig().App.CookieAge/2) * time.Second) if now.Before(tokenExpire.Time) { @@ -103,47 +104,32 @@ func resignAndPatchCtxJWT(c *gin.Context, appInstance app.Application, t jwt.Map return nil } - token, err := utils.GetJwtTokenFromMap(conf.JWTSecret(appInstance.GetConfig()), - time.Now().Unix(), - int64(appInstance.GetConfig().App.CookieAge), - map[string]string{defs.UserIDKey: utils.ToStr(t[defs.UserIDKey])}) + tokenStr, err := SetToken(c, appInstance, userID, t) if err != nil { - c.Set(defs.TokenKey, tokenStr) logger.Logger(c).WithError(err).Errorf("resign jwt error") return err } + PushTokenStr(c, appInstance, tokenStr) + logger.Logger(c).Infof("jwt going to expire, resigning token") - c.Header(defs.SetAuthorizationKey, token) - c.SetCookie(appInstance.GetConfig().App.CookieName, - token, - appInstance.GetConfig().App.CookieAge, - appInstance.GetConfig().App.CookiePath, - appInstance.GetConfig().App.CookieDomain, - appInstance.GetConfig().App.CookieSecure, - appInstance.GetConfig().App.CookieHTTPOnly) - c.Set(defs.TokenKey, token) return nil } -func SetToken(c *gin.Context, appInstance app.Application, uid string) (string, error) { - logger.Logger(c).Infof("set token for uid:[%s]", uid) - token, err := utils.GetJwtTokenFromMap(conf.JWTSecret(appInstance.GetConfig()), - time.Now().Unix(), - int64(appInstance.GetConfig().App.CookieAge), - map[string]string{defs.UserIDKey: uid}) +// SetToken 设置新token并写入ctx +func SetToken(c *gin.Context, appInstance app.Application, userID int, payload jwt.MapClaims) (string, error) { + logger.Logger(c).Debugf("set token for userID:[%d]", userID) + if payload == nil { + payload = make(map[string]interface{}) + } + + payload[defs.UserIDKey] = userID + + token, err := conf.GetJWTWithPayload(appInstance.GetConfig(), userID, payload) if err != nil { return "", err } - c.SetCookie(appInstance.GetConfig().App.CookieName, - token, - appInstance.GetConfig().App.CookieAge, - appInstance.GetConfig().App.CookiePath, - appInstance.GetConfig().App.CookieDomain, - appInstance.GetConfig().App.CookieSecure, - appInstance.GetConfig().App.CookieHTTPOnly) c.Set(defs.TokenKey, token) - c.Header(defs.SetAuthorizationKey, token) return token, nil } diff --git a/middleware/rbac.go b/middleware/rbac.go new file mode 100644 index 0000000..22f1048 --- /dev/null +++ b/middleware/rbac.go @@ -0,0 +1,82 @@ +package middleware + +import ( + "regexp" + + "github.com/VaalaCat/frp-panel/common" + "github.com/VaalaCat/frp-panel/pb" + "github.com/VaalaCat/frp-panel/services/app" + "github.com/VaalaCat/frp-panel/utils" + "github.com/VaalaCat/frp-panel/utils/logger" + "github.com/gin-gonic/gin" +) + +func RBAC(appInstance app.Application) func(*gin.Context) { + return func(c *gin.Context) { + // appCtx := app.NewContext(c, appInstance) + perms, err := common.GetTokenPermission(c) + userInfo := common.GetUserInfo(c) + token := common.GetTokenString(c) + path := c.Request.URL.Path + method := c.Request.Method + + if err != nil { + logger.Logger(c).WithError(err).Errorf("get token permission error, token: [%s], userInfo:[%s]", token, utils.MarshalForJson(userInfo.GetSafeUserInfo())) + common.ErrResp(c, &pb.CommonResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}}, err.Error()) + c.Abort() + return + } + + if len(perms) == 0 { + logger.Logger(c).WithError(err).Errorf("user has no permission, token: [%s], userInfo:[%s]", token, utils.MarshalForJson(userInfo.GetSafeUserInfo())) + common.ErrResp(c, &pb.CommonResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "user has no permission"}}, "user has no permission") + c.Abort() + return + } + + for _, perm := range perms { + if ruleMatched(ruleMatchParam{ + RuleMethod: perm.Method, + RulePath: perm.Path, + RequestPath: path, + RequestMethod: method, + }) { + logger.Logger(c).Infof("user has api permission, continue") + c.Next() + return + } + } + + logger.Logger(c).Errorf("user has no permission, perms: %s, userInfo: [%s], ", utils.MarshalForJson(perms), utils.MarshalForJson(userInfo.GetSafeUserInfo())) + common.ErrResp(c, &pb.CommonResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "user has no permission"}}, "user has no permission") + c.Abort() + return + } +} + +type ruleMatchParam struct { + RuleMethod string + RulePath string + RequestPath string + RequestMethod string +} + +func ruleMatched(param ruleMatchParam) bool { + methodMatch := false + if param.RuleMethod == param.RequestMethod || param.RuleMethod == "*" { + methodMatch = true + } + + if !methodMatch { + return false + } + + pathMatch := false + if param.RulePath == "*" { + pathMatch = true + } else { + pathMatch = regexp.MustCompile(param.RulePath).MatchString(param.RequestPath) + } + + return pathMatch +} diff --git a/models/cert.go b/models/cert.go index ee05f90..60704bb 100644 --- a/models/cert.go +++ b/models/cert.go @@ -4,7 +4,7 @@ import "gorm.io/gorm" type Cert struct { gorm.Model - Name string `gorm:"uniqueIndex"` + Name string `gorm:"type:varchar(255);uniqueIndex"` CertFile []byte KeyFile []byte CaFile []byte diff --git a/models/client.go b/models/client.go index 26120f0..e93d7e8 100644 --- a/models/client.go +++ b/models/client.go @@ -25,10 +25,13 @@ type ClientEntity struct { Comment string `json:"comment"` IsShadow bool `json:"is_shadow" gorm:"index"` OriginClientID string `json:"origin_client_id" gorm:"index"` - FRPsUrl string `json:"frps_url" gorm:"index"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` + FrpsUrl string `json:"frps_url" gorm:"index"` + Ephemeral bool `json:"ephemeral" gorm:"index"` + + LastSeenAt *time.Time `json:"last_seen_at" gorm:"index"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` } func (*Client) TableName() string { diff --git a/models/db.go b/models/db.go index c1ec611..e52b7a8 100644 --- a/models/db.go +++ b/models/db.go @@ -3,79 +3,80 @@ package models import ( "context" + "github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/utils/logger" "gorm.io/gorm" ) -type DBManager interface { - GetDB(dbType string) *gorm.DB - GetDefaultDB() *gorm.DB - SetDB(dbType string, db *gorm.DB) - RemoveDB(dbType string) - SetDebug(bool) - Init() -} - type dbManagerImpl struct { - DBs map[string]*gorm.DB // key: db type + DBs map[string]map[string]*gorm.DB // map[db type]map[db role]*gorm.DB defaultDBType string debug bool } func (dbm *dbManagerImpl) Init() { - for _, db := range dbm.DBs { - if err := db.AutoMigrate(&Client{}); err != nil { - logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Client{}).TableName()) - } - if err := db.AutoMigrate(&User{}); err != nil { - logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&User{}).TableName()) - } - if err := db.AutoMigrate(&Server{}); err != nil { - logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Server{}).TableName()) - } - if err := db.AutoMigrate(&Cert{}); err != nil { - logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Cert{}).TableName()) - } - if err := db.AutoMigrate(&ProxyStats{}); err != nil { - logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyStats{}).TableName()) - } - if err := db.AutoMigrate(&HistoryProxyStats{}); err != nil { - logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&HistoryProxyStats{}).TableName()) - } - if err := db.AutoMigrate(&ProxyConfig{}); err != nil { - logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyConfig{}).TableName()) + for _, dbGroup := range dbm.DBs { + for _, db := range dbGroup { + if err := db.AutoMigrate(&Client{}); err != nil { + logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Client{}).TableName()) + } + if err := db.AutoMigrate(&User{}); err != nil { + logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&User{}).TableName()) + } + if err := db.AutoMigrate(&Server{}); err != nil { + logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Server{}).TableName()) + } + if err := db.AutoMigrate(&Cert{}); err != nil { + logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Cert{}).TableName()) + } + if err := db.AutoMigrate(&ProxyStats{}); err != nil { + logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyStats{}).TableName()) + } + if err := db.AutoMigrate(&HistoryProxyStats{}); err != nil { + logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&HistoryProxyStats{}).TableName()) + } + if err := db.AutoMigrate(&ProxyConfig{}); err != nil { + logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyConfig{}).TableName()) + } + if err := db.AutoMigrate(&UserGroup{}); err != nil { + logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&UserGroup{}).TableName()) + } } } } -func NewDBManager(dbs map[string]*gorm.DB, defaultDBType string) *dbManagerImpl { - if dbs == nil { - dbs = make(map[string]*gorm.DB) - } +func NewDBManager(defaultDBType string) *dbManagerImpl { + dbs := map[string]map[string]*gorm.DB{} return &dbManagerImpl{ DBs: dbs, defaultDBType: defaultDBType, } } -func (dbm *dbManagerImpl) GetDB(dbType string) *gorm.DB { - return dbm.DBs[dbType] +func (dbm *dbManagerImpl) GetDB(dbType string, dbRole string) *gorm.DB { + return dbm.DBs[dbType][dbRole] } -func (dbm *dbManagerImpl) SetDB(dbType string, db *gorm.DB) { - dbm.DBs[dbType] = db +func (dbm *dbManagerImpl) SetDB(dbType string, dbRole string, db *gorm.DB) { + if dbm.DBs[dbType] == nil { + dbm.DBs[dbType] = map[string]*gorm.DB{} + } + dbm.DBs[dbType][dbRole] = db } -func (dbm *dbManagerImpl) RemoveDB(dbType string) { - delete(dbm.DBs, dbType) +func (dbm *dbManagerImpl) RemoveDB(dbType string, dbRole string) { + if dbm.DBs[dbType] == nil { + return + } + delete(dbm.DBs[dbType], dbRole) } func (dbm *dbManagerImpl) GetDefaultDB() *gorm.DB { - db := dbm.DBs[dbm.defaultDBType] + dbGroup := dbm.DBs[dbm.defaultDBType] if dbm.debug { - return db.Debug() + return dbGroup[defs.DBRoleDefault].Debug() } - return db + return dbGroup[defs.DBRoleDefault] } func (dbm *dbManagerImpl) SetDebug(debug bool) { diff --git a/models/enums.go b/models/enums.go index dc29fc4..7c58b39 100644 --- a/models/enums.go +++ b/models/enums.go @@ -1,9 +1,6 @@ package models -const ( - ROLE_ADMIN = "admin" - ROLE_NORMAL = "normal" -) + const ( STATUS_UNKNOWN = iota diff --git a/models/server.go b/models/server.go index 4dd755e..2e051bc 100644 --- a/models/server.go +++ b/models/server.go @@ -16,13 +16,13 @@ type Server struct { type ServerEntity struct { ServerID string `json:"client_id" gorm:"uniqueIndex;not null;primaryKey"` - TenantID int `json:"tenant_id" gorm:"not null"` + TenantID int `json:"tenant_id" gorm:"not null,index"` UserID int `json:"user_id" gorm:"not null"` ServerIP string `json:"server_ip"` ConfigContent []byte `json:"config_content"` ConnectSecret string `json:"connect_secret" gorm:"not null"` Comment string `json:"comment"` - FRPsUrls GormArray[string] `json:"frps_urls"` + FrpsUrls GormArray[string] `json:"frps_urls"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` diff --git a/models/user.go b/models/user.go index d5c1483..d97edf4 100644 --- a/models/user.go +++ b/models/user.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/VaalaCat/frp-panel/defs" "gorm.io/gorm" ) @@ -30,9 +31,9 @@ var _ UserInfo = (*UserEntity)(nil) type UserEntity struct { UserID int `json:"user_id" gorm:"primaryKey"` - UserName string `json:"user_name" gorm:"uniqueIndex;not null"` + UserName string `json:"user_name" gorm:"type:varchar(255);uniqueIndex;not null"` Password string `json:"password"` - Email string `json:"email" gorm:"uniqueIndex;not null"` + Email string `json:"email" gorm:"type:varchar(255);uniqueIndex;not null"` Status int `json:"status"` Role string `json:"role"` TenantID int `json:"tenant_id"` @@ -40,6 +41,8 @@ type UserEntity struct { CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` + + Groups []*UserGroup `json:"groups,omitempty" gorm:"many2many:user_group_memberships;"` } func (u *UserEntity) GetUserID() int { @@ -99,7 +102,7 @@ func (u *UserEntity) Valid() bool { } func (u *UserEntity) IsAdmin() bool { - return u.Role == ROLE_ADMIN + return u.Role == defs.UserRole_Admin } func (u *User) TableName() string { diff --git a/models/user_group.go b/models/user_group.go new file mode 100644 index 0000000..9f00364 --- /dev/null +++ b/models/user_group.go @@ -0,0 +1,18 @@ +package models + +import "time" + +type UserGroup struct { + GroupID string `json:"group_id" gorm:"primaryKey"` + GroupName string `json:"group_name" gorm:"type:varchar(255);uniqueIndex:idx_group_tenant_name;not null"` + TenantID int `json:"tenant_id" gorm:"uniqueIndex:idx_group_tenant_name;not null"` + Comment string `json:"comment"` + CreatedAt time.Time + UpdatedAt time.Time + + Users []*User `json:"users,omitempty" gorm:"many2many:user_group_memberships;"` +} + +func (u *UserGroup) TableName() string { + return "user_groups" +} diff --git a/pb/api_auth.pb.go b/pb/api_auth.pb.go index 08d79a7..fcc993c 100644 --- a/pb/api_auth.pb.go +++ b/pb/api_auth.pb.go @@ -229,6 +229,162 @@ func (x *RegisterResponse) GetStatus() *Status { return nil } +type APIPermission struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method *string `protobuf:"bytes,1,opt,name=method,proto3,oneof" json:"method,omitempty"` + Path *string `protobuf:"bytes,2,opt,name=path,proto3,oneof" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *APIPermission) Reset() { + *x = APIPermission{} + mi := &file_api_auth_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *APIPermission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APIPermission) ProtoMessage() {} + +func (x *APIPermission) ProtoReflect() protoreflect.Message { + mi := &file_api_auth_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APIPermission.ProtoReflect.Descriptor instead. +func (*APIPermission) Descriptor() ([]byte, []int) { + return file_api_auth_proto_rawDescGZIP(), []int{4} +} + +func (x *APIPermission) GetMethod() string { + if x != nil && x.Method != nil { + return *x.Method + } + return "" +} + +func (x *APIPermission) GetPath() string { + if x != nil && x.Path != nil { + return *x.Path + } + return "" +} + +type SignTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ExpiresIn *int64 `protobuf:"varint,1,opt,name=expires_in,json=expiresIn,proto3,oneof" json:"expires_in,omitempty"` + Permissions []*APIPermission `protobuf:"bytes,2,rep,name=permissions,proto3" json:"permissions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignTokenRequest) Reset() { + *x = SignTokenRequest{} + mi := &file_api_auth_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignTokenRequest) ProtoMessage() {} + +func (x *SignTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_auth_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignTokenRequest.ProtoReflect.Descriptor instead. +func (*SignTokenRequest) Descriptor() ([]byte, []int) { + return file_api_auth_proto_rawDescGZIP(), []int{5} +} + +func (x *SignTokenRequest) GetExpiresIn() int64 { + if x != nil && x.ExpiresIn != nil { + return *x.ExpiresIn + } + return 0 +} + +func (x *SignTokenRequest) GetPermissions() []*APIPermission { + if x != nil { + return x.Permissions + } + return nil +} + +type SignTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status *Status `protobuf:"bytes,1,opt,name=status,proto3,oneof" json:"status,omitempty"` + Token *string `protobuf:"bytes,2,opt,name=token,proto3,oneof" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignTokenResponse) Reset() { + *x = SignTokenResponse{} + mi := &file_api_auth_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignTokenResponse) ProtoMessage() {} + +func (x *SignTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_auth_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignTokenResponse.ProtoReflect.Descriptor instead. +func (*SignTokenResponse) Descriptor() ([]byte, []int) { + return file_api_auth_proto_rawDescGZIP(), []int{6} +} + +func (x *SignTokenResponse) GetStatus() *Status { + if x != nil { + return x.Status + } + return nil +} + +func (x *SignTokenResponse) GetToken() string { + if x != nil && x.Token != nil { + return *x.Token + } + return "" +} + var File_api_auth_proto protoreflect.FileDescriptor const file_api_auth_proto_rawDesc = "" + @@ -253,7 +409,22 @@ const file_api_auth_proto_rawDesc = "" + "\x06_email\"J\n" + "\x10RegisterResponse\x12+\n" + "\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01B\t\n" + - "\a_statusB\aZ\x05../pbb\x06proto3" + "\a_status\"Y\n" + + "\rAPIPermission\x12\x1b\n" + + "\x06method\x18\x01 \x01(\tH\x00R\x06method\x88\x01\x01\x12\x17\n" + + "\x04path\x18\x02 \x01(\tH\x01R\x04path\x88\x01\x01B\t\n" + + "\a_methodB\a\n" + + "\x05_path\"\x80\x01\n" + + "\x10SignTokenRequest\x12\"\n" + + "\n" + + "expires_in\x18\x01 \x01(\x03H\x00R\texpiresIn\x88\x01\x01\x129\n" + + "\vpermissions\x18\x02 \x03(\v2\x17.api_auth.APIPermissionR\vpermissionsB\r\n" + + "\v_expires_in\"p\n" + + "\x11SignTokenResponse\x12+\n" + + "\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\x12\x19\n" + + "\x05token\x18\x02 \x01(\tH\x01R\x05token\x88\x01\x01B\t\n" + + "\a_statusB\b\n" + + "\x06_tokenB\aZ\x05../pbb\x06proto3" var ( file_api_auth_proto_rawDescOnce sync.Once @@ -267,22 +438,27 @@ func file_api_auth_proto_rawDescGZIP() []byte { return file_api_auth_proto_rawDescData } -var file_api_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_api_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_api_auth_proto_goTypes = []any{ - (*LoginRequest)(nil), // 0: api_auth.LoginRequest - (*LoginResponse)(nil), // 1: api_auth.LoginResponse - (*RegisterRequest)(nil), // 2: api_auth.RegisterRequest - (*RegisterResponse)(nil), // 3: api_auth.RegisterResponse - (*Status)(nil), // 4: common.Status + (*LoginRequest)(nil), // 0: api_auth.LoginRequest + (*LoginResponse)(nil), // 1: api_auth.LoginResponse + (*RegisterRequest)(nil), // 2: api_auth.RegisterRequest + (*RegisterResponse)(nil), // 3: api_auth.RegisterResponse + (*APIPermission)(nil), // 4: api_auth.APIPermission + (*SignTokenRequest)(nil), // 5: api_auth.SignTokenRequest + (*SignTokenResponse)(nil), // 6: api_auth.SignTokenResponse + (*Status)(nil), // 7: common.Status } var file_api_auth_proto_depIdxs = []int32{ - 4, // 0: api_auth.LoginResponse.status:type_name -> common.Status - 4, // 1: api_auth.RegisterResponse.status:type_name -> common.Status - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 7, // 0: api_auth.LoginResponse.status:type_name -> common.Status + 7, // 1: api_auth.RegisterResponse.status:type_name -> common.Status + 4, // 2: api_auth.SignTokenRequest.permissions:type_name -> api_auth.APIPermission + 7, // 3: api_auth.SignTokenResponse.status:type_name -> common.Status + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_api_auth_proto_init() } @@ -295,13 +471,16 @@ func file_api_auth_proto_init() { file_api_auth_proto_msgTypes[1].OneofWrappers = []any{} file_api_auth_proto_msgTypes[2].OneofWrappers = []any{} file_api_auth_proto_msgTypes[3].OneofWrappers = []any{} + file_api_auth_proto_msgTypes[4].OneofWrappers = []any{} + file_api_auth_proto_msgTypes[5].OneofWrappers = []any{} + file_api_auth_proto_msgTypes[6].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_auth_proto_rawDesc), len(file_api_auth_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/pb/api_client.pb.go b/pb/api_client.pb.go index 2270bd9..f34a4dc 100644 --- a/pb/api_client.pb.go +++ b/pb/api_client.pb.go @@ -24,6 +24,7 @@ const ( type InitClientRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ClientId *string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3,oneof" json:"client_id,omitempty"` + Ephemeral *bool `protobuf:"varint,2,opt,name=ephemeral,proto3,oneof" json:"ephemeral,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -65,6 +66,13 @@ func (x *InitClientRequest) GetClientId() string { return "" } +func (x *InitClientRequest) GetEphemeral() bool { + if x != nil && x.Ephemeral != nil { + return *x.Ephemeral + } + return false +} + type InitClientResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Status *Status `protobuf:"bytes,1,opt,name=status,proto3,oneof" json:"status,omitempty"` @@ -1498,11 +1506,14 @@ var File_api_client_proto protoreflect.FileDescriptor const file_api_client_proto_rawDesc = "" + "\n" + "\x10api_client.proto\x12\n" + - "api_client\x1a\fcommon.proto\"C\n" + + "api_client\x1a\fcommon.proto\"t\n" + "\x11InitClientRequest\x12 \n" + - "\tclient_id\x18\x01 \x01(\tH\x00R\bclientId\x88\x01\x01B\f\n" + + "\tclient_id\x18\x01 \x01(\tH\x00R\bclientId\x88\x01\x01\x12!\n" + + "\tephemeral\x18\x02 \x01(\bH\x01R\tephemeral\x88\x01\x01B\f\n" + "\n" + - "_client_id\"|\n" + + "_client_idB\f\n" + + "\n" + + "_ephemeral\"|\n" + "\x12InitClientResponse\x12+\n" + "\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\x12 \n" + "\tclient_id\x18\x02 \x01(\tH\x01R\bclientId\x88\x01\x01B\t\n" + diff --git a/pb/common.pb.go b/pb/common.pb.go index 63f3610..6a25013 100644 --- a/pb/common.pb.go +++ b/pb/common.pb.go @@ -289,7 +289,9 @@ type Client struct { Stopped *bool `protobuf:"varint,7,opt,name=stopped,proto3,oneof" json:"stopped,omitempty"` ClientIds []string `protobuf:"bytes,8,rep,name=client_ids,json=clientIds,proto3" json:"client_ids,omitempty"` // some client can connected to more than one server, make a shadow client to handle this OriginClientId *string `protobuf:"bytes,9,opt,name=origin_client_id,json=originClientId,proto3,oneof" json:"origin_client_id,omitempty"` - FrpsUrl *string `protobuf:"bytes,10,opt,name=frps_url,json=frpsUrl,proto3,oneof" json:"frps_url,omitempty"` // 客户端用于连接frps的url,解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000 + FrpsUrl *string `protobuf:"bytes,10,opt,name=frps_url,json=frpsUrl,proto3,oneof" json:"frps_url,omitempty"` // 客户端用于连接frps的url,解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000 + Ephemeral *bool `protobuf:"varint,11,opt,name=ephemeral,proto3,oneof" json:"ephemeral,omitempty"` // 是否临时节点 + LastSeenAt *int64 `protobuf:"varint,12,opt,name=last_seen_at,json=lastSeenAt,proto3,oneof" json:"last_seen_at,omitempty"` // 最后一次心跳时间戳 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -387,6 +389,20 @@ func (x *Client) GetFrpsUrl() string { return "" } +func (x *Client) GetEphemeral() bool { + if x != nil && x.Ephemeral != nil { + return *x.Ephemeral + } + return false +} + +func (x *Client) GetLastSeenAt() int64 { + if x != nil && x.LastSeenAt != nil { + return *x.LastSeenAt + } + return 0 +} + type Server struct { state protoimpl.MessageState `protogen:"open.v1"` Id *string `protobuf:"bytes,1,opt,name=id,proto3,oneof" json:"id,omitempty"` @@ -862,7 +878,7 @@ const file_common_proto_rawDesc = "" + "\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\x12\x17\n" + "\x04data\x18\x02 \x01(\tH\x01R\x04data\x88\x01\x01B\t\n" + "\a_statusB\a\n" + - "\x05_data\"\x8a\x03\n" + + "\x05_data\"\xf3\x03\n" + "\x06Client\x12\x13\n" + "\x02id\x18\x01 \x01(\tH\x00R\x02id\x88\x01\x01\x12\x1b\n" + "\x06secret\x18\x02 \x01(\tH\x01R\x06secret\x88\x01\x01\x12\x1b\n" + @@ -874,7 +890,10 @@ const file_common_proto_rawDesc = "" + "client_ids\x18\b \x03(\tR\tclientIds\x12-\n" + "\x10origin_client_id\x18\t \x01(\tH\x06R\x0eoriginClientId\x88\x01\x01\x12\x1e\n" + "\bfrps_url\x18\n" + - " \x01(\tH\aR\afrpsUrl\x88\x01\x01B\x05\n" + + " \x01(\tH\aR\afrpsUrl\x88\x01\x01\x12!\n" + + "\tephemeral\x18\v \x01(\bH\bR\tephemeral\x88\x01\x01\x12%\n" + + "\flast_seen_at\x18\f \x01(\x03H\tR\n" + + "lastSeenAt\x88\x01\x01B\x05\n" + "\x03_idB\t\n" + "\a_secretB\t\n" + "\a_configB\n" + @@ -885,7 +904,10 @@ const file_common_proto_rawDesc = "" + "\n" + "\b_stoppedB\x13\n" + "\x11_origin_client_idB\v\n" + - "\t_frps_url\"\xd8\x01\n" + + "\t_frps_urlB\f\n" + + "\n" + + "_ephemeralB\x0f\n" + + "\r_last_seen_at\"\xd8\x01\n" + "\x06Server\x12\x13\n" + "\x02id\x18\x01 \x01(\tH\x00R\x02id\x88\x01\x01\x12\x1b\n" + "\x06secret\x18\x02 \x01(\tH\x01R\x06secret\x88\x01\x01\x12\x13\n" + diff --git a/services/app/app_impl.go b/services/app/app_impl.go index 5bfa69b..5eac677 100644 --- a/services/app/app_impl.go +++ b/services/app/app_impl.go @@ -4,6 +4,7 @@ import ( "sync" "github.com/VaalaCat/frp-panel/conf" + "github.com/casbin/casbin/v2" "google.golang.org/grpc/credentials" ) @@ -22,6 +23,39 @@ type application struct { serverController ServerController rpcCred credentials.TransportCredentials conf conf.Config + currentRole string + permManager PermissionManager + enforcer *casbin.Enforcer +} + +// GetEnforcer implements Application. +func (a *application) GetEnforcer() *casbin.Enforcer { + return a.enforcer +} + +// SetEnforcer implements Application. +func (a *application) SetEnforcer(c *casbin.Enforcer) { + a.enforcer = c +} + +// GetPermManager implements Application. +func (a *application) GetPermManager() PermissionManager { + return a.permManager +} + +// SetPermManager implements Application. +func (a *application) SetPermManager(p PermissionManager) { + a.permManager = p +} + +// GetCurrentRole implements Application. +func (a *application) GetCurrentRole() string { + return a.currentRole +} + +// SetCurrentRole implements Application. +func (a *application) SetCurrentRole(r string) { + a.currentRole = r } // GetClientCred implements Application. diff --git a/services/app/application.go b/services/app/application.go index 4312c8e..9582411 100644 --- a/services/app/application.go +++ b/services/app/application.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/VaalaCat/frp-panel/conf" + "github.com/casbin/casbin/v2" "github.com/gin-gonic/gin" "google.golang.org/grpc/credentials" ) @@ -36,6 +37,12 @@ type Application interface { SetConfig(conf.Config) GetRPCCred() credentials.TransportCredentials SetRPCCred(credentials.TransportCredentials) + GetCurrentRole() string + SetCurrentRole(string) + GetEnforcer() *casbin.Enforcer + SetEnforcer(*casbin.Enforcer) + GetPermManager() PermissionManager + SetPermManager(PermissionManager) } type Context struct { diff --git a/services/app/provider.go b/services/app/provider.go index d652fc1..7b2847b 100644 --- a/services/app/provider.go +++ b/services/app/provider.go @@ -4,8 +4,10 @@ import ( "sync" "time" + "github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/utils" + "github.com/casbin/casbin/v2" "github.com/fatedier/frp/client/proxy" v1 "github.com/fatedier/frp/pkg/config/v1" @@ -66,10 +68,10 @@ type ClientLogManager interface { // models/db.go type DBManager interface { - GetDB(dbType string) *gorm.DB + GetDB(dbType string, dbRole string) *gorm.DB GetDefaultDB() *gorm.DB - SetDB(dbType string, db *gorm.DB) - RemoveDB(dbType string) + SetDB(dbType string, dbRole string, db *gorm.DB) + RemoveDB(dbType string, dbRole string) SetDebug(bool) Init() } @@ -157,3 +159,15 @@ type ServerHandler interface { type MasterClient interface { Call() pb.MasterClient } + +// services/rbac/perm_manager.go +type PermissionManager interface { + AddUserToGroup(userID int, groupID string, tenantID int) (bool, error) + CheckPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) + Enforcer() *casbin.Enforcer + GrantGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) + GrantUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) + RemoveUserFromGroup(userID int, groupID string, tenantID int) (bool, error) + RevokeGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) + RevokeUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) +} diff --git a/services/dao/client.go b/services/dao/client.go index c1d2ff0..37335f5 100644 --- a/services/dao/client.go +++ b/services/dao/client.go @@ -2,6 +2,7 @@ package dao import ( "fmt" + "time" "github.com/VaalaCat/frp-panel/models" "github.com/samber/lo" @@ -175,8 +176,6 @@ func (q *queryImpl) ListClients(userInfo models.UserInfo, page, pageSize int) ([ Where( db.Where( normalClientFilter(db), - ).Or( - "is_shadow = ?", true, ), ).Offset(offset).Limit(pageSize).Find(&clients).Error if err != nil { @@ -331,10 +330,30 @@ func (q *queryImpl) AdminGetClientIDsInShadowByClientID(clientID string) ([]stri }), nil } +func (q *queryImpl) AdminUpdateClientLastSeen(clientID string) error { + db := q.ctx.GetApp().GetDBManager().GetDefaultDB() + return db.Model(&models.Client{ + ClientEntity: &models.ClientEntity{ + ClientID: clientID, + }}).Update("last_seen_at", time.Now()).Error +} + func normalClientFilter(db *gorm.DB) *gorm.DB { // 1. 没shadow过的老client // 2. shadow过的shadow client - return db.Where("origin_client_id is NULL"). - Or("is_shadow = ?", true). - Or("LENGTH(origin_client_id) = ?", 0) + // 3. 非临时节点 + return db.Where( + db.Where("origin_client_id is NULL"). + Or("is_shadow = ?", true). + Or("LENGTH(origin_client_id) = ?", 0), + ). + Where( + db.Where( + db.Where("ephemeral is NULL"). + Or("ephemeral = ?", false), + ).Or( + db.Where("ephemeral = ?", true). + Where("last_seen_at > ?", time.Now().Add(-5*time.Minute)), + ), + ) } diff --git a/services/dao/user.go b/services/dao/user.go index f72e4b4..ca80905 100644 --- a/services/dao/user.go +++ b/services/dao/user.go @@ -62,6 +62,20 @@ func (q *queryImpl) UpdateUser(userInfo models.UserInfo, user *models.UserEntity }).Error } +func (q *queryImpl) AdminUpdateUser(userInfo models.UserInfo, user *models.UserEntity) error { + db := q.ctx.GetApp().GetDBManager().GetDefaultDB() + user.UserID = userInfo.GetUserID() + return db.Model(&models.User{}).Where( + &models.User{ + UserEntity: &models.UserEntity{ + UserID: userInfo.GetUserID(), + }, + }, + ).Save(&models.User{ + UserEntity: user, + }).Error +} + func (q *queryImpl) GetUserByUserName(userName string) (*models.UserEntity, error) { if userName == "" { return nil, fmt.Errorf("invalid user name") diff --git a/services/dao/user_group.go b/services/dao/user_group.go new file mode 100644 index 0000000..a588194 --- /dev/null +++ b/services/dao/user_group.go @@ -0,0 +1,45 @@ +package dao + +import ( + "fmt" + + "github.com/VaalaCat/frp-panel/defs" + "github.com/VaalaCat/frp-panel/models" +) + +func (q *queryImpl) CreateGroup(userInfo models.UserInfo, groupID, groupName, comment string) (*models.UserGroup, error) { + if groupID == "" || groupName == "" { + return nil, fmt.Errorf("invalid group id or group name") + } + + if userInfo.GetRole() != defs.UserRole_Admin { + return nil, fmt.Errorf("only admin can create group") + } + + db := q.ctx.GetApp().GetDBManager().GetDefaultDB() + + g := &models.UserGroup{ + TenantID: userInfo.GetTenantID(), + GroupID: groupID, + GroupName: groupName, + Comment: comment, + } + err := db.Create(g).Error + if err != nil { + return nil, err + } + + return g, nil +} + +func (q *queryImpl) DeleteGroup(userInfo models.UserInfo, groupID string) error { + if userInfo.GetRole() != defs.UserRole_Admin { + return fmt.Errorf("only admin can delete group") + } + + db := q.ctx.GetApp().GetDBManager().GetDefaultDB() + return db.Unscoped().Where(&models.UserGroup{ + TenantID: userInfo.GetTenantID(), + GroupID: groupID, + }).Delete(&models.UserGroup{}).Error +} diff --git a/services/master/grpc_server.go b/services/master/grpc_server.go index 335649e..461a591 100644 --- a/services/master/grpc_server.go +++ b/services/master/grpc_server.go @@ -134,6 +134,12 @@ func (s *server) ServerSend(sender pb.Master_ServerSendServer) error { return fmt.Errorf("invalid secret, %s id: [%s]", req.GetEvent().String(), req.GetClientId()) } + if cliType == defs.CliTypeClient { + if err := dao.NewQuery(ctx).AdminUpdateClientLastSeen(req.GetClientId()); err != nil { + logger.Logger(ctx).Errorf("cannot update client last seen, %s id: [%s]", req.GetEvent().String(), req.GetClientId()) + } + } + s.appInstance.GetClientsManager().Set(req.GetClientId(), cliType, sender) done = rpc.Recv(s.appInstance, req.GetClientId()) sender.Send(&pb.ServerMessage{ diff --git a/services/rbac/init.go b/services/rbac/init.go new file mode 100644 index 0000000..8f5f92a --- /dev/null +++ b/services/rbac/init.go @@ -0,0 +1,43 @@ +package rbac + +import ( + "github.com/VaalaCat/frp-panel/services/app" + "github.com/VaalaCat/frp-panel/utils/logger" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + "gorm.io/gorm" +) + +func InitializeCasbin(ctx *app.Context, db *gorm.DB) (*casbin.Enforcer, error) { + adapter, err := gormadapter.NewAdapterByDB(db) + if err != nil { + logger.Logger(ctx).Fatalf("error creating Casbin GORM adapter: %v", err) + return nil, err + } + + // Load the Casbin model from file + m, err := model.NewModelFromString(RBAC_MODEL) + if err != nil { + logger.Logger(ctx).Fatalf("error loading Casbin model: %v", err) + return nil, err + } + + // Create the Casbin enforcer + enforcer, err := casbin.NewEnforcer(m, adapter) + if err != nil { + logger.Logger(ctx).Fatalf("error creating Casbin enforcer: %v", err) + return nil, err + } + + err = enforcer.LoadPolicy() + if err != nil { + logger.Logger(ctx).WithError(err).Warnf("could not load Casbin policy from DB") + // return nil, err + } + + enforcer.EnableAutoSave(true) + + logger.Logger(ctx).Infof("Casbin Enforcer initialized successfully.") + return enforcer, nil +} diff --git a/services/rbac/perm_manager.go b/services/rbac/perm_manager.go new file mode 100644 index 0000000..8312a69 --- /dev/null +++ b/services/rbac/perm_manager.go @@ -0,0 +1,82 @@ +package rbac + +import ( + "fmt" + + "github.com/VaalaCat/frp-panel/defs" + "github.com/casbin/casbin/v2" +) + +type permManager struct { + enforcer *casbin.Enforcer +} + +func (pm *permManager) Enforcer() *casbin.Enforcer { + return pm.enforcer +} + +func NewPermManager(enforcer *casbin.Enforcer) *permManager { + return &permManager{ + enforcer: enforcer, + } +} + +func identity[T defs.RBACObj | defs.RBACSubject | defs.RBACDomain, U string | int | uint | int64](rType T, objID U) string { + return string(rType) + ":" + fmt.Sprint(objID) +} + +func (pm *permManager) GrantGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) { + groupSubject := identity(defs.RBACSubjectGroup, groupID) + objSubject := identity(objType, objID) + domain := identity(defs.RBACDomainTenant, tenantID) + + return pm.enforcer.AddPolicy(groupSubject, objSubject, string(action), domain) +} + +func (pm *permManager) RevokeGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) { + groupSubject := identity(defs.RBACSubjectGroup, groupID) + objSubject := identity(objType, objID) + domain := identity(defs.RBACDomainTenant, tenantID) + + return pm.enforcer.RemovePolicy(groupSubject, objSubject, string(action), domain) +} + +func (pm *permManager) GrantUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) { + userSubject := identity(defs.RBACSubjectUser, userID) + objSubject := identity(objType, objID) + domain := identity(defs.RBACDomainTenant, tenantID) + + return pm.enforcer.AddPolicy(userSubject, objSubject, string(action), domain) +} + +func (pm *permManager) RevokeUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) { + userSubject := identity(defs.RBACSubjectUser, userID) + objSubject := identity(objType, objID) + domain := identity(defs.RBACDomainTenant, tenantID) + + return pm.enforcer.RemovePolicy(userSubject, objSubject, string(action), domain) +} + +func (pm *permManager) CheckPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) { + userSubject := identity(defs.RBACSubjectUser, userID) + objSubject := identity(objType, objID) + domain := identity(defs.RBACDomainTenant, tenantID) + + return pm.enforcer.Enforce(userSubject, objSubject, string(action), domain) +} + +func (pm *permManager) AddUserToGroup(userID int, groupID string, tenantID int) (bool, error) { + userSub := identity(defs.RBACSubjectUser, userID) + groupSub := identity(defs.RBACSubjectGroup, groupID) + domain := identity(defs.RBACDomainTenant, tenantID) + + return pm.enforcer.AddGroupingPolicy(userSub, groupSub, domain) +} + +func (pm *permManager) RemoveUserFromGroup(userID int, groupID string, tenantID int) (bool, error) { + userSub := identity(defs.RBACSubjectUser, userID) + groupSub := identity(defs.RBACSubjectGroup, groupID) + domain := identity(defs.RBACDomainTenant, tenantID) + + return pm.enforcer.RemoveGroupingPolicy(userSub, groupSub, domain) +} diff --git a/services/rbac/rbac_models.go b/services/rbac/rbac_models.go new file mode 100644 index 0000000..b94e4d4 --- /dev/null +++ b/services/rbac/rbac_models.go @@ -0,0 +1,17 @@ +package rbac + +const RBAC_MODEL = ` +[request_definition] +r = sub, obj, act, dom + +[policy_definition] +p = sub, obj, act, dom + +[role_definition] +g = _, _, dom + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act` diff --git a/services/rpc/master.go b/services/rpc/master.go index 623d9b6..3a61b74 100644 --- a/services/rpc/master.go +++ b/services/rpc/master.go @@ -105,13 +105,14 @@ func GetClientCert(appInstance app.Application, clientID, clientSecret string, c return resp.Cert } -func InitClient(appInstance app.Application, clientID, joinToken string) (*pb.InitClientResponse, error) { - apiEndpoint := conf.GetAPIURL(appInstance.GetConfig()) +func InitClient(cfg conf.Config, clientID, joinToken string, ephemeral bool) (*pb.InitClientResponse, error) { + apiEndpoint := conf.GetAPIURL(cfg) c := httpCli() rawReq, err := proto.Marshal(&pb.InitClientRequest{ - ClientId: &clientID, + ClientId: &clientID, + Ephemeral: &ephemeral, }) if err != nil { return nil, err @@ -132,8 +133,8 @@ func InitClient(appInstance app.Application, clientID, joinToken string) (*pb.In return resp, nil } -func GetClient(appInstance app.Application, clientID, joinToken string) (*pb.GetClientResponse, error) { - apiEndpoint := conf.GetAPIURL(appInstance.GetConfig()) +func GetClient(cfg conf.Config, clientID, joinToken string) (*pb.GetClientResponse, error) { + apiEndpoint := conf.GetAPIURL(cfg) c := httpCli() rawReq, err := proto.Marshal(&pb.GetClientRequest{ diff --git a/utils/ctx.go b/utils/ctx.go index 169f39c..3cd8c2a 100644 --- a/utils/ctx.go +++ b/utils/ctx.go @@ -18,7 +18,7 @@ func GetValue[T any](c context.Context, key string) (T, bool) { return v, true } -func getValue[T any](c context.Context, key string) (interface{}, bool) { +func getValue[T any](c context.Context, key string) (any, bool) { val := c.Value(key) if val == nil { return *new(T), false diff --git a/utils/json.go b/utils/json.go new file mode 100644 index 0000000..24226cc --- /dev/null +++ b/utils/json.go @@ -0,0 +1,11 @@ +package utils + +import "encoding/json" + +func MarshalForJson(v any) string { + ret, err := json.Marshal(v) + if err != nil { + return "" + } + return string(ret) +} diff --git a/utils/jwt.go b/utils/jwt.go index c0d0ed4..85276d4 100644 --- a/utils/jwt.go +++ b/utils/jwt.go @@ -24,7 +24,7 @@ func GetJwtToken(secretKey string, iat, seconds int64, payload string) (string, // @iat: 时间戳 // @seconds: 过期时间,单位秒 // @payload: 数据载体 -func GetJwtTokenFromMap(secretKey string, iat, seconds int64, payload map[string]string) (string, error) { +func GetJwtTokenFromMap(secretKey string, iat, seconds int64, payload map[string]interface{}) (string, error) { claims := make(jwt.MapClaims) claims["exp"] = iat + seconds claims["iat"] = iat diff --git a/www/api/user.ts b/www/api/user.ts index 64f565f..684f3c4 100644 --- a/www/api/user.ts +++ b/www/api/user.ts @@ -1,5 +1,6 @@ import http from '@/api/http' import { API_PATH } from '@/lib/consts' +import { SignTokenRequest, SignTokenResponse } from '@/lib/pb/api_auth' import { GetUserInfoRequest, GetUserInfoResponse, @@ -20,3 +21,8 @@ export const updateUserInfo = async (req: UpdateUserInfoRequest) => { const res = await http.post(API_PATH + '/user/update', UpdateUserInfoRequest.toJson(req)) return UpdateUserInfoResponse.fromJson((res.data as BaseResponse).body) } + +export const signToken = async (req: SignTokenRequest) => { + const res = await http.post(API_PATH + '/user/sign-token', SignTokenRequest.toJson(req)) + return SignTokenResponse.fromJson((res.data as BaseResponse).body) +} \ No newline at end of file diff --git a/www/components/frpc/client_item.tsx b/www/components/frpc/client_item.tsx index 8a208da..cf97ac0 100644 --- a/www/components/frpc/client_item.tsx +++ b/www/components/frpc/client_item.tsx @@ -227,6 +227,10 @@ export const ClientInfo = ({ client }: { client: ClientTableSchema }) => { {`需要升级!`} } + {client.originClient.ephemeral && + {`临时`} + } + ) } diff --git a/www/components/frpc/client_join_button.tsx b/www/components/frpc/client_join_button.tsx index 45daa84..c874a56 100644 --- a/www/components/frpc/client_join_button.tsx +++ b/www/components/frpc/client_join_button.tsx @@ -1,14 +1,18 @@ import { Button } from '@/components/ui/button' -import React from 'react' +import React, { useState } from 'react' import { JoinCommandStr } from '@/lib/consts' import { useStore } from '@nanostores/react' -import { $platformInfo, $token } from '@/store/user' +import { $platformInfo } from '@/store/user' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { useTranslation } from 'react-i18next' +import { signToken } from '@/api/user' +import { useQuery } from '@tanstack/react-query' +import { toast } from 'sonner' +import { RespCode } from '@/lib/pb/common' export const ClientJoinButton = () => { const platformInfo = useStore($platformInfo) - const token = useStore($token) + const [joinToken, setJoinToken] = useState(undefined) const { t } = useTranslation() @@ -19,6 +23,25 @@ export const ClientJoinButton = () => { ) } + const handleNewToken = async () => { + try { + const resp = await signToken({ + expiresIn: BigInt(1000000000), + permissions: [ + { method: 'POST', path: '/api/v1/client/get', }, + { method: 'POST', path: '/api/v1/client/init', }, + ], + }) + if (!resp || !resp.status || resp.status.code !== RespCode.SUCCESS) { + toast.error('server error') + return + } + setJoinToken(resp.token) + } catch (error) { + toast.error(JSON.stringify(error)) + } + } + return ( @@ -32,24 +55,36 @@ export const ClientJoinButton = () => { {t('client.join.description')} ({t('common.download')})

- {token != undefined &&
-
-              {JoinCommandStr(platformInfo, token)}
-            
+
+ {joinToken != undefined && <> +
+                {JoinCommandStr(platformInfo, joinToken)}
+              
+ + + } -
} +
diff --git a/www/config/notify.ts b/www/config/notify.ts index 8a8c6d5..22a4aaa 100644 --- a/www/config/notify.ts +++ b/www/config/notify.ts @@ -5,5 +5,15 @@ export function NeedUpgrade(version: ClientVersion | undefined) { if (!version.gitVersion) return false const versionString = version?.gitVersion const [a, b, c] = versionString.split('.') - return Number(b) < 1 + if (Number(b) < 1) { + return true + } + + console.log(Number(a), Number(b), Number(c)) + + if (a=='v0' && Number(b)<=1 && Number(c) <= 10) { + return true + } + + return false } \ No newline at end of file diff --git a/www/i18n/locales/en.json b/www/i18n/locales/en.json index 01df0e6..a072d03 100644 --- a/www/i18n/locales/en.json +++ b/www/i18n/locales/en.json @@ -258,7 +258,8 @@ "button": "Batch Configuration", "title": "Batch Configuration", "description": "Download the binary files in advance, run the following command, and the client will automatically generate and save the configuration file.", - "copy": "Copy Command" + "copy": "Copy Command", + "sign_token": "Generate Token" }, "start": { "title": "Start Command", diff --git a/www/i18n/locales/zh.json b/www/i18n/locales/zh.json index b416700..93a2e56 100644 --- a/www/i18n/locales/zh.json +++ b/www/i18n/locales/zh.json @@ -263,7 +263,8 @@ "button": "批量配置", "title": "批量配置", "description": "提前下载好二进制文件,运行以下命令,client 将自动生成配置文件保存", - "copy": "复制命令" + "copy": "复制命令", + "sign_token": "生成令牌" }, "actions_menu": { "title": "客户端操作", diff --git a/www/lib/consts.ts b/www/lib/consts.ts index 0228057..26c6943 100644 --- a/www/lib/consts.ts +++ b/www/lib/consts.ts @@ -78,7 +78,7 @@ export const LinuxInstallCommand = ( item: T, info: GetPlatformInfoResponse, ) => { - return `curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, ' ')}` + return `curl -fSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, ' ')}` } export const ClientEnvFile = ( diff --git a/www/lib/pb/api_auth.ts b/www/lib/pb/api_auth.ts index 78f0f1d..d638d20 100644 --- a/www/lib/pb/api_auth.ts +++ b/www/lib/pb/api_auth.ts @@ -63,6 +63,45 @@ export interface RegisterResponse { */ status?: Status; } +/** + * @generated from protobuf message api_auth.APIPermission + */ +export interface APIPermission { + /** + * @generated from protobuf field: optional string method = 1; + */ + method?: string; + /** + * @generated from protobuf field: optional string path = 2; + */ + path?: string; +} +/** + * @generated from protobuf message api_auth.SignTokenRequest + */ +export interface SignTokenRequest { + /** + * @generated from protobuf field: optional int64 expires_in = 1; + */ + expiresIn?: bigint; + /** + * @generated from protobuf field: repeated api_auth.APIPermission permissions = 2; + */ + permissions: APIPermission[]; +} +/** + * @generated from protobuf message api_auth.SignTokenResponse + */ +export interface SignTokenResponse { + /** + * @generated from protobuf field: optional common.Status status = 1; + */ + status?: Status; + /** + * @generated from protobuf field: optional string token = 2; + */ + token?: string; +} // @generated message type with reflection information, may provide speed optimized methods class LoginRequest$Type extends MessageType { constructor() { @@ -275,3 +314,163 @@ class RegisterResponse$Type extends MessageType { * @generated MessageType for protobuf message api_auth.RegisterResponse */ export const RegisterResponse = new RegisterResponse$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class APIPermission$Type extends MessageType { + constructor() { + super("api_auth.APIPermission", [ + { no: 1, name: "method", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "path", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): APIPermission { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: APIPermission): APIPermission { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional string method */ 1: + message.method = reader.string(); + break; + case /* optional string path */ 2: + message.path = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: APIPermission, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional string method = 1; */ + if (message.method !== undefined) + writer.tag(1, WireType.LengthDelimited).string(message.method); + /* optional string path = 2; */ + if (message.path !== undefined) + writer.tag(2, WireType.LengthDelimited).string(message.path); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message api_auth.APIPermission + */ +export const APIPermission = new APIPermission$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SignTokenRequest$Type extends MessageType { + constructor() { + super("api_auth.SignTokenRequest", [ + { no: 1, name: "expires_in", kind: "scalar", opt: true, T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ }, + { no: 2, name: "permissions", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => APIPermission } + ]); + } + create(value?: PartialMessage): SignTokenRequest { + const message = globalThis.Object.create((this.messagePrototype!)); + message.permissions = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SignTokenRequest): SignTokenRequest { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional int64 expires_in */ 1: + message.expiresIn = reader.int64().toBigInt(); + break; + case /* repeated api_auth.APIPermission permissions */ 2: + message.permissions.push(APIPermission.internalBinaryRead(reader, reader.uint32(), options)); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: SignTokenRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional int64 expires_in = 1; */ + if (message.expiresIn !== undefined) + writer.tag(1, WireType.Varint).int64(message.expiresIn); + /* repeated api_auth.APIPermission permissions = 2; */ + for (let i = 0; i < message.permissions.length; i++) + APIPermission.internalBinaryWrite(message.permissions[i], writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message api_auth.SignTokenRequest + */ +export const SignTokenRequest = new SignTokenRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SignTokenResponse$Type extends MessageType { + constructor() { + super("api_auth.SignTokenResponse", [ + { no: 1, name: "status", kind: "message", T: () => Status }, + { no: 2, name: "token", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): SignTokenResponse { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SignTokenResponse): SignTokenResponse { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional common.Status status */ 1: + message.status = Status.internalBinaryRead(reader, reader.uint32(), options, message.status); + break; + case /* optional string token */ 2: + message.token = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: SignTokenResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional common.Status status = 1; */ + if (message.status) + Status.internalBinaryWrite(message.status, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + /* optional string token = 2; */ + if (message.token !== undefined) + writer.tag(2, WireType.LengthDelimited).string(message.token); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message api_auth.SignTokenResponse + */ +export const SignTokenResponse = new SignTokenResponse$Type(); diff --git a/www/lib/pb/api_client.ts b/www/lib/pb/api_client.ts index 46fc627..729ea02 100644 --- a/www/lib/pb/api_client.ts +++ b/www/lib/pb/api_client.ts @@ -23,6 +23,10 @@ export interface InitClientRequest { * @generated from protobuf field: optional string client_id = 1; */ clientId?: string; + /** + * @generated from protobuf field: optional bool ephemeral = 2; + */ + ephemeral?: boolean; } /** * @generated from protobuf message api_client.InitClientResponse @@ -391,7 +395,8 @@ export interface GetProxyConfigResponse { class InitClientRequest$Type extends MessageType { constructor() { super("api_client.InitClientRequest", [ - { no: 1, name: "client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + { no: 1, name: "client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "ephemeral", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ } ]); } create(value?: PartialMessage): InitClientRequest { @@ -408,6 +413,9 @@ class InitClientRequest$Type extends MessageType { case /* optional string client_id */ 1: message.clientId = reader.string(); break; + case /* optional bool ephemeral */ 2: + message.ephemeral = reader.bool(); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -423,6 +431,9 @@ class InitClientRequest$Type extends MessageType { /* optional string client_id = 1; */ if (message.clientId !== undefined) writer.tag(1, WireType.LengthDelimited).string(message.clientId); + /* optional bool ephemeral = 2; */ + if (message.ephemeral !== undefined) + writer.tag(2, WireType.Varint).bool(message.ephemeral); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); diff --git a/www/lib/pb/common.ts b/www/lib/pb/common.ts index bf2aa74..a442a3f 100644 --- a/www/lib/pb/common.ts +++ b/www/lib/pb/common.ts @@ -85,6 +85,14 @@ export interface Client { * @generated from protobuf field: optional string frps_url = 10; */ frpsUrl?: string; // 客户端用于连接frps的url,解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000 + /** + * @generated from protobuf field: optional bool ephemeral = 11; + */ + ephemeral?: boolean; // 是否临时节点 + /** + * @generated from protobuf field: optional int64 last_seen_at = 12; + */ + lastSeenAt?: bigint; // 最后一次心跳时间戳 } /** * @generated from protobuf message common.Server @@ -467,7 +475,9 @@ class Client$Type extends MessageType { { no: 7, name: "stopped", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ }, { no: 8, name: "client_ids", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, { no: 9, name: "origin_client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, - { no: 10, name: "frps_url", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + { no: 10, name: "frps_url", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 11, name: "ephemeral", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ }, + { no: 12, name: "last_seen_at", kind: "scalar", opt: true, T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ } ]); } create(value?: PartialMessage): Client { @@ -509,6 +519,12 @@ class Client$Type extends MessageType { case /* optional string frps_url */ 10: message.frpsUrl = reader.string(); break; + case /* optional bool ephemeral */ 11: + message.ephemeral = reader.bool(); + break; + case /* optional int64 last_seen_at */ 12: + message.lastSeenAt = reader.int64().toBigInt(); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -548,6 +564,12 @@ class Client$Type extends MessageType { /* optional string frps_url = 10; */ if (message.frpsUrl !== undefined) writer.tag(10, WireType.LengthDelimited).string(message.frpsUrl); + /* optional bool ephemeral = 11; */ + if (message.ephemeral !== undefined) + writer.tag(11, WireType.Varint).bool(message.ephemeral); + /* optional int64 last_seen_at = 12; */ + if (message.lastSeenAt !== undefined) + writer.tag(12, WireType.Varint).int64(message.lastSeenAt); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);