feat: rbac and temp node

This commit is contained in:
VaalaCat
2025-04-28 11:31:30 +00:00
parent 50001f9afc
commit e06b92b216
73 changed files with 1664 additions and 712 deletions

View File

@@ -1,13 +1,14 @@
package auth package auth
import ( import (
"fmt"
"github.com/VaalaCat/frp-panel/conf" "github.com/VaalaCat/frp-panel/conf"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/middleware" "github.com/VaalaCat/frp-panel/middleware"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app" "github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao" "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) { 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 }, 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() ginCtx := ctx.GetGinCtx()
middleware.PushTokenStr(ginCtx, ctx.GetApp(), tokenStr) middleware.PushTokenStr(ginCtx, ctx.GetApp(), tokenStr)

View File

@@ -3,6 +3,7 @@ package auth
import ( import (
"fmt" "fmt"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app" "github.com/VaalaCat/frp-panel/services/app"
@@ -47,10 +48,14 @@ func RegisterHandler(c *app.Context, req *pb.RegisterRequest) (*pb.RegisterRespo
Password: hashedPassword, Password: hashedPassword,
Email: email, Email: email,
Status: models.STATUS_NORMAL, Status: models.STATUS_NORMAL,
Role: models.ROLE_NORMAL, Role: defs.UserRole_Normal,
Token: uuid.New().String(), Token: uuid.New().String(),
} }
if userCount == 0 {
newUser.Role = defs.UserRole_Admin
}
err = dao.NewQuery(c).CreateUser(newUser) err = dao.NewQuery(c).CreateUser(newUser)
if err != nil { if err != nil {
return &pb.RegisterResponse{ return &pb.RegisterResponse{

View File

@@ -7,6 +7,7 @@ import (
"github.com/VaalaCat/frp-panel/services/app" "github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao" "github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/google/uuid" "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) 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, if err := dao.NewQuery(c).CreateClient(userInfo,
&models.ClientEntity{ &models.ClientEntity{
ClientID: globalClientID, ClientID: globalClientID,
@@ -35,6 +38,7 @@ func InitClientHandler(c *app.Context, req *pb.InitClientRequest) (*pb.InitClien
UserID: userInfo.GetUserID(), UserID: userInfo.GetUserID(),
ConnectSecret: uuid.New().String(), ConnectSecret: uuid.New().String(),
IsShadow: true, IsShadow: true,
Ephemeral: req.GetEphemeral(),
}); err != nil { }); err != nil {
return &pb.InitClientResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}}, err return &pb.InitClientResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}}, err
} }

View File

@@ -1,6 +1,8 @@
package client package client
import ( import (
"strings"
"github.com/VaalaCat/frp-panel/common" "github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/pb"
@@ -31,6 +33,10 @@ func GetClientHandler(ctx *app.Context, req *pb.GetClientRequest) (*pb.GetClient
}, nil }, nil
} }
if !strings.Contains(clientID, ".") {
clientID = app.GlobalClientID(userInfo.GetUserName(), "c", clientID)
}
respCli := &pb.Client{} respCli := &pb.Client{}
if len(serverID) == 0 { if len(serverID) == 0 {
client, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID) 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), Stopped: lo.ToPtr(client.Stopped),
Comment: lo.ToPtr(client.Comment), Comment: lo.ToPtr(client.Comment),
ClientIds: clientIDs, ClientIds: clientIDs,
Ephemeral: &client.Ephemeral,
// LastSeenAt: lo.ToPtr(client.LastSeenAt.UnixMilli()),
} }
} else { } else {
client, err := dao.NewQuery(ctx).GetClientByFilter(userInfo, &models.ClientEntity{ 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), ServerId: lo.ToPtr(client.ServerID),
Stopped: lo.ToPtr(client.Stopped), Stopped: lo.ToPtr(client.Stopped),
Comment: lo.ToPtr(client.Comment), Comment: lo.ToPtr(client.Comment),
FrpsUrl: lo.ToPtr(client.FRPsUrl), FrpsUrl: lo.ToPtr(client.FrpsUrl),
Ephemeral: &client.Ephemeral,
ClientIds: nil, ClientIds: nil,
} }
if client.LastSeenAt != nil {
respCli.LastSeenAt = lo.ToPtr(client.LastSeenAt.UnixMilli())
}
} }
return &pb.GetClientResponse{ return &pb.GetClientResponse{

View File

@@ -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) 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), Id: lo.ToPtr(c.ClientID),
Secret: lo.ToPtr(c.ConnectSecret), Secret: lo.ToPtr(c.ConnectSecret),
Config: lo.ToPtr(string(c.ConfigContent)), 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), Stopped: lo.ToPtr(c.Stopped),
Comment: lo.ToPtr(c.Comment), Comment: lo.ToPtr(c.Comment),
ClientIds: clientIDs, ClientIds: clientIDs,
Ephemeral: lo.ToPtr(c.Ephemeral),
} }
if c.LastSeenAt != nil {
respCli.LastSeenAt = lo.ToPtr(c.LastSeenAt.UnixMilli())
}
return respCli
}) })
return &pb.ListClientsResponse{ return &pb.ListClientsResponse{

View File

@@ -1,8 +1,6 @@
package client package client
import ( import (
"fmt"
"github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app" "github.com/VaalaCat/frp-panel/services/app"
@@ -23,6 +21,11 @@ func RPCPullConfig(ctx *app.Context, req *pb.PullClientConfigReq) (*pb.PullClien
return nil, err 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 { if cli.IsShadow {
proxies, err := dao.NewQuery(ctx).AdminListProxyConfigsWithFilters(&models.ProxyConfigEntity{ proxies, err := dao.NewQuery(ctx).AdminListProxyConfigsWithFilters(&models.ProxyConfigEntity{
OriginClientID: cli.ClientID, OriginClientID: cli.ClientID,
@@ -34,7 +37,12 @@ func RPCPullConfig(ctx *app.Context, req *pb.PullClientConfigReq) (*pb.PullClien
} }
if cli.Stopped && cli.IsShadow { 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{ return &pb.PullClientConfigResp{

View File

@@ -102,7 +102,7 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
cliCfg.ServerPort = srvConf.BindPort cliCfg.ServerPort = srvConf.BindPort
} }
if len(req.GetFrpsUrl()) > 0 || len(cli.FRPsUrl) > 0 { if len(req.GetFrpsUrl()) > 0 || len(cli.FrpsUrl) > 0 {
// 有一个有就需要覆盖优先请求的url // 有一个有就需要覆盖优先请求的url
var ( var (
parsedFrpsUrl *url.URL parsedFrpsUrl *url.URL
@@ -120,21 +120,21 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
urlToParse = req.GetFrpsUrl() urlToParse = req.GetFrpsUrl()
} }
if len(cli.FRPsUrl) > 0 && parsedFrpsUrl == nil { if len(cli.FrpsUrl) > 0 && parsedFrpsUrl == nil {
parsedFrpsUrl, err = ValidateFrpsUrl(cli.FRPsUrl) parsedFrpsUrl, err = ValidateFrpsUrl(cli.FrpsUrl)
if err != nil { 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{ return &pb.UpdateFRPCResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}, Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()},
}, err }, err
} }
urlToParse = cli.FRPsUrl urlToParse = cli.FrpsUrl
} }
cliCfg.ServerAddr = parsedFrpsUrl.Hostname() cliCfg.ServerAddr = parsedFrpsUrl.Hostname()
cliCfg.ServerPort = cast.ToInt(parsedFrpsUrl.Port()) cliCfg.ServerPort = cast.ToInt(parsedFrpsUrl.Port())
cliCfg.Transport.Protocol = parsedFrpsUrl.Scheme cliCfg.Transport.Protocol = parsedFrpsUrl.Scheme
cli.FRPsUrl = urlToParse cli.FrpsUrl = urlToParse
} }
cliCfg.User = userInfo.GetUserName() cliCfg.User = userInfo.GetUserName()

View File

@@ -27,52 +27,51 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) {
router.POST("/auth", auth.MakeGinHandlerFunc(appInstance, auth.HandleLogin)) router.POST("/auth", auth.MakeGinHandlerFunc(appInstance, auth.HandleLogin))
api := router.Group("/api") 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") userRouter := v1.Group("/user")
{
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.POST("/get", app.Wrapper(appInstance, user.GetUserInfoHandler)) userRouter.POST("/get", app.Wrapper(appInstance, user.GetUserInfoHandler))
userRouter.POST("/update", app.Wrapper(appInstance, user.UpdateUserInfoHander)) 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.GET("/baseinfo", platform.GetPlatformInfo(appInstance))
platformRouter.POST("/clientsstatus", app.Wrapper(appInstance, platform.GetClientsStatus)) 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("/get", app.Wrapper(appInstance, client.GetClientHandler))
clientRouter.POST("/init", app.Wrapper(appInstance, client.InitClientHandler)) clientRouter.POST("/init", app.Wrapper(appInstance, client.InitClientHandler))
clientRouter.POST("/delete", app.Wrapper(appInstance, client.DeleteClientHandler)) clientRouter.POST("/delete", app.Wrapper(appInstance, client.DeleteClientHandler))
clientRouter.POST("/list", app.Wrapper(appInstance, client.ListClientsHandler)) 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("/get", app.Wrapper(appInstance, server.GetServerHandler))
serverRouter.POST("/init", app.Wrapper(appInstance, server.InitServerHandler)) serverRouter.POST("/init", app.Wrapper(appInstance, server.InitServerHandler))
serverRouter.POST("/delete", app.Wrapper(appInstance, server.DeleteServerHandler)) serverRouter.POST("/delete", app.Wrapper(appInstance, server.DeleteServerHandler))
serverRouter.POST("/list", app.Wrapper(appInstance, server.ListServersHandler)) 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("/update", app.Wrapper(appInstance, client.UpdateFrpcHander))
frpcRouter.POST("/delete", app.Wrapper(appInstance, client.RemoveFrpcHandler)) frpcRouter.POST("/delete", app.Wrapper(appInstance, client.RemoveFrpcHandler))
frpcRouter.POST("/stop", app.Wrapper(appInstance, client.StopFRPCHandler)) frpcRouter.POST("/stop", app.Wrapper(appInstance, client.StopFRPCHandler))
frpcRouter.POST("/start", app.Wrapper(appInstance, client.StartFRPCHandler)) 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("/update", app.Wrapper(appInstance, server.UpdateFrpsHander))
frpsRouter.POST("/delete", app.Wrapper(appInstance, server.RemoveFrpsHandler)) 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_cid", app.Wrapper(appInstance, proxy.GetProxyStatsByClientID))
proxyRouter.POST("/get_by_sid", app.Wrapper(appInstance, proxy.GetProxyStatsByServerID)) 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("/delete_config", app.Wrapper(appInstance, proxy.DeleteProxyConfig))
proxyRouter.POST("/get_config", app.Wrapper(appInstance, proxy.GetProxyConfig)) proxyRouter.POST("/get_config", app.Wrapper(appInstance, proxy.GetProxyConfig))
} }
v1.GET("/pty/:clientID", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), shell.PTYHandler(appInstance)) v1.GET("/pty/:clientID", shell.PTYHandler(appInstance))
v1.GET("/log", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), streamlog.GetLogHandler(appInstance)) v1.GET("/log", streamlog.GetLogHandler(appInstance))
} }
} }

