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
import (
"fmt"
"github.com/VaalaCat/frp-panel/conf"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/middleware"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils/logger"
)
func LoginHandler(ctx *app.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
@@ -24,7 +25,28 @@ func LoginHandler(ctx *app.Context, req *pb.LoginRequest) (*pb.LoginResponse, er
}, nil
}
tokenStr := conf.GetCommonJWT(ctx.GetApp().GetConfig(), fmt.Sprint(user.GetUserID()))
userCount, err := dao.NewQuery(ctx).AdminCountUsers()
if err != nil {
logger.Logger(ctx).WithError(err).Error("get user count failed")
}
if userCount == 1 && user.GetSafeUserInfo().Role != defs.UserRole_Admin {
userEntity, ok := user.(models.User)
if !ok {
logger.Logger(ctx).Errorf("trans user entity failed, invalid user entity")
return &pb.LoginResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "invalid user"},
}, nil
}
userEntity.Role = defs.UserRole_Admin
dao.NewQuery(ctx).AdminUpdateUser(&models.UserEntity{
UserID: user.GetUserID(),
}, userEntity.UserEntity)
}
tokenStr := conf.GetJWTWithAllPermission(ctx.GetApp().GetConfig(), user.GetUserID())
ginCtx := ctx.GetGinCtx()
middleware.PushTokenStr(ginCtx, ctx.GetApp(), tokenStr)

View File

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

View File

@@ -7,6 +7,7 @@ import (
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/google/uuid"
)
@@ -28,6 +29,8 @@ func InitClientHandler(c *app.Context, req *pb.InitClientRequest) (*pb.InitClien
globalClientID := app.GlobalClientID(userInfo.GetUserName(), "c", userClientID)
logger.Logger(c).Infof("start to init client, request:[%s], transformed global client id:[%s]", req.String(), globalClientID)
if err := dao.NewQuery(c).CreateClient(userInfo,
&models.ClientEntity{
ClientID: globalClientID,
@@ -35,6 +38,7 @@ func InitClientHandler(c *app.Context, req *pb.InitClientRequest) (*pb.InitClien
UserID: userInfo.GetUserID(),
ConnectSecret: uuid.New().String(),
IsShadow: true,
Ephemeral: req.GetEphemeral(),
}); err != nil {
return &pb.InitClientResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}}, err
}

