feat: frps can have multi endpoint

This commit is contained in:
VaalaCat
2025-04-27 13:27:45 +00:00
parent e919b8b477
commit 50001f9afc
40 changed files with 578 additions and 132 deletions

View File

@@ -73,6 +73,7 @@ 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),
ClientIds: nil,
}
}

View File

@@ -2,6 +2,7 @@ package client
import (
"fmt"
"net/url"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
@@ -9,6 +10,7 @@ import (
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/samber/lo"
"github.com/spf13/cast"
"github.com/tiendc/go-deepcopy"
)
@@ -113,3 +115,34 @@ func ChildClientForServer(c *app.Context, serverID string, clientEntity *models.
return copiedClient, nil
}
func ValidFrpsScheme(scheme string) bool {
return scheme == "tcp" ||
scheme == "kcp" || scheme == "quic" ||
scheme == "websocket" || scheme == "wss"
}
func ValidateFrpsUrl(urlStr string) (*url.URL, error) {
parsedFrpsUrl, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("parse frps url error")
}
if !ValidFrpsScheme(parsedFrpsUrl.Scheme) {
return nil, fmt.Errorf("invalid frps scheme")
}
if len(parsedFrpsUrl.Host) == 0 {
return nil, fmt.Errorf("invalid frps host")
}
if len(parsedFrpsUrl.Hostname()) == 0 {
return nil, fmt.Errorf("invalid frps hostname")
}
if cast.ToInt(parsedFrpsUrl.Port()) == 0 {
return nil, fmt.Errorf("invalid frps port")
}
return parsedFrpsUrl, nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/url"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/defs"
@@ -16,6 +17,7 @@ import (
"github.com/VaalaCat/frp-panel/utils/logger"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/samber/lo"
"github.com/spf13/cast"
)
func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPCResponse, error) {
@@ -77,6 +79,13 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
}, fmt.Errorf("cannot get server")
}
if len(srv.ConfigContent) == 0 {
logger.Logger(c).Errorf("cannot get server, server is not prepared, id: [%s]", req.GetServerId())
return &pb.UpdateFRPCResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "server is not prepared, pls update server config first"},
}, fmt.Errorf("server is not prepared, pls update server config first")
}
srvConf, err := srv.GetConfigContent()
if srvConf == nil || err != nil {
logger.Logger(c).WithError(err).Errorf("cannot get server, id: [%s]", serverID)
@@ -85,8 +94,6 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
cliCfg.ServerAddr = srv.ServerIP
switch cliCfg.Transport.Protocol {
case "tcp":
cliCfg.ServerPort = srvConf.BindPort
case "kcp":
cliCfg.ServerPort = srvConf.KCPBindPort
case "quic":
@@ -95,6 +102,41 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
cliCfg.ServerPort = srvConf.BindPort
}
if len(req.GetFrpsUrl()) > 0 || len(cli.FRPsUrl) > 0 {
// 有一个有就需要覆盖优先请求的url
var (
parsedFrpsUrl *url.URL
err error
urlToParse string
)
if len(req.GetFrpsUrl()) > 0 {
parsedFrpsUrl, err = ValidateFrpsUrl(req.GetFrpsUrl())
if err != nil {
logger.Logger(c).WithError(err).Errorf("invalid frps url, url: [%s]", req.GetFrpsUrl())
return &pb.UpdateFRPCResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()},
}, err
}
urlToParse = req.GetFrpsUrl()
}
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)
return &pb.UpdateFRPCResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()},
}, err
}
urlToParse = cli.FRPsUrl
}
cliCfg.ServerAddr = parsedFrpsUrl.Hostname()
cliCfg.ServerPort = cast.ToInt(parsedFrpsUrl.Port())
cliCfg.Transport.Protocol = parsedFrpsUrl.Scheme
cli.FRPsUrl = urlToParse
}
cliCfg.User = userInfo.GetUserName()
if cliCfg.Metadatas == nil {

View File

@@ -34,11 +34,12 @@ func GetServerHandler(c *app.Context, req *pb.GetServerRequest) (*pb.GetServerRe
return &pb.GetServerResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
Server: &pb.Server{
Id: lo.ToPtr(serverEntity.ServerID),
Config: lo.ToPtr(string(serverEntity.ConfigContent)),
Secret: lo.ToPtr(serverEntity.ConnectSecret),
Comment: lo.ToPtr(serverEntity.Comment),
Ip: lo.ToPtr(serverEntity.ServerIP),
Id: lo.ToPtr(serverEntity.ServerID),
Config: lo.ToPtr(string(serverEntity.ConfigContent)),
Secret: lo.ToPtr(serverEntity.ConnectSecret),
Comment: lo.ToPtr(serverEntity.Comment),
Ip: lo.ToPtr(serverEntity.ServerIP),
FrpsUrls: serverEntity.FRPsUrls,
},
}, nil
}

View File