View File

@@ -39,7 +39,7 @@ func GetServerHandler(c *app.Context, req *pb.GetServerRequest) (*pb.GetServerRe
Secret: lo.ToPtr(serverEntity.ConnectSecret), Secret: lo.ToPtr(serverEntity.ConnectSecret),
Comment: lo.ToPtr(serverEntity.Comment), Comment: lo.ToPtr(serverEntity.Comment),
Ip: lo.ToPtr(serverEntity.ServerIP), Ip: lo.ToPtr(serverEntity.ServerIP),
FrpsUrls: serverEntity.FRPsUrls, FrpsUrls: serverEntity.FrpsUrls,
}, },
}, nil }, nil
} }

View File

@@ -54,7 +54,7 @@ func ListServersHandler(c *app.Context, req *pb.ListServersRequest) (*pb.ListSer
Secret: lo.ToPtr(c.ConnectSecret), Secret: lo.ToPtr(c.ConnectSecret),
Ip: lo.ToPtr(c.ServerIP), Ip: lo.ToPtr(c.ServerIP),
Comment: lo.ToPtr(c.Comment), Comment: lo.ToPtr(c.Comment),
FrpsUrls: c.FRPsUrls, FrpsUrls: c.FrpsUrls,
} }
}), }),
Total: lo.ToPtr(int32(serverCounts)), Total: lo.ToPtr(int32(serverCounts)),

View File