View File

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

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)
}
return &pb.Client{
respCli := &pb.Client{
Id: lo.ToPtr(c.ClientID),
Secret: lo.ToPtr(c.ConnectSecret),
Config: lo.ToPtr(string(c.ConfigContent)),
@@ -67,7 +67,12 @@ func ListClientsHandler(ctx *app.Context, req *pb.ListClientsRequest) (*pb.ListC
Stopped: lo.ToPtr(c.Stopped),
Comment: lo.ToPtr(c.Comment),
ClientIds: clientIDs,
Ephemeral: lo.ToPtr(c.Ephemeral),
}
if c.LastSeenAt != nil {
respCli.LastSeenAt = lo.ToPtr(c.LastSeenAt.UnixMilli())
}
return respCli
})
return &pb.ListClientsResponse{

View File

@@ -1,8 +1,6 @@
package client
import (
"fmt"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
@@ -23,6 +21,11 @@ func RPCPullConfig(ctx *app.Context, req *pb.PullClientConfigReq) (*pb.PullClien
return nil, err
}
if err := dao.NewQuery(ctx).AdminUpdateClientLastSeen(cli.ClientID); err != nil {
logger.Logger(ctx).WithError(err).Errorf("update client last_seen_at time error, req:[%s] clientId:[%s]",
req.String(), cli.ClientID)
}
if cli.IsShadow {
proxies, err := dao.NewQuery(ctx).AdminListProxyConfigsWithFilters(&models.ProxyConfigEntity{
OriginClientID: cli.ClientID,
@@ -34,7 +37,12 @@ func RPCPullConfig(ctx *app.Context, req *pb.PullClientConfigReq) (*pb.PullClien
}
if cli.Stopped && cli.IsShadow {
return nil, fmt.Errorf("client is stopped")
return &pb.PullClientConfigResp{
Client: &pb.Client{
Id: lo.ToPtr(cli.ClientID),
Stopped: lo.ToPtr(true),
},
}, nil
}
return &pb.PullClientConfigResp{

View File

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

View File

@@ -27,52 +27,51 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) {
router.POST("/auth", auth.MakeGinHandlerFunc(appInstance, auth.HandleLogin))
api := router.Group("/api")
v1 := api.Group("/v1")
api.POST("/v1/auth/cert", app.Wrapper(appInstance, auth.GetClientCert))
api.POST("/v1/auth/login", app.Wrapper(appInstance, auth.LoginHandler))
api.POST("/v1/auth/register", app.Wrapper(appInstance, auth.RegisterHandler))
api.GET("/v1/auth/logout", auth.RemoveJWTHandler(appInstance))
v1 := api.Group("/v1", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), middleware.RBAC(appInstance))
{
authRouter := v1.Group("/auth")
{
authRouter.POST("/login", app.Wrapper(appInstance, auth.LoginHandler))
authRouter.POST("/register", app.Wrapper(appInstance, auth.RegisterHandler))
authRouter.GET("/logout", auth.RemoveJWTHandler(appInstance))
authRouter.POST("/cert", app.Wrapper(appInstance, auth.GetClientCert))
}
userRouter := v1.Group("/user", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
userRouter := v1.Group("/user")
{
userRouter.POST("/get", app.Wrapper(appInstance, user.GetUserInfoHandler))
userRouter.POST("/update", app.Wrapper(appInstance, user.UpdateUserInfoHander))
userRouter.POST("/sign-token", app.Wrapper(appInstance, user.SignTokenHandler))
}
platformRouter := v1.Group("/platform", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
platformRouter := v1.Group("/platform")
{
platformRouter.GET("/baseinfo", platform.GetPlatformInfo(appInstance))
platformRouter.POST("/clientsstatus", app.Wrapper(appInstance, platform.GetClientsStatus))
}
clientRouter := v1.Group("/client", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
clientRouter := v1.Group("/client")
{
clientRouter.POST("/get", app.Wrapper(appInstance, client.GetClientHandler))
clientRouter.POST("/init", app.Wrapper(appInstance, client.InitClientHandler))
clientRouter.POST("/delete", app.Wrapper(appInstance, client.DeleteClientHandler))
clientRouter.POST("/list", app.Wrapper(appInstance, client.ListClientsHandler))
}
serverRouter := v1.Group("/server", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
serverRouter := v1.Group("/server")
{
serverRouter.POST("/get", app.Wrapper(appInstance, server.GetServerHandler))
serverRouter.POST("/init", app.Wrapper(appInstance, server.InitServerHandler))
serverRouter.POST("/delete", app.Wrapper(appInstance, server.DeleteServerHandler))
serverRouter.POST("/list", app.Wrapper(appInstance, server.ListServersHandler))
}
frpcRouter := v1.Group("/frpc", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
frpcRouter := v1.Group("/frpc")
{
frpcRouter.POST("/update", app.Wrapper(appInstance, client.UpdateFrpcHander))
frpcRouter.POST("/delete", app.Wrapper(appInstance, client.RemoveFrpcHandler))
frpcRouter.POST("/stop", app.Wrapper(appInstance, client.StopFRPCHandler))
frpcRouter.POST("/start", app.Wrapper(appInstance, client.StartFRPCHandler))
}
frpsRouter := v1.Group("/frps", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
frpsRouter := v1.Group("/frps")
{
frpsRouter.POST("/update", app.Wrapper(appInstance, server.UpdateFrpsHander))
frpsRouter.POST("/delete", app.Wrapper(appInstance, server.RemoveFrpsHandler))
}
proxyRouter := v1.Group("/proxy", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
proxyRouter := v1.Group("/proxy")
{
proxyRouter.POST("/get_by_cid", app.Wrapper(appInstance, proxy.GetProxyStatsByClientID))
proxyRouter.POST("/get_by_sid", app.Wrapper(appInstance, proxy.GetProxyStatsByServerID))
@@ -82,7 +81,7 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) {
proxyRouter.POST("/delete_config", app.Wrapper(appInstance, proxy.DeleteProxyConfig))
proxyRouter.POST("/get_config", app.Wrapper(appInstance, proxy.GetProxyConfig))
}
v1.GET("/pty/:clientID", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), shell.PTYHandler(appInstance))
v1.GET("/log", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), streamlog.GetLogHandler(appInstance))
v1.GET("/pty/:clientID", shell.PTYHandler(appInstance))
v1.GET("/log", streamlog.GetLogHandler(appInstance))
}
}

View File

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

View File

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

View File

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

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
import (
"embed"
"github.com/VaalaCat/frp-panel/cmd/frpp/shared"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/fatedier/golib/crypto"
"github.com/spf13/cobra"
)
//go:embed all:out
var fs embed.FS
func main() {
crypto.DefaultSalt = "frp"
logger.InitLogger()
cobra.MousetrapHelpText = ""
rootCmd := buildCommand()
setMasterCommandIfNonePresent(rootCmd)
rootCmd := shared.BuildCommand(fs)
shared.SetMasterCommandIfNonePresent(rootCmd)
rootCmd.Execute()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package main
package shared
import (
"context"
@@ -15,6 +15,7 @@ import (
"github.com/VaalaCat/frp-panel/biz/master/streamlog"
bizserver "github.com/VaalaCat/frp-panel/biz/server"
"github.com/VaalaCat/frp-panel/conf"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/api"
@@ -22,11 +23,13 @@ import (
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/master"
"github.com/VaalaCat/frp-panel/services/mux"
"github.com/VaalaCat/frp-panel/services/rbac"
"github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/services/watcher"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/VaalaCat/frp-panel/utils/wsgrpc"
"github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/gorilla/websocket"
@@ -38,9 +41,6 @@ import (
"gorm.io/gorm"
)
//go:embed all:out
var fs embed.FS
func NewLogHookManager() app.StreamLogHookMgr {
return &bizcommon.HookMgr{}
}
@@ -70,10 +70,6 @@ func NewClientsManager() app.ClientsManager {
return rpc.NewClientsManager()
}
func NewFs() embed.FS {
return fs
}
func NewPatchedConfig(param struct {
fx.In
@@ -96,7 +92,7 @@ func NewClientLogManager() app.ClientLogManager {
func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager {
logger.Logger(ctx).Infof("start to init database, type: %s", appInstance.GetConfig().DB.Type)
mgr := models.NewDBManager(nil, appInstance.GetConfig().DB.Type)
mgr := models.NewDBManager(appInstance.GetConfig().DB.Type)
appInstance.SetDBManager(mgr)
if appInstance.GetConfig().IsDebug {
@@ -104,7 +100,7 @@ func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager {
}
switch appInstance.GetConfig().DB.Type {
case "sqlite3":
case defs.DBTypeSQLite3:
if err := utils.EnsureDirectoryExists(appInstance.GetConfig().DB.DSN); err != nil {
logger.Logger(ctx).WithError(err).Warnf("ensure directory failed, data location: [%s], keep data in current directory",
appInstance.GetConfig().DB.DSN)
@@ -117,27 +113,34 @@ func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager {
if sqlitedb, err := gorm.Open(sqlite.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil {
logger.Logger(ctx).Panic(err)
} else {
appInstance.GetDBManager().SetDB("sqlite3", sqlitedb)
appInstance.GetDBManager().SetDB(defs.DBTypeSQLite3, defs.DBRoleDefault, sqlitedb)
logger.Logger(ctx).Infof("init database success, data location: [%s]", appInstance.GetConfig().DB.DSN)
}
case "mysql":
case defs.DBTypeMysql:
if mysqlDB, err := gorm.Open(mysql.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil {
logger.Logger(ctx).Panic(err)
} else {
appInstance.GetDBManager().SetDB("mysql", mysqlDB)
appInstance.GetDBManager().SetDB(defs.DBTypeMysql, defs.DBRoleDefault, mysqlDB)
logger.Logger(ctx).Infof("init database success, data type: [%s]", "mysql")
}
case "postgres":
case defs.DBTypePostgres:
if postgresDB, err := gorm.Open(postgres.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil {
logger.Logger(ctx).Panic(err)
} else {
appInstance.GetDBManager().SetDB("postgres", postgresDB)
appInstance.GetDBManager().SetDB(defs.DBTypePostgres, defs.DBRoleDefault, postgresDB)
logger.Logger(ctx).Infof("init database success, data type: [%s]", "postgres")
}
default:
logger.Logger(ctx).Panicf("currently unsupported database type: %s", appInstance.GetConfig().DB.Type)
}
memoryDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
logger.Logger(ctx).Panic(err)
}
appInstance.GetDBManager().SetDB(defs.DBTypeSQLite3, defs.DBRoleRam, memoryDB)
logger.Logger(ctx).Infof("init memory database success")
appInstance.GetDBManager().Init()
return mgr
}
@@ -279,7 +282,94 @@ func NewDefaultServerConfig(ctx *app.Context) conf.Config {
const splitter = "\n--------------------------------------------\n"
func NewConfigPrinter(ctx *app.Context, config conf.Config) {
func NewConfigPrinter(param struct {
fx.In
Ctx *app.Context
Config conf.Config
}) {
var (
ctx = param.Ctx
config = param.Config
)
logger.Logger(ctx).Infof("%srunning config is: %s%s", splitter, config.PrintStr(), splitter)
logger.Logger(ctx).Infof("%scurrent version: \n%s%s", splitter, conf.GetVersion().String(), splitter)
}
func NewAutoJoin(param struct {
fx.In
Role defs.AppRole
Ctx *app.Context
Cfg conf.Config `name:"argsPatchedConfig"`
CommonArgs CommonArgs
}) conf.Config {
var (
ctx = param.Ctx
clientID = param.Cfg.Client.ID
clientSecret = param.Cfg.Client.Secret
autoJoin = false
appInstance = param.Ctx.GetApp()
)
appInstance.SetConfig(param.Cfg)
if param.Role != defs.AppRole_Client {
return param.Cfg
}
// 用户不输入clientID和clientSecret时使用autoJoin
if len(clientSecret) == 0 || len(clientID) == 0 {
if param.CommonArgs.JoinToken != nil && len(*param.CommonArgs.JoinToken) > 0 {
autoJoin = true
} else {
if len(clientSecret) == 0 {
logger.Logger(ctx).Fatal("client secret cannot be empty")
}
if len(clientID) == 0 {
logger.Logger(ctx).Fatal("client id cannot be empty")
}
}
}
if autoJoin {
logger.Logger(ctx).Infof("start to try join master, clientID: [%s], clientSecret: [%s]", clientID, clientSecret)
cli, err := JoinMaster(param.Cfg, param.CommonArgs)
if err != nil {
logger.Logger(ctx).Fatalf("join master failed: %s", err.Error())
}
logger.Logger(ctx).Infof("join master success, clientID: [%s], clientInfo: [%s]", cli.GetId(), cli.String())
tmpCfg := appInstance.GetConfig()
tmpCfg.Client.ID = cli.GetId()
tmpCfg.Client.Secret = cli.GetSecret()
appInstance.SetConfig(tmpCfg)
}
return appInstance.GetConfig()
}
func NewPermissionManager(param struct {
fx.In
Enforcer *casbin.Enforcer
AppInstance app.Application
}) app.PermissionManager {
permMgr := rbac.NewPermManager(param.Enforcer)
param.AppInstance.SetPermManager(permMgr)
return permMgr
}
func NewEnforcer(param struct {
fx.In
Ctx *app.Context
DBmanager app.DBManager
AppInstance app.Application
}) *casbin.Enforcer {
e, err := rbac.InitializeCasbin(param.Ctx, param.DBmanager.GetDefaultDB())
if err != nil {
logger.Logger(param.Ctx).WithError(err).Fatal("initialize casbin failed")
}
param.AppInstance.SetEnforcer(e)
return e
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package common
import (
"context"
"encoding/json"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/models"
@@ -20,3 +21,27 @@ func GetUserInfo(c context.Context) models.UserInfo {
return u
}
func GetTokenPermission(c context.Context) ([]defs.APIPermission, error) {
val := c.Value(defs.TokenPayloadKey_Permissions)
if val == nil {
return nil, nil
}
raw, err := json.Marshal(val)
if err != nil {
return nil, err
}
perms := []defs.APIPermission{}
err = json.Unmarshal(raw, &perms)
if err != nil {
return nil, err
}
return perms, nil
}
func GetTokenString(c context.Context) string {
return c.Value(defs.TokenKey).(string)
}

View File

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

View File

@@ -25,7 +25,7 @@ type RespType interface {
pb.StartFRPCResponse | pb.StopFRPCResponse | pb.StartFRPSResponse | pb.StopFRPSResponse |
pb.GetProxyStatsByClientIDResponse | pb.GetProxyStatsByServerIDResponse |
pb.CreateProxyConfigResponse | pb.ListProxyConfigsResponse | pb.UpdateProxyConfigResponse |
pb.DeleteProxyConfigResponse | pb.GetProxyConfigResponse
pb.DeleteProxyConfigResponse | pb.GetProxyConfigResponse | pb.SignTokenResponse
}
func OKResp[T RespType](c *gin.Context, origin *T) {

View File

@@ -58,19 +58,38 @@ func FRPsAuthOption(cfg Config, isDefault bool) v1.HTTPPluginOptions {
}
}
func GetCommonJWT(cfg Config, uid string) string {
token, _ := utils.GetJwtTokenFromMap(JWTSecret(cfg),
func GetJWTWithPayload(cfg Config, uid int, payload map[string]interface{}) (string, error) {
payload[defs.UserIDKey] = uid
return utils.GetJwtTokenFromMap(JWTSecret(cfg),
time.Now().Unix(),
int64(cfg.App.CookieAge),
map[string]string{defs.UserIDKey: uid})
payload)
}
func GetJWTWithAllPermission(cfg Config, uid int) string {
token, _ := GetJWTWithPayload(cfg, uid, map[string]interface{}{
defs.TokenPayloadKey_Permissions: AllPermission(),
})
return token
}
func GetCommonJWTWithExpireTime(cfg Config, uid string, expSec int) string {
func AllPermission() []defs.APIPermission {
return []defs.APIPermission{{
Method: "*",
Path: "*",
}}
}
func GetCommonJWT(cfg Config, uid int) string {
token, _ := GetJWTWithPayload(cfg, uid, map[string]interface{}{})
return token
}
func GetCommonJWTWithExpireTime(cfg Config, uid int64, expSec int) string {
token, _ := utils.GetJwtTokenFromMap(JWTSecret(cfg),
time.Now().Unix(),
int64(expSec),
map[string]string{defs.UserIDKey: uid})
map[string]interface{}{defs.UserIDKey: uid})
return token
}

View File

@@ -54,6 +54,14 @@ const (
CliTypeClient = "client"
)
type AppRole string
const (
AppRole_Client AppRole = CliTypeClient
AppRole_Server AppRole = CliTypeServer
AppRole_Master AppRole = "master"
)
const (
DefaultServerID = "default"
DefaultAdminUserID = 1
@@ -77,3 +85,27 @@ const (
EnvClientAPIUrl = "CLIENT_API_URL"
EnvClientRPCUrl = "CLIENT_RPC_URL"
)
const (
DBRoleDefault = "default"
DBRoleRam = "ram"
)
const (
DBTypeSQLite3 = "sqlite3"
DBTypeMysql = "mysql"
DBTypePostgres = "postgres"
)
const (
UserRole_Admin = "admin"
UserRole_Normal = "normal"
)
type TokenStatus string
const (
TokenStatusActive TokenStatus = "active"
TokenStatusInactive TokenStatus = "inactive"
TokenStatusRevoked TokenStatus = "revoked"
)

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 为例
```
curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
curl -fSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
```
如果你在国内可以增加github加速到安装脚本前
```
curl -sSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
curl -fSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
```
注意,如果你使用 反向代理 TLS需要修改这行命令类似如下
```bash
curl -sSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- frp-panel client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https
curl -fSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- frp-panel client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https
```
## Docker Compose 部署

20
go.mod
View File

@@ -6,6 +6,8 @@ toolchain go1.24.1
require (
github.com/UserExistsError/conpty v0.1.4
github.com/casbin/casbin/v2 v2.105.0
github.com/casbin/gorm-adapter/v3 v3.32.0
github.com/coocood/freecache v1.2.4
github.com/creack/pty v1.1.24
github.com/fatedier/frp v0.62.0
@@ -37,9 +39,9 @@ require (
golang.org/x/net v0.39.0
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.36.5
gorm.io/driver/mysql v1.5.2
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12
k8s.io/apimachinery v0.28.8
)
@@ -49,7 +51,9 @@ require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
@@ -68,6 +72,9 @@ require (
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/gorilla/mux v1.8.1 // indirect
@@ -77,7 +84,8 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
@@ -88,6 +96,7 @@ require (
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/microsoft/go-mssqldb v1.6.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
@@ -110,7 +119,6 @@ require (
github.com/refraction-networking/utls v1.6.7 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
@@ -145,6 +153,8 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/sqlserver v1.5.3 // indirect
gorm.io/plugin/dbresolver v1.5.3 // indirect
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
modernc.org/libc v1.22.5 // indirect

107
go.sum
View File

@@ -1,6 +1,24 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
@@ -13,9 +31,17 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I=
github.com/casbin/casbin/v2 v2.105.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
github.com/casbin/gorm-adapter/v3 v3.32.0 h1:Au+IOILBIE9clox5BJhI2nA3p9t7Ep1ePlupdGbGfus=
github.com/casbin/gorm-adapter/v3 v3.32.0/go.mod h1:Zre/H8p17mpv5U3EaWgPoxLILLdXO3gHW5aoQQpUDZI=
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -38,6 +64,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
@@ -90,10 +118,20 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
@@ -117,10 +155,13 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -128,6 +169,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/iamacarpet/go-winpty v1.0.4 h1:r42xaLLRZcUqjX6vHZeHos2haACfWkooOJTnFdogBfI=
@@ -142,10 +185,18 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackpal/gateway v1.0.16 h1:mTBRuHSW8qviVqX7kXnxKevqlfS/OA01ys6k6fxSX7w=
github.com/jackpal/gateway v1.0.16/go.mod h1:IOn1OUbso/cGYmnCBZbCEqhNCLSz0xxdtIpUpri5/nA=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -170,20 +221,28 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc=
github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
@@ -202,6 +261,8 @@ github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouAN
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -297,6 +358,7 @@ github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lL
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@@ -318,10 +380,14 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
@@ -331,6 +397,7 @@ golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudw
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
@@ -341,10 +408,14 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
@@ -356,6 +427,7 @@ golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
@@ -369,7 +441,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -385,6 +460,7 @@ golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
@@ -393,10 +469,13 @@ golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
@@ -405,14 +484,18 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
@@ -442,6 +525,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -449,13 +533,18 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0=
gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d h1:cCKla0V7sa6eixh74LtGQXakTu5QJEzkcX7DzNRhFOE=
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -23,3 +23,18 @@ message RegisterRequest {
message RegisterResponse {
optional common.Status status = 1;
}
message APIPermission {
optional string method = 1;
optional string path = 2;
}
message SignTokenRequest {
optional int64 expires_in = 1;
repeated APIPermission permissions = 2;
}
message SignTokenResponse {
optional common.Status status = 1;
optional string token = 2;
}

View File

@@ -7,6 +7,7 @@ option go_package="../pb";
message InitClientRequest {
optional string client_id = 1;
optional bool ephemeral = 2;
}
message InitClientResponse {

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
optional string origin_client_id = 9;
optional string frps_url = 10; // 客户端用于连接frps的url解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000
optional bool ephemeral = 11; // 是否临时节点
optional int64 last_seen_at = 12; // 最后一次心跳时间戳
}
message Server {

View File

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

View File

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

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 {
gorm.Model
Name string `gorm:"uniqueIndex"`
Name string `gorm:"type:varchar(255);uniqueIndex"`
CertFile []byte
KeyFile []byte
CaFile []byte

View File

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

View File

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

View File

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

View File

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

View File

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

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
}
type APIPermission struct {
state protoimpl.MessageState `protogen:"open.v1"`
Method *string `protobuf:"bytes,1,opt,name=method,proto3,oneof" json:"method,omitempty"`
Path *string `protobuf:"bytes,2,opt,name=path,proto3,oneof" json:"path,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *APIPermission) Reset() {
*x = APIPermission{}
mi := &file_api_auth_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *APIPermission) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*APIPermission) ProtoMessage() {}
func (x *APIPermission) ProtoReflect() protoreflect.Message {
mi := &file_api_auth_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use APIPermission.ProtoReflect.Descriptor instead.
func (*APIPermission) Descriptor() ([]byte, []int) {
return file_api_auth_proto_rawDescGZIP(), []int{4}
}
func (x *APIPermission) GetMethod() string {
if x != nil && x.Method != nil {
return *x.Method
}
return ""
}
func (x *APIPermission) GetPath() string {
if x != nil && x.Path != nil {
return *x.Path
}
return ""
}
type SignTokenRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ExpiresIn *int64 `protobuf:"varint,1,opt,name=expires_in,json=expiresIn,proto3,oneof" json:"expires_in,omitempty"`
Permissions []*APIPermission `protobuf:"bytes,2,rep,name=permissions,proto3" json:"permissions,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SignTokenRequest) Reset() {
*x = SignTokenRequest{}
mi := &file_api_auth_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SignTokenRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SignTokenRequest) ProtoMessage() {}
func (x *SignTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_auth_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SignTokenRequest.ProtoReflect.Descriptor instead.
func (*SignTokenRequest) Descriptor() ([]byte, []int) {
return file_api_auth_proto_rawDescGZIP(), []int{5}
}
func (x *SignTokenRequest) GetExpiresIn() int64 {
if x != nil && x.ExpiresIn != nil {
return *x.ExpiresIn
}
return 0
}
func (x *SignTokenRequest) GetPermissions() []*APIPermission {
if x != nil {
return x.Permissions
}
return nil
}
type SignTokenResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status *Status `protobuf:"bytes,1,opt,name=status,proto3,oneof" json:"status,omitempty"`
Token *string `protobuf:"bytes,2,opt,name=token,proto3,oneof" json:"token,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SignTokenResponse) Reset() {
*x = SignTokenResponse{}
mi := &file_api_auth_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SignTokenResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SignTokenResponse) ProtoMessage() {}
func (x *SignTokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_auth_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SignTokenResponse.ProtoReflect.Descriptor instead.
func (*SignTokenResponse) Descriptor() ([]byte, []int) {
return file_api_auth_proto_rawDescGZIP(), []int{6}
}
func (x *SignTokenResponse) GetStatus() *Status {
if x != nil {
return x.Status
}
return nil
}
func (x *SignTokenResponse) GetToken() string {
if x != nil && x.Token != nil {
return *x.Token
}
return ""
}
var File_api_auth_proto protoreflect.FileDescriptor
const file_api_auth_proto_rawDesc = "" +
@@ -253,7 +409,22 @@ const file_api_auth_proto_rawDesc = "" +
"\x06_email\"J\n" +
"\x10RegisterResponse\x12+\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01B\t\n" +
"\a_statusB\aZ\x05../pbb\x06proto3"
"\a_status\"Y\n" +
"\rAPIPermission\x12\x1b\n" +
"\x06method\x18\x01 \x01(\tH\x00R\x06method\x88\x01\x01\x12\x17\n" +
"\x04path\x18\x02 \x01(\tH\x01R\x04path\x88\x01\x01B\t\n" +
"\a_methodB\a\n" +
"\x05_path\"\x80\x01\n" +
"\x10SignTokenRequest\x12\"\n" +
"\n" +
"expires_in\x18\x01 \x01(\x03H\x00R\texpiresIn\x88\x01\x01\x129\n" +
"\vpermissions\x18\x02 \x03(\v2\x17.api_auth.APIPermissionR\vpermissionsB\r\n" +
"\v_expires_in\"p\n" +
"\x11SignTokenResponse\x12+\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\x12\x19\n" +
"\x05token\x18\x02 \x01(\tH\x01R\x05token\x88\x01\x01B\t\n" +
"\a_statusB\b\n" +
"\x06_tokenB\aZ\x05../pbb\x06proto3"
var (
file_api_auth_proto_rawDescOnce sync.Once
@@ -267,22 +438,27 @@ func file_api_auth_proto_rawDescGZIP() []byte {
return file_api_auth_proto_rawDescData
}
var file_api_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_api_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_api_auth_proto_goTypes = []any{
(*LoginRequest)(nil), // 0: api_auth.LoginRequest
(*LoginResponse)(nil), // 1: api_auth.LoginResponse
(*RegisterRequest)(nil), // 2: api_auth.RegisterRequest
(*RegisterResponse)(nil), // 3: api_auth.RegisterResponse
(*Status)(nil), // 4: common.Status
(*LoginRequest)(nil), // 0: api_auth.LoginRequest
(*LoginResponse)(nil), // 1: api_auth.LoginResponse
(*RegisterRequest)(nil), // 2: api_auth.RegisterRequest
(*RegisterResponse)(nil), // 3: api_auth.RegisterResponse
(*APIPermission)(nil), // 4: api_auth.APIPermission
(*SignTokenRequest)(nil), // 5: api_auth.SignTokenRequest
(*SignTokenResponse)(nil), // 6: api_auth.SignTokenResponse
(*Status)(nil), // 7: common.Status
}
var file_api_auth_proto_depIdxs = []int32{
4, // 0: api_auth.LoginResponse.status:type_name -> common.Status
4, // 1: api_auth.RegisterResponse.status:type_name -> common.Status
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
7, // 0: api_auth.LoginResponse.status:type_name -> common.Status
7, // 1: api_auth.RegisterResponse.status:type_name -> common.Status
4, // 2: api_auth.SignTokenRequest.permissions:type_name -> api_auth.APIPermission
7, // 3: api_auth.SignTokenResponse.status:type_name -> common.Status
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_api_auth_proto_init() }
@@ -295,13 +471,16 @@ func file_api_auth_proto_init() {
file_api_auth_proto_msgTypes[1].OneofWrappers = []any{}
file_api_auth_proto_msgTypes[2].OneofWrappers = []any{}
file_api_auth_proto_msgTypes[3].OneofWrappers = []any{}
file_api_auth_proto_msgTypes[4].OneofWrappers = []any{}
file_api_auth_proto_msgTypes[5].OneofWrappers = []any{}
file_api_auth_proto_msgTypes[6].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_auth_proto_rawDesc), len(file_api_auth_proto_rawDesc)),
NumEnums: 0,
NumMessages: 4,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},

View File

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

View File

@@ -289,7 +289,9 @@ type Client struct {
Stopped *bool `protobuf:"varint,7,opt,name=stopped,proto3,oneof" json:"stopped,omitempty"`
ClientIds []string `protobuf:"bytes,8,rep,name=client_ids,json=clientIds,proto3" json:"client_ids,omitempty"` // some client can connected to more than one server, make a shadow client to handle this
OriginClientId *string `protobuf:"bytes,9,opt,name=origin_client_id,json=originClientId,proto3,oneof" json:"origin_client_id,omitempty"`
FrpsUrl *string `protobuf:"bytes,10,opt,name=frps_url,json=frpsUrl,proto3,oneof" json:"frps_url,omitempty"` // 客户端用于连接frps的url解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000
FrpsUrl *string `protobuf:"bytes,10,opt,name=frps_url,json=frpsUrl,proto3,oneof" json:"frps_url,omitempty"` // 客户端用于连接frps的url解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000
Ephemeral *bool `protobuf:"varint,11,opt,name=ephemeral,proto3,oneof" json:"ephemeral,omitempty"` // 是否临时节点
LastSeenAt *int64 `protobuf:"varint,12,opt,name=last_seen_at,json=lastSeenAt,proto3,oneof" json:"last_seen_at,omitempty"` // 最后一次心跳时间戳
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -387,6 +389,20 @@ func (x *Client) GetFrpsUrl() string {
return ""
}
func (x *Client) GetEphemeral() bool {
if x != nil && x.Ephemeral != nil {
return *x.Ephemeral
}
return false
}
func (x *Client) GetLastSeenAt() int64 {
if x != nil && x.LastSeenAt != nil {
return *x.LastSeenAt
}
return 0
}
type Server struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id *string `protobuf:"bytes,1,opt,name=id,proto3,oneof" json:"id,omitempty"`
@@ -862,7 +878,7 @@ const file_common_proto_rawDesc = "" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\x12\x17\n" +
"\x04data\x18\x02 \x01(\tH\x01R\x04data\x88\x01\x01B\t\n" +
"\a_statusB\a\n" +
"\x05_data\"\x8a\x03\n" +
"\x05_data\"\xf3\x03\n" +
"\x06Client\x12\x13\n" +
"\x02id\x18\x01 \x01(\tH\x00R\x02id\x88\x01\x01\x12\x1b\n" +
"\x06secret\x18\x02 \x01(\tH\x01R\x06secret\x88\x01\x01\x12\x1b\n" +
@@ -874,7 +890,10 @@ const file_common_proto_rawDesc = "" +
"client_ids\x18\b \x03(\tR\tclientIds\x12-\n" +
"\x10origin_client_id\x18\t \x01(\tH\x06R\x0eoriginClientId\x88\x01\x01\x12\x1e\n" +
"\bfrps_url\x18\n" +
" \x01(\tH\aR\afrpsUrl\x88\x01\x01B\x05\n" +
" \x01(\tH\aR\afrpsUrl\x88\x01\x01\x12!\n" +
"\tephemeral\x18\v \x01(\bH\bR\tephemeral\x88\x01\x01\x12%\n" +
"\flast_seen_at\x18\f \x01(\x03H\tR\n" +
"lastSeenAt\x88\x01\x01B\x05\n" +
"\x03_idB\t\n" +
"\a_secretB\t\n" +
"\a_configB\n" +
@@ -885,7 +904,10 @@ const file_common_proto_rawDesc = "" +
"\n" +
"\b_stoppedB\x13\n" +
"\x11_origin_client_idB\v\n" +
"\t_frps_url\"\xd8\x01\n" +
"\t_frps_urlB\f\n" +
"\n" +
"_ephemeralB\x0f\n" +
"\r_last_seen_at\"\xd8\x01\n" +
"\x06Server\x12\x13\n" +
"\x02id\x18\x01 \x01(\tH\x00R\x02id\x88\x01\x01\x12\x1b\n" +
"\x06secret\x18\x02 \x01(\tH\x01R\x06secret\x88\x01\x01\x12\x13\n" +

View File

@@ -4,6 +4,7 @@ import (
"sync"
"github.com/VaalaCat/frp-panel/conf"
"github.com/casbin/casbin/v2"
"google.golang.org/grpc/credentials"
)
@@ -22,6 +23,39 @@ type application struct {
serverController ServerController
rpcCred credentials.TransportCredentials
conf conf.Config
currentRole string
permManager PermissionManager
enforcer *casbin.Enforcer
}
// GetEnforcer implements Application.
func (a *application) GetEnforcer() *casbin.Enforcer {
return a.enforcer
}
// SetEnforcer implements Application.
func (a *application) SetEnforcer(c *casbin.Enforcer) {
a.enforcer = c
}
// GetPermManager implements Application.
func (a *application) GetPermManager() PermissionManager {
return a.permManager
}
// SetPermManager implements Application.
func (a *application) SetPermManager(p PermissionManager) {
a.permManager = p
}
// GetCurrentRole implements Application.
func (a *application) GetCurrentRole() string {
return a.currentRole
}
// SetCurrentRole implements Application.
func (a *application) SetCurrentRole(r string) {
a.currentRole = r
}
// GetClientCred implements Application.

View File

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

View File

@@ -4,8 +4,10 @@ import (
"sync"
"time"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/utils"
"github.com/casbin/casbin/v2"
"github.com/fatedier/frp/client/proxy"
v1 "github.com/fatedier/frp/pkg/config/v1"
@@ -66,10 +68,10 @@ type ClientLogManager interface {
// models/db.go
type DBManager interface {
GetDB(dbType string) *gorm.DB
GetDB(dbType string, dbRole string) *gorm.DB
GetDefaultDB() *gorm.DB
SetDB(dbType string, db *gorm.DB)
RemoveDB(dbType string)
SetDB(dbType string, dbRole string, db *gorm.DB)
RemoveDB(dbType string, dbRole string)
SetDebug(bool)
Init()
}
@@ -157,3 +159,15 @@ type ServerHandler interface {
type MasterClient interface {
Call() pb.MasterClient
}
// services/rbac/perm_manager.go
type PermissionManager interface {
AddUserToGroup(userID int, groupID string, tenantID int) (bool, error)
CheckPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
Enforcer() *casbin.Enforcer
GrantGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
GrantUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
RemoveUserFromGroup(userID int, groupID string, tenantID int) (bool, error)
RevokeGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
RevokeUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
}

View File

@@ -2,6 +2,7 @@ package dao
import (
"fmt"
"time"
"github.com/VaalaCat/frp-panel/models"
"github.com/samber/lo"
@@ -175,8 +176,6 @@ func (q *queryImpl) ListClients(userInfo models.UserInfo, page, pageSize int) ([
Where(
db.Where(
normalClientFilter(db),
).Or(
"is_shadow = ?", true,
),
).Offset(offset).Limit(pageSize).Find(&clients).Error
if err != nil {
@@ -331,10 +330,30 @@ func (q *queryImpl) AdminGetClientIDsInShadowByClientID(clientID string) ([]stri
}), nil
}
func (q *queryImpl) AdminUpdateClientLastSeen(clientID string) error {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Model(&models.Client{
ClientEntity: &models.ClientEntity{
ClientID: clientID,
}}).Update("last_seen_at", time.Now()).Error
}
func normalClientFilter(db *gorm.DB) *gorm.DB {
// 1. 没shadow过的老client
// 2. shadow过的shadow client
return db.Where("origin_client_id is NULL").
Or("is_shadow = ?", true).
Or("LENGTH(origin_client_id) = ?", 0)
// 3. 非临时节点
return db.Where(
db.Where("origin_client_id is NULL").
Or("is_shadow = ?", true).
Or("LENGTH(origin_client_id) = ?", 0),
).
Where(
db.Where(
db.Where("ephemeral is NULL").
Or("ephemeral = ?", false),
).Or(
db.Where("ephemeral = ?", true).
Where("last_seen_at > ?", time.Now().Add(-5*time.Minute)),
),
)
}

View File

@@ -62,6 +62,20 @@ func (q *queryImpl) UpdateUser(userInfo models.UserInfo, user *models.UserEntity
}).Error
}
func (q *queryImpl) AdminUpdateUser(userInfo models.UserInfo, user *models.UserEntity) error {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
user.UserID = userInfo.GetUserID()
return db.Model(&models.User{}).Where(
&models.User{
UserEntity: &models.UserEntity{
UserID: userInfo.GetUserID(),
},
},
).Save(&models.User{
UserEntity: user,
}).Error
}
func (q *queryImpl) GetUserByUserName(userName string) (*models.UserEntity, error) {
if userName == "" {
return nil, fmt.Errorf("invalid user name")

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())
}
if cliType == defs.CliTypeClient {
if err := dao.NewQuery(ctx).AdminUpdateClientLastSeen(req.GetClientId()); err != nil {
logger.Logger(ctx).Errorf("cannot update client last seen, %s id: [%s]", req.GetEvent().String(), req.GetClientId())
}
}
s.appInstance.GetClientsManager().Set(req.GetClientId(), cliType, sender)
done = rpc.Recv(s.appInstance, req.GetClientId())
sender.Send(&pb.ServerMessage{

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

View File

@@ -18,7 +18,7 @@ func GetValue[T any](c context.Context, key string) (T, bool) {
return v, true
}
func getValue[T any](c context.Context, key string) (interface{}, bool) {
func getValue[T any](c context.Context, key string) (any, bool) {
val := c.Value(key)
if val == nil {
return *new(T), false

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

View File

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

View File

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

View File

@@ -1,14 +1,18 @@
import { Button } from '@/components/ui/button'
import React from 'react'
import React, { useState } from 'react'
import { JoinCommandStr } from '@/lib/consts'
import { useStore } from '@nanostores/react'
import { $platformInfo, $token } from '@/store/user'
import { $platformInfo } from '@/store/user'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { useTranslation } from 'react-i18next'
import { signToken } from '@/api/user'
import { useQuery } from '@tanstack/react-query'
import { toast } from 'sonner'
import { RespCode } from '@/lib/pb/common'
export const ClientJoinButton = () => {
const platformInfo = useStore($platformInfo)
const token = useStore($token)
const [joinToken, setJoinToken] = useState<undefined | string>(undefined)
const { t } = useTranslation()
@@ -19,6 +23,25 @@ export const ClientJoinButton = () => {
)
}
const handleNewToken = async () => {
try {
const resp = await signToken({
expiresIn: BigInt(1000000000),
permissions: [
{ method: 'POST', path: '/api/v1/client/get', },
{ method: 'POST', path: '/api/v1/client/init', },
],
})
if (!resp || !resp.status || resp.status.code !== RespCode.SUCCESS) {
toast.error('server error')
return
}
setJoinToken(resp.token)
} catch (error) {
toast.error(JSON.stringify(error))
}
}
return (
<Popover>
<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>)
</p>
</div>
{token != undefined && <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">
{JoinCommandStr(platformInfo, token)}
</pre>
<div className="grid gap-2">
{joinToken != undefined && <>
<pre className="bg-muted p-3 rounded-md font-mono text-sm overflow-x-auto whitespace-pre-wrap break-all">
{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
size="sm"
variant="outline"
className="w-full"
onClick={() => {
if (token) {
navigator.clipboard.writeText(JoinCommandStr(platformInfo, token))
}
}}
onClick={handleNewToken}
disabled={!platformInfo}
>
{t('common.copy')}
{t('client.join.sign_token')}
</Button>
</div>}
</div>
</div>
</PopoverContent>
</Popover>

View File

@@ -5,5 +5,15 @@ export function NeedUpgrade(version: ClientVersion | undefined) {
if (!version.gitVersion) return false
const versionString = version?.gitVersion
const [a, b, c] = versionString.split('.')
return Number(b) < 1
if (Number(b) < 1) {
return true
}
console.log(Number(a), Number(b), Number(c))
if (a=='v0' && Number(b)<=1 && Number(c) <= 10) {
return true
}
return false
}

View File

@@ -258,7 +258,8 @@
"button": "Batch Configuration",
"title": "Batch Configuration",
"description": "Download the binary files in advance, run the following command, and the client will automatically generate and save the configuration file.",
"copy": "Copy Command"
"copy": "Copy Command",
"sign_token": "Generate Token"
},
"start": {
"title": "Start Command",

View File

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

View File

@@ -78,7 +78,7 @@ export const LinuxInstallCommand = <T extends Client | Server>(
item: T,
info: GetPlatformInfoResponse,
) => {
return `curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, ' ')}`
return `curl -fSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, ' ')}`
}
export const ClientEnvFile = <T extends Client | Server>(

View File

@@ -63,6 +63,45 @@ export interface RegisterResponse {
*/
status?: Status;
}
/**
* @generated from protobuf message api_auth.APIPermission
*/
export interface APIPermission {
/**
* @generated from protobuf field: optional string method = 1;
*/
method?: string;
/**
* @generated from protobuf field: optional string path = 2;
*/
path?: string;
}
/**
* @generated from protobuf message api_auth.SignTokenRequest
*/
export interface SignTokenRequest {
/**
* @generated from protobuf field: optional int64 expires_in = 1;
*/
expiresIn?: bigint;
/**
* @generated from protobuf field: repeated api_auth.APIPermission permissions = 2;
*/
permissions: APIPermission[];
}
/**
* @generated from protobuf message api_auth.SignTokenResponse
*/
export interface SignTokenResponse {
/**
* @generated from protobuf field: optional common.Status status = 1;
*/
status?: Status;
/**
* @generated from protobuf field: optional string token = 2;
*/
token?: string;
}
// @generated message type with reflection information, may provide speed optimized methods
class LoginRequest$Type extends MessageType<LoginRequest> {
constructor() {
@@ -275,3 +314,163 @@ class RegisterResponse$Type extends MessageType<RegisterResponse> {
* @generated MessageType for protobuf message api_auth.RegisterResponse
*/
export const RegisterResponse = new RegisterResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class APIPermission$Type extends MessageType<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;
*/
clientId?: string;
/**
* @generated from protobuf field: optional bool ephemeral = 2;
*/
ephemeral?: boolean;
}
/**
* @generated from protobuf message api_client.InitClientResponse
@@ -391,7 +395,8 @@ export interface GetProxyConfigResponse {
class InitClientRequest$Type extends MessageType<InitClientRequest> {
constructor() {
super("api_client.InitClientRequest", [
{ no: 1, name: "client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
{ no: 1, name: "client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "ephemeral", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ }
]);
}
create(value?: PartialMessage<InitClientRequest>): InitClientRequest {
@@ -408,6 +413,9 @@ class InitClientRequest$Type extends MessageType<InitClientRequest> {
case /* optional string client_id */ 1:
message.clientId = reader.string();
break;
case /* optional bool ephemeral */ 2:
message.ephemeral = reader.bool();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -423,6 +431,9 @@ class InitClientRequest$Type extends MessageType<InitClientRequest> {
/* optional string client_id = 1; */
if (message.clientId !== undefined)
writer.tag(1, WireType.LengthDelimited).string(message.clientId);
/* optional bool ephemeral = 2; */
if (message.ephemeral !== undefined)
writer.tag(2, WireType.Varint).bool(message.ephemeral);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);

View File

@@ -85,6 +85,14 @@ export interface Client {
* @generated from protobuf field: optional string frps_url = 10;
*/
frpsUrl?: string; // 客户端用于连接frps的url解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000
/**
* @generated from protobuf field: optional bool ephemeral = 11;
*/
ephemeral?: boolean; // 是否临时节点
/**
* @generated from protobuf field: optional int64 last_seen_at = 12;
*/
lastSeenAt?: bigint; // 最后一次心跳时间戳
}
/**
* @generated from protobuf message common.Server
@@ -467,7 +475,9 @@ class Client$Type extends MessageType<Client> {
{ no: 7, name: "stopped", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ },
{ no: 8, name: "client_ids", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
{ no: 9, name: "origin_client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 10, name: "frps_url", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
{ no: 10, name: "frps_url", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 11, name: "ephemeral", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ },
{ no: 12, name: "last_seen_at", kind: "scalar", opt: true, T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ }
]);
}
create(value?: PartialMessage<Client>): Client {
@@ -509,6 +519,12 @@ class Client$Type extends MessageType<Client> {
case /* optional string frps_url */ 10:
message.frpsUrl = reader.string();
break;
case /* optional bool ephemeral */ 11:
message.ephemeral = reader.bool();
break;
case /* optional int64 last_seen_at */ 12:
message.lastSeenAt = reader.int64().toBigInt();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -548,6 +564,12 @@ class Client$Type extends MessageType<Client> {
/* optional string frps_url = 10; */
if (message.frpsUrl !== undefined)
writer.tag(10, WireType.LengthDelimited).string(message.frpsUrl);
/* optional bool ephemeral = 11; */
if (message.ephemeral !== undefined)
writer.tag(11, WireType.Varint).bool(message.ephemeral);
/* optional int64 last_seen_at = 12; */
if (message.lastSeenAt !== undefined)
writer.tag(12, WireType.Varint).int64(message.lastSeenAt);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);