@@ -49,11 +49,12 @@ func ListServersHandler(c *app.Context, req *pb.ListServersRequest) (*pb.ListSer
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
Servers: lo.Map(servers, func(c *models.ServerEntity, _ int) *pb.Server {
return &pb.Server{
Id: lo.ToPtr(c.ServerID),
Config: lo.ToPtr(string(c.ConfigContent)),
Secret: lo.ToPtr(c.ConnectSecret),
Ip: lo.ToPtr(c.ServerIP),
Comment: lo.ToPtr(c.Comment),
Id: lo.ToPtr(c.ServerID),
Config: lo.ToPtr(string(c.ConfigContent)),
Secret: lo.ToPtr(c.ConnectSecret),
Ip: lo.ToPtr(c.ServerIP),
Comment: lo.ToPtr(c.Comment),
FrpsUrls: c.FRPsUrls,
}
}),
Total: lo.ToPtr(int32(serverCounts)),

View File

@@ -52,6 +52,10 @@ func UpdateFrpsHander(c *app.Context, req *pb.UpdateFRPSRequest) (*pb.UpdateFRPS
srv.ServerIP = req.GetServerIp()
}
if len(req.GetFrpsUrls()) > 0 {
srv.FRPsUrls = req.GetFrpsUrls()
}
if err := dao.NewQuery(c).UpdateServer(userInfo, srv); err != nil {
logger.Logger(context.Background()).WithError(err).Errorf("cannot update server, id: [%s]", serverID)
return nil, err

View File

@@ -35,6 +35,11 @@ type CommonArgs struct {
func buildCommand() *cobra.Command {
cfg := conf.NewConfig()
logger.UpdateLoggerOpt(
cfg.Logger.FRPLoggerLevel,
cfg.Logger.DefaultLoggerLevel,
)
return NewRootCmd(
NewMasterCmd(cfg),
NewClientCmd(cfg),
@@ -392,7 +397,7 @@ func patchConfig(appInstance app.Application, commonArgs CommonArgs) conf.Config
func warnDepParam(cmd *cobra.Command) {
if appSecret, _ := cmd.Flags().GetString("app"); len(appSecret) != 0 {
logger.Logger(context.Background()).Fatalf(
logger.Logger(context.Background()).Errorf(
"\n⚠\n\n-a / -app / APP_SECRET 参数已停止使用,请删除该参数重新启动\n\n" +
"The -a / -app / APP_SECRET parameter is deprecated. Please remove it and restart.\n\n")
}

View File

@@ -53,6 +53,10 @@ type Config struct {
TLSInsecureSkipVerify bool `env:"TLS_INSECURE_SKIP_VERIFY" env-default:"true" env-description:"skip tls verify"`
} `env-prefix:"CLIENT_"`
IsDebug bool `env:"IS_DEBUG" env-default:"false" env-description:"is debug mode"`
Logger struct {
DefaultLoggerLevel string `env:"DEFAULT_LOGGER_LEVEL" env-default:"info" env-description:"frp-panel internal default logger level"`
FRPLoggerLevel string `env:"FRP_LOGGER_LEVEL" env-default:"info" env-description:"frp logger level"`
} `env-prefix:"LOGGER_"`
}
func NewConfig() Config {

3
go.mod
View File

@@ -27,8 +27,10 @@ require (
github.com/shirou/gopsutil/v4 v4.24.11
github.com/sirupsen/logrus v1.9.3
github.com/sourcegraph/conc v0.3.0
github.com/spf13/cast v1.7.1
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/tidwall/pretty v1.2.1
github.com/tiendc/go-deepcopy v1.2.0
go.uber.org/fx v1.23.0
golang.org/x/crypto v0.37.0
@@ -114,7 +116,6 @@ require (
github.com/stretchr/testify v1.10.0 // indirect
github.com/templexxx/cpu v0.1.1 // indirect
github.com/templexxx/xorsimd v0.4.3 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect

4
go.sum
View File

@@ -49,6 +49,8 @@ github.com/fatedier/frp v0.62.0 h1:0ti+kNLCRcs8TmFyxUKiXuyCDRF+hkuh7MI3yrhGcEs=
github.com/fatedier/frp v0.62.0/go.mod h1:Kfctx9dDnV+8vr2BYfwmKIbNPSbgyvIcWsVSkHWq2jM=
github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -242,6 +244,8 @@ github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=

View File

@@ -49,6 +49,7 @@ message UpdateFRPCRequest {
optional string server_id = 2;
optional bytes config = 3;
optional string comment = 4;
optional string frps_url = 5;
}
message UpdateFRPCResponse {

View File

@@ -49,6 +49,7 @@ message UpdateFRPSRequest {
optional bytes config = 2;
optional string comment = 3;
optional string server_ip = 4;
repeated string frps_urls = 5;
}
message UpdateFRPSResponse {

View File

@@ -42,6 +42,7 @@ message Client {
optional bool stopped = 7;
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
}
message Server {
@@ -50,6 +51,7 @@ message Server {
optional string ip = 3;
optional string config = 4; // 在定义上ip和port只是为了方便使用
optional string comment = 5; // 用户自定义的备注
repeated string frps_urls = 6; // 客户端用于连接frps的url解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000可以有多个
}
message User {

View File

@@ -25,6 +25,7 @@ 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"`

View File

@@ -15,13 +15,14 @@ type Server struct {
}
type ServerEntity struct {
ServerID string `json:"client_id" gorm:"uniqueIndex;not null;primaryKey"`
TenantID int `json:"tenant_id" gorm:"not null"`
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"`
ServerID string `json:"client_id" gorm:"uniqueIndex;not null;primaryKey"`
TenantID int `json:"tenant_id" gorm:"not null"`
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"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`

28
models/types.go Normal file
View File

@@ -0,0 +1,28 @@
package models
import (
"database/sql/driver"
"encoding/json"
)
type GormArray[T any] []T
func (p GormArray[T]) Value() (driver.Value, error) {
return json.Marshal(p)
}
func (p *GormArray[T]) Scan(data interface{}) error {
return json.Unmarshal(data.([]byte), &p)
}
type JSON[T any] struct {
Data T
}
func (j JSON[T]) Value() (driver.Value, error) {
return json.Marshal(j)
}
func (j *JSON[T]) Scan(value interface{}) error {
return json.Unmarshal(value.([]byte), &j)
}

View File

@@ -435,6 +435,7 @@ type UpdateFRPCRequest struct {
ServerId *string `protobuf:"bytes,2,opt,name=server_id,json=serverId,proto3,oneof" json:"server_id,omitempty"`
Config []byte `protobuf:"bytes,3,opt,name=config,proto3,oneof" json:"config,omitempty"`
Comment *string `protobuf:"bytes,4,opt,name=comment,proto3,oneof" json:"comment,omitempty"`
FrpsUrl *string `protobuf:"bytes,5,opt,name=frps_url,json=frpsUrl,proto3,oneof" json:"frps_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -497,6 +498,13 @@ func (x *UpdateFRPCRequest) GetComment() string {
return ""
}
func (x *UpdateFRPCRequest) GetFrpsUrl() string {
if x != nil && x.FrpsUrl != nil {
return *x.FrpsUrl
}
return ""
}
type UpdateFRPCResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status *Status `protobuf:"bytes,1,opt,name=status,proto3,oneof" json:"status,omitempty"`
@@ -1534,19 +1542,21 @@ const file_api_client_proto_rawDesc = "" +
"_client_id\"N\n" +
"\x14DeleteClientResponse\x12+\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01B\t\n" +
"\a_status\"\xc6\x01\n" +
"\a_status\"\xf3\x01\n" +
"\x11UpdateFRPCRequest\x12 \n" +
"\tclient_id\x18\x01 \x01(\tH\x00R\bclientId\x88\x01\x01\x12 \n" +
"\tserver_id\x18\x02 \x01(\tH\x01R\bserverId\x88\x01\x01\x12\x1b\n" +
"\x06config\x18\x03 \x01(\fH\x02R\x06config\x88\x01\x01\x12\x1d\n" +
"\acomment\x18\x04 \x01(\tH\x03R\acomment\x88\x01\x01B\f\n" +
"\acomment\x18\x04 \x01(\tH\x03R\acomment\x88\x01\x01\x12\x1e\n" +
"\bfrps_url\x18\x05 \x01(\tH\x04R\afrpsUrl\x88\x01\x01B\f\n" +
"\n" +
"_client_idB\f\n" +
"\n" +
"_server_idB\t\n" +
"\a_configB\n" +
"\n" +
"\b_comment\"L\n" +
"\b_commentB\v\n" +
"\t_frps_url\"L\n" +
"\x12UpdateFRPCResponse\x12+\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01B\t\n" +
"\a_status\"C\n" +

View File

@@ -443,6 +443,7 @@ type UpdateFRPSRequest struct {
Config []byte `protobuf:"bytes,2,opt,name=config,proto3,oneof" json:"config,omitempty"`
Comment *string `protobuf:"bytes,3,opt,name=comment,proto3,oneof" json:"comment,omitempty"`
ServerIp *string `protobuf:"bytes,4,opt,name=server_ip,json=serverIp,proto3,oneof" json:"server_ip,omitempty"`
FrpsUrls []string `protobuf:"bytes,5,rep,name=frps_urls,json=frpsUrls,proto3" json:"frps_urls,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -505,6 +506,13 @@ func (x *UpdateFRPSRequest) GetServerIp() string {
return ""
}
func (x *UpdateFRPSRequest) GetFrpsUrls() []string {
if x != nil {
return x.FrpsUrls
}
return nil
}
type UpdateFRPSResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status *Status `protobuf:"bytes,1,opt,name=status,proto3,oneof" json:"status,omitempty"`
@@ -961,12 +969,13 @@ const file_api_server_proto_rawDesc = "" +
"_server_id\"N\n" +
"\x14DeleteServerResponse\x12+\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01B\t\n" +
"\a_status\"\xc6\x01\n" +
"\a_status\"\xe3\x01\n" +
"\x11UpdateFRPSRequest\x12 \n" +
"\tserver_id\x18\x01 \x01(\tH\x00R\bserverId\x88\x01\x01\x12\x1b\n" +
"\x06config\x18\x02 \x01(\fH\x01R\x06config\x88\x01\x01\x12\x1d\n" +
"\acomment\x18\x03 \x01(\tH\x02R\acomment\x88\x01\x01\x12 \n" +
"\tserver_ip\x18\x04 \x01(\tH\x03R\bserverIp\x88\x01\x01B\f\n" +
"\tserver_ip\x18\x04 \x01(\tH\x03R\bserverIp\x88\x01\x01\x12\x1b\n" +
"\tfrps_urls\x18\x05 \x03(\tR\bfrpsUrlsB\f\n" +
"\n" +
"_server_idB\t\n" +
"\a_configB\n" +

View File

@@ -289,6 +289,7 @@ 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
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -379,13 +380,21 @@ func (x *Client) GetOriginClientId() string {
return ""
}
func (x *Client) GetFrpsUrl() string {
if x != nil && x.FrpsUrl != nil {
return *x.FrpsUrl
}
return ""
}
type Server struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id *string `protobuf:"bytes,1,opt,name=id,proto3,oneof" json:"id,omitempty"`
Secret *string `protobuf:"bytes,2,opt,name=secret,proto3,oneof" json:"secret,omitempty"`
Ip *string `protobuf:"bytes,3,opt,name=ip,proto3,oneof" json:"ip,omitempty"`
Config *string `protobuf:"bytes,4,opt,name=config,proto3,oneof" json:"config,omitempty"` // 在定义上ip和port只是为了方便使用
Comment *string `protobuf:"bytes,5,opt,name=comment,proto3,oneof" json:"comment,omitempty"` // 用户自定义的备注
Config *string `protobuf:"bytes,4,opt,name=config,proto3,oneof" json:"config,omitempty"` // 在定义上ip和port只是为了方便使用
Comment *string `protobuf:"bytes,5,opt,name=comment,proto3,oneof" json:"comment,omitempty"` // 用户自定义的备注
FrpsUrls []string `protobuf:"bytes,6,rep,name=frps_urls,json=frpsUrls,proto3" json:"frps_urls,omitempty"` // 客户端用于连接frps的url解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000可以有多个
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -455,6 +464,13 @@ func (x *Server) GetComment() string {
return ""
}
func (x *Server) GetFrpsUrls() []string {
if x != nil {
return x.FrpsUrls
}
return nil
}
type User struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserID *int64 `protobuf:"varint,1,opt,name=UserID,proto3,oneof" json:"UserID,omitempty"`
@@ -846,7 +862,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\"\xdd\x02\n" +
"\x05_data\"\x8a\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" +
@@ -856,7 +872,9 @@ const file_common_proto_rawDesc = "" +
"\astopped\x18\a \x01(\bH\x05R\astopped\x88\x01\x01\x12\x1d\n" +
"\n" +
"client_ids\x18\b \x03(\tR\tclientIds\x12-\n" +
"\x10origin_client_id\x18\t \x01(\tH\x06R\x0eoriginClientId\x88\x01\x01B\x05\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" +
"\x03_idB\t\n" +
"\a_secretB\t\n" +
"\a_configB\n" +
@@ -866,13 +884,15 @@ const file_common_proto_rawDesc = "" +
"_server_idB\n" +
"\n" +
"\b_stoppedB\x13\n" +
"\x11_origin_client_id\"\xbb\x01\n" +
"\x11_origin_client_idB\v\n" +
"\t_frps_url\"\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" +
"\x02ip\x18\x03 \x01(\tH\x02R\x02ip\x88\x01\x01\x12\x1b\n" +
"\x06config\x18\x04 \x01(\tH\x03R\x06config\x88\x01\x01\x12\x1d\n" +
"\acomment\x18\x05 \x01(\tH\x04R\acomment\x88\x01\x01B\x05\n" +
"\acomment\x18\x05 \x01(\tH\x04R\acomment\x88\x01\x01\x12\x1b\n" +
"\tfrps_urls\x18\x06 \x03(\tR\bfrpsUrlsB\x05\n" +
"\x03_idB\t\n" +
"\a_secretB\x05\n" +
"\x03_ipB\t\n" +

View File

@@ -14,6 +14,7 @@ import (
"github.com/fatedier/frp/client/proxy"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/featuregate"
"github.com/samber/lo"
"github.com/sourcegraph/conc"
)
@@ -32,6 +33,12 @@ func NewClientHandler(commonCfg *v1.ClientCommonConfig,
visitorCfgs []v1.VisitorConfigurer) app.ClientHandler {
ctx := context.Background()
if len(commonCfg.FeatureGates) > 0 {
if err := featuregate.SetFromMap(commonCfg.FeatureGates); err != nil {
logger.Logger(ctx).WithError(err).Errorf("there's a feature gate settings, but set failed: %+v, skip", commonCfg.FeatureGates)
}
}
warning, err := validation.ValidateAllClientConfig(commonCfg, proxyCfgs, visitorCfgs)
if warning != nil {
logger.Logger(ctx).WithError(err).Warnf("validate client config warning: %+v", warning)

View File

@@ -2,6 +2,7 @@ package logger
import (
"bufio"
"context"
"fmt"
"os"
"path/filepath"
@@ -14,17 +15,16 @@ import (
"github.com/sirupsen/logrus"
)
func InitFrpLogger() {
func initFrpLogger(frpLogLevel log.Level) {
frplog.Logger = log.New(
log.WithCaller(true),
log.AddCallerSkip(1),
log.WithLevel(log.InfoLevel),
log.WithLevel(frpLogLevel),
log.WithOutput(logger))
}
func InitLogger() {
// projectRoot, projectPkg, _ := findProjectRootAndModule()
InitFrpLogger()
Instance().SetReportCaller(true)
Instance().SetFormatter(NewCustomFormatter(false, true))
@@ -34,6 +34,36 @@ func InitLogger() {
logrus.SetFormatter(NewCustomFormatter(false, true))
}
func UpdateLoggerOpt(frpLogLevel string, logrusLevel string) {
ctx := context.Background()
frpLogLevel = strings.ToLower(frpLogLevel)
logrusLevel = strings.ToLower(logrusLevel)
if frpLogLevel == "" {
frpLogLevel = "info"
}
if logrusLevel == "" {
logrusLevel = "info"
}
frpLv, err := log.ParseLevel(frpLogLevel)
if err != nil {
Logger(ctx).WithError(err).Errorf("invalid frp log level: %s, use info", frpLogLevel)
frpLv = log.InfoLevel
}
logrusLv, err := logrus.ParseLevel(logrusLevel)
if err != nil {
Logger(ctx).WithError(err).Errorf("invalid logrus log level: %s, use info", logrusLevel)
logrusLv = logrus.InfoLevel
}
Instance().SetLevel(logrusLv)
logrus.SetLevel(logrusLv)
initFrpLogger(frpLv)
}
func NewCallerPrettyfier(projectRoot, projectPkg string) func(frame *runtime.Frame) (function string, file string) {
return func(frame *runtime.Frame) (function string, file string) {
file = frame.File

View File

@@ -117,19 +117,19 @@ export const APITest = () => {
<Separator />
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
<Button
onClick={async () => {
console.log(
'attempting update frps:',
await updateFRPS({
serverId: serverID,
config: Buffer.from(
JSON.stringify({
bindPort: 1122,
} as ServerConfig),
),
}),
)
}}
// onClick={async () => {
// console.log(
// 'attempting update frps:',
// await updateFRPS({
// serverId: serverID,
// config: Buffer.from(
// JSON.stringify({
// bindPort: 1122,
// } as ServerConfig),
// ),
// }),
// )
// }}
>
update frps
</Button>
@@ -181,11 +181,11 @@ export const APITest = () => {
await updateFRPC({
clientId: clientID,
serverId: serverID,
config: Buffer.from(
JSON.stringify({
proxies: [{ name: 'test', type: 'tcp', localIP: '127.0.0.1', localPort: 1234, remotePort: 4321 }],
} as ClientConfig),
),
// config: Buffer.from(
// JSON.stringify({
// proxies: [{ name: 'test', type: 'tcp', localIP: '127.0.0.1', localPort: 1234, remotePort: 4321 }],
// } as ClientConfig),
// ),
}),
)
}}

View File

@@ -3,14 +3,16 @@ import { Badge } from '../ui/badge';
import { Input } from '../ui/input';
import { Button } from '../ui/button';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
interface StringListInputProps {
value: string[];
onChange: React.Dispatch<React.SetStateAction<string[]>>;
placeholder?: string;
className?: string;
}
const StringListInput: React.FC<StringListInputProps> = ({ value, onChange, placeholder }) => {
const StringListInput: React.FC<StringListInputProps> = ({ value, onChange, placeholder, className }) => {
const { t } = useTranslation();
const [inputValue, setInputValue] = useState('');
@@ -34,7 +36,7 @@ const StringListInput: React.FC<StringListInputProps> = ({ value, onChange, plac
};
return (
<div className="mx-auto">
<div className={cn("mx-auto", className)}>
<div className="flex items-center mb-4">
<Input
type="text"

View File

@@ -37,7 +37,7 @@ export const ServerSelector: React.FC<ServerSelectorProps> = ({
useEffect(() => {
if (serverID) {
setServer && setServer(serverList?.servers.find((server) => server.id == serverID) || {})
setServer && setServer(serverList?.servers.find((server) => server.id == serverID) || {frpsUrls: []})
}
}, [serverID])

View File

@@ -0,0 +1,144 @@
// src/components/ui/SuggestiveInput.tsx
import * as React from "react";
import { cn } from "@/lib/utils"; // 调整路径根据你的项目结构
import {
Command,
CommandEmpty,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
interface SuggestiveInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
/** 当前输入框的值 (受控) */
value: string;
/** 值改变时的回调函数 */
onChange: (value: string) => void;
/** 建议选项列表 */
suggestions: string[];
/** 输入框的占位符 */
placeholder?: string;
/** 自定义 CSS 类名 */
className?: string;
/** Popover 内容的 CSS 类名 */
popoverClassName?: string;
/** 没有建议时的提示信息 */
emptyMessage?: string;
}
const SuggestiveInput = React.forwardRef<HTMLInputElement, SuggestiveInputProps>(
(
{
value,
onChange,
suggestions,
placeholder,
className,
popoverClassName,
emptyMessage = "", // Default empty message
...props
},
ref
) => {
const [open, setOpen] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const [finalSuggestions, setFinalSuggestions] = React.useState(suggestions);
React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
onChange(newValue);
setFinalSuggestions([newValue, ...suggestions]);
if (newValue && !open) {
setOpen(true);
}
if (!newValue && open) {
setOpen(false);
}
};
const handleSuggestionSelect = (selectedValue: string) => {
if (selectedValue !== value) {
onChange(selectedValue);
}
setOpen(false);
};
const filteredSuggestions = React.useMemo(() => {
const lowerCaseValue = value?.toLowerCase() || "";
const validSuggestions = finalSuggestions.filter(s => s);
if (!lowerCaseValue) {
return validSuggestions;
}
return validSuggestions.filter(s =>
s.toLowerCase().includes(lowerCaseValue)
);
}, [value, finalSuggestions]);
const shouldShowPopover = ((filteredSuggestions.length > 0 || (value && filteredSuggestions.length === 0)) && open) ? true : false;
return (
<Popover open={shouldShowPopover}>
<PopoverTrigger asChild>
<Input
ref={inputRef}
type="text"
value={value}
onChange={handleInputChange}
onFocus={() => {
// 仅当有建议或已有内容时才在聚焦时打开
if (finalSuggestions.length > 0) {
setOpen(true);
}
}}
placeholder={placeholder}
className={cn("w-full text-sm", className)}
role="combobox" // Accessibility role
aria-expanded={shouldShowPopover} // Accessibility state
aria-autocomplete="list" // Accessibility hint
autoComplete="off" // Prevent browser's default autocomplete
{...props} // Pass down other standard input props like 'id', 'name', etc.
/>
</PopoverTrigger>
{shouldShowPopover && (
<PopoverContent
className={cn("w-[--radix-popover-trigger-width] p-1", popoverClassName)}
style={{ zIndex: 50 }}
onOpenAutoFocus={(e) => e.preventDefault()}
>
<Command shouldFilter={false}>
<CommandList className="pt-0">
{value && filteredSuggestions.length === 0 ? (
<CommandEmpty>{emptyMessage}</CommandEmpty>
) : null}
{filteredSuggestions.map((suggestion) => (
<CommandItem
key={suggestion}
value={suggestion}
onSelect={handleSuggestionSelect}
className="cursor-pointer"
>
{suggestion}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
)}
</Popover>
);
}
);
SuggestiveInput.displayName = "SuggestiveInput";
export { SuggestiveInput };

View File

@@ -48,6 +48,7 @@ export type ClientTableSchema = {
info?: string
config?: string
originClient: Client
clientIds: string[]
}
export interface TableMetaType extends TableMeta<ClientTableSchema> {

View File

@@ -15,6 +15,9 @@ import { TypedProxyConfig } from '@/types/proxy'
import { ClientSelector } from '../base/client-selector'
import { ServerSelector } from '../base/server-selector'
import { useTranslation } from 'react-i18next'
import { Input } from '../ui/input'
import { Server } from '@/lib/pb/common'
import { SuggestiveInput } from '../base/suggestive-input'
export interface FRPCFormCardProps {
clientID?: string
@@ -32,6 +35,8 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({
const searchParams = useSearchParams()
const paramClientID = searchParams.get('clientID')
const [clientProxyConfigs, setClientProxyConfigs] = useState<TypedProxyConfig[]>([])
const [frpsUrl, setFrpsUrl] = useState<string | undefined>()
const [selectedServer, setSelectedServer] = useState<Server | undefined>(undefined)
useEffect(() => {
if (defaultClientID) {
@@ -69,6 +74,10 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({
if (clientConf != undefined && clientConf.proxies == undefined) {
setClientProxyConfigs([])
}
if (client?.client?.frpsUrl) {
setFrpsUrl(client?.client?.frpsUrl)
}
}, [client, refetchClient, setClientProxyConfigs])
useEffect(() => {
@@ -105,9 +114,12 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({
</div>
<div className="flex flex-col w-full pt-2 space-y-2">
<Label className="text-sm font-medium">{t('frpc.form.server')}</Label>
<ServerSelector serverID={serverID} setServerID={setServerID} />
<ServerSelector serverID={serverID} setServerID={setServerID} setServer={setSelectedServer} />
<Label className="text-sm font-medium">{t('frpc.form.client')}</Label>
<ClientSelector clientID={clientID} setClientID={setClientID} />
<Label className="text-sm font-medium">{t('frpc.form.frps_url.title')}</Label>
<p className="text-sm text-muted-foreground">{t('frpc.form.frps_url.hint')}</p>
<SuggestiveInput value={frpsUrl || ''} onChange={setFrpsUrl} suggestions={selectedServer?.frpsUrls || []} />
</div>
{clientID && !advanceMode && <div className='flex flex-col w-full pt-2 space-y-2'>
<Label className="text-sm font-medium">{t('frpc.form.comment.title', { id: clientID })}</Label>
@@ -120,14 +132,18 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({
clientConfig={JSON.parse(client?.client?.config || '{}') as ClientConfig} refetchClient={refetchClient}
clientID={clientID} serverID={serverID}
clientProxyConfigs={clientProxyConfigs}
setClientProxyConfigs={setClientProxyConfigs} />
setClientProxyConfigs={setClientProxyConfigs}
frpsUrl={frpsUrl}
/>
}
{clientID && serverID && advanceMode && <FRPCEditor
client={client?.client}
clientConfig={JSON.parse(client?.client?.config || '{}') as ClientConfig} refetchClient={refetchClient}
clientID={clientID} serverID={serverID}
clientProxyConfigs={clientProxyConfigs}
setClientProxyConfigs={setClientProxyConfigs} />
setClientProxyConfigs={setClientProxyConfigs}
frpsUrl={frpsUrl}
/>
}
</CardContent>
</Card>

View File

@@ -9,7 +9,7 @@ import { RespCode } from '@/lib/pb/common'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
export const FRPCEditor: React.FC<FRPCFormProps> = ({ clientID, serverID, client, refetchClient }) => {
export const FRPCEditor: React.FC<FRPCFormProps> = ({ clientID, serverID, client, refetchClient, frpsUrl }) => {
const { t } = useTranslation()
const [configContent, setConfigContent] = useState<string>('{}')
@@ -25,6 +25,7 @@ export const FRPCEditor: React.FC<FRPCFormProps> = ({ clientID, serverID, client
config: Buffer.from(editorValue),
serverId: serverID,
comment: clientComment,
frpsUrl: frpsUrl,
})
if (res.status?.code !== RespCode.SUCCESS) {
toast(t('client.operation.update_failed', {

View File

@@ -24,12 +24,13 @@ export interface FRPCFormProps {
serverID: string
client?: Client
clientConfig: ClientConfig
frpsUrl?: string
refetchClient: (options?: RefetchOptions) => Promise<QueryObserverResult<GetClientResponse, Error>>
clientProxyConfigs: TypedProxyConfig[]
setClientProxyConfigs: React.Dispatch<React.SetStateAction<TypedProxyConfig[]>>
}
export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID, clientConfig, client, refetchClient, clientProxyConfigs, setClientProxyConfigs }) => {
export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID, clientConfig, client, refetchClient, clientProxyConfigs, setClientProxyConfigs, frpsUrl }) => {
const { t } = useTranslation()
const [proxyType, setProxyType] = useState<ProxyType>('http')
const [proxyName, setProxyName] = useState<string | undefined>()
@@ -84,6 +85,7 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID, clientCo
),
serverId: serverID,
clientId: clientID,
frpsUrl: frpsUrl,
})
await refetchClient()
toast(t('proxy.status.update'), {

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { use, useEffect, useState } from 'react'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { getServer } from '@/api/server'
@@ -9,6 +9,7 @@ import FRPSForm from './frps_form'
import { useSearchParams } from 'next/navigation'
import { ServerSelector } from '../base/server-selector'
import { useTranslation } from 'react-i18next';
import StringListInput from '../base/list-input'
export interface FRPSFormCardProps {
serverID?: string
@@ -18,6 +19,8 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
const [serverID, setServerID] = useState<string | undefined>()
const searchParams = useSearchParams()
const paramServerID = searchParams.get('serverID')
const [frpsUrls, setFrpsUrls] = useState<string[]>([])
const { data: server, refetch: refetchServer } = useQuery({
queryKey: ['getServer', serverID],
queryFn: () => {
@@ -26,6 +29,10 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
})
const { t } = useTranslation();
useEffect(() => {
setFrpsUrls(server?.server?.frpsUrls || [])
}, [server])
useEffect(() => {
if (defaultServerID) {
setServerID(defaultServerID)
@@ -64,13 +71,18 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
</div>
<Switch onCheckedChange={setAdvanceMode} />
</div>
<div className="flex flex-col w-full pt-2">
<div className="flex flex-col w-full pt-2 gap-2">
<Label className="text-sm font-medium">{t('server.serverLabel')}</Label>
<ServerSelector serverID={serverID} setServerID={handleServerChange} onOpenChange={refetchServer} />
<Label className="text-sm font-medium">{t('server.frpsUrl.title')}</Label>
<p className="text-sm text-muted-foreground">{t('server.frpsUrl.description')}</p>
<StringListInput className='w-full' value={frpsUrls} onChange={setFrpsUrls} placeholder='eg. tcp://example.com:7000' />
</div>
{serverID && server && server.server && !advanceMode && <FRPSForm key={serverID} serverID={serverID} server={server.server} />}
{serverID && server && server.server && !advanceMode && (
<FRPSForm key={serverID} serverID={serverID} server={server.server} frpsUrls={frpsUrls} />
)}
{serverID && server && server.server && advanceMode && (
<FRPSEditor serverID={serverID} server={server.server} />
<FRPSEditor serverID={serverID} server={server.server} frpsUrls={frpsUrls} />
)}
</CardContent>
<CardFooter></CardFooter>

View File

@@ -10,7 +10,7 @@ import { RespCode } from '@/lib/pb/common'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
export const FRPSEditor: React.FC<FRPSFormProps> = ({ server, serverID }) => {
export const FRPSEditor: React.FC<FRPSFormProps> = ({ server, serverID, frpsUrls }) => {
const { t } = useTranslation()
const { data: serverResp, refetch: refetchServer } = useQuery({
queryKey: ['getServer', serverID],
@@ -27,6 +27,7 @@ export const FRPSEditor: React.FC<FRPSFormProps> = ({ server, serverID }) => {
const handleSubmit = async () => {
try {
let res = await updateFrps.mutateAsync({
frpsUrls: frpsUrls,
serverId: serverID,
//@ts-ignore
config: Buffer.from(editorValue),

View File

@@ -30,9 +30,10 @@ export const ServerConfigZodSchema = ServerConfigSchema
export interface FRPSFormProps {
serverID: string
server: Server
frpsUrls: string[]
}
const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server }) => {
const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server, frpsUrls }) => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof ServerConfigZodSchema>>({
resolver: zodResolver(ServerConfigZodSchema),
@@ -54,6 +55,7 @@ const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server }) => {
let resp = await updateFrps.mutateAsync({
serverIp: publicHost,
serverId: serverID,
frpsUrls: frpsUrls,
// @ts-ignore
config: Buffer.from(
JSON.stringify({

View File

@@ -46,6 +46,7 @@ export type ServerTableSchema = {
info?: string
ip: string
config?: string
frpsUrls: string[]
}
export const columns: ColumnDef<ServerTableSchema>[] = [

View File

@@ -107,7 +107,7 @@ function VisitPreviewField({ row }: { row: Row<ProxyConfigTableSchema> }) {
const typedProxyConfig = JSON.parse(row.original.config || '{}') as TypedProxyConfig
return <VisitPreview
server={server?.server || {}}
server={server?.server || {frpsUrls: []}}
typedProxyConfig={typedProxyConfig} />
}

View File

@@ -134,6 +134,10 @@
"description": "Edit server raw configuration file"
},
"serverLabel": "Server",
"frpsUrl": {
"title": "FRP server override address - Optional",
"description": "Clients can use this address to connect instead of the default IP. This is useful for port forwarding/CDN/reverse proxy. Format: [tcp/kcp/websocket]://127.0.0.1:7000"
},
"id": "ID (Click for install command)",
"status": "Configuration Status",
"info": "Running Info/Version",
@@ -204,7 +208,7 @@
"vhost_http_port": "HTTP Listen Port",
"subdomain_host": "Subdomain Host",
"quic_bind_port": "Quic Bind Port",
"kcp_bind_port":"KCP Bind Port"
"kcp_bind_port": "KCP Bind Port"
},
"editor": {
"comment": "Node {{id}} Comment",
@@ -452,6 +456,10 @@
"title": "Node {{id}} Comment",
"hint": "You can modify the comment in advanced mode!",
"empty": "Nothing here"
},
"frps_url": {
"title": "FRP server override address - Optional",
"hint": "You can override the server address stored in the master to handle the inconsistency between the domain name/IP/port and the real port during port forwarding or reverse proxy/CDN. Format: [tcp/kcp/websocket]://127.0.0.1:7000"
}
}
},
@@ -511,4 +519,4 @@
}
}
}
}
}

View File

@@ -147,6 +147,10 @@
"description": "编辑服务器原始配置文件"
},
"serverLabel": "服务器",
"frpsUrl": {
"title": "FRP 服务器覆盖地址 - 可选",
"description": "客户端可以使用该地址连接而不是使用默认的IP。在端口转发/CDN/反向代理时很有用。格式:[tcp/kcp/websocket]://127.0.0.1:7000"
},
"install": {
"title": "安装命令",
"description": "请选择您的操作系统并复制相应的安装命令",
@@ -211,7 +215,7 @@
"vhost_http_port": "HTTP 监听端口",
"subdomain_host": "域名后缀",
"quic_bind_port": "Quic 监听端口",
"kcp_bind_port":"KCP 监听端口"
"kcp_bind_port": "KCP 监听端口"
},
"create": {
"button": "新建",
@@ -452,6 +456,10 @@
"title": "节点 {{id}} 的备注",
"hint": "可以到高级模式修改备注哦!",
"empty": "空空如也"
},
"frps_url": {
"title": "FRP 服务端覆盖地址 - 可选",
"hint": "可以覆盖 master 存储的服务端地址,用于处理端口转发或反向代理/CDN时域名/IP/端口和真实端口不一致的问题。格式:[tcp/kcp/websocket]://127.0.0.1:7000"
}
}
},
@@ -510,4 +518,4 @@
}
}
}
}
}

View File

@@ -135,6 +135,10 @@ export interface UpdateFRPCRequest {
* @generated from protobuf field: optional string comment = 4;
*/
comment?: string;
/**
* @generated from protobuf field: optional string frps_url = 5;
*/
frpsUrl?: string;
}
/**
* @generated from protobuf message api_client.UpdateFRPCResponse
@@ -808,7 +812,8 @@ class UpdateFRPCRequest$Type extends MessageType<UpdateFRPCRequest> {
{ no: 1, name: "client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "server_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "config", kind: "scalar", opt: true, T: 12 /*ScalarType.BYTES*/ },
{ no: 4, name: "comment", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
{ no: 4, name: "comment", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 5, name: "frps_url", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<UpdateFRPCRequest>): UpdateFRPCRequest {
@@ -834,6 +839,9 @@ class UpdateFRPCRequest$Type extends MessageType<UpdateFRPCRequest> {
case /* optional string comment */ 4:
message.comment = reader.string();
break;
case /* optional string frps_url */ 5:
message.frpsUrl = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -858,6 +866,9 @@ class UpdateFRPCRequest$Type extends MessageType<UpdateFRPCRequest> {
/* optional string comment = 4; */
if (message.comment !== undefined)
writer.tag(4, WireType.LengthDelimited).string(message.comment);
/* optional string frps_url = 5; */
if (message.frpsUrl !== undefined)
writer.tag(5, WireType.LengthDelimited).string(message.frpsUrl);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);

View File

@@ -137,6 +137,10 @@ export interface UpdateFRPSRequest {
* @generated from protobuf field: optional string server_ip = 4;
*/
serverIp?: string;
/**
* @generated from protobuf field: repeated string frps_urls = 5;
*/
frpsUrls: string[];
}
/**
* @generated from protobuf message api_server.UpdateFRPSResponse
@@ -655,11 +659,13 @@ class UpdateFRPSRequest$Type extends MessageType<UpdateFRPSRequest> {
{ no: 1, name: "server_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "config", kind: "scalar", opt: true, T: 12 /*ScalarType.BYTES*/ },
{ no: 3, name: "comment", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 4, name: "server_ip", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
{ no: 4, name: "server_ip", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 5, name: "frps_urls", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<UpdateFRPSRequest>): UpdateFRPSRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.frpsUrls = [];
if (value !== undefined)
reflectionMergePartial<UpdateFRPSRequest>(this, message, value);
return message;
@@ -681,6 +687,9 @@ class UpdateFRPSRequest$Type extends MessageType<UpdateFRPSRequest> {
case /* optional string server_ip */ 4:
message.serverIp = reader.string();
break;
case /* repeated string frps_urls */ 5:
message.frpsUrls.push(reader.string());
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -705,6 +714,9 @@ class UpdateFRPSRequest$Type extends MessageType<UpdateFRPSRequest> {
/* optional string server_ip = 4; */
if (message.serverIp !== undefined)
writer.tag(4, WireType.LengthDelimited).string(message.serverIp);
/* repeated string frps_urls = 5; */
for (let i = 0; i < message.frpsUrls.length; i++)
writer.tag(5, WireType.LengthDelimited).string(message.frpsUrls[i]);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);

View File

@@ -81,6 +81,10 @@ export interface Client {
* @generated from protobuf field: optional string origin_client_id = 9;
*/
originClientId?: string;
/**
* @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 message common.Server
@@ -106,6 +110,10 @@ export interface Server {
* @generated from protobuf field: optional string comment = 5;
*/
comment?: string; // 用户自定义的备注
/**
* @generated from protobuf field: repeated string frps_urls = 6;
*/
frpsUrls: string[]; // 客户端用于连接frps的url解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000可以有多个
}
/**
* @generated from protobuf message common.User
@@ -458,7 +466,8 @@ class Client$Type extends MessageType<Client> {
{ no: 6, name: "server_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ 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: 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*/ }
]);
}
create(value?: PartialMessage<Client>): Client {
@@ -497,6 +506,9 @@ class Client$Type extends MessageType<Client> {
case /* optional string origin_client_id */ 9:
message.originClientId = reader.string();
break;
case /* optional string frps_url */ 10:
message.frpsUrl = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -533,6 +545,9 @@ class Client$Type extends MessageType<Client> {
/* optional string origin_client_id = 9; */
if (message.originClientId !== undefined)
writer.tag(9, WireType.LengthDelimited).string(message.originClientId);
/* optional string frps_url = 10; */
if (message.frpsUrl !== undefined)
writer.tag(10, WireType.LengthDelimited).string(message.frpsUrl);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -551,11 +566,13 @@ class Server$Type extends MessageType<Server> {
{ no: 2, name: "secret", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "ip", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 4, name: "config", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 5, name: "comment", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
{ no: 5, name: "comment", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 6, name: "frps_urls", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<Server>): Server {
const message = globalThis.Object.create((this.messagePrototype!));
message.frpsUrls = [];
if (value !== undefined)
reflectionMergePartial<Server>(this, message, value);
return message;
@@ -580,6 +597,9 @@ class Server$Type extends MessageType<Server> {
case /* optional string comment */ 5:
message.comment = reader.string();
break;
case /* repeated string frps_urls */ 6:
message.frpsUrls.push(reader.string());
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -607,6 +627,9 @@ class Server$Type extends MessageType<Server> {
/* optional string comment = 5; */
if (message.comment !== undefined)
writer.tag(5, WireType.LengthDelimited).string(message.comment);
/* repeated string frps_urls = 6; */
for (let i = 0; i < message.frpsUrls.length; i++)
writer.tag(6, WireType.LengthDelimited).string(message.frpsUrls[i]);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);

View File

@@ -1,62 +1,58 @@
import { FRPCFormCard } from '@/components/frpc/frpc_card'
import { Providers } from '@/components/providers'
import { APITest } from '@/components/apitest'
import { Separator } from '@/components/ui/separator'
import { FRPSFormCard } from '@/components/frps/frps_card'
import { RootLayout } from '@/components/layout'
import { Header } from '@/components/header'
import { createProxyConfig, listProxyConfig } from '@/api/proxy'
import { Button } from '@/components/ui/button'
import { TypedProxyConfig } from '@/types/proxy'
import { ClientConfig } from '@/types/client'
import { ProxyConfigList } from '@/components/proxy/proxy_config_list'
import { Input } from '@/components/ui/input'
import { useState } from 'react'
// import { Providers } from '@/components/providers'
// import { RootLayout } from '@/components/layout'
// import { Header } from '@/components/header'
// import { Button } from '@/components/ui/button'
// import { ProxyConfigList } from '@/components/proxy/proxy_config_list'
// import { Input } from '@/components/ui/input'
// import { createProxyConfig } from '@/api/proxy'
// import { TypedProxyConfig } from '@/types/proxy'
// import { ClientConfig } from '@/types/client'
// import { useState } from 'react'
export default function Test() {
const [name, setName] = useState<string>('')
const [triggerRefetch, setTriggerRefetch] = useState<number>(0)
// const [name, setName] = useState<string>('')
// const [triggerRefetch, setTriggerRefetch] = useState<number>(0)
function create() {
const buffer = Buffer.from(
JSON.stringify({
proxies: [{
name: name,
type: 'tcp',
localIP: '127.0.0.1',
localPort: 1234,
remotePort: 4321,
} as TypedProxyConfig]
} as ClientConfig),
)
const uint8Array: Uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
createProxyConfig({
clientId: 'admin.c.test',
config: uint8Array,
serverId: 'default',
})
.then(() => {
setTriggerRefetch(triggerRefetch + 1)
})
.catch((err) => {
console.log(err)
})
}
// function create() {
// const buffer = Buffer.from(
// JSON.stringify({
// proxies: [{
// name: name,
// type: 'tcp',
// localIP: '127.0.0.1',
// localPort: 1234,
// remotePort: 4321,
// } as TypedProxyConfig]
// } as ClientConfig),
// )
// const uint8Array: Uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
// createProxyConfig({
// clientId: 'admin.c.test',
// config: uint8Array,
// serverId: 'default',
// })
// .then(() => {
// setTriggerRefetch(triggerRefetch + 1)
// })
// .catch((err) => {
// console.log(err)
// })
// }
return (
// <>
// </>
<Providers>
<RootLayout mainHeader={<Header />}>
<div className="w-full">
<div className="flex flex-1 flex-col">
<div className="flex flex-1 flex-row mb-2 gap-2">
<Button onClick={create}></Button>
<Input value={name} onChange={(e) => setName(e.target.value)} ></Input>
</div>
<ProxyConfigList Keyword="" ProxyConfigs={[]} TriggerRefetch={triggerRefetch.toString()} />
</div>
</div>
</RootLayout>
</Providers>
<>
</>
// <Providers>
// <RootLayout mainHeader={<Header />}>
// <div className="w-full">
// <div className="flex flex-1 flex-col">
// <div className="flex flex-1 flex-row mb-2 gap-2">
// <Button onClick={create}>新建</Button>
// <Input value={name} onChange={(e) => setName(e.target.value)} ></Input>
// </div>
// <ProxyConfigList Keyword="" ProxyConfigs={[]} TriggerRefetch={triggerRefetch.toString()} />
// </div>
// </div>
// </RootLayout>
// </Providers>
)
}