@@ -53,7 +53,7 @@ func UpdateFrpsHander(c *app.Context, req *pb.UpdateFRPSRequest) (*pb.UpdateFRPS
} }
if len(req.GetFrpsUrls()) > 0 { if len(req.GetFrpsUrls()) > 0 {
srv.FRPsUrls = req.GetFrpsUrls() srv.FrpsUrls = req.GetFrpsUrls()
} }
if err := dao.NewQuery(c).UpdateServer(userInfo, srv); err != nil { if err := dao.NewQuery(c).UpdateServer(userInfo, srv); err != nil {

View File

@@ -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
}

View File

@@ -1,16 +1,23 @@
package main package main
import ( import (
"embed"
"github.com/VaalaCat/frp-panel/cmd/frpp/shared"
"github.com/VaalaCat/frp-panel/utils/logger" "github.com/VaalaCat/frp-panel/utils/logger"
"github.com/fatedier/golib/crypto" "github.com/fatedier/golib/crypto"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
//go:embed all:out
var fs embed.FS
func main() { func main() {
crypto.DefaultSalt = "frp" crypto.DefaultSalt = "frp"
logger.InitLogger() logger.InitLogger()
cobra.MousetrapHelpText = "" cobra.MousetrapHelpText = ""
rootCmd := buildCommand()
setMasterCommandIfNonePresent(rootCmd) rootCmd := shared.BuildCommand(fs)
shared.SetMasterCommandIfNonePresent(rootCmd)
rootCmd.Execute() rootCmd.Execute()
} }

View File

@@ -1,4 +1,4 @@
package main package shared
import ( import (
"context" "context"

View File

@@ -1,7 +1,8 @@
package main package shared
import ( import (
"context" "context"
"embed"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -30,9 +31,10 @@ type CommonArgs struct {
RpcPort *int RpcPort *int
ApiPort *int ApiPort *int
ApiScheme *string ApiScheme *string
JoinToken *string
} }
func buildCommand() *cobra.Command { func BuildCommand(fs embed.FS) *cobra.Command {
cfg := conf.NewConfig() cfg := conf.NewConfig()
logger.UpdateLoggerOpt( logger.UpdateLoggerOpt(
@@ -41,7 +43,7 @@ func buildCommand() *cobra.Command {
) )
return NewRootCmd( return NewRootCmd(
NewMasterCmd(cfg), NewMasterCmd(cfg, fs),
NewClientCmd(cfg), NewClientCmd(cfg),
NewServerCmd(cfg), NewServerCmd(cfg),
NewJoinCmd(), NewJoinCmd(),
@@ -59,6 +61,7 @@ func AddCommonFlags(commonCmd *cobra.Command) {
commonCmd.Flags().StringP("id", "i", "", "client id") 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("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().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 // deprecated start
commonCmd.Flags().StringP("app", "a", "", "app secret") commonCmd.Flags().StringP("app", "a", "", "app secret")
@@ -109,12 +112,11 @@ func GetCommonArgs(cmd *cobra.Command) CommonArgs {
commonArgs.ApiScheme = &apiScheme commonArgs.ApiScheme = &apiScheme
} }
return commonArgs if joinToken, err := cmd.Flags().GetString("join-token"); err == nil {
} commonArgs.JoinToken = &joinToken
}
type JoinArgs struct { return commonArgs
CommonArgs
JoinToken *string
} }
func NewJoinCmd() *cobra.Command { 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]", Use: "join [-j join token] [-r rpc host] [-p api port] [-e api scheme]",
Short: "join to master with token, save param to config", Short: "join to master with token, save param to config",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
commonArgs := GetCommonArgs(cmd) commonArgs := GetCommonArgs(cmd)
warnDepParam(cmd) warnDepParam(cmd)
joinArgs := &JoinArgs{ cli, err := JoinMaster(conf.NewConfig(), commonArgs)
CommonArgs: commonArgs, if err != nil {
logger.Logger(ctx).Fatalf("join master failed: %s", err.Error())
} }
if joinToken, err := cmd.Flags().GetString("join-token"); err == nil { saveConfig(ctx, cli, commonArgs)
joinArgs.JoinToken = &joinToken
}
appInstance := app.NewApp()
pullRunConfig(appInstance, joinArgs)
}, },
} }
joinCmd.Flags().StringP("join-token", "j", "", "your token from master")
AddCommonFlags(joinCmd) AddCommonFlags(joinCmd)
return joinCmd return joinCmd
} }
func NewMasterCmd(cfg conf.Config) *cobra.Command { func NewMasterCmd(cfg conf.Config, fs embed.FS) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "master", Use: "master",
Short: "run frp-panel manager", Short: "run frp-panel manager",
@@ -159,6 +157,8 @@ func NewMasterCmd(cfg conf.Config) *cobra.Command {
fx.Supply( fx.Supply(
CommonArgs{}, CommonArgs{},
fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)), fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)),
fs,
defs.AppRole_Master,
), ),
fx.Provide(fx.Annotate(NewDefaultServerConfig, fx.ResultTags(`name:"defaultServerConfig"`))), fx.Provide(fx.Annotate(NewDefaultServerConfig, fx.ResultTags(`name:"defaultServerConfig"`))),
fx.Invoke(NewConfigPrinter), fx.Invoke(NewConfigPrinter),
@@ -202,6 +202,7 @@ func NewClientCmd(cfg conf.Config) *cobra.Command {
fx.Supply( fx.Supply(
commonArgs, commonArgs,
fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)), fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)),
defs.AppRole_Client,
), ),
fx.Invoke(NewConfigPrinter), fx.Invoke(NewConfigPrinter),
fx.Invoke(runClient), fx.Invoke(runClient),
@@ -246,6 +247,7 @@ func NewServerCmd(cfg conf.Config) *cobra.Command {
fx.Supply( fx.Supply(
commonArgs, commonArgs,
fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)), fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)),
defs.AppRole_Server,
), ),
fx.Invoke(runServer), 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:]) cmd, _, err := rootCmd.Find(os.Args[1:])
if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp { if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp {
args := append([]string{"master"}, os.Args[1:]...) 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() c := context.Background()
if err := checkPullParams(joinArgs); err != nil { if err := checkPullParams(joinArgs); err != nil {
logger.Logger(c).Errorf("check pull params failed: %s", err.Error()) logger.Logger(c).Errorf("check pull params failed: %s", err.Error())
return return nil, err
}
if err := utils.EnsureDirectoryExists(defs.SysEnvPath); err != nil {
logger.Logger(c).Errorf("ensure directory failed: %s", err.Error())
return
} }
var clientID string var clientID string
if cliID := joinArgs.ClientID; cliID == nil || len(*cliID) == 0 { if cliID := joinArgs.ClientID; cliID == nil || len(*cliID) == 0 {
clientID = utils.GetHostnameWithIP() clientID = utils.GetHostnameWithIP()
} else {
clientID = *cliID
} }
clientID = utils.MakeClientIDPermited(clientID) clientID = utils.MakeClientIDPermited(clientID)
patchConfig(appInstance, joinArgs.CommonArgs)
initResp, err := rpc.InitClient(appInstance, clientID, *joinArgs.JoinToken) logger.Logger(c).Infof("join master with param, clientId:[%s] joinArgs:[%s]", clientID, utils.MarshalForJson(joinArgs))
if err != nil {
logger.Logger(c).Errorf("init client failed: %s", err.Error()) // 检测是否存在已有的client
return clientResp, err := rpc.GetClient(cfg, clientID, *joinArgs.JoinToken)
} if err != nil || clientResp == nil || clientResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS {
if initResp == nil { logger.Logger(c).Infof("client [%s] not found, try to init client", clientID)
logger.Logger(c).Errorf("init resp is nil")
return // 创建短期client
} initResp, err := rpc.InitClient(cfg, clientID, *joinArgs.JoinToken, true)
if initResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS { if err != nil {
logger.Logger(c).Errorf("init client failed with status: %s", initResp.GetStatus().GetMessage()) logger.Logger(c).Errorf("init client failed: %s", err.Error())
return 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 { if clientResp == nil {
logger.Logger(c).Errorf("client resp is nil") logger.Logger(c).Errorf("client resp is nil")
return return nil, err
} }
if clientResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS { if clientResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS {
logger.Logger(c).Errorf("client resp code is not success: %s", clientResp.GetStatus().GetMessage()) logger.Logger(c).Errorf("client resp code is not success: %s", clientResp.GetStatus().GetMessage())
return return nil, err
} }
client := clientResp.GetClient() client := clientResp.GetClient()
if client == nil { if client == nil {
logger.Logger(c).Errorf("client is 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 return
} }
envMap, err := godotenv.Read(defs.SysEnvPath) envMap, err := godotenv.Read(defs.SysEnvPath)
if err != nil { if err != nil {
envMap = make(map[string]string) 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.EnvClientID] = cli.GetId()
envMap[defs.EnvClientSecret] = client.GetSecret() envMap[defs.EnvClientSecret] = cli.GetSecret()
envMap[defs.EnvClientAPIUrl] = *joinArgs.ApiUrl envMap[defs.EnvClientAPIUrl] = *joinArgs.ApiUrl
envMap[defs.EnvClientRPCUrl] = *joinArgs.RpcUrl envMap[defs.EnvClientRPCUrl] = *joinArgs.RpcUrl
if err = godotenv.Write(envMap, defs.SysEnvPath); err != nil { 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 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 { if joinToken := joinArgs.JoinToken; joinToken != nil && len(*joinToken) == 0 {
return errors.New("join token is empty") return errors.New("join token is empty")
} }
if apiUrl := joinArgs.ApiUrl; apiUrl == nil || len(*apiUrl) == 0 { var (
if apiHost := joinArgs.ApiHost; apiHost == nil || len(*apiHost) == 0 { apiUrlAvaliable = joinArgs.ApiUrl != nil && len(*joinArgs.ApiUrl) > 0
return errors.New("api host is empty") rpcUrlAvaliable = joinArgs.RpcUrl != nil && len(*joinArgs.RpcUrl) > 0
} )
if apiScheme := joinArgs.ApiScheme; apiScheme == nil || len(*apiScheme) == 0 {
return errors.New("api scheme is empty") if !apiUrlAvaliable {
} return errors.New("api url is empty")
} }
if apiPort := joinArgs.ApiPort; apiPort == nil || *apiPort == 0 { if !rpcUrlAvaliable {
return errors.New("api port is empty") return errors.New("rpc url is empty")
} }
return nil return nil

View File

@@ -1,4 +1,4 @@
package main package shared
import ( import (
"context" "context"
@@ -35,6 +35,7 @@ type runMasterParam struct {
TaskManager watcher.Client `name:"masterTaskManager"` TaskManager watcher.Client `name:"masterTaskManager"`
WsListener *wsgrpc.WSListener WsListener *wsgrpc.WSListener
DefaultServerConfig conf.Config `name:"defaultServerConfig"` DefaultServerConfig conf.Config `name:"defaultServerConfig"`
PermManager app.PermissionManager
} }
func runMaster(param runMasterParam) { func runMaster(param runMasterParam) {

View File

@@ -1,13 +1,14 @@
package main package shared
import ( import (
"go.uber.org/fx" "go.uber.org/fx"
) )
var ( var (
clientMod = fx.Module("cmd.client", fx.Provide( clientMod = fx.Module("cmd.client",
fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)), fx.Provide(
)) fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)),
))
serverMod = fx.Module("cmd.server", fx.Provide( serverMod = fx.Module("cmd.server", fx.Provide(
fx.Annotate(NewServerAPI, fx.ResultTags(`name:"serverApiService"`)), fx.Annotate(NewServerAPI, fx.ResultTags(`name:"serverApiService"`)),
@@ -16,12 +17,13 @@ var (
)) ))
masterMod = fx.Module("cmd.master", fx.Provide( masterMod = fx.Module("cmd.master", fx.Provide(
NewPermissionManager,
NewEnforcer,
NewListenerOptions, NewListenerOptions,
NewDBManager, NewDBManager,
NewWSListener, NewWSListener,
NewMasterTLSConfig, NewMasterTLSConfig,
NewWSUpgrader, NewWSUpgrader,
NewFs,
NewClientLogManager, NewClientLogManager,
fx.Annotate(NewWatcher, fx.ResultTags(`name:"masterTaskManager"`)), fx.Annotate(NewWatcher, fx.ResultTags(`name:"masterTaskManager"`)),
fx.Annotate(NewMasterRouter, fx.ResultTags(`name:"masterRouter"`)), fx.Annotate(NewMasterRouter, fx.ResultTags(`name:"masterRouter"`)),
@@ -34,11 +36,12 @@ var (
)) ))
commonMod = fx.Module("common", fx.Provide( commonMod = fx.Module("common", fx.Provide(
NewPatchedConfig,
NewLogHookManager, NewLogHookManager,
NewPTYManager, NewPTYManager,
NewBaseApp, NewBaseApp,
NewContext, NewContext,
NewClientsManager, NewClientsManager,
NewAutoJoin,
fx.Annotate(NewPatchedConfig, fx.ResultTags(`name:"argsPatchedConfig"`)),
)) ))
) )

View File

@@ -1,4 +1,4 @@
package main package shared
import ( import (
"context" "context"
@@ -15,6 +15,7 @@ import (
"github.com/VaalaCat/frp-panel/biz/master/streamlog" "github.com/VaalaCat/frp-panel/biz/master/streamlog"
bizserver "github.com/VaalaCat/frp-panel/biz/server" bizserver "github.com/VaalaCat/frp-panel/biz/server"
"github.com/VaalaCat/frp-panel/conf" "github.com/VaalaCat/frp-panel/conf"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/api" "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/dao"
"github.com/VaalaCat/frp-panel/services/master" "github.com/VaalaCat/frp-panel/services/master"
"github.com/VaalaCat/frp-panel/services/mux" "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/rpc"
"github.com/VaalaCat/frp-panel/services/watcher" "github.com/VaalaCat/frp-panel/services/watcher"
"github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger" "github.com/VaalaCat/frp-panel/utils/logger"
"github.com/VaalaCat/frp-panel/utils/wsgrpc" "github.com/VaalaCat/frp-panel/utils/wsgrpc"
"github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@@ -38,9 +41,6 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
//go:embed all:out
var fs embed.FS
func NewLogHookManager() app.StreamLogHookMgr { func NewLogHookManager() app.StreamLogHookMgr {
return &bizcommon.HookMgr{} return &bizcommon.HookMgr{}
} }
@@ -70,10 +70,6 @@ func NewClientsManager() app.ClientsManager {
return rpc.NewClientsManager() return rpc.NewClientsManager()
} }
func NewFs() embed.FS {
return fs
}
func NewPatchedConfig(param struct { func NewPatchedConfig(param struct {
fx.In fx.In
@@ -96,7 +92,7 @@ func NewClientLogManager() app.ClientLogManager {
func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager { func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager {
logger.Logger(ctx).Infof("start to init database, type: %s", appInstance.GetConfig().DB.Type) 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) appInstance.SetDBManager(mgr)
if appInstance.GetConfig().IsDebug { if appInstance.GetConfig().IsDebug {
@@ -104,7 +100,7 @@ func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager {
} }
switch appInstance.GetConfig().DB.Type { switch appInstance.GetConfig().DB.Type {
case "sqlite3": case defs.DBTypeSQLite3:
if err := utils.EnsureDirectoryExists(appInstance.GetConfig().DB.DSN); err != nil { 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", logger.Logger(ctx).WithError(err).Warnf("ensure directory failed, data location: [%s], keep data in current directory",
appInstance.GetConfig().DB.DSN) 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 { if sqlitedb, err := gorm.Open(sqlite.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil {
logger.Logger(ctx).Panic(err) logger.Logger(ctx).Panic(err)
} else { } 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) 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 { if mysqlDB, err := gorm.Open(mysql.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil {
logger.Logger(ctx).Panic(err) logger.Logger(ctx).Panic(err)
} else { } 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") 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 { if postgresDB, err := gorm.Open(postgres.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil {
logger.Logger(ctx).Panic(err) logger.Logger(ctx).Panic(err)
} else { } 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") logger.Logger(ctx).Infof("init database success, data type: [%s]", "postgres")
} }
default: default:
logger.Logger(ctx).Panicf("currently unsupported database type: %s", appInstance.GetConfig().DB.Type) 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() appInstance.GetDBManager().Init()
return mgr return mgr
} }
@@ -279,7 +282,94 @@ func NewDefaultServerConfig(ctx *app.Context) conf.Config {
const splitter = "\n--------------------------------------------\n" 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("%srunning config is: %s%s", splitter, config.PrintStr(), splitter)
logger.Logger(ctx).Infof("%scurrent version: \n%s%s", splitter, conf.GetVersion().String(), 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
}

View File

@@ -1,4 +1,4 @@
package main package shared
import ( import (
"context" "context"

View File

@@ -1,4 +1,4 @@
package main package shared
// func serverThings() { // func serverThings() {
// time.Sleep(5 * time.Second) // time.Sleep(5 * time.Second)

View File

@@ -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")
}
}

View File

@@ -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
}

View File

@@ -1,13 +1,8 @@
package main package main
import ( import (
"sync" "github.com/VaalaCat/frp-panel/cmd/frpp/shared"
"github.com/VaalaCat/frp-panel/biz/common"
"github.com/VaalaCat/frp-panel/biz/master/shell"
"github.com/VaalaCat/frp-panel/conf" "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/VaalaCat/frp-panel/utils/logger"
"github.com/fatedier/golib/crypto" "github.com/fatedier/golib/crypto"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -15,20 +10,20 @@ import (
func main() { func main() {
crypto.DefaultSalt = "frp" crypto.DefaultSalt = "frp"
logger.InitLogger() logger.InitLogger()
cobra.MousetrapHelpText = "" cobra.MousetrapHelpText = ""
cfg := conf.NewConfig()
appInstance := app.NewApp() rootCmd := shared.NewRootCmd(
appInstance.SetConfig(cfg) shared.NewClientCmd(conf.NewConfig()),
appInstance.SetClientsManager(rpc.NewClientsManager()) shared.NewJoinCmd(),
appInstance.SetStreamLogHookMgr(&common.HookMgr{}) shared.NewInstallServiceCmd(),
appInstance.SetShellPTYMgr(shell.NewPTYMgr()) shared.NewUninstallServiceCmd(),
appInstance.SetClientRecvMap(&sync.Map{}) shared.NewStartServiceCmd(),
shared.NewStopServiceCmd(),
shared.NewRestartServiceCmd(),
shared.NewVersionCmd(),
)
initCommand(appInstance) shared.SetClientCommandIfNonePresent(rootCmd)
setMasterCommandIfNonePresent()
rootCmd.Execute() rootCmd.Execute()
} }

View File

@@ -2,6 +2,7 @@ package common
import ( import (
"context" "context"
"encoding/json"
"github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/models"
@@ -20,3 +21,27 @@ func GetUserInfo(c context.Context) models.UserInfo {
return u 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)
}

View File

@@ -24,7 +24,7 @@ type ReqType interface {
pb.StartFRPCRequest | pb.StopFRPCRequest | pb.StartFRPSRequest | pb.StopFRPSRequest | pb.StartFRPCRequest | pb.StopFRPCRequest | pb.StartFRPSRequest | pb.StopFRPSRequest |
pb.GetProxyStatsByClientIDRequest | pb.GetProxyStatsByServerIDRequest | pb.GetProxyStatsByClientIDRequest | pb.GetProxyStatsByServerIDRequest |
pb.CreateProxyConfigRequest | pb.ListProxyConfigsRequest | pb.UpdateProxyConfigRequest | 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) { func GetProtoRequest[T ReqType](c *gin.Context) (r *T, err error) {

View File

@@ -25,7 +25,7 @@ type RespType interface {
pb.StartFRPCResponse | pb.StopFRPCResponse | pb.StartFRPSResponse | pb.StopFRPSResponse | pb.StartFRPCResponse | pb.StopFRPCResponse | pb.StartFRPSResponse | pb.StopFRPSResponse |
pb.GetProxyStatsByClientIDResponse | pb.GetProxyStatsByServerIDResponse | pb.GetProxyStatsByClientIDResponse | pb.GetProxyStatsByServerIDResponse |
pb.CreateProxyConfigResponse | pb.ListProxyConfigsResponse | pb.UpdateProxyConfigResponse | 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) { func OKResp[T RespType](c *gin.Context, origin *T) {

View File

@@ -58,19 +58,38 @@ func FRPsAuthOption(cfg Config, isDefault bool) v1.HTTPPluginOptions {
} }
} }
func GetCommonJWT(cfg Config, uid string) string { func GetJWTWithPayload(cfg Config, uid int, payload map[string]interface{}) (string, error) {
token, _ := utils.GetJwtTokenFromMap(JWTSecret(cfg), payload[defs.UserIDKey] = uid
return utils.GetJwtTokenFromMap(JWTSecret(cfg),
time.Now().Unix(), time.Now().Unix(),
int64(cfg.App.CookieAge), 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 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), token, _ := utils.GetJwtTokenFromMap(JWTSecret(cfg),
time.Now().Unix(), time.Now().Unix(),
int64(expSec), int64(expSec),
map[string]string{defs.UserIDKey: uid}) map[string]interface{}{defs.UserIDKey: uid})
return token return token
} }

View File

@@ -54,6 +54,14 @@ const (
CliTypeClient = "client" CliTypeClient = "client"
) )
type AppRole string
const (
AppRole_Client AppRole = CliTypeClient
AppRole_Server AppRole = CliTypeServer
AppRole_Master AppRole = "master"
)
const ( const (
DefaultServerID = "default" DefaultServerID = "default"
DefaultAdminUserID = 1 DefaultAdminUserID = 1
@@ -77,3 +85,27 @@ const (
EnvClientAPIUrl = "CLIENT_API_URL" EnvClientAPIUrl = "CLIENT_API_URL"
EnvClientRPCUrl = "CLIENT_RPC_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"
)

1
defs/error.go Normal file
View File

@@ -0,0 +1 @@
package defs

43
defs/rbac_consts.go Normal file
View File

@@ -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"
)

View File

@@ -19,19 +19,19 @@ Client 推荐使用 docker 部署,直接部署在客户机中,可以通过
点击对应客户端的 `ID (点击查看安装命令)` 一列,弹出不同系统的安装命令,粘贴到对应终端即可安装,这里以 Linux 为例 点击对应客户端的 `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加速到安装脚本前 如果你在国内可以增加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需要修改这行命令类似如下 注意,如果你使用 反向代理 TLS需要修改这行命令类似如下
```bash ```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 部署 ## Docker Compose 部署

20
go.mod
View File

@@ -6,6 +6,8 @@ toolchain go1.24.1
require ( require (
github.com/UserExistsError/conpty v0.1.4 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/coocood/freecache v1.2.4
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/fatedier/frp v0.62.0 github.com/fatedier/frp v0.62.0
@@ -37,9 +39,9 @@ require (
golang.org/x/net v0.39.0 golang.org/x/net v0.39.0
google.golang.org/grpc v1.67.1 google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.36.5 google.golang.org/protobuf v1.36.5
gorm.io/driver/mysql v1.5.2 gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.4 gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.5 gorm.io/gorm v1.25.12
k8s.io/apimachinery v0.28.8 k8s.io/apimachinery v0.28.8
) )
@@ -49,7 +51,9 @@ require (
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect
github.com/beorn7/perks v1.0.1 // 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/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/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudflare/circl v1.6.1 // 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-sql-driver/mysql v1.7.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.2 // 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/golang/snappy v0.0.4 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
@@ -77,7 +84,8 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // 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/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.4.0 // 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/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.19 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // 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/refraction-networking/utls v1.6.7 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // 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/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect github.com/stretchr/testify v1.10.0 // indirect
@@ -145,6 +153,8 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // 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 gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect

107
go.sum
View File

@@ -1,6 +1,24 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 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 v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 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= 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/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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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.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 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 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/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.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= 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/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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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/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.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.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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.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/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 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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= 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/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 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 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 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/iamacarpet/go-winpty v1.0.4 h1:r42xaLLRZcUqjX6vHZeHos2haACfWkooOJTnFdogBfI= 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/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 h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 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.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= 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 h1:mTBRuHSW8qviVqX7kXnxKevqlfS/OA01ys6k6fxSX7w=
github.com/jackpal/gateway v1.0.16/go.mod h1:IOn1OUbso/cGYmnCBZbCEqhNCLSz0xxdtIpUpri5/nA= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 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/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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.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 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 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 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 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.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 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= 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/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 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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= 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/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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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/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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 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-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-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-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.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.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.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 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 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-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-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-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-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-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.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.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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 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-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-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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 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-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-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-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-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-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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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-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.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.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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 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= 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.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.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.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.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.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.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.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 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 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-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-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-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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 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-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-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 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 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= 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/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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.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.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 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 h1:cCKla0V7sa6eixh74LtGQXakTu5QJEzkcX7DzNRhFOE=
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= 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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -22,4 +22,19 @@ message RegisterRequest {
message RegisterResponse { message RegisterResponse {
optional common.Status status = 1; 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;
} }

View File

@@ -7,6 +7,7 @@ option go_package="../pb";
message InitClientRequest { message InitClientRequest {
optional string client_id = 1; optional string client_id = 1;
optional bool ephemeral = 2;
} }
message InitClientResponse { message InitClientResponse {
@@ -145,4 +146,4 @@ message GetProxyConfigResponse {
optional common.Status status = 1; optional common.Status status = 1;
optional common.ProxyConfig proxy_config = 2; optional common.ProxyConfig proxy_config = 2;
optional common.ProxyWorkingStatus working_status = 3; optional common.ProxyWorkingStatus working_status = 3;
} }

View File

@@ -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 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 origin_client_id = 9;
optional string frps_url = 10; // 客户端用于连接frps的url解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000 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 { message Server {
@@ -93,4 +95,4 @@ message ProxyWorkingStatus {
optional string status = 3; optional string status = 3;
optional string err = 4; optional string err = 4;
optional string remote_addr = 5; optional string remote_addr = 5;
} }

View File

@@ -6,9 +6,9 @@ import (
"github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/services/app" "github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao" "github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger" "github.com/VaalaCat/frp-panel/utils/logger"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/spf13/cast"
) )
func AuthCtx(appInstance app.Application) func(*gin.Context) { 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") logger.Logger(c).Info("finish auth user middleware")
}() }()
userID, ok := utils.GetValue[int](c, defs.UserIDKey) userID, err := cast.ToIntE(c.Value(defs.UserIDKey))
if !ok { if err != nil {
logger.Logger(c).Errorf("invalid user id") logger.Logger(c).WithError(err).Errorf("invalid user id: %v", c.Value(defs.UserIDKey))
common.ErrUnAuthorized(c, "token invalid") common.ErrUnAuthorized(c, "token invalid")
c.Abort() c.Abort()
return return
@@ -61,7 +61,7 @@ func AuthCtx(appInstance app.Application) func(*gin.Context) {
func AuthAdmin(c *gin.Context) { func AuthAdmin(c *gin.Context) {
u := common.GetUserInfo(c) 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") common.ErrUnAuthorized(c, "permission denied")
c.Abort() c.Abort()
return return

View File

@@ -13,6 +13,7 @@ import (
"github.com/VaalaCat/frp-panel/utils/logger" "github.com/VaalaCat/frp-panel/utils/logger"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/spf13/cast"
) )
// JWTMAuth check if authed and set uid to context // 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) c.Set(k, v)
} }
logger.Logger(c).Infof("query auth success") 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") logger.Logger(c).WithError(err).Errorf("resign jwt error")
common.ErrUnAuthorized(c, "resign jwt error") common.ErrUnAuthorized(c, "resign jwt error")
c.Abort() c.Abort()
return return
} }
c.Next() c.Next()
SetToken(c, appInstance, utils.ToStr(t[defs.UserIDKey])) SetToken(c, appInstance, cast.ToInt(t[defs.UserIDKey]), t)
return return
} }
logger.Logger(c).Infof("query auth failed") logger.Logger(c).Infof("query auth failed")
@@ -50,7 +51,7 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) {
c.Set(k, v) c.Set(k, v)
} }
logger.Logger(c).Infof("cookie auth success") 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") logger.Logger(c).WithError(err).Errorf("resign jwt error")
common.ErrUnAuthorized(c, "resign jwt error") common.ErrUnAuthorized(c, "resign jwt error")
c.Abort() c.Abort()
@@ -80,7 +81,7 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) {
c.Set(k, v) c.Set(k, v)
} }
logger.Logger(c).Infof("header auth success") 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") logger.Logger(c).WithError(err).Errorf("resign jwt error")
common.ErrUnAuthorized(c, "resign jwt error") common.ErrUnAuthorized(c, "resign jwt error")
c.Abort() 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() tokenExpire, _ := t.GetExpirationTime()
now := time.Now().Add(time.Duration(appInstance.GetConfig().App.CookieAge/2) * time.Second) now := time.Now().Add(time.Duration(appInstance.GetConfig().App.CookieAge/2) * time.Second)
if now.Before(tokenExpire.Time) { if now.Before(tokenExpire.Time) {
@@ -103,47 +104,32 @@ func resignAndPatchCtxJWT(c *gin.Context, appInstance app.Application, t jwt.Map
return nil return nil
} }
token, err := utils.GetJwtTokenFromMap(conf.JWTSecret(appInstance.GetConfig()), tokenStr, err := SetToken(c, appInstance, userID, t)
time.Now().Unix(),
int64(appInstance.GetConfig().App.CookieAge),
map[string]string{defs.UserIDKey: utils.ToStr(t[defs.UserIDKey])})
if err != nil { if err != nil {
c.Set(defs.TokenKey, tokenStr)
logger.Logger(c).WithError(err).Errorf("resign jwt error") logger.Logger(c).WithError(err).Errorf("resign jwt error")
return err return err
} }
PushTokenStr(c, appInstance, tokenStr)
logger.Logger(c).Infof("jwt going to expire, resigning token") 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 return nil
} }
func SetToken(c *gin.Context, appInstance app.Application, uid string) (string, error) { // SetToken 设置新token并写入ctx
logger.Logger(c).Infof("set token for uid:[%s]", uid) func SetToken(c *gin.Context, appInstance app.Application, userID int, payload jwt.MapClaims) (string, error) {
token, err := utils.GetJwtTokenFromMap(conf.JWTSecret(appInstance.GetConfig()), logger.Logger(c).Debugf("set token for userID:[%d]", userID)
time.Now().Unix(), if payload == nil {
int64(appInstance.GetConfig().App.CookieAge), payload = make(map[string]interface{})
map[string]string{defs.UserIDKey: uid}) }
payload[defs.UserIDKey] = userID
token, err := conf.GetJWTWithPayload(appInstance.GetConfig(), userID, payload)
if err != nil { if err != nil {
return "", err 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.Set(defs.TokenKey, token)
c.Header(defs.SetAuthorizationKey, token)
return token, nil return token, nil
} }

82
middleware/rbac.go Normal file
View File

@@ -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
}

View File

@@ -4,7 +4,7 @@ import "gorm.io/gorm"
type Cert struct { type Cert struct {
gorm.Model gorm.Model
Name string `gorm:"uniqueIndex"` Name string `gorm:"type:varchar(255);uniqueIndex"`
CertFile []byte CertFile []byte
KeyFile []byte KeyFile []byte
CaFile []byte CaFile []byte

View File

@@ -25,10 +25,13 @@ type ClientEntity struct {
Comment string `json:"comment"` Comment string `json:"comment"`
IsShadow bool `json:"is_shadow" gorm:"index"` IsShadow bool `json:"is_shadow" gorm:"index"`
OriginClientID string `json:"origin_client_id" gorm:"index"` OriginClientID string `json:"origin_client_id" gorm:"index"`
FRPsUrl string `json:"frps_url" gorm:"index"` FrpsUrl string `json:"frps_url" gorm:"index"`
CreatedAt time.Time Ephemeral bool `json:"ephemeral" gorm:"index"`
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `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 { func (*Client) TableName() string {

View File

@@ -3,79 +3,80 @@ package models
import ( import (
"context" "context"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/utils/logger" "github.com/VaalaCat/frp-panel/utils/logger"
"gorm.io/gorm" "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 { 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 defaultDBType string
debug bool debug bool
} }
func (dbm *dbManagerImpl) Init() { func (dbm *dbManagerImpl) Init() {
for _, db := range dbm.DBs { for _, dbGroup := range dbm.DBs {
if err := db.AutoMigrate(&Client{}); err != nil { for _, db := range dbGroup {
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Client{}).TableName()) 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(&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(&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(&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(&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(&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(&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 { func NewDBManager(defaultDBType string) *dbManagerImpl {
if dbs == nil { dbs := map[string]map[string]*gorm.DB{}
dbs = make(map[string]*gorm.DB)
}
return &dbManagerImpl{ return &dbManagerImpl{
DBs: dbs, DBs: dbs,
defaultDBType: defaultDBType, defaultDBType: defaultDBType,
} }
} }
func (dbm *dbManagerImpl) GetDB(dbType string) *gorm.DB { func (dbm *dbManagerImpl) GetDB(dbType string, dbRole string) *gorm.DB {
return dbm.DBs[dbType] return dbm.DBs[dbType][dbRole]
} }
func (dbm *dbManagerImpl) SetDB(dbType string, db *gorm.DB) { func (dbm *dbManagerImpl) SetDB(dbType string, dbRole string, db *gorm.DB) {
dbm.DBs[dbType] = db if dbm.DBs[dbType] == nil {
dbm.DBs[dbType] = map[string]*gorm.DB{}
}
dbm.DBs[dbType][dbRole] = db
} }
func (dbm *dbManagerImpl) RemoveDB(dbType string) { func (dbm *dbManagerImpl) RemoveDB(dbType string, dbRole string) {
delete(dbm.DBs, dbType) if dbm.DBs[dbType] == nil {
return
}
delete(dbm.DBs[dbType], dbRole)
} }
func (dbm *dbManagerImpl) GetDefaultDB() *gorm.DB { func (dbm *dbManagerImpl) GetDefaultDB() *gorm.DB {
db := dbm.DBs[dbm.defaultDBType] dbGroup := dbm.DBs[dbm.defaultDBType]
if dbm.debug { if dbm.debug {
return db.Debug() return dbGroup[defs.DBRoleDefault].Debug()
} }
return db return dbGroup[defs.DBRoleDefault]
} }
func (dbm *dbManagerImpl) SetDebug(debug bool) { func (dbm *dbManagerImpl) SetDebug(debug bool) {

View File

@@ -1,9 +1,6 @@
package models package models
const (
ROLE_ADMIN = "admin"
ROLE_NORMAL = "normal"
)
const ( const (
STATUS_UNKNOWN = iota STATUS_UNKNOWN = iota

View File

@@ -16,13 +16,13 @@ type Server struct {
type ServerEntity struct { type ServerEntity struct {
ServerID string `json:"client_id" gorm:"uniqueIndex;not null;primaryKey"` 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"` UserID int `json:"user_id" gorm:"not null"`
ServerIP string `json:"server_ip"` ServerIP string `json:"server_ip"`
ConfigContent []byte `json:"config_content"` ConfigContent []byte `json:"config_content"`
ConnectSecret string `json:"connect_secret" gorm:"not null"` ConnectSecret string `json:"connect_secret" gorm:"not null"`
Comment string `json:"comment"` Comment string `json:"comment"`
FRPsUrls GormArray[string] `json:"frps_urls"` FrpsUrls GormArray[string] `json:"frps_urls"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/VaalaCat/frp-panel/defs"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -30,9 +31,9 @@ var _ UserInfo = (*UserEntity)(nil)
type UserEntity struct { type UserEntity struct {
UserID int `json:"user_id" gorm:"primaryKey"` 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"` 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"` Status int `json:"status"`
Role string `json:"role"` Role string `json:"role"`
TenantID int `json:"tenant_id"` TenantID int `json:"tenant_id"`
@@ -40,6 +41,8 @@ type UserEntity struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
Groups []*UserGroup `json:"groups,omitempty" gorm:"many2many:user_group_memberships;"`
} }
func (u *UserEntity) GetUserID() int { func (u *UserEntity) GetUserID() int {
@@ -99,7 +102,7 @@ func (u *UserEntity) Valid() bool {
} }
func (u *UserEntity) IsAdmin() bool { func (u *UserEntity) IsAdmin() bool {
return u.Role == ROLE_ADMIN return u.Role == defs.UserRole_Admin
} }
func (u *User) TableName() string { func (u *User) TableName() string {

18
models/user_group.go Normal file
View File

@@ -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"
}

View File

@@ -229,6 +229,162 @@ func (x *RegisterResponse) GetStatus() *Status {
return nil 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 var File_api_auth_proto protoreflect.FileDescriptor
const file_api_auth_proto_rawDesc = "" + const file_api_auth_proto_rawDesc = "" +
@@ -253,7 +409,22 @@ const file_api_auth_proto_rawDesc = "" +
"\x06_email\"J\n" + "\x06_email\"J\n" +
"\x10RegisterResponse\x12+\n" + "\x10RegisterResponse\x12+\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01B\t\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 ( var (
file_api_auth_proto_rawDescOnce sync.Once file_api_auth_proto_rawDescOnce sync.Once
@@ -267,22 +438,27 @@ func file_api_auth_proto_rawDescGZIP() []byte {
return file_api_auth_proto_rawDescData 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{ var file_api_auth_proto_goTypes = []any{
(*LoginRequest)(nil), // 0: api_auth.LoginRequest (*LoginRequest)(nil), // 0: api_auth.LoginRequest
(*LoginResponse)(nil), // 1: api_auth.LoginResponse (*LoginResponse)(nil), // 1: api_auth.LoginResponse
(*RegisterRequest)(nil), // 2: api_auth.RegisterRequest (*RegisterRequest)(nil), // 2: api_auth.RegisterRequest
(*RegisterResponse)(nil), // 3: api_auth.RegisterResponse (*RegisterResponse)(nil), // 3: api_auth.RegisterResponse
(*Status)(nil), // 4: common.Status (*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{ var file_api_auth_proto_depIdxs = []int32{
4, // 0: api_auth.LoginResponse.status:type_name -> common.Status 7, // 0: api_auth.LoginResponse.status:type_name -> common.Status
4, // 1: api_auth.RegisterResponse.status:type_name -> common.Status 7, // 1: api_auth.RegisterResponse.status:type_name -> common.Status
2, // [2:2] is the sub-list for method output_type 4, // 2: api_auth.SignTokenRequest.permissions:type_name -> api_auth.APIPermission
2, // [2:2] is the sub-list for method input_type 7, // 3: api_auth.SignTokenResponse.status:type_name -> common.Status
2, // [2:2] is the sub-list for extension type_name 4, // [4:4] is the sub-list for method output_type
2, // [2:2] is the sub-list for extension extendee 4, // [4:4] is the sub-list for method input_type
0, // [0:2] is the sub-list for field type_name 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() } 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[1].OneofWrappers = []any{}
file_api_auth_proto_msgTypes[2].OneofWrappers = []any{} file_api_auth_proto_msgTypes[2].OneofWrappers = []any{}
file_api_auth_proto_msgTypes[3].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{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_auth_proto_rawDesc), len(file_api_auth_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_auth_proto_rawDesc), len(file_api_auth_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 4, NumMessages: 7,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@@ -24,6 +24,7 @@ const (
type InitClientRequest struct { type InitClientRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
ClientId *string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3,oneof" json:"client_id,omitempty"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -65,6 +66,13 @@ func (x *InitClientRequest) GetClientId() string {
return "" return ""
} }
func (x *InitClientRequest) GetEphemeral() bool {
if x != nil && x.Ephemeral != nil {
return *x.Ephemeral
}
return false
}
type InitClientResponse struct { type InitClientResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Status *Status `protobuf:"bytes,1,opt,name=status,proto3,oneof" json:"status,omitempty"` 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 = "" + const file_api_client_proto_rawDesc = "" +
"\n" + "\n" +
"\x10api_client.proto\x12\n" + "\x10api_client.proto\x12\n" +
"api_client\x1a\fcommon.proto\"C\n" + "api_client\x1a\fcommon.proto\"t\n" +
"\x11InitClientRequest\x12 \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" + "\n" +
"_client_id\"|\n" + "_client_idB\f\n" +
"\n" +
"_ephemeral\"|\n" +
"\x12InitClientResponse\x12+\n" + "\x12InitClientResponse\x12+\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\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" + "\tclient_id\x18\x02 \x01(\tH\x01R\bclientId\x88\x01\x01B\t\n" +

View File

@@ -289,7 +289,9 @@ type Client struct {
Stopped *bool `protobuf:"varint,7,opt,name=stopped,proto3,oneof" json:"stopped,omitempty"` 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 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -387,6 +389,20 @@ func (x *Client) GetFrpsUrl() string {
return "" 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 { type Server struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id *string `protobuf:"bytes,1,opt,name=id,proto3,oneof" json:"id,omitempty"` 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" + "\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" + "\x04data\x18\x02 \x01(\tH\x01R\x04data\x88\x01\x01B\t\n" +
"\a_statusB\a\n" + "\a_statusB\a\n" +
"\x05_data\"\x8a\x03\n" + "\x05_data\"\xf3\x03\n" +
"\x06Client\x12\x13\n" + "\x06Client\x12\x13\n" +
"\x02id\x18\x01 \x01(\tH\x00R\x02id\x88\x01\x01\x12\x1b\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" + "\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" + "client_ids\x18\b \x03(\tR\tclientIds\x12-\n" +
"\x10origin_client_id\x18\t \x01(\tH\x06R\x0eoriginClientId\x88\x01\x01\x12\x1e\n" + "\x10origin_client_id\x18\t \x01(\tH\x06R\x0eoriginClientId\x88\x01\x01\x12\x1e\n" +
"\bfrps_url\x18\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" + "\x03_idB\t\n" +
"\a_secretB\t\n" + "\a_secretB\t\n" +
"\a_configB\n" + "\a_configB\n" +
@@ -885,7 +904,10 @@ const file_common_proto_rawDesc = "" +
"\n" + "\n" +
"\b_stoppedB\x13\n" + "\b_stoppedB\x13\n" +
"\x11_origin_client_idB\v\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" + "\x06Server\x12\x13\n" +
"\x02id\x18\x01 \x01(\tH\x00R\x02id\x88\x01\x01\x12\x1b\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" + "\x06secret\x18\x02 \x01(\tH\x01R\x06secret\x88\x01\x01\x12\x13\n" +

View File

@@ -4,6 +4,7 @@ import (
"sync" "sync"
"github.com/VaalaCat/frp-panel/conf" "github.com/VaalaCat/frp-panel/conf"
"github.com/casbin/casbin/v2"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
) )
@@ -22,6 +23,39 @@ type application struct {
serverController ServerController serverController ServerController
rpcCred credentials.TransportCredentials rpcCred credentials.TransportCredentials
conf conf.Config 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. // GetClientCred implements Application.

View File

@@ -5,6 +5,7 @@ import (
"sync" "sync"
"github.com/VaalaCat/frp-panel/conf" "github.com/VaalaCat/frp-panel/conf"
"github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
) )
@@ -36,6 +37,12 @@ type Application interface {
SetConfig(conf.Config) SetConfig(conf.Config)
GetRPCCred() credentials.TransportCredentials GetRPCCred() credentials.TransportCredentials
SetRPCCred(credentials.TransportCredentials) SetRPCCred(credentials.TransportCredentials)
GetCurrentRole() string
SetCurrentRole(string)
GetEnforcer() *casbin.Enforcer
SetEnforcer(*casbin.Enforcer)
GetPermManager() PermissionManager
SetPermManager(PermissionManager)
} }
type Context struct { type Context struct {

View File

@@ -4,8 +4,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/utils"
"github.com/casbin/casbin/v2"
"github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/client/proxy"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
@@ -66,10 +68,10 @@ type ClientLogManager interface {
// models/db.go // models/db.go
type DBManager interface { type DBManager interface {
GetDB(dbType string) *gorm.DB GetDB(dbType string, dbRole string) *gorm.DB
GetDefaultDB() *gorm.DB GetDefaultDB() *gorm.DB
SetDB(dbType string, db *gorm.DB) SetDB(dbType string, dbRole string, db *gorm.DB)
RemoveDB(dbType string) RemoveDB(dbType string, dbRole string)
SetDebug(bool) SetDebug(bool)
Init() Init()
} }
@@ -157,3 +159,15 @@ type ServerHandler interface {
type MasterClient interface { type MasterClient interface {
Call() pb.MasterClient 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)
}

View File

@@ -2,6 +2,7 @@ package dao
import ( import (
"fmt" "fmt"
"time"
"github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/models"
"github.com/samber/lo" "github.com/samber/lo"
@@ -175,8 +176,6 @@ func (q *queryImpl) ListClients(userInfo models.UserInfo, page, pageSize int) ([
Where( Where(
db.Where( db.Where(
normalClientFilter(db), normalClientFilter(db),
).Or(
"is_shadow = ?", true,
), ),
).Offset(offset).Limit(pageSize).Find(&clients).Error ).Offset(offset).Limit(pageSize).Find(&clients).Error
if err != nil { if err != nil {
@@ -331,10 +330,30 @@ func (q *queryImpl) AdminGetClientIDsInShadowByClientID(clientID string) ([]stri
}), nil }), 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 { func normalClientFilter(db *gorm.DB) *gorm.DB {
// 1. 没shadow过的老client // 1. 没shadow过的老client
// 2. shadow过的shadow client // 2. shadow过的shadow client
return db.Where("origin_client_id is NULL"). // 3. 非临时节点
Or("is_shadow = ?", true). return db.Where(
Or("LENGTH(origin_client_id) = ?", 0) 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)),
),
)
} }

View File

@@ -62,6 +62,20 @@ func (q *queryImpl) UpdateUser(userInfo models.UserInfo, user *models.UserEntity
}).Error }).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) { func (q *queryImpl) GetUserByUserName(userName string) (*models.UserEntity, error) {
if userName == "" { if userName == "" {
return nil, fmt.Errorf("invalid user name") return nil, fmt.Errorf("invalid user name")

View File

@@ -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
}

View File

@@ -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()) 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) s.appInstance.GetClientsManager().Set(req.GetClientId(), cliType, sender)
done = rpc.Recv(s.appInstance, req.GetClientId()) done = rpc.Recv(s.appInstance, req.GetClientId())
sender.Send(&pb.ServerMessage{ sender.Send(&pb.ServerMessage{

43
services/rbac/init.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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`

View File

@@ -105,13 +105,14 @@ func GetClientCert(appInstance app.Application, clientID, clientSecret string, c
return resp.Cert return resp.Cert
} }
func InitClient(appInstance app.Application, clientID, joinToken string) (*pb.InitClientResponse, error) { func InitClient(cfg conf.Config, clientID, joinToken string, ephemeral bool) (*pb.InitClientResponse, error) {
apiEndpoint := conf.GetAPIURL(appInstance.GetConfig()) apiEndpoint := conf.GetAPIURL(cfg)
c := httpCli() c := httpCli()
rawReq, err := proto.Marshal(&pb.InitClientRequest{ rawReq, err := proto.Marshal(&pb.InitClientRequest{
ClientId: &clientID, ClientId: &clientID,
Ephemeral: &ephemeral,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -132,8 +133,8 @@ func InitClient(appInstance app.Application, clientID, joinToken string) (*pb.In
return resp, nil return resp, nil
} }
func GetClient(appInstance app.Application, clientID, joinToken string) (*pb.GetClientResponse, error) { func GetClient(cfg conf.Config, clientID, joinToken string) (*pb.GetClientResponse, error) {
apiEndpoint := conf.GetAPIURL(appInstance.GetConfig()) apiEndpoint := conf.GetAPIURL(cfg)
c := httpCli() c := httpCli()
rawReq, err := proto.Marshal(&pb.GetClientRequest{ rawReq, err := proto.Marshal(&pb.GetClientRequest{

View File

@@ -18,7 +18,7 @@ func GetValue[T any](c context.Context, key string) (T, bool) {
return v, true 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) val := c.Value(key)
if val == nil { if val == nil {
return *new(T), false return *new(T), false

11
utils/json.go Normal file
View File

@@ -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)
}

View File

@@ -24,7 +24,7 @@ func GetJwtToken(secretKey string, iat, seconds int64, payload string) (string,
// @iat: 时间戳 // @iat: 时间戳
// @seconds: 过期时间,单位秒 // @seconds: 过期时间,单位秒
// @payload: 数据载体 // @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 := make(jwt.MapClaims)
claims["exp"] = iat + seconds claims["exp"] = iat + seconds
claims["iat"] = iat claims["iat"] = iat

View File

@@ -1,5 +1,6 @@
import http from '@/api/http' import http from '@/api/http'
import { API_PATH } from '@/lib/consts' import { API_PATH } from '@/lib/consts'
import { SignTokenRequest, SignTokenResponse } from '@/lib/pb/api_auth'
import { import {
GetUserInfoRequest, GetUserInfoRequest,
GetUserInfoResponse, GetUserInfoResponse,
@@ -20,3 +21,8 @@ export const updateUserInfo = async (req: UpdateUserInfoRequest) => {
const res = await http.post(API_PATH + '/user/update', UpdateUserInfoRequest.toJson(req)) const res = await http.post(API_PATH + '/user/update', UpdateUserInfoRequest.toJson(req))
return UpdateUserInfoResponse.fromJson((res.data as BaseResponse).body) 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)
}

View File

@@ -227,6 +227,10 @@ export const ClientInfo = ({ client }: { client: ClientTableSchema }) => {
{`需要升级!`} {`需要升级!`}
</Badge> </Badge>
} }
{client.originClient.ephemeral && <Badge variant={"secondary"} className={`p-2 border font-mono w-fit text-nowrap rounded-full h-6`}>
{`临时`}
</Badge>}
</div> </div>
) )
} }

View File

@@ -1,14 +1,18 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import React from 'react' import React, { useState } from 'react'
import { JoinCommandStr } from '@/lib/consts' import { JoinCommandStr } from '@/lib/consts'
import { useStore } from '@nanostores/react' 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 { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { useTranslation } from 'react-i18next' 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 = () => { export const ClientJoinButton = () => {
const platformInfo = useStore($platformInfo) const platformInfo = useStore($platformInfo)
const token = useStore($token) const [joinToken, setJoinToken] = useState<undefined | string>(undefined)
const { t } = useTranslation() 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 ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -32,24 +55,36 @@ export const ClientJoinButton = () => {
{t('client.join.description')} (<a className='text-blue-500' href='https://github.com/VaalaCat/frp-panel/releases' target="_blank" rel="noopener noreferrer">{t('common.download')}</a>) {t('client.join.description')} (<a className='text-blue-500' href='https://github.com/VaalaCat/frp-panel/releases' target="_blank" rel="noopener noreferrer">{t('common.download')}</a>)
</p> </p>
</div> </div>
{token != undefined && <div className="grid gap-2"> <div className="grid gap-2">
<pre className="bg-muted p-3 rounded-md font-mono text-sm overflow-x-auto whitespace-pre-wrap break-all"> {joinToken != undefined && <>
{JoinCommandStr(platformInfo, token)} <pre className="bg-muted p-3 rounded-md font-mono text-sm overflow-x-auto whitespace-pre-wrap break-all">
</pre> {JoinCommandStr(platformInfo, joinToken)}
</pre>
<Button
size="sm"
variant="outline"
className="w-full"
onClick={() => {
if (joinToken) {
navigator.clipboard.writeText(JoinCommandStr(platformInfo, joinToken))
}
}}
disabled={!platformInfo}
>
{t('common.copy')}
</Button>
</>
}
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
className="w-full" className="w-full"
onClick={() => { onClick={handleNewToken}
if (token) {
navigator.clipboard.writeText(JoinCommandStr(platformInfo, token))
}
}}
disabled={!platformInfo} disabled={!platformInfo}
> >
{t('common.copy')} {t('client.join.sign_token')}
</Button> </Button>
</div>} </div>
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@@ -5,5 +5,15 @@ export function NeedUpgrade(version: ClientVersion | undefined) {
if (!version.gitVersion) return false if (!version.gitVersion) return false
const versionString = version?.gitVersion const versionString = version?.gitVersion
const [a, b, c] = versionString.split('.') 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
} }

View File

@@ -258,7 +258,8 @@
"button": "Batch Configuration", "button": "Batch Configuration",
"title": "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.", "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": { "start": {
"title": "Start Command", "title": "Start Command",

View File

@@ -263,7 +263,8 @@
"button": "批量配置", "button": "批量配置",
"title": "批量配置", "title": "批量配置",
"description": "提前下载好二进制文件运行以下命令client 将自动生成配置文件保存", "description": "提前下载好二进制文件运行以下命令client 将自动生成配置文件保存",
"copy": "复制命令" "copy": "复制命令",
"sign_token": "生成令牌"
}, },
"actions_menu": { "actions_menu": {
"title": "客户端操作", "title": "客户端操作",

View File

@@ -78,7 +78,7 @@ export const LinuxInstallCommand = <T extends Client | Server>(
item: T, item: T,
info: GetPlatformInfoResponse, 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 = <T extends Client | Server>( export const ClientEnvFile = <T extends Client | Server>(

View File

@@ -63,6 +63,45 @@ export interface RegisterResponse {
*/ */
status?: Status; 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 // @generated message type with reflection information, may provide speed optimized methods
class LoginRequest$Type extends MessageType<LoginRequest> { class LoginRequest$Type extends MessageType<LoginRequest> {
constructor() { constructor() {
@@ -275,3 +314,163 @@ class RegisterResponse$Type extends MessageType<RegisterResponse> {
* @generated MessageType for protobuf message api_auth.RegisterResponse * @generated MessageType for protobuf message api_auth.RegisterResponse
*/ */
export const RegisterResponse = new RegisterResponse$Type(); export const RegisterResponse = new RegisterResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class APIPermission$Type extends MessageType<APIPermission> {
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>): APIPermission {
const message = globalThis.Object.create((this.messagePrototype!));
if (value !== undefined)
reflectionMergePartial<APIPermission>(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<SignTokenRequest> {
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>): SignTokenRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.permissions = [];
if (value !== undefined)
reflectionMergePartial<SignTokenRequest>(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<SignTokenResponse> {
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>): SignTokenResponse {
const message = globalThis.Object.create((this.messagePrototype!));
if (value !== undefined)
reflectionMergePartial<SignTokenResponse>(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();

View File

@@ -23,6 +23,10 @@ export interface InitClientRequest {
* @generated from protobuf field: optional string client_id = 1; * @generated from protobuf field: optional string client_id = 1;
*/ */
clientId?: string; clientId?: string;
/**
* @generated from protobuf field: optional bool ephemeral = 2;
*/
ephemeral?: boolean;
} }
/** /**
* @generated from protobuf message api_client.InitClientResponse * @generated from protobuf message api_client.InitClientResponse
@@ -391,7 +395,8 @@ export interface GetProxyConfigResponse {
class InitClientRequest$Type extends MessageType<InitClientRequest> { class InitClientRequest$Type extends MessageType<InitClientRequest> {
constructor() { constructor() {
super("api_client.InitClientRequest", [ 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>): InitClientRequest { create(value?: PartialMessage<InitClientRequest>): InitClientRequest {
@@ -408,6 +413,9 @@ class InitClientRequest$Type extends MessageType<InitClientRequest> {
case /* optional string client_id */ 1: case /* optional string client_id */ 1:
message.clientId = reader.string(); message.clientId = reader.string();
break; break;
case /* optional bool ephemeral */ 2:
message.ephemeral = reader.bool();
break;
default: default:
let u = options.readUnknownField; let u = options.readUnknownField;
if (u === "throw") if (u === "throw")
@@ -423,6 +431,9 @@ class InitClientRequest$Type extends MessageType<InitClientRequest> {
/* optional string client_id = 1; */ /* optional string client_id = 1; */
if (message.clientId !== undefined) if (message.clientId !== undefined)
writer.tag(1, WireType.LengthDelimited).string(message.clientId); 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; let u = options.writeUnknownFields;
if (u !== false) if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);

View File

@@ -85,6 +85,14 @@ export interface Client {
* @generated from protobuf field: optional string frps_url = 10; * @generated from protobuf field: optional string frps_url = 10;
*/ */
frpsUrl?: string; // 客户端用于连接frps的url解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000 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 * @generated from protobuf message common.Server
@@ -467,7 +475,9 @@ class Client$Type extends MessageType<Client> {
{ no: 7, name: "stopped", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ }, { 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: 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: 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>): Client { create(value?: PartialMessage<Client>): Client {
@@ -509,6 +519,12 @@ class Client$Type extends MessageType<Client> {
case /* optional string frps_url */ 10: case /* optional string frps_url */ 10:
message.frpsUrl = reader.string(); message.frpsUrl = reader.string();
break; 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: default:
let u = options.readUnknownField; let u = options.readUnknownField;
if (u === "throw") if (u === "throw")
@@ -548,6 +564,12 @@ class Client$Type extends MessageType<Client> {
/* optional string frps_url = 10; */ /* optional string frps_url = 10; */
if (message.frpsUrl !== undefined) if (message.frpsUrl !== undefined)
writer.tag(10, WireType.LengthDelimited).string(message.frpsUrl); 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; let u = options.writeUnknownFields;
if (u !== false) if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);