mirror of
https://github.com/VaalaCat/frp-panel.git
synced 2025-09-26 19:31:18 +08:00
feat: frps can have multi endpoint
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)),
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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
3
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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"`
|
||||
|
@@ -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
28
models/types.go
Normal 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)
|
||||
}
|
@@ -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" +
|
||||
|
@@ -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" +
|
||||
|
@@ -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" +
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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),
|
||||
// ),
|
||||
}),
|
||||
)
|
||||
}}
|
||||
|
@@ -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"
|
||||
|
@@ -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])
|
||||
|
||||
|
144
www/components/base/suggestive-input.tsx
Normal file
144
www/components/base/suggestive-input.tsx
Normal 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 };
|
@@ -48,6 +48,7 @@ export type ClientTableSchema = {
|
||||
info?: string
|
||||
config?: string
|
||||
originClient: Client
|
||||
clientIds: string[]
|
||||
}
|
||||
|
||||
export interface TableMetaType extends TableMeta<ClientTableSchema> {
|
||||
|
@@ -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>
|
||||
|
@@ -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', {
|
||||
|
@@ -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'), {
|
||||
|
@@ -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>
|
||||
|
@@ -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),
|
||||
|
@@ -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({
|
||||
|
@@ -46,6 +46,7 @@ export type ServerTableSchema = {
|
||||
info?: string
|
||||
ip: string
|
||||
config?: string
|
||||
frpsUrls: string[]
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<ServerTableSchema>[] = [
|
||||
|
@@ -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} />
|
||||
}
|
||||
|
||||
|
@@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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>
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user