mirror of
https://github.com/fatedier/frp.git
synced 2026-03-08 10:59:11 +08:00
refactor: restructure API packages into client/http and server/http with typed proxy/visitor models (#5193)
This commit is contained in:
@@ -17,7 +17,7 @@ package client
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/api"
|
adminapi "github.com/fatedier/frp/client/http"
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
@@ -65,9 +65,9 @@ func healthz(w http.ResponseWriter, _ *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIController(svr *Service) *api.Controller {
|
func newAPIController(svr *Service) *adminapi.Controller {
|
||||||
manager := newServiceConfigManager(svr)
|
manager := newServiceConfigManager(svr)
|
||||||
return api.NewController(api.ControllerParams{
|
return adminapi.NewController(adminapi.ControllerParams{
|
||||||
ServerAddr: svr.common.ServerAddr,
|
ServerAddr: svr.common.ServerAddr,
|
||||||
Manager: manager,
|
Manager: manager,
|
||||||
})
|
})
|
||||||
@@ -133,12 +133,13 @@ func (m *serviceConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, e
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error {
|
func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||||
return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
name := cfg.GetBaseConfig().Name
|
||||||
|
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
if err := storeSource.AddProxy(cfg); err != nil {
|
if err := storeSource.AddProxy(cfg); err != nil {
|
||||||
if errors.Is(err, source.ErrAlreadyExists) {
|
if errors.Is(err, source.ErrAlreadyExists) {
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||||
@@ -146,30 +147,30 @@ func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Infof("store: created proxy %q", name)
|
||||||
log.Infof("store: created proxy %q", cfg.GetBaseConfig().Name)
|
return persisted, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error {
|
func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
bodyName := cfg.GetBaseConfig().Name
|
bodyName := cfg.GetBaseConfig().Name
|
||||||
if bodyName != name {
|
if bodyName != name {
|
||||||
return fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||||
return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
if err := storeSource.UpdateProxy(cfg); err != nil {
|
if err := storeSource.UpdateProxy(cfg); err != nil {
|
||||||
if errors.Is(err, source.ErrNotFound) {
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
@@ -177,12 +178,13 @@ func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigu
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("store: updated proxy %q", name)
|
log.Infof("store: updated proxy %q", name)
|
||||||
return nil
|
return persisted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) DeleteStoreProxy(name string) error {
|
func (m *serviceConfigManager) DeleteStoreProxy(name string) error {
|
||||||
@@ -231,12 +233,13 @@ func (m *serviceConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigure
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) error {
|
func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||||
return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
name := cfg.GetBaseConfig().Name
|
||||||
|
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
if err := storeSource.AddVisitor(cfg); err != nil {
|
if err := storeSource.AddVisitor(cfg); err != nil {
|
||||||
if errors.Is(err, source.ErrAlreadyExists) {
|
if errors.Is(err, source.ErrAlreadyExists) {
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||||
@@ -244,30 +247,31 @@ func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("store: created visitor %q", cfg.GetBaseConfig().Name)
|
log.Infof("store: created visitor %q", name)
|
||||||
return nil
|
return persisted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error {
|
func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
bodyName := cfg.GetBaseConfig().Name
|
bodyName := cfg.GetBaseConfig().Name
|
||||||
if bodyName != name {
|
if bodyName != name {
|
||||||
return fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||||
return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
if err := storeSource.UpdateVisitor(cfg); err != nil {
|
if err := storeSource.UpdateVisitor(cfg); err != nil {
|
||||||
if errors.Is(err, source.ErrNotFound) {
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
@@ -275,12 +279,13 @@ func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorCon
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("store: updated visitor %q", name)
|
log.Infof("store: updated visitor %q", name)
|
||||||
return nil
|
return persisted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) DeleteStoreVisitor(name string) error {
|
func (m *serviceConfigManager) DeleteStoreVisitor(name string) error {
|
||||||
@@ -340,6 +345,58 @@ func (m *serviceConfigManager) withStoreMutationAndReload(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreProxyMutationAndReload(
|
||||||
|
name string,
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) (v1.ProxyConfigurer, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted := storeSource.GetProxy(name)
|
||||||
|
if persisted == nil {
|
||||||
|
return nil, fmt.Errorf("%w: proxy %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||||
|
}
|
||||||
|
return persisted.Clone(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreVisitorMutationAndReload(
|
||||||
|
name string,
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) (v1.VisitorConfigurer, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted := storeSource.GetVisitor(name)
|
||||||
|
if persisted == nil {
|
||||||
|
return nil, fmt.Errorf("%w: visitor %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||||
|
}
|
||||||
|
return persisted.Clone(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error {
|
func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return fmt.Errorf("invalid proxy config")
|
return fmt.Errorf("invalid proxy config")
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TestServiceConfigManagerCreateStoreProxyConflict(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected conflict error")
|
t.Fatal("expected conflict error")
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ func TestServiceConfigManagerCreateStoreProxyKeepsStoreOnReloadFailure(t *testin
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected apply config error")
|
t.Fatal("expected apply config error")
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ func TestServiceConfigManagerCreateStoreProxyStoreDisabled(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
_, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected store disabled error")
|
t.Fatal("expected store disabled error")
|
||||||
}
|
}
|
||||||
@@ -116,10 +116,13 @@ func TestServiceConfigManagerCreateStoreProxyDoesNotPersistRuntimeDefaults(t *te
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy"))
|
persisted, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create store proxy: %v", err)
|
t.Fatalf("create store proxy: %v", err)
|
||||||
}
|
}
|
||||||
|
if persisted == nil {
|
||||||
|
t.Fatal("expected persisted proxy to be returned")
|
||||||
|
}
|
||||||
|
|
||||||
got := storeSource.GetProxy("raw-proxy")
|
got := storeSource.GetProxy("raw-proxy")
|
||||||
if got == nil {
|
if got == nil {
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ type ConfigManager interface {
|
|||||||
|
|
||||||
ListStoreProxies() ([]v1.ProxyConfigurer, error)
|
ListStoreProxies() ([]v1.ProxyConfigurer, error)
|
||||||
GetStoreProxy(name string) (v1.ProxyConfigurer, error)
|
GetStoreProxy(name string) (v1.ProxyConfigurer, error)
|
||||||
CreateStoreProxy(cfg v1.ProxyConfigurer) error
|
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error
|
UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
DeleteStoreProxy(name string) error
|
DeleteStoreProxy(name string) error
|
||||||
|
|
||||||
ListStoreVisitors() ([]v1.VisitorConfigurer, error)
|
ListStoreVisitors() ([]v1.VisitorConfigurer, error)
|
||||||
GetStoreVisitor(name string) (v1.VisitorConfigurer, error)
|
GetStoreVisitor(name string) (v1.VisitorConfigurer, error)
|
||||||
CreateStoreVisitor(cfg v1.VisitorConfigurer) error
|
CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error
|
UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
DeleteStoreVisitor(name string) error
|
DeleteStoreVisitor(name string) error
|
||||||
|
|
||||||
GracefulClose(d time.Duration)
|
GracefulClose(d time.Duration)
|
||||||
|
|||||||
@@ -12,11 +12,10 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package api
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -26,9 +25,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/configmgmt"
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/http/model"
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Controller handles HTTP API requests for frpc.
|
// Controller handles HTTP API requests for frpc.
|
||||||
@@ -67,15 +67,6 @@ func (c *Controller) toHTTPError(err error) error {
|
|||||||
return httppkg.NewError(code, err.Error())
|
return httppkg.NewError(code, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(fatedier): Remove this lock wrapper after migrating typed config
|
|
||||||
// decoding to encoding/json/v2 with per-call options.
|
|
||||||
// TypedProxyConfig/TypedVisitorConfig currently read global strictness state.
|
|
||||||
func unmarshalTypedConfig[T any](body []byte, out *T) error {
|
|
||||||
return v1.WithDisallowUnknownFields(false, func() error {
|
|
||||||
return json.Unmarshal(body, out)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload handles GET /api/reload
|
// Reload handles GET /api/reload
|
||||||
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
||||||
strictConfigMode := false
|
strictConfigMode := false
|
||||||
@@ -98,7 +89,7 @@ func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
|||||||
|
|
||||||
// Status handles GET /api/status
|
// Status handles GET /api/status
|
||||||
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
||||||
res := make(StatusResp)
|
res := make(model.StatusResp)
|
||||||
ps := c.manager.GetProxyStatus()
|
ps := c.manager.GetProxyStatus()
|
||||||
if ps == nil {
|
if ps == nil {
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -112,7 +103,7 @@ func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
|||||||
if len(arrs) <= 1 {
|
if len(arrs) <= 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
slices.SortFunc(arrs, func(a, b model.ProxyStatusResp) int {
|
||||||
return cmp.Compare(a.Name, b.Name)
|
return cmp.Compare(a.Name, b.Name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -145,8 +136,8 @@ func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp {
|
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.ProxyStatusResp {
|
||||||
psr := ProxyStatusResp{
|
psr := model.ProxyStatusResp{
|
||||||
Name: status.Name,
|
Name: status.Name,
|
||||||
Type: status.Type,
|
Type: status.Type,
|
||||||
Status: status.Phase,
|
Status: status.Phase,
|
||||||
@@ -166,7 +157,7 @@ func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.manager.IsStoreProxyEnabled(status.Name) {
|
if c.manager.IsStoreProxyEnabled(status.Name) {
|
||||||
psr.Source = SourceStore
|
psr.Source = model.SourceStore
|
||||||
}
|
}
|
||||||
return psr
|
return psr
|
||||||
}
|
}
|
||||||
@@ -177,18 +168,17 @@ func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := ProxyListResp{Proxies: make([]ProxyConfig, 0, len(proxies))}
|
resp := model.ProxyListResp{Proxies: make([]model.ProxyDefinition, 0, len(proxies))}
|
||||||
for _, p := range proxies {
|
for _, p := range proxies {
|
||||||
cfg, err := configurerToMap(p)
|
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
resp.Proxies = append(resp.Proxies, ProxyConfig{
|
resp.Proxies = append(resp.Proxies, payload)
|
||||||
Name: p.GetBaseConfig().Name,
|
|
||||||
Type: p.GetBaseConfig().Type,
|
|
||||||
Config: cfg,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
slices.SortFunc(resp.Proxies, func(a, b model.ProxyDefinition) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,16 +193,12 @@ func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := configurerToMap(p)
|
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return ProxyConfig{
|
return payload, nil
|
||||||
Name: p.GetBaseConfig().Name,
|
|
||||||
Type: p.GetBaseConfig().Type,
|
|
||||||
Config: cfg,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -221,19 +207,28 @@ func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var typed v1.TypedProxyConfig
|
var payload model.ProxyDefinition
|
||||||
if err := unmarshalTypedConfig(body, &typed); err != nil {
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if typed.ProxyConfigurer == nil {
|
if err := payload.Validate("", false); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required")
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
if err := c.manager.CreateStoreProxy(typed.ProxyConfigurer); err != nil {
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
created, err := c.manager.CreateStoreProxy(cfg)
|
||||||
|
if err != nil {
|
||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
resp, err := model.ProxyDefinitionFromConfigurer(created)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -247,19 +242,28 @@ func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var typed v1.TypedProxyConfig
|
var payload model.ProxyDefinition
|
||||||
if err := unmarshalTypedConfig(body, &typed); err != nil {
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if typed.ProxyConfigurer == nil {
|
if err := payload.Validate(name, true); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required")
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
if err := c.manager.UpdateStoreProxy(name, typed.ProxyConfigurer); err != nil {
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
updated, err := c.manager.UpdateStoreProxy(name, cfg)
|
||||||
|
if err != nil {
|
||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
resp, err := model.ProxyDefinitionFromConfigurer(updated)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -280,18 +284,17 @@ func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := VisitorListResp{Visitors: make([]VisitorConfig, 0, len(visitors))}
|
resp := model.VisitorListResp{Visitors: make([]model.VisitorDefinition, 0, len(visitors))}
|
||||||
for _, v := range visitors {
|
for _, v := range visitors {
|
||||||
cfg, err := configurerToMap(v)
|
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
resp.Visitors = append(resp.Visitors, VisitorConfig{
|
resp.Visitors = append(resp.Visitors, payload)
|
||||||
Name: v.GetBaseConfig().Name,
|
|
||||||
Type: v.GetBaseConfig().Type,
|
|
||||||
Config: cfg,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
slices.SortFunc(resp.Visitors, func(a, b model.VisitorDefinition) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,16 +309,12 @@ func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := configurerToMap(v)
|
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return VisitorConfig{
|
return payload, nil
|
||||||
Name: v.GetBaseConfig().Name,
|
|
||||||
Type: v.GetBaseConfig().Type,
|
|
||||||
Config: cfg,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -324,19 +323,28 @@ func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var typed v1.TypedVisitorConfig
|
var payload model.VisitorDefinition
|
||||||
if err := unmarshalTypedConfig(body, &typed); err != nil {
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if typed.VisitorConfigurer == nil {
|
if err := payload.Validate("", false); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required")
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
if err := c.manager.CreateStoreVisitor(typed.VisitorConfigurer); err != nil {
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
created, err := c.manager.CreateStoreVisitor(cfg)
|
||||||
|
if err != nil {
|
||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
resp, err := model.VisitorDefinitionFromConfigurer(created)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -350,19 +358,28 @@ func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var typed v1.TypedVisitorConfig
|
var payload model.VisitorDefinition
|
||||||
if err := unmarshalTypedConfig(body, &typed); err != nil {
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if typed.VisitorConfigurer == nil {
|
if err := payload.Validate(name, true); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required")
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
if err := c.manager.UpdateStoreVisitor(name, typed.VisitorConfigurer); err != nil {
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
updated, err := c.manager.UpdateStoreVisitor(name, cfg)
|
||||||
|
if err != nil {
|
||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
resp, err := model.VisitorDefinitionFromConfigurer(updated)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -376,15 +393,3 @@ func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func configurerToMap(v any) (map[string]any, error) {
|
|
||||||
data, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var m map[string]any
|
|
||||||
if err := json.Unmarshal(data, &m); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package api
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/configmgmt"
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/http/model"
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
@@ -28,13 +29,13 @@ type fakeConfigManager struct {
|
|||||||
|
|
||||||
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
||||||
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
||||||
createStoreProxyFn func(cfg v1.ProxyConfigurer) error
|
createStoreProxyFn func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) error
|
updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
deleteStoreProxyFn func(name string) error
|
deleteStoreProxyFn func(name string) error
|
||||||
listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error)
|
listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error)
|
||||||
getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error)
|
getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error)
|
||||||
createStoreVisitFn func(cfg v1.VisitorConfigurer) error
|
createStoreVisitFn func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) error
|
updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
deleteStoreVisitFn func(name string) error
|
deleteStoreVisitFn func(name string) error
|
||||||
gracefulCloseFn func(d time.Duration)
|
gracefulCloseFn func(d time.Duration)
|
||||||
}
|
}
|
||||||
@@ -95,18 +96,18 @@ func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, erro
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error {
|
func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
if m.createStoreProxyFn != nil {
|
if m.createStoreProxyFn != nil {
|
||||||
return m.createStoreProxyFn(cfg)
|
return m.createStoreProxyFn(cfg)
|
||||||
}
|
}
|
||||||
return nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error {
|
func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
if m.updateStoreProxyFn != nil {
|
if m.updateStoreProxyFn != nil {
|
||||||
return m.updateStoreProxyFn(name, cfg)
|
return m.updateStoreProxyFn(name, cfg)
|
||||||
}
|
}
|
||||||
return nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakeConfigManager) DeleteStoreProxy(name string) error {
|
func (m *fakeConfigManager) DeleteStoreProxy(name string) error {
|
||||||
@@ -130,18 +131,18 @@ func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer,
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) error {
|
func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
if m.createStoreVisitFn != nil {
|
if m.createStoreVisitFn != nil {
|
||||||
return m.createStoreVisitFn(cfg)
|
return m.createStoreVisitFn(cfg)
|
||||||
}
|
}
|
||||||
return nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error {
|
func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
if m.updateStoreVisitFn != nil {
|
if m.updateStoreVisitFn != nil {
|
||||||
return m.updateStoreVisitFn(name, cfg)
|
return m.updateStoreVisitFn(name, cfg)
|
||||||
}
|
}
|
||||||
return nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakeConfigManager) DeleteStoreVisitor(name string) error {
|
func (m *fakeConfigManager) DeleteStoreVisitor(name string) error {
|
||||||
@@ -157,25 +158,6 @@ func (m *fakeConfigManager) GracefulClose(d time.Duration) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDisallowUnknownFieldsForTest(t *testing.T, value bool) func() {
|
|
||||||
t.Helper()
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
prev := v1.DisallowUnknownFields
|
|
||||||
v1.DisallowUnknownFields = value
|
|
||||||
v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
return func() {
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
v1.DisallowUnknownFields = prev
|
|
||||||
v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDisallowUnknownFieldsForTest() bool {
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
defer v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
return v1.DisallowUnknownFields
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
||||||
return &v1.TCPProxyConfig{
|
return &v1.TCPProxyConfig{
|
||||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
@@ -188,18 +170,6 @@ func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRawXTCPVisitorConfig(name string) *v1.XTCPVisitorConfig {
|
|
||||||
return &v1.XTCPVisitorConfig{
|
|
||||||
VisitorBaseConfig: v1.VisitorBaseConfig{
|
|
||||||
Name: name,
|
|
||||||
Type: "xtcp",
|
|
||||||
ServerName: "server",
|
|
||||||
BindPort: 10081,
|
|
||||||
SecretKey: "secret",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) {
|
func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) {
|
||||||
status := &proxy.WorkingStatus{
|
status := &proxy.WorkingStatus{
|
||||||
Name: "shared-proxy",
|
Name: "shared-proxy",
|
||||||
@@ -265,22 +235,20 @@ func TestStoreProxyErrorMapping(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
body, err := json.Marshal(newRawTCPProxyConfig("shared-proxy"))
|
body := []byte(`{"name":"shared-proxy","type":"tcp","tcp":{"localPort":10080}}`)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body))
|
||||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
controller := &Controller{
|
controller := &Controller{
|
||||||
manager: &fakeConfigManager{
|
manager: &fakeConfigManager{
|
||||||
updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) error { return tc.err },
|
updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
return nil, tc.err
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = controller.UpdateStoreProxy(ctx)
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error")
|
t.Fatal("expected error")
|
||||||
}
|
}
|
||||||
@@ -290,11 +258,7 @@ func TestStoreProxyErrorMapping(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStoreVisitorErrorMapping(t *testing.T) {
|
func TestStoreVisitorErrorMapping(t *testing.T) {
|
||||||
body, err := json.Marshal(newRawXTCPVisitorConfig("shared-visitor"))
|
body := []byte(`{"name":"shared-visitor","type":"xtcp","xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret"}}`)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body))
|
||||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"})
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"})
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
@@ -307,70 +271,208 @@ func TestStoreVisitorErrorMapping(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = controller.DeleteStoreVisitor(ctx)
|
_, err := controller.DeleteStoreVisitor(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error")
|
t.Fatal("expected error")
|
||||||
}
|
}
|
||||||
assertHTTPCode(t, err, http.StatusNotFound)
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateStoreProxy_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) {
|
func TestCreateStoreProxyIgnoresUnknownFields(t *testing.T) {
|
||||||
restore := setDisallowUnknownFieldsForTest(t, true)
|
|
||||||
t.Cleanup(restore)
|
|
||||||
|
|
||||||
var gotName string
|
var gotName string
|
||||||
controller := &Controller{
|
controller := &Controller{
|
||||||
manager: &fakeConfigManager{
|
manager: &fakeConfigManager{
|
||||||
createStoreProxyFn: func(cfg v1.ProxyConfigurer) error {
|
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
gotName = cfg.GetBaseConfig().Name
|
gotName = cfg.GetBaseConfig().Name
|
||||||
return nil
|
return cfg, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
body := []byte(`{"name":"raw-proxy","type":"tcp","localPort":10080,"unexpected":"value"}`)
|
body := []byte(`{"name":"raw-proxy","type":"tcp","unexpected":"value","tcp":{"localPort":10080,"unknownInBlock":"value"}}`)
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
_, err := controller.CreateStoreProxy(ctx)
|
resp, err := controller.CreateStoreProxy(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create store proxy: %v", err)
|
t.Fatalf("create store proxy: %v", err)
|
||||||
}
|
}
|
||||||
if gotName != "raw-proxy" {
|
if gotName != "raw-proxy" {
|
||||||
t.Fatalf("unexpected proxy name: %q", gotName)
|
t.Fatalf("unexpected proxy name: %q", gotName)
|
||||||
}
|
}
|
||||||
if !getDisallowUnknownFieldsForTest() {
|
|
||||||
t.Fatal("global strictness flag was not restored")
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Type != "tcp" || payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateStoreVisitor_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) {
|
func TestCreateStoreVisitorIgnoresUnknownFields(t *testing.T) {
|
||||||
restore := setDisallowUnknownFieldsForTest(t, true)
|
|
||||||
t.Cleanup(restore)
|
|
||||||
|
|
||||||
var gotName string
|
var gotName string
|
||||||
controller := &Controller{
|
controller := &Controller{
|
||||||
manager: &fakeConfigManager{
|
manager: &fakeConfigManager{
|
||||||
createStoreVisitFn: func(cfg v1.VisitorConfigurer) error {
|
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
gotName = cfg.GetBaseConfig().Name
|
gotName = cfg.GetBaseConfig().Name
|
||||||
return nil
|
return cfg, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
body := []byte(`{"name":"raw-visitor","type":"xtcp","serverName":"server","bindPort":10081,"secretKey":"secret","unexpected":"value"}`)
|
body := []byte(`{
|
||||||
|
"name":"raw-visitor","type":"xtcp","unexpected":"value",
|
||||||
|
"xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret","unknownInBlock":"value"}
|
||||||
|
}`)
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
_, err := controller.CreateStoreVisitor(ctx)
|
resp, err := controller.CreateStoreVisitor(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create store visitor: %v", err)
|
t.Fatalf("create store visitor: %v", err)
|
||||||
}
|
}
|
||||||
if gotName != "raw-visitor" {
|
if gotName != "raw-visitor" {
|
||||||
t.Fatalf("unexpected visitor name: %q", gotName)
|
t.Fatalf("unexpected visitor name: %q", gotName)
|
||||||
}
|
}
|
||||||
if !getDisallowUnknownFieldsForTest() {
|
|
||||||
t.Fatal("global strictness flag was not restored")
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Type != "xtcp" || payload.XTCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreProxyPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||||
|
var gotPluginType string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{"name":"plugin-proxy","type":"tcp","tcp":{"plugin":{"type":"http2https","localAddr":"127.0.0.1:8080","unknownInPlugin":"value"}}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store proxy: %v", err)
|
||||||
|
}
|
||||||
|
if gotPluginType != "http2https" {
|
||||||
|
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
pluginType := payload.TCP.Plugin.Type
|
||||||
|
|
||||||
|
if pluginType != "http2https" {
|
||||||
|
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreVisitorPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||||
|
var gotPluginType string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{
|
||||||
|
"name":"plugin-visitor","type":"stcp",
|
||||||
|
"stcp":{"serverName":"server","bindPort":10081,"plugin":{"type":"virtual_net","destinationIP":"10.0.0.1","unknownInPlugin":"value"}}
|
||||||
|
}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreVisitor(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store visitor: %v", err)
|
||||||
|
}
|
||||||
|
if gotPluginType != "virtual_net" {
|
||||||
|
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.STCP == nil {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
pluginType := payload.STCP.Plugin.Type
|
||||||
|
|
||||||
|
if pluginType != "virtual_net" {
|
||||||
|
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyRejectsMismatchedTypeBlock(t *testing.T) {
|
||||||
|
controller := &Controller{manager: &fakeConfigManager{}}
|
||||||
|
body := []byte(`{"name":"p1","type":"tcp","udp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyRejectsNameMismatch(t *testing.T) {
|
||||||
|
controller := &Controller{manager: &fakeConfigManager{}}
|
||||||
|
body := []byte(`{"name":"p2","type":"tcp","tcp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListStoreProxiesReturnsSortedPayload(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
listStoreProxiesFn: func() ([]v1.ProxyConfigurer, error) {
|
||||||
|
b := newRawTCPProxyConfig("b")
|
||||||
|
a := newRawTCPProxyConfig("a")
|
||||||
|
return []v1.ProxyConfigurer{b, a}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/store/proxies", nil))
|
||||||
|
|
||||||
|
resp, err := controller.ListStoreProxies(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list store proxies: %v", err)
|
||||||
|
}
|
||||||
|
out, ok := resp.(model.ProxyListResp)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if len(out.Proxies) != 2 {
|
||||||
|
t.Fatalf("unexpected proxy count: %d", len(out.Proxies))
|
||||||
|
}
|
||||||
|
if out.Proxies[0].Name != "a" || out.Proxies[1].Name != "b" {
|
||||||
|
t.Fatalf("proxies are not sorted by name: %#v", out.Proxies)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,3 +490,42 @@ func assertHTTPCode(t *testing.T, err error, expected int) {
|
|||||||
t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected)
|
t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
updateStoreProxyFn: func(_ string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]any{
|
||||||
|
"name": "shared-proxy",
|
||||||
|
"type": "tcp",
|
||||||
|
"tcp": map[string]any{
|
||||||
|
"localPort": 10080,
|
||||||
|
"remotePort": 7000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(data))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("update store proxy: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.TCP == nil || payload.TCP.RemotePort != 7000 {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
148
client/http/model/proxy_definition.go
Normal file
148
client/http/model/proxy_definition.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProxyDefinition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
TCP *v1.TCPProxyConfig `json:"tcp,omitempty"`
|
||||||
|
UDP *v1.UDPProxyConfig `json:"udp,omitempty"`
|
||||||
|
HTTP *v1.HTTPProxyConfig `json:"http,omitempty"`
|
||||||
|
HTTPS *v1.HTTPSProxyConfig `json:"https,omitempty"`
|
||||||
|
TCPMux *v1.TCPMuxProxyConfig `json:"tcpmux,omitempty"`
|
||||||
|
STCP *v1.STCPProxyConfig `json:"stcp,omitempty"`
|
||||||
|
SUDP *v1.SUDPProxyConfig `json:"sudp,omitempty"`
|
||||||
|
XTCP *v1.XTCPProxyConfig `json:"xtcp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) Validate(pathName string, isUpdate bool) error {
|
||||||
|
if strings.TrimSpace(p.Name) == "" {
|
||||||
|
return fmt.Errorf("proxy name is required")
|
||||||
|
}
|
||||||
|
if !IsProxyType(p.Type) {
|
||||||
|
return fmt.Errorf("invalid proxy type: %s", p.Type)
|
||||||
|
}
|
||||||
|
if isUpdate && pathName != "" && pathName != p.Name {
|
||||||
|
return fmt.Errorf("proxy name in URL must match name in body")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blockType, blockCount := p.activeBlock()
|
||||||
|
if blockCount != 1 {
|
||||||
|
return fmt.Errorf("exactly one proxy type block is required")
|
||||||
|
}
|
||||||
|
if blockType != p.Type {
|
||||||
|
return fmt.Errorf("proxy type block %q does not match type %q", blockType, p.Type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) ToConfigurer() (v1.ProxyConfigurer, error) {
|
||||||
|
block, _, _ := p.activeBlock()
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("exactly one proxy type block is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := block
|
||||||
|
cfg.GetBaseConfig().Name = p.Name
|
||||||
|
cfg.GetBaseConfig().Type = p.Type
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyDefinitionFromConfigurer(cfg v1.ProxyConfigurer) (ProxyDefinition, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return ProxyDefinition{}, fmt.Errorf("proxy config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
base := cfg.GetBaseConfig()
|
||||||
|
payload := ProxyDefinition{
|
||||||
|
Name: base.Name,
|
||||||
|
Type: base.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := cfg.(type) {
|
||||||
|
case *v1.TCPProxyConfig:
|
||||||
|
payload.TCP = c
|
||||||
|
case *v1.UDPProxyConfig:
|
||||||
|
payload.UDP = c
|
||||||
|
case *v1.HTTPProxyConfig:
|
||||||
|
payload.HTTP = c
|
||||||
|
case *v1.HTTPSProxyConfig:
|
||||||
|
payload.HTTPS = c
|
||||||
|
case *v1.TCPMuxProxyConfig:
|
||||||
|
payload.TCPMux = c
|
||||||
|
case *v1.STCPProxyConfig:
|
||||||
|
payload.STCP = c
|
||||||
|
case *v1.SUDPProxyConfig:
|
||||||
|
payload.SUDP = c
|
||||||
|
case *v1.XTCPProxyConfig:
|
||||||
|
payload.XTCP = c
|
||||||
|
default:
|
||||||
|
return ProxyDefinition{}, fmt.Errorf("unsupported proxy configurer type %T", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) activeBlock() (v1.ProxyConfigurer, string, int) {
|
||||||
|
count := 0
|
||||||
|
var block v1.ProxyConfigurer
|
||||||
|
var blockType string
|
||||||
|
|
||||||
|
if p.TCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.TCP
|
||||||
|
blockType = "tcp"
|
||||||
|
}
|
||||||
|
if p.UDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.UDP
|
||||||
|
blockType = "udp"
|
||||||
|
}
|
||||||
|
if p.HTTP != nil {
|
||||||
|
count++
|
||||||
|
block = p.HTTP
|
||||||
|
blockType = "http"
|
||||||
|
}
|
||||||
|
if p.HTTPS != nil {
|
||||||
|
count++
|
||||||
|
block = p.HTTPS
|
||||||
|
blockType = "https"
|
||||||
|
}
|
||||||
|
if p.TCPMux != nil {
|
||||||
|
count++
|
||||||
|
block = p.TCPMux
|
||||||
|
blockType = "tcpmux"
|
||||||
|
}
|
||||||
|
if p.STCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.STCP
|
||||||
|
blockType = "stcp"
|
||||||
|
}
|
||||||
|
if p.SUDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.SUDP
|
||||||
|
blockType = "sudp"
|
||||||
|
}
|
||||||
|
if p.XTCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.XTCP
|
||||||
|
blockType = "xtcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
return block, blockType, count
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsProxyType(typ string) bool {
|
||||||
|
switch typ {
|
||||||
|
case "tcp", "udp", "http", "https", "tcpmux", "stcp", "sudp", "xtcp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package api
|
package model
|
||||||
|
|
||||||
const SourceStore = "store"
|
const SourceStore = "store"
|
||||||
|
|
||||||
@@ -31,26 +31,12 @@ type ProxyStatusResp struct {
|
|||||||
Source string `json:"source,omitempty"` // "store" or "config"
|
Source string `json:"source,omitempty"` // "store" or "config"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyConfig wraps proxy configuration for API requests/responses.
|
|
||||||
type ProxyConfig struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Config map[string]any `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitorConfig wraps visitor configuration for API requests/responses.
|
|
||||||
type VisitorConfig struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Config map[string]any `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProxyListResp is the response for GET /api/store/proxies
|
// ProxyListResp is the response for GET /api/store/proxies
|
||||||
type ProxyListResp struct {
|
type ProxyListResp struct {
|
||||||
Proxies []ProxyConfig `json:"proxies"`
|
Proxies []ProxyDefinition `json:"proxies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VisitorListResp is the response for GET /api/store/visitors
|
// VisitorListResp is the response for GET /api/store/visitors
|
||||||
type VisitorListResp struct {
|
type VisitorListResp struct {
|
||||||
Visitors []VisitorConfig `json:"visitors"`
|
Visitors []VisitorDefinition `json:"visitors"`
|
||||||
}
|
}
|
||||||
107
client/http/model/visitor_definition.go
Normal file
107
client/http/model/visitor_definition.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VisitorDefinition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
STCP *v1.STCPVisitorConfig `json:"stcp,omitempty"`
|
||||||
|
SUDP *v1.SUDPVisitorConfig `json:"sudp,omitempty"`
|
||||||
|
XTCP *v1.XTCPVisitorConfig `json:"xtcp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) Validate(pathName string, isUpdate bool) error {
|
||||||
|
if strings.TrimSpace(p.Name) == "" {
|
||||||
|
return fmt.Errorf("visitor name is required")
|
||||||
|
}
|
||||||
|
if !IsVisitorType(p.Type) {
|
||||||
|
return fmt.Errorf("invalid visitor type: %s", p.Type)
|
||||||
|
}
|
||||||
|
if isUpdate && pathName != "" && pathName != p.Name {
|
||||||
|
return fmt.Errorf("visitor name in URL must match name in body")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blockType, blockCount := p.activeBlock()
|
||||||
|
if blockCount != 1 {
|
||||||
|
return fmt.Errorf("exactly one visitor type block is required")
|
||||||
|
}
|
||||||
|
if blockType != p.Type {
|
||||||
|
return fmt.Errorf("visitor type block %q does not match type %q", blockType, p.Type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) ToConfigurer() (v1.VisitorConfigurer, error) {
|
||||||
|
block, _, _ := p.activeBlock()
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("exactly one visitor type block is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := block
|
||||||
|
cfg.GetBaseConfig().Name = p.Name
|
||||||
|
cfg.GetBaseConfig().Type = p.Type
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VisitorDefinitionFromConfigurer(cfg v1.VisitorConfigurer) (VisitorDefinition, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return VisitorDefinition{}, fmt.Errorf("visitor config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
base := cfg.GetBaseConfig()
|
||||||
|
payload := VisitorDefinition{
|
||||||
|
Name: base.Name,
|
||||||
|
Type: base.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := cfg.(type) {
|
||||||
|
case *v1.STCPVisitorConfig:
|
||||||
|
payload.STCP = c
|
||||||
|
case *v1.SUDPVisitorConfig:
|
||||||
|
payload.SUDP = c
|
||||||
|
case *v1.XTCPVisitorConfig:
|
||||||
|
payload.XTCP = c
|
||||||
|
default:
|
||||||
|
return VisitorDefinition{}, fmt.Errorf("unsupported visitor configurer type %T", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) activeBlock() (v1.VisitorConfigurer, string, int) {
|
||||||
|
count := 0
|
||||||
|
var block v1.VisitorConfigurer
|
||||||
|
var blockType string
|
||||||
|
|
||||||
|
if p.STCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.STCP
|
||||||
|
blockType = "stcp"
|
||||||
|
}
|
||||||
|
if p.SUDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.SUDP
|
||||||
|
blockType = "sudp"
|
||||||
|
}
|
||||||
|
if p.XTCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.XTCP
|
||||||
|
blockType = "xtcp"
|
||||||
|
}
|
||||||
|
return block, blockType, count
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsVisitorType(typ string) bool {
|
||||||
|
switch typ {
|
||||||
|
case "stcp", "sudp", "xtcp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -33,6 +32,7 @@ import (
|
|||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,45 +129,54 @@ func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert to JSON and decode with strict validation
|
// Convert to JSON and decode with strict validation
|
||||||
jsonBytes, err := json.Marshal(temp)
|
jsonBytes, err := jsonx.Marshal(temp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
return decodeJSONContent(jsonBytes, target, true)
|
||||||
decoder.DisallowUnknownFields()
|
}
|
||||||
return decoder.Decode(target)
|
|
||||||
|
func decodeJSONContent(content []byte, target any, strict bool) error {
|
||||||
|
if clientCfg, ok := target.(*v1.ClientConfig); ok {
|
||||||
|
decoded, err := v1.DecodeClientConfigJSON(content, v1.DecodeOptions{
|
||||||
|
DisallowUnknownFields: strict,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*clientCfg = decoded
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonx.UnmarshalWithOptions(content, target, jsonx.DecodeOptions{
|
||||||
|
RejectUnknownMembers: strict,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
||||||
// Now it supports json, yaml and toml format.
|
// Now it supports json, yaml and toml format.
|
||||||
func LoadConfigure(b []byte, c any, strict bool) error {
|
func LoadConfigure(b []byte, c any, strict bool) error {
|
||||||
return v1.WithDisallowUnknownFields(strict, func() error {
|
var tomlObj any
|
||||||
var tomlObj any
|
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
|
||||||
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
|
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
||||||
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
var err error
|
||||||
var err error
|
b, err = jsonx.Marshal(&tomlObj)
|
||||||
b, err = json.Marshal(&tomlObj)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
|
|
||||||
if yaml.IsJSONBuffer(b) {
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if strict {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
return decoder.Decode(c)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
|
||||||
|
if yaml.IsJSONBuffer(b) {
|
||||||
|
return decodeJSONContent(b, c, strict)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle YAML content
|
// Handle YAML content
|
||||||
if strict {
|
if strict {
|
||||||
// In strict mode, always use our custom handler to support YAML merge
|
// In strict mode, always use our custom handler to support YAML merge
|
||||||
return parseYAMLWithDotFieldsHandling(b, c)
|
return parseYAMLWithDotFieldsHandling(b, c)
|
||||||
}
|
}
|
||||||
// Non-strict mode, parse normally
|
// Non-strict mode, parse normally
|
||||||
return yaml.Unmarshal(b, c)
|
return yaml.Unmarshal(b, c)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
||||||
|
|||||||
@@ -189,6 +189,31 @@ unixPath = "/tmp/uds.sock"
|
|||||||
require.Error(err)
|
require.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadClientConfigStrictMode_UnknownPluginField(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
content := `
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "test"
|
||||||
|
type = "tcp"
|
||||||
|
localPort = 6000
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "http2https"
|
||||||
|
localAddr = "127.0.0.1:8080"
|
||||||
|
unknownInPlugin = "value"
|
||||||
|
`
|
||||||
|
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
|
||||||
|
err := LoadConfigure([]byte(content), &clientCfg, false)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
err = LoadConfigure([]byte(content), &clientCfg, true)
|
||||||
|
require.ErrorContains(err, "unknownInPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
|
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
|
||||||
// even in strict mode by properly handling dot-prefixed fields
|
// even in strict mode by properly handling dot-prefixed fields
|
||||||
func TestYAMLMergeInStrictMode(t *testing.T) {
|
func TestYAMLMergeInStrictMode(t *testing.T) {
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StoreSourceConfig struct {
|
type StoreSourceConfig struct {
|
||||||
@@ -74,36 +74,44 @@ func (s *StoreSource) loadFromFileUnlocked() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var stored storeData
|
type rawStoreData struct {
|
||||||
if err := v1.WithDisallowUnknownFields(false, func() error {
|
Proxies []jsonx.RawMessage `json:"proxies,omitempty"`
|
||||||
return json.Unmarshal(data, &stored)
|
Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
|
||||||
}); err != nil {
|
}
|
||||||
|
stored := rawStoreData{}
|
||||||
|
if err := jsonx.Unmarshal(data, &stored); err != nil {
|
||||||
return fmt.Errorf("failed to parse JSON: %w", err)
|
return fmt.Errorf("failed to parse JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.proxies = make(map[string]v1.ProxyConfigurer)
|
s.proxies = make(map[string]v1.ProxyConfigurer)
|
||||||
s.visitors = make(map[string]v1.VisitorConfigurer)
|
s.visitors = make(map[string]v1.VisitorConfigurer)
|
||||||
|
|
||||||
for _, tp := range stored.Proxies {
|
for i, proxyData := range stored.Proxies {
|
||||||
if tp.ProxyConfigurer != nil {
|
proxyCfg, err := v1.DecodeProxyConfigurerJSON(proxyData, v1.DecodeOptions{
|
||||||
proxyCfg := tp.ProxyConfigurer
|
DisallowUnknownFields: false,
|
||||||
name := proxyCfg.GetBaseConfig().Name
|
})
|
||||||
if name == "" {
|
if err != nil {
|
||||||
return fmt.Errorf("proxy name cannot be empty")
|
return fmt.Errorf("failed to decode proxy at index %d: %w", i, err)
|
||||||
}
|
|
||||||
s.proxies[name] = proxyCfg
|
|
||||||
}
|
}
|
||||||
|
name := proxyCfg.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("proxy name cannot be empty")
|
||||||
|
}
|
||||||
|
s.proxies[name] = proxyCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tv := range stored.Visitors {
|
for i, visitorData := range stored.Visitors {
|
||||||
if tv.VisitorConfigurer != nil {
|
visitorCfg, err := v1.DecodeVisitorConfigurerJSON(visitorData, v1.DecodeOptions{
|
||||||
visitorCfg := tv.VisitorConfigurer
|
DisallowUnknownFields: false,
|
||||||
name := visitorCfg.GetBaseConfig().Name
|
})
|
||||||
if name == "" {
|
if err != nil {
|
||||||
return fmt.Errorf("visitor name cannot be empty")
|
return fmt.Errorf("failed to decode visitor at index %d: %w", i, err)
|
||||||
}
|
|
||||||
s.visitors[name] = visitorCfg
|
|
||||||
}
|
}
|
||||||
|
name := visitorCfg.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("visitor name cannot be empty")
|
||||||
|
}
|
||||||
|
s.visitors[name] = visitorCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -122,7 +130,7 @@ func (s *StoreSource) saveToFileUnlocked() error {
|
|||||||
stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
|
stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.MarshalIndent(stored, "", " ")
|
data, err := jsonx.MarshalIndent(stored, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -23,27 +22,9 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setDisallowUnknownFieldsForStoreTest(t *testing.T, value bool) func() {
|
|
||||||
t.Helper()
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
prev := v1.DisallowUnknownFields
|
|
||||||
v1.DisallowUnknownFields = value
|
|
||||||
v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
return func() {
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
v1.DisallowUnknownFields = prev
|
|
||||||
v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDisallowUnknownFieldsForStoreTest() bool {
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
defer v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
return v1.DisallowUnknownFields
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
@@ -99,7 +80,7 @@ func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
|||||||
Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}},
|
Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}},
|
||||||
Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}},
|
Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}},
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(stored)
|
data, err := jsonx.Marshal(stored)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
err = os.WriteFile(path, data, 0o600)
|
err = os.WriteFile(path, data, 0o600)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
@@ -117,12 +98,9 @@ func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
|||||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) {
|
func TestStoreSource_LoadFromFile_UnknownFieldsAreIgnored(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
restore := setDisallowUnknownFieldsForStoreTest(t, true)
|
|
||||||
t.Cleanup(restore)
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "store.json")
|
path := filepath.Join(t.TempDir(), "store.json")
|
||||||
raw := []byte(`{
|
raw := []byte(`{
|
||||||
"proxies": [
|
"proxies": [
|
||||||
@@ -140,5 +118,4 @@ func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t
|
|||||||
|
|
||||||
require.NotNil(storeSource.GetProxy("proxy1"))
|
require.NotNil(storeSource.GetProxy("proxy1"))
|
||||||
require.NotNil(storeSource.GetVisitor("visitor1"))
|
require.NotNil(storeSource.GetVisitor("visitor1"))
|
||||||
require.True(getDisallowUnknownFieldsForStoreTest())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,35 +16,10 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(fatedier): Migrate typed config decoding to encoding/json/v2 when it is stable for production use.
|
|
||||||
// The current encoding/json(v1) path cannot propagate DisallowUnknownFields into custom UnmarshalJSON
|
|
||||||
// methods, so we temporarily keep this global strictness flag protected by a mutex.
|
|
||||||
//
|
|
||||||
// https://github.com/golang/go/issues/41144
|
|
||||||
// https://github.com/golang/go/discussions/63397
|
|
||||||
var (
|
|
||||||
DisallowUnknownFields = false
|
|
||||||
DisallowUnknownFieldsMu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// WithDisallowUnknownFields temporarily overrides typed config JSON strictness.
|
|
||||||
// It restores the previous value before returning.
|
|
||||||
func WithDisallowUnknownFields(disallow bool, fn func() error) error {
|
|
||||||
DisallowUnknownFieldsMu.Lock()
|
|
||||||
prev := DisallowUnknownFields
|
|
||||||
DisallowUnknownFields = disallow
|
|
||||||
defer func() {
|
|
||||||
DisallowUnknownFields = prev
|
|
||||||
DisallowUnknownFieldsMu.Unlock()
|
|
||||||
}()
|
|
||||||
return fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthScope string
|
type AuthScope string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
195
pkg/config/v1/decode.go
Normal file
195
pkg/config/v1/decode.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// Copyright 2026 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DecodeOptions struct {
|
||||||
|
DisallowUnknownFields bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSONWithOptions(b []byte, out any, options DecodeOptions) error {
|
||||||
|
return jsonx.UnmarshalWithOptions(b, out, jsonx.DecodeOptions{
|
||||||
|
RejectUnknownMembers: options.DisallowUnknownFields,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSONNull(b []byte) bool {
|
||||||
|
return len(b) == 0 || string(b) == "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
type typedEnvelope struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Plugin jsonx.RawMessage `json:"plugin,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeProxyConfigurerJSON(b []byte, options DecodeOptions) (ProxyConfigurer, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return nil, errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurer := NewProxyConfigurerByType(ProxyType(env.Type))
|
||||||
|
if configurer == nil {
|
||||||
|
return nil, fmt.Errorf("unknown proxy type: %s", env.Type)
|
||||||
|
}
|
||||||
|
if err := decodeJSONWithOptions(b, configurer, options); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal ProxyConfig error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) {
|
||||||
|
plugin, err := DecodeClientPluginOptionsJSON(env.Plugin, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal proxy plugin error: %v", err)
|
||||||
|
}
|
||||||
|
configurer.GetBaseConfig().Plugin = plugin
|
||||||
|
}
|
||||||
|
return configurer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeVisitorConfigurerJSON(b []byte, options DecodeOptions) (VisitorConfigurer, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return nil, errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurer := NewVisitorConfigurerByType(VisitorType(env.Type))
|
||||||
|
if configurer == nil {
|
||||||
|
return nil, fmt.Errorf("unknown visitor type: %s", env.Type)
|
||||||
|
}
|
||||||
|
if err := decodeJSONWithOptions(b, configurer, options); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal VisitorConfig error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) {
|
||||||
|
plugin, err := DecodeVisitorPluginOptionsJSON(env.Plugin, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal visitor plugin error: %v", err)
|
||||||
|
}
|
||||||
|
configurer.GetBaseConfig().Plugin = plugin
|
||||||
|
}
|
||||||
|
return configurer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeClientPluginOptionsJSON(b []byte, options DecodeOptions) (TypedClientPluginOptions, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return TypedClientPluginOptions{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return TypedClientPluginOptions{}, err
|
||||||
|
}
|
||||||
|
if env.Type == "" {
|
||||||
|
return TypedClientPluginOptions{}, errors.New("plugin type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := clientPluginOptionsTypeMap[env.Type]
|
||||||
|
if !ok {
|
||||||
|
return TypedClientPluginOptions{}, fmt.Errorf("unknown plugin type: %s", env.Type)
|
||||||
|
}
|
||||||
|
optionsStruct := reflect.New(v).Interface().(ClientPluginOptions)
|
||||||
|
if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
|
||||||
|
return TypedClientPluginOptions{}, fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
|
||||||
|
}
|
||||||
|
return TypedClientPluginOptions{
|
||||||
|
Type: env.Type,
|
||||||
|
ClientPluginOptions: optionsStruct,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeVisitorPluginOptionsJSON(b []byte, options DecodeOptions) (TypedVisitorPluginOptions, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return TypedVisitorPluginOptions{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return TypedVisitorPluginOptions{}, err
|
||||||
|
}
|
||||||
|
if env.Type == "" {
|
||||||
|
return TypedVisitorPluginOptions{}, errors.New("visitor plugin type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := visitorPluginOptionsTypeMap[env.Type]
|
||||||
|
if !ok {
|
||||||
|
return TypedVisitorPluginOptions{}, fmt.Errorf("unknown visitor plugin type: %s", env.Type)
|
||||||
|
}
|
||||||
|
optionsStruct := reflect.New(v).Interface().(VisitorPluginOptions)
|
||||||
|
if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
|
||||||
|
return TypedVisitorPluginOptions{}, fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
|
||||||
|
}
|
||||||
|
return TypedVisitorPluginOptions{
|
||||||
|
Type: env.Type,
|
||||||
|
VisitorPluginOptions: optionsStruct,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeClientConfigJSON(b []byte, options DecodeOptions) (ClientConfig, error) {
|
||||||
|
type rawClientConfig struct {
|
||||||
|
ClientCommonConfig
|
||||||
|
Proxies []jsonx.RawMessage `json:"proxies,omitempty"`
|
||||||
|
Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := rawClientConfig{}
|
||||||
|
if err := decodeJSONWithOptions(b, &raw, options); err != nil {
|
||||||
|
return ClientConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := ClientConfig{
|
||||||
|
ClientCommonConfig: raw.ClientCommonConfig,
|
||||||
|
Proxies: make([]TypedProxyConfig, 0, len(raw.Proxies)),
|
||||||
|
Visitors: make([]TypedVisitorConfig, 0, len(raw.Visitors)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, proxyData := range raw.Proxies {
|
||||||
|
proxyCfg, err := DecodeProxyConfigurerJSON(proxyData, options)
|
||||||
|
if err != nil {
|
||||||
|
return ClientConfig{}, fmt.Errorf("decode proxy at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
cfg.Proxies = append(cfg.Proxies, TypedProxyConfig{
|
||||||
|
Type: proxyCfg.GetBaseConfig().Type,
|
||||||
|
ProxyConfigurer: proxyCfg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, visitorData := range raw.Visitors {
|
||||||
|
visitorCfg, err := DecodeVisitorConfigurerJSON(visitorData, options)
|
||||||
|
if err != nil {
|
||||||
|
return ClientConfig{}, fmt.Errorf("decode visitor at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
cfg.Visitors = append(cfg.Visitors, TypedVisitorConfig{
|
||||||
|
Type: visitorCfg.GetBaseConfig().Type,
|
||||||
|
VisitorConfigurer: visitorCfg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
86
pkg/config/v1/decode_test.go
Normal file
86
pkg/config/v1/decode_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright 2026 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeProxyConfigurerJSON_StrictPluginUnknownFields(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
data := []byte(`{
|
||||||
|
"name":"p1",
|
||||||
|
"type":"tcp",
|
||||||
|
"localPort":10080,
|
||||||
|
"plugin":{
|
||||||
|
"type":"http2https",
|
||||||
|
"localAddr":"127.0.0.1:8080",
|
||||||
|
"unknownInPlugin":"value"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
||||||
|
require.ErrorContains(err, "unknownInPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeVisitorConfigurerJSON_StrictPluginUnknownFields(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
data := []byte(`{
|
||||||
|
"name":"v1",
|
||||||
|
"type":"stcp",
|
||||||
|
"serverName":"server",
|
||||||
|
"bindPort":10081,
|
||||||
|
"plugin":{
|
||||||
|
"type":"virtual_net",
|
||||||
|
"destinationIP":"10.0.0.1",
|
||||||
|
"unknownInPlugin":"value"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
||||||
|
require.ErrorContains(err, "unknownInPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeClientConfigJSON_StrictUnknownProxyField(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
data := []byte(`{
|
||||||
|
"serverPort":7000,
|
||||||
|
"proxies":[
|
||||||
|
{
|
||||||
|
"name":"p1",
|
||||||
|
"type":"tcp",
|
||||||
|
"localPort":10080,
|
||||||
|
"unknownField":"value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
||||||
|
require.ErrorContains(err, "unknownField")
|
||||||
|
}
|
||||||
@@ -15,16 +15,13 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
"maps"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -202,35 +199,18 @@ type TypedProxyConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
configurer, err := DecodeProxyConfigurerJSON(b, DecodeOptions{})
|
||||||
return errors.New("type is required")
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Type = typeStruct.Type
|
c.Type = configurer.GetBaseConfig().Type
|
||||||
configurer := NewProxyConfigurerByType(ProxyType(typeStruct.Type))
|
|
||||||
if configurer == nil {
|
|
||||||
return fmt.Errorf("unknown proxy type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
if err := decoder.Decode(configurer); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal ProxyConfig error: %v", err)
|
|
||||||
}
|
|
||||||
c.ProxyConfigurer = configurer
|
c.ProxyConfigurer = configurer
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.ProxyConfigurer)
|
return jsonx.Marshal(c.ProxyConfigurer)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyConfigurer interface {
|
type ProxyConfigurer interface {
|
||||||
|
|||||||
@@ -15,14 +15,11 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,42 +68,16 @@ func (c TypedClientPluginOptions) Clone() TypedClientPluginOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
decoded, err := DecodeClientPluginOptionsJSON(b, DecodeOptions{})
|
||||||
return nil
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
*c = decoded
|
||||||
c.Type = typeStruct.Type
|
|
||||||
if c.Type == "" {
|
|
||||||
return errors.New("plugin type is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := clientPluginOptionsTypeMap[typeStruct.Type]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
options := reflect.New(v).Interface().(ClientPluginOptions)
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := decoder.Decode(options); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
|
|
||||||
}
|
|
||||||
c.ClientPluginOptions = options
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.ClientPluginOptions)
|
return jsonx.Marshal(c.ClientPluginOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTP2HTTPSPluginOptions struct {
|
type HTTP2HTTPSPluginOptions struct {
|
||||||
|
|||||||
@@ -15,12 +15,9 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,35 +90,18 @@ type TypedVisitorConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
configurer, err := DecodeVisitorConfigurerJSON(b, DecodeOptions{})
|
||||||
return errors.New("type is required")
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Type = typeStruct.Type
|
c.Type = configurer.GetBaseConfig().Type
|
||||||
configurer := NewVisitorConfigurerByType(VisitorType(typeStruct.Type))
|
|
||||||
if configurer == nil {
|
|
||||||
return fmt.Errorf("unknown visitor type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
if err := decoder.Decode(configurer); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal VisitorConfig error: %v", err)
|
|
||||||
}
|
|
||||||
c.VisitorConfigurer = configurer
|
c.VisitorConfigurer = configurer
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.VisitorConfigurer)
|
return jsonx.Marshal(c.VisitorConfigurer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
||||||
|
|||||||
@@ -15,11 +15,9 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -49,42 +47,16 @@ func (c TypedVisitorPluginOptions) Clone() TypedVisitorPluginOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
decoded, err := DecodeVisitorPluginOptionsJSON(b, DecodeOptions{})
|
||||||
return nil
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
*c = decoded
|
||||||
c.Type = typeStruct.Type
|
|
||||||
if c.Type == "" {
|
|
||||||
return errors.New("visitor plugin type is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := visitorPluginOptionsTypeMap[typeStruct.Type]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unknown visitor plugin type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
options := reflect.New(v).Interface().(VisitorPluginOptions)
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := decoder.Decode(options); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
|
|
||||||
}
|
|
||||||
c.VisitorPluginOptions = options
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.VisitorPluginOptions)
|
return jsonx.Marshal(c.VisitorPluginOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type VirtualNetVisitorPluginOptions struct {
|
type VirtualNetVisitorPluginOptions struct {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/api"
|
"github.com/fatedier/frp/client/http/model"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ func (c *Client) SetAuth(user, pwd string) {
|
|||||||
c.authPwd = pwd
|
c.authPwd = pwd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxyStatusResp, error) {
|
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*model.ProxyStatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -41,7 +41,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(api.StatusResp)
|
allStatus := make(model.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta
|
|||||||
return nil, fmt.Errorf("no proxy status found")
|
return nil, fmt.Errorf("no proxy status found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error) {
|
func (c *Client) GetAllProxyStatus(ctx context.Context) (model.StatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -64,7 +64,7 @@ func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(api.StatusResp)
|
allStatus := make(model.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
|
|||||||
45
pkg/util/jsonx/json_v1.go
Normal file
45
pkg/util/jsonx/json_v1.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright 2026 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package jsonx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DecodeOptions struct {
|
||||||
|
RejectUnknownMembers bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Marshal(v any) ([]byte, error) {
|
||||||
|
return json.Marshal(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||||
|
return json.MarshalIndent(v, prefix, indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unmarshal(data []byte, out any) error {
|
||||||
|
return json.Unmarshal(data, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalWithOptions(data []byte, out any, options DecodeOptions) error {
|
||||||
|
if !options.RejectUnknownMembers {
|
||||||
|
return json.Unmarshal(data, out)
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
return decoder.Decode(out)
|
||||||
|
}
|
||||||
36
pkg/util/jsonx/raw_message.go
Normal file
36
pkg/util/jsonx/raw_message.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2026 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package jsonx
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// RawMessage stores a raw encoded JSON value.
|
||||||
|
// It is equivalent to encoding/json.RawMessage behavior.
|
||||||
|
type RawMessage []byte
|
||||||
|
|
||||||
|
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
|
if m == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("jsonx.RawMessage: UnmarshalJSON on nil pointer")
|
||||||
|
}
|
||||||
|
*m = append((*m)[:0], data...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
64
server/api_router.go
Normal file
64
server/api_router.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
adminapi "github.com/fatedier/frp/server/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||||
|
helper.Router.HandleFunc("/healthz", healthz)
|
||||||
|
subRouter := helper.Router.NewRoute().Subrouter()
|
||||||
|
|
||||||
|
subRouter.Use(helper.AuthMiddleware)
|
||||||
|
subRouter.Use(httppkg.NewRequestLogger)
|
||||||
|
|
||||||
|
// metrics
|
||||||
|
if svr.cfg.EnablePrometheus {
|
||||||
|
subRouter.Handle("/metrics", promhttp.Handler())
|
||||||
|
}
|
||||||
|
|
||||||
|
apiController := adminapi.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager)
|
||||||
|
|
||||||
|
// apis
|
||||||
|
subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.DeleteProxies)).Methods("DELETE")
|
||||||
|
|
||||||
|
// view
|
||||||
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||||
|
subRouter.PathPrefix("/static/").Handler(
|
||||||
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||||
|
).Methods("GET")
|
||||||
|
|
||||||
|
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
@@ -12,11 +12,10 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package api
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -29,6 +28,7 @@ import (
|
|||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
|
"github.com/fatedier/frp/server/http/model"
|
||||||
"github.com/fatedier/frp/server/proxy"
|
"github.com/fatedier/frp/server/proxy"
|
||||||
"github.com/fatedier/frp/server/registry"
|
"github.com/fatedier/frp/server/registry"
|
||||||
)
|
)
|
||||||
@@ -59,7 +59,7 @@ func NewController(
|
|||||||
// /api/serverinfo
|
// /api/serverinfo
|
||||||
func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) {
|
||||||
serverStats := mem.StatsCollector.GetServer()
|
serverStats := mem.StatsCollector.GetServer()
|
||||||
svrResp := ServerInfoResp{
|
svrResp := model.ServerInfoResp{
|
||||||
Version: version.Full(),
|
Version: version.Full(),
|
||||||
BindPort: c.serverCfg.BindPort,
|
BindPort: c.serverCfg.BindPort,
|
||||||
VhostHTTPPort: c.serverCfg.VhostHTTPPort,
|
VhostHTTPPort: c.serverCfg.VhostHTTPPort,
|
||||||
@@ -80,22 +80,6 @@ func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) {
|
|||||||
ClientCounts: serverStats.ClientCounts,
|
ClientCounts: serverStats.ClientCounts,
|
||||||
ProxyTypeCounts: serverStats.ProxyTypeCounts,
|
ProxyTypeCounts: serverStats.ProxyTypeCounts,
|
||||||
}
|
}
|
||||||
// For API that returns struct, we can just return it.
|
|
||||||
// But current GeneralResponse.Msg in legacy code expects a JSON string.
|
|
||||||
// Since MakeHTTPHandlerFunc handles struct by encoding to JSON, we can return svrResp directly?
|
|
||||||
// The original code wraps it in GeneralResponse{Msg: string(json)}.
|
|
||||||
// If we return svrResp, the response body will be the JSON of svrResp.
|
|
||||||
// We should check if the frontend expects { "code": 200, "msg": "{...}" } or just {...}.
|
|
||||||
// Looking at previous code:
|
|
||||||
// res := GeneralResponse{Code: 200}
|
|
||||||
// buf, _ := json.Marshal(&svrResp)
|
|
||||||
// res.Msg = string(buf)
|
|
||||||
// Response body: {"code": 200, "msg": "{\"version\":...}"}
|
|
||||||
// Wait, is it double encoded JSON? Yes it seems so!
|
|
||||||
// Let's check dashboard_api.go original code again.
|
|
||||||
// Yes: res.Msg = string(buf).
|
|
||||||
// So the frontend expects { "code": 200, "msg": "JSON_STRING" }.
|
|
||||||
// This is kind of ugly, but we must preserve compatibility.
|
|
||||||
|
|
||||||
return svrResp, nil
|
return svrResp, nil
|
||||||
}
|
}
|
||||||
@@ -112,7 +96,7 @@ func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) {
|
|||||||
statusFilter := strings.ToLower(ctx.Query("status"))
|
statusFilter := strings.ToLower(ctx.Query("status"))
|
||||||
|
|
||||||
records := c.clientRegistry.List()
|
records := c.clientRegistry.List()
|
||||||
items := make([]ClientInfoResp, 0, len(records))
|
items := make([]model.ClientInfoResp, 0, len(records))
|
||||||
for _, info := range records {
|
for _, info := range records {
|
||||||
if userFilter != "" && info.User != userFilter {
|
if userFilter != "" && info.User != userFilter {
|
||||||
continue
|
continue
|
||||||
@@ -129,7 +113,7 @@ func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) {
|
|||||||
items = append(items, buildClientInfoResp(info))
|
items = append(items, buildClientInfoResp(info))
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.SortFunc(items, func(a, b ClientInfoResp) int {
|
slices.SortFunc(items, func(a, b model.ClientInfoResp) int {
|
||||||
if v := cmp.Compare(a.User, b.User); v != 0 {
|
if v := cmp.Compare(a.User, b.User); v != 0 {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
@@ -165,9 +149,9 @@ func (c *Controller) APIClientDetail(ctx *httppkg.Context) (any, error) {
|
|||||||
func (c *Controller) APIProxyByType(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) APIProxyByType(ctx *httppkg.Context) (any, error) {
|
||||||
proxyType := ctx.Param("type")
|
proxyType := ctx.Param("type")
|
||||||
|
|
||||||
proxyInfoResp := GetProxyInfoResp{}
|
proxyInfoResp := model.GetProxyInfoResp{}
|
||||||
proxyInfoResp.Proxies = c.getProxyStatsByType(proxyType)
|
proxyInfoResp.Proxies = c.getProxyStatsByType(proxyType)
|
||||||
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
|
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *model.ProxyStatsInfo) int {
|
||||||
return cmp.Compare(a.Name, b.Name)
|
return cmp.Compare(a.Name, b.Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -191,7 +175,7 @@ func (c *Controller) APIProxyByTypeAndName(ctx *httppkg.Context) (any, error) {
|
|||||||
func (c *Controller) APIProxyTraffic(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) APIProxyTraffic(ctx *httppkg.Context) (any, error) {
|
||||||
name := ctx.Param("name")
|
name := ctx.Param("name")
|
||||||
|
|
||||||
trafficResp := GetProxyTrafficResp{}
|
trafficResp := model.GetProxyTrafficResp{}
|
||||||
trafficResp.Name = name
|
trafficResp.Name = name
|
||||||
proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name)
|
proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name)
|
||||||
|
|
||||||
@@ -213,7 +197,7 @@ func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found")
|
return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found")
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyInfo := GetProxyStatsResp{
|
proxyInfo := model.GetProxyStatsResp{
|
||||||
Name: ps.Name,
|
Name: ps.Name,
|
||||||
User: ps.User,
|
User: ps.User,
|
||||||
ClientID: ps.ClientID,
|
ClientID: ps.ClientID,
|
||||||
@@ -225,16 +209,7 @@ func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pxy, ok := c.pxyManager.GetByName(name); ok {
|
if pxy, ok := c.pxyManager.GetByName(name); ok {
|
||||||
content, err := json.Marshal(pxy.GetConfigurer())
|
proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer())
|
||||||
if err != nil {
|
|
||||||
log.Warnf("marshal proxy [%s] conf info error: %v", name, err)
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error")
|
|
||||||
}
|
|
||||||
proxyInfo.Conf = getConfByType(ps.Type)
|
|
||||||
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
||||||
log.Warnf("unmarshal proxy [%s] conf info error: %v", name, err)
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error")
|
|
||||||
}
|
|
||||||
proxyInfo.Status = "online"
|
proxyInfo.Status = "online"
|
||||||
} else {
|
} else {
|
||||||
proxyInfo.Status = "offline"
|
proxyInfo.Status = "offline"
|
||||||
@@ -254,25 +229,16 @@ func (c *Controller) DeleteProxies(ctx *httppkg.Context) (any, error) {
|
|||||||
return httppkg.GeneralResponse{Code: 200, Msg: "success"}, nil
|
return httppkg.GeneralResponse{Code: 200, Msg: "success"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
|
func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*model.ProxyStatsInfo) {
|
||||||
proxyStats := mem.StatsCollector.GetProxiesByType(proxyType)
|
proxyStats := mem.StatsCollector.GetProxiesByType(proxyType)
|
||||||
proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats))
|
proxyInfos = make([]*model.ProxyStatsInfo, 0, len(proxyStats))
|
||||||
for _, ps := range proxyStats {
|
for _, ps := range proxyStats {
|
||||||
proxyInfo := &ProxyStatsInfo{
|
proxyInfo := &model.ProxyStatsInfo{
|
||||||
User: ps.User,
|
User: ps.User,
|
||||||
ClientID: ps.ClientID,
|
ClientID: ps.ClientID,
|
||||||
}
|
}
|
||||||
if pxy, ok := c.pxyManager.GetByName(ps.Name); ok {
|
if pxy, ok := c.pxyManager.GetByName(ps.Name); ok {
|
||||||
content, err := json.Marshal(pxy.GetConfigurer())
|
proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer())
|
||||||
if err != nil {
|
|
||||||
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
proxyInfo.Conf = getConfByType(ps.Type)
|
|
||||||
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
||||||
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
proxyInfo.Status = "online"
|
proxyInfo.Status = "online"
|
||||||
} else {
|
} else {
|
||||||
proxyInfo.Status = "offline"
|
proxyInfo.Status = "offline"
|
||||||
@@ -288,7 +254,7 @@ func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) {
|
func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo model.GetProxyStatsResp, code int, msg string) {
|
||||||
proxyInfo.Name = proxyName
|
proxyInfo.Name = proxyName
|
||||||
ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName)
|
ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName)
|
||||||
if ps == nil {
|
if ps == nil {
|
||||||
@@ -298,20 +264,7 @@ func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName stri
|
|||||||
proxyInfo.User = ps.User
|
proxyInfo.User = ps.User
|
||||||
proxyInfo.ClientID = ps.ClientID
|
proxyInfo.ClientID = ps.ClientID
|
||||||
if pxy, ok := c.pxyManager.GetByName(proxyName); ok {
|
if pxy, ok := c.pxyManager.GetByName(proxyName); ok {
|
||||||
content, err := json.Marshal(pxy.GetConfigurer())
|
proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer())
|
||||||
if err != nil {
|
|
||||||
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
code = 400
|
|
||||||
msg = "parse conf error"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
proxyInfo.Conf = getConfByType(ps.Type)
|
|
||||||
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
||||||
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
code = 400
|
|
||||||
msg = "parse conf error"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
proxyInfo.Status = "online"
|
proxyInfo.Status = "online"
|
||||||
} else {
|
} else {
|
||||||
proxyInfo.Status = "offline"
|
proxyInfo.Status = "offline"
|
||||||
@@ -327,8 +280,8 @@ func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName stri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildClientInfoResp(info registry.ClientInfo) ClientInfoResp {
|
func buildClientInfoResp(info registry.ClientInfo) model.ClientInfoResp {
|
||||||
resp := ClientInfoResp{
|
resp := model.ClientInfoResp{
|
||||||
Key: info.Key,
|
Key: info.Key,
|
||||||
User: info.User,
|
User: info.User,
|
||||||
ClientID: info.ClientID(),
|
ClientID: info.ClientID(),
|
||||||
@@ -366,23 +319,37 @@ func matchStatusFilter(online bool, filter string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfByType(proxyType string) any {
|
func getConfFromConfigurer(cfg v1.ProxyConfigurer) any {
|
||||||
switch v1.ProxyType(proxyType) {
|
outBase := model.BaseOutConf{ProxyBaseConfig: *cfg.GetBaseConfig()}
|
||||||
case v1.ProxyTypeTCP:
|
|
||||||
return &TCPOutConf{}
|
switch c := cfg.(type) {
|
||||||
case v1.ProxyTypeTCPMUX:
|
case *v1.TCPProxyConfig:
|
||||||
return &TCPMuxOutConf{}
|
return &model.TCPOutConf{BaseOutConf: outBase, RemotePort: c.RemotePort}
|
||||||
case v1.ProxyTypeUDP:
|
case *v1.UDPProxyConfig:
|
||||||
return &UDPOutConf{}
|
return &model.UDPOutConf{BaseOutConf: outBase, RemotePort: c.RemotePort}
|
||||||
case v1.ProxyTypeHTTP:
|
case *v1.HTTPProxyConfig:
|
||||||
return &HTTPOutConf{}
|
return &model.HTTPOutConf{
|
||||||
case v1.ProxyTypeHTTPS:
|
BaseOutConf: outBase,
|
||||||
return &HTTPSOutConf{}
|
DomainConfig: c.DomainConfig,
|
||||||
case v1.ProxyTypeSTCP:
|
Locations: c.Locations,
|
||||||
return &STCPOutConf{}
|
HostHeaderRewrite: c.HostHeaderRewrite,
|
||||||
case v1.ProxyTypeXTCP:
|
}
|
||||||
return &XTCPOutConf{}
|
case *v1.HTTPSProxyConfig:
|
||||||
default:
|
return &model.HTTPSOutConf{
|
||||||
return nil
|
BaseOutConf: outBase,
|
||||||
|
DomainConfig: c.DomainConfig,
|
||||||
|
}
|
||||||
|
case *v1.TCPMuxProxyConfig:
|
||||||
|
return &model.TCPMuxOutConf{
|
||||||
|
BaseOutConf: outBase,
|
||||||
|
DomainConfig: c.DomainConfig,
|
||||||
|
Multiplexer: c.Multiplexer,
|
||||||
|
RouteByHTTPUser: c.RouteByHTTPUser,
|
||||||
|
}
|
||||||
|
case *v1.STCPProxyConfig:
|
||||||
|
return &model.STCPOutConf{BaseOutConf: outBase}
|
||||||
|
case *v1.XTCPProxyConfig:
|
||||||
|
return &model.XTCPOutConf{BaseOutConf: outBase}
|
||||||
}
|
}
|
||||||
|
return outBase
|
||||||
}
|
}
|
||||||
71
server/http/controller_test.go
Normal file
71
server/http/controller_test.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2026 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetConfFromConfigurerKeepsPluginFields(t *testing.T) {
|
||||||
|
cfg := &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: "test-proxy",
|
||||||
|
Type: string(v1.ProxyTypeTCP),
|
||||||
|
ProxyBackend: v1.ProxyBackend{
|
||||||
|
Plugin: v1.TypedClientPluginOptions{
|
||||||
|
Type: v1.PluginHTTPProxy,
|
||||||
|
ClientPluginOptions: &v1.HTTPProxyPluginOptions{
|
||||||
|
Type: v1.PluginHTTPProxy,
|
||||||
|
HTTPUser: "user",
|
||||||
|
HTTPPassword: "password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RemotePort: 6000,
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := json.Marshal(getConfFromConfigurer(cfg))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal conf failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out map[string]any
|
||||||
|
if err := json.Unmarshal(content, &out); err != nil {
|
||||||
|
t.Fatalf("unmarshal conf failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginValue, ok := out["plugin"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("plugin field missing in output: %v", out)
|
||||||
|
}
|
||||||
|
plugin, ok := pluginValue.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("plugin field should be object, got: %#v", pluginValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := plugin["type"]; got != v1.PluginHTTPProxy {
|
||||||
|
t.Fatalf("plugin type mismatch, want %q got %#v", v1.PluginHTTPProxy, got)
|
||||||
|
}
|
||||||
|
if got := plugin["httpUser"]; got != "user" {
|
||||||
|
t.Fatalf("plugin httpUser mismatch, want %q got %#v", "user", got)
|
||||||
|
}
|
||||||
|
if got := plugin["httpPassword"]; got != "password" {
|
||||||
|
t.Fatalf("plugin httpPassword mismatch, want %q got %#v", "password", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package api
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
@@ -28,7 +28,6 @@ import (
|
|||||||
"github.com/fatedier/golib/crypto"
|
"github.com/fatedier/golib/crypto"
|
||||||
"github.com/fatedier/golib/net/mux"
|
"github.com/fatedier/golib/net/mux"
|
||||||
fmux "github.com/hashicorp/yamux"
|
fmux "github.com/hashicorp/yamux"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
quic "github.com/quic-go/quic-go"
|
quic "github.com/quic-go/quic-go"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
@@ -48,7 +47,6 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
"github.com/fatedier/frp/pkg/util/vhost"
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
"github.com/fatedier/frp/server/api"
|
|
||||||
"github.com/fatedier/frp/server/controller"
|
"github.com/fatedier/frp/server/controller"
|
||||||
"github.com/fatedier/frp/server/group"
|
"github.com/fatedier/frp/server/group"
|
||||||
"github.com/fatedier/frp/server/metrics"
|
"github.com/fatedier/frp/server/metrics"
|
||||||
@@ -690,42 +688,3 @@ func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVis
|
|||||||
return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
|
return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
|
||||||
newMsg.UseEncryption, newMsg.UseCompression, visitorUser)
|
newMsg.UseEncryption, newMsg.UseCompression, visitorUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
|
||||||
helper.Router.HandleFunc("/healthz", healthz)
|
|
||||||
subRouter := helper.Router.NewRoute().Subrouter()
|
|
||||||
|
|
||||||
subRouter.Use(helper.AuthMiddleware)
|
|
||||||
subRouter.Use(httppkg.NewRequestLogger)
|
|
||||||
|
|
||||||
// metrics
|
|
||||||
if svr.cfg.EnablePrometheus {
|
|
||||||
subRouter.Handle("/metrics", promhttp.Handler())
|
|
||||||
}
|
|
||||||
|
|
||||||
apiController := api.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager)
|
|
||||||
|
|
||||||
// apis
|
|
||||||
subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.DeleteProxies)).Methods("DELETE")
|
|
||||||
|
|
||||||
// view
|
|
||||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
|
||||||
subRouter.PathPrefix("/static/").Handler(
|
|
||||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
|
||||||
).Methods("GET")
|
|
||||||
|
|
||||||
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
w.WriteHeader(200)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,11 +34,13 @@ var _ = ginkgo.Describe("[Feature: Store]", func() {
|
|||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
proxyConfig := map[string]any{
|
proxyConfig := map[string]any{
|
||||||
"name": "test-tcp",
|
"name": "test-tcp",
|
||||||
"type": "tcp",
|
"type": "tcp",
|
||||||
"localIP": "127.0.0.1",
|
"tcp": map[string]any{
|
||||||
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
"localIP": "127.0.0.1",
|
||||||
"remotePort": remotePort,
|
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
||||||
|
"remotePort": remotePort,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
proxyBody, _ := json.Marshal(proxyConfig)
|
proxyBody, _ := json.Marshal(proxyConfig)
|
||||||
|
|
||||||
@@ -73,11 +75,13 @@ var _ = ginkgo.Describe("[Feature: Store]", func() {
|
|||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
proxyConfig := map[string]any{
|
proxyConfig := map[string]any{
|
||||||
"name": "test-tcp",
|
"name": "test-tcp",
|
||||||
"type": "tcp",
|
"type": "tcp",
|
||||||
"localIP": "127.0.0.1",
|
"tcp": map[string]any{
|
||||||
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
"localIP": "127.0.0.1",
|
||||||
"remotePort": remotePort1,
|
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
||||||
|
"remotePort": remotePort1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
proxyBody, _ := json.Marshal(proxyConfig)
|
proxyBody, _ := json.Marshal(proxyConfig)
|
||||||
|
|
||||||
@@ -92,7 +96,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() {
|
|||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
framework.NewRequestExpect(f).Port(remotePort1).Ensure()
|
framework.NewRequestExpect(f).Port(remotePort1).Ensure()
|
||||||
|
|
||||||
proxyConfig["remotePort"] = remotePort2
|
proxyConfig["tcp"].(map[string]any)["remotePort"] = remotePort2
|
||||||
proxyBody, _ = json.Marshal(proxyConfig)
|
proxyBody, _ = json.Marshal(proxyConfig)
|
||||||
|
|
||||||
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
@@ -125,11 +129,13 @@ var _ = ginkgo.Describe("[Feature: Store]", func() {
|
|||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
proxyConfig := map[string]any{
|
proxyConfig := map[string]any{
|
||||||
"name": "test-tcp",
|
"name": "test-tcp",
|
||||||
"type": "tcp",
|
"type": "tcp",
|
||||||
"localIP": "127.0.0.1",
|
"tcp": map[string]any{
|
||||||
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
"localIP": "127.0.0.1",
|
||||||
"remotePort": remotePort,
|
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
||||||
|
"remotePort": remotePort,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
proxyBody, _ := json.Marshal(proxyConfig)
|
proxyBody, _ := json.Marshal(proxyConfig)
|
||||||
|
|
||||||
@@ -171,11 +177,13 @@ var _ = ginkgo.Describe("[Feature: Store]", func() {
|
|||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
proxyConfig := map[string]any{
|
proxyConfig := map[string]any{
|
||||||
"name": "test-tcp",
|
"name": "test-tcp",
|
||||||
"type": "tcp",
|
"type": "tcp",
|
||||||
"localIP": "127.0.0.1",
|
"tcp": map[string]any{
|
||||||
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
"localIP": "127.0.0.1",
|
||||||
"remotePort": remotePort,
|
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
||||||
|
"remotePort": remotePort,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
proxyBody, _ := json.Marshal(proxyConfig)
|
proxyBody, _ := json.Marshal(proxyConfig)
|
||||||
|
|
||||||
@@ -226,5 +234,90 @@ var _ = ginkgo.Describe("[Feature: Store]", func() {
|
|||||||
return resp.Code == 404
|
return resp.Code == 404
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ginkgo.It("rejects mismatched type block", func() {
|
||||||
|
adminPort := f.AllocPort()
|
||||||
|
|
||||||
|
serverConf := consts.DefaultServerConfig
|
||||||
|
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
|
||||||
|
webServer.addr = "127.0.0.1"
|
||||||
|
webServer.port = %d
|
||||||
|
|
||||||
|
[store]
|
||||||
|
path = "%s/store.json"
|
||||||
|
`, adminPort, f.TempDirectory)
|
||||||
|
|
||||||
|
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
invalidBody, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "bad-proxy",
|
||||||
|
"type": "tcp",
|
||||||
|
"udp": map[string]any{
|
||||||
|
"localPort": 1234,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}).Body(invalidBody)
|
||||||
|
}).Ensure(func(resp *request.Response) bool {
|
||||||
|
return resp.Code == 400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("rejects path/body name mismatch on update", func() {
|
||||||
|
adminPort := f.AllocPort()
|
||||||
|
remotePort := f.AllocPort()
|
||||||
|
|
||||||
|
serverConf := consts.DefaultServerConfig
|
||||||
|
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
|
||||||
|
webServer.addr = "127.0.0.1"
|
||||||
|
webServer.port = %d
|
||||||
|
|
||||||
|
[store]
|
||||||
|
path = "%s/store.json"
|
||||||
|
`, adminPort, f.TempDirectory)
|
||||||
|
|
||||||
|
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
createBody, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "proxy-a",
|
||||||
|
"type": "tcp",
|
||||||
|
"tcp": map[string]any{
|
||||||
|
"localIP": "127.0.0.1",
|
||||||
|
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
||||||
|
"remotePort": remotePort,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}).Body(createBody)
|
||||||
|
}).Ensure(func(resp *request.Response) bool {
|
||||||
|
return resp.Code == 200
|
||||||
|
})
|
||||||
|
|
||||||
|
updateBody, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "proxy-b",
|
||||||
|
"type": "tcp",
|
||||||
|
"tcp": map[string]any{
|
||||||
|
"localIP": "127.0.0.1",
|
||||||
|
"localPort": f.PortByName(framework.TCPEchoServerPort),
|
||||||
|
"remotePort": remotePort,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/proxy-a").HTTPParams("PUT", "", "/api/store/proxies/proxy-a", map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}).Body(updateBody)
|
||||||
|
}).Ensure(func(resp *request.Response) bool {
|
||||||
|
return resp.Code == 400
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { http } from './http'
|
import { http } from './http'
|
||||||
import type {
|
import type {
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
StoreProxyListResp,
|
ProxyListResp,
|
||||||
StoreProxyConfig,
|
ProxyDefinition,
|
||||||
StoreVisitorListResp,
|
VisitorListResp,
|
||||||
StoreVisitorConfig,
|
VisitorDefinition,
|
||||||
} from '../types/proxy'
|
} from '../types/proxy'
|
||||||
|
|
||||||
export const getStatus = () => {
|
export const getStatus = () => {
|
||||||
@@ -25,21 +25,21 @@ export const reloadConfig = () => {
|
|||||||
|
|
||||||
// Store API - Proxies
|
// Store API - Proxies
|
||||||
export const listStoreProxies = () => {
|
export const listStoreProxies = () => {
|
||||||
return http.get<StoreProxyListResp>('/api/store/proxies')
|
return http.get<ProxyListResp>('/api/store/proxies')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStoreProxy = (name: string) => {
|
export const getStoreProxy = (name: string) => {
|
||||||
return http.get<StoreProxyConfig>(
|
return http.get<ProxyDefinition>(
|
||||||
`/api/store/proxies/${encodeURIComponent(name)}`,
|
`/api/store/proxies/${encodeURIComponent(name)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createStoreProxy = (config: Record<string, any>) => {
|
export const createStoreProxy = (config: ProxyDefinition) => {
|
||||||
return http.post<void>('/api/store/proxies', config)
|
return http.post<ProxyDefinition>('/api/store/proxies', config)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateStoreProxy = (name: string, config: Record<string, any>) => {
|
export const updateStoreProxy = (name: string, config: ProxyDefinition) => {
|
||||||
return http.put<void>(
|
return http.put<ProxyDefinition>(
|
||||||
`/api/store/proxies/${encodeURIComponent(name)}`,
|
`/api/store/proxies/${encodeURIComponent(name)}`,
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
@@ -51,24 +51,24 @@ export const deleteStoreProxy = (name: string) => {
|
|||||||
|
|
||||||
// Store API - Visitors
|
// Store API - Visitors
|
||||||
export const listStoreVisitors = () => {
|
export const listStoreVisitors = () => {
|
||||||
return http.get<StoreVisitorListResp>('/api/store/visitors')
|
return http.get<VisitorListResp>('/api/store/visitors')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStoreVisitor = (name: string) => {
|
export const getStoreVisitor = (name: string) => {
|
||||||
return http.get<StoreVisitorConfig>(
|
return http.get<VisitorDefinition>(
|
||||||
`/api/store/visitors/${encodeURIComponent(name)}`,
|
`/api/store/visitors/${encodeURIComponent(name)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createStoreVisitor = (config: Record<string, any>) => {
|
export const createStoreVisitor = (config: VisitorDefinition) => {
|
||||||
return http.post<void>('/api/store/visitors', config)
|
return http.post<VisitorDefinition>('/api/store/visitors', config)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateStoreVisitor = (
|
export const updateStoreVisitor = (
|
||||||
name: string,
|
name: string,
|
||||||
config: Record<string, any>,
|
config: VisitorDefinition,
|
||||||
) => {
|
) => {
|
||||||
return http.put<void>(
|
return http.put<VisitorDefinition>(
|
||||||
`/api/store/visitors/${encodeURIComponent(name)}`,
|
`/api/store/visitors/${encodeURIComponent(name)}`,
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,24 +20,33 @@ export type StatusResponse = Record<string, ProxyStatus[]>
|
|||||||
// STORE API TYPES
|
// STORE API TYPES
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
export interface StoreProxyConfig {
|
export interface ProxyDefinition {
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: ProxyType
|
||||||
config: Record<string, any>
|
tcp?: Record<string, any>
|
||||||
|
udp?: Record<string, any>
|
||||||
|
http?: Record<string, any>
|
||||||
|
https?: Record<string, any>
|
||||||
|
tcpmux?: Record<string, any>
|
||||||
|
stcp?: Record<string, any>
|
||||||
|
sudp?: Record<string, any>
|
||||||
|
xtcp?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreVisitorConfig {
|
export interface VisitorDefinition {
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: VisitorType
|
||||||
config: Record<string, any>
|
stcp?: Record<string, any>
|
||||||
|
sudp?: Record<string, any>
|
||||||
|
xtcp?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreProxyListResp {
|
export interface ProxyListResp {
|
||||||
proxies: StoreProxyConfig[]
|
proxies: ProxyDefinition[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreVisitorListResp {
|
export interface VisitorListResp {
|
||||||
visitors: StoreVisitorConfig[]
|
visitors: VisitorDefinition[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -255,29 +264,24 @@ export function createDefaultVisitorForm(): VisitorFormData {
|
|||||||
// CONVERTERS: Form -> Store API
|
// CONVERTERS: Form -> Store API
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
export function formToStoreProxy(form: ProxyFormData): Record<string, any> {
|
export function formToStoreProxy(form: ProxyFormData): ProxyDefinition {
|
||||||
const config: Record<string, any> = {
|
const block: Record<string, any> = {}
|
||||||
name: form.name,
|
|
||||||
type: form.type,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabled (nil/true = enabled, false = disabled)
|
// Enabled (nil/true = enabled, false = disabled)
|
||||||
if (!form.enabled) {
|
if (!form.enabled) {
|
||||||
config.enabled = false
|
block.enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend - LocalIP/LocalPort
|
// Backend - LocalIP/LocalPort
|
||||||
if (form.pluginType === '') {
|
if (form.pluginType === '') {
|
||||||
// No plugin, use local backend
|
|
||||||
if (form.localIP && form.localIP !== '127.0.0.1') {
|
if (form.localIP && form.localIP !== '127.0.0.1') {
|
||||||
config.localIP = form.localIP
|
block.localIP = form.localIP
|
||||||
}
|
}
|
||||||
if (form.localPort != null) {
|
if (form.localPort != null) {
|
||||||
config.localPort = form.localPort
|
block.localPort = form.localPort
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Plugin backend
|
block.plugin = {
|
||||||
config.plugin = {
|
|
||||||
type: form.pluginType,
|
type: form.pluginType,
|
||||||
...form.pluginConfig,
|
...form.pluginConfig,
|
||||||
}
|
}
|
||||||
@@ -291,109 +295,102 @@ export function formToStoreProxy(form: ProxyFormData): Record<string, any> {
|
|||||||
(form.bandwidthLimitMode && form.bandwidthLimitMode !== 'client') ||
|
(form.bandwidthLimitMode && form.bandwidthLimitMode !== 'client') ||
|
||||||
form.proxyProtocolVersion
|
form.proxyProtocolVersion
|
||||||
) {
|
) {
|
||||||
config.transport = {}
|
block.transport = {}
|
||||||
if (form.useEncryption) config.transport.useEncryption = true
|
if (form.useEncryption) block.transport.useEncryption = true
|
||||||
if (form.useCompression) config.transport.useCompression = true
|
if (form.useCompression) block.transport.useCompression = true
|
||||||
if (form.bandwidthLimit)
|
if (form.bandwidthLimit) block.transport.bandwidthLimit = form.bandwidthLimit
|
||||||
config.transport.bandwidthLimit = form.bandwidthLimit
|
|
||||||
if (form.bandwidthLimitMode && form.bandwidthLimitMode !== 'client') {
|
if (form.bandwidthLimitMode && form.bandwidthLimitMode !== 'client') {
|
||||||
config.transport.bandwidthLimitMode = form.bandwidthLimitMode
|
block.transport.bandwidthLimitMode = form.bandwidthLimitMode
|
||||||
}
|
}
|
||||||
if (form.proxyProtocolVersion) {
|
if (form.proxyProtocolVersion) {
|
||||||
config.transport.proxyProtocolVersion = form.proxyProtocolVersion
|
block.transport.proxyProtocolVersion = form.proxyProtocolVersion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Balancer
|
// Load Balancer
|
||||||
if (form.loadBalancerGroup) {
|
if (form.loadBalancerGroup) {
|
||||||
config.loadBalancer = {
|
block.loadBalancer = {
|
||||||
group: form.loadBalancerGroup,
|
group: form.loadBalancerGroup,
|
||||||
}
|
}
|
||||||
if (form.loadBalancerGroupKey) {
|
if (form.loadBalancerGroupKey) {
|
||||||
config.loadBalancer.groupKey = form.loadBalancerGroupKey
|
block.loadBalancer.groupKey = form.loadBalancerGroupKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health Check
|
// Health Check
|
||||||
if (form.healthCheckType) {
|
if (form.healthCheckType) {
|
||||||
config.healthCheck = {
|
block.healthCheck = {
|
||||||
type: form.healthCheckType,
|
type: form.healthCheckType,
|
||||||
}
|
}
|
||||||
if (form.healthCheckTimeoutSeconds != null) {
|
if (form.healthCheckTimeoutSeconds != null) {
|
||||||
config.healthCheck.timeoutSeconds = form.healthCheckTimeoutSeconds
|
block.healthCheck.timeoutSeconds = form.healthCheckTimeoutSeconds
|
||||||
}
|
}
|
||||||
if (form.healthCheckMaxFailed != null) {
|
if (form.healthCheckMaxFailed != null) {
|
||||||
config.healthCheck.maxFailed = form.healthCheckMaxFailed
|
block.healthCheck.maxFailed = form.healthCheckMaxFailed
|
||||||
}
|
}
|
||||||
if (form.healthCheckIntervalSeconds != null) {
|
if (form.healthCheckIntervalSeconds != null) {
|
||||||
config.healthCheck.intervalSeconds = form.healthCheckIntervalSeconds
|
block.healthCheck.intervalSeconds = form.healthCheckIntervalSeconds
|
||||||
}
|
}
|
||||||
if (form.healthCheckPath) {
|
if (form.healthCheckPath) {
|
||||||
config.healthCheck.path = form.healthCheckPath
|
block.healthCheck.path = form.healthCheckPath
|
||||||
}
|
}
|
||||||
if (form.healthCheckHTTPHeaders.length > 0) {
|
if (form.healthCheckHTTPHeaders.length > 0) {
|
||||||
config.healthCheck.httpHeaders = form.healthCheckHTTPHeaders
|
block.healthCheck.httpHeaders = form.healthCheckHTTPHeaders
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
if (form.metadatas.length > 0) {
|
if (form.metadatas.length > 0) {
|
||||||
config.metadatas = Object.fromEntries(
|
block.metadatas = Object.fromEntries(
|
||||||
form.metadatas.map((m) => [m.key, m.value]),
|
form.metadatas.map((m) => [m.key, m.value]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotations
|
// Annotations
|
||||||
if (form.annotations.length > 0) {
|
if (form.annotations.length > 0) {
|
||||||
config.annotations = Object.fromEntries(
|
block.annotations = Object.fromEntries(
|
||||||
form.annotations.map((a) => [a.key, a.value]),
|
form.annotations.map((a) => [a.key, a.value]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type-specific fields
|
// Type-specific fields
|
||||||
if (form.type === 'tcp' || form.type === 'udp') {
|
if ((form.type === 'tcp' || form.type === 'udp') && form.remotePort != null) {
|
||||||
if (form.remotePort != null) {
|
block.remotePort = form.remotePort
|
||||||
config.remotePort = form.remotePort
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.type === 'http' || form.type === 'https' || form.type === 'tcpmux') {
|
if (form.type === 'http' || form.type === 'https' || form.type === 'tcpmux') {
|
||||||
// Domain config
|
|
||||||
if (form.customDomains) {
|
if (form.customDomains) {
|
||||||
config.customDomains = form.customDomains
|
block.customDomains = form.customDomains
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
if (form.subdomain) {
|
if (form.subdomain) {
|
||||||
config.subdomain = form.subdomain
|
block.subdomain = form.subdomain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.type === 'http') {
|
if (form.type === 'http') {
|
||||||
// HTTP specific
|
|
||||||
if (form.locations) {
|
if (form.locations) {
|
||||||
config.locations = form.locations
|
block.locations = form.locations
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
if (form.httpUser) config.httpUser = form.httpUser
|
if (form.httpUser) block.httpUser = form.httpUser
|
||||||
if (form.httpPassword) config.httpPassword = form.httpPassword
|
if (form.httpPassword) block.httpPassword = form.httpPassword
|
||||||
if (form.hostHeaderRewrite)
|
if (form.hostHeaderRewrite) block.hostHeaderRewrite = form.hostHeaderRewrite
|
||||||
config.hostHeaderRewrite = form.hostHeaderRewrite
|
if (form.routeByHTTPUser) block.routeByHTTPUser = form.routeByHTTPUser
|
||||||
if (form.routeByHTTPUser) config.routeByHTTPUser = form.routeByHTTPUser
|
|
||||||
|
|
||||||
// Header operations
|
|
||||||
if (form.requestHeaders.length > 0) {
|
if (form.requestHeaders.length > 0) {
|
||||||
config.requestHeaders = {
|
block.requestHeaders = {
|
||||||
set: Object.fromEntries(
|
set: Object.fromEntries(
|
||||||
form.requestHeaders.map((h) => [h.key, h.value]),
|
form.requestHeaders.map((h) => [h.key, h.value]),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (form.responseHeaders.length > 0) {
|
if (form.responseHeaders.length > 0) {
|
||||||
config.responseHeaders = {
|
block.responseHeaders = {
|
||||||
set: Object.fromEntries(
|
set: Object.fromEntries(
|
||||||
form.responseHeaders.map((h) => [h.key, h.value]),
|
form.responseHeaders.map((h) => [h.key, h.value]),
|
||||||
),
|
),
|
||||||
@@ -402,107 +399,194 @@ export function formToStoreProxy(form: ProxyFormData): Record<string, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (form.type === 'tcpmux') {
|
if (form.type === 'tcpmux') {
|
||||||
// TCPMux specific
|
if (form.httpUser) block.httpUser = form.httpUser
|
||||||
if (form.httpUser) config.httpUser = form.httpUser
|
if (form.httpPassword) block.httpPassword = form.httpPassword
|
||||||
if (form.httpPassword) config.httpPassword = form.httpPassword
|
if (form.routeByHTTPUser) block.routeByHTTPUser = form.routeByHTTPUser
|
||||||
if (form.routeByHTTPUser) config.routeByHTTPUser = form.routeByHTTPUser
|
|
||||||
if (form.multiplexer && form.multiplexer !== 'httpconnect') {
|
if (form.multiplexer && form.multiplexer !== 'httpconnect') {
|
||||||
config.multiplexer = form.multiplexer
|
block.multiplexer = form.multiplexer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.type === 'stcp' || form.type === 'sudp' || form.type === 'xtcp') {
|
if (form.type === 'stcp' || form.type === 'sudp' || form.type === 'xtcp') {
|
||||||
// Secure proxy types
|
if (form.secretKey) block.secretKey = form.secretKey
|
||||||
if (form.secretKey) config.secretKey = form.secretKey
|
|
||||||
if (form.allowUsers) {
|
if (form.allowUsers) {
|
||||||
config.allowUsers = form.allowUsers
|
block.allowUsers = form.allowUsers
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.type === 'xtcp') {
|
if (form.type === 'xtcp' && form.natTraversalDisableAssistedAddrs) {
|
||||||
// XTCP NAT traversal
|
block.natTraversal = {
|
||||||
if (form.natTraversalDisableAssistedAddrs) {
|
disableAssistedAddrs: true,
|
||||||
config.natTraversal = {
|
|
||||||
disableAssistedAddrs: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return withStoreProxyBlock(
|
||||||
|
{
|
||||||
|
name: form.name,
|
||||||
|
type: form.type,
|
||||||
|
},
|
||||||
|
form.type,
|
||||||
|
block,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formToStoreVisitor(form: VisitorFormData): Record<string, any> {
|
export function formToStoreVisitor(form: VisitorFormData): VisitorDefinition {
|
||||||
const config: Record<string, any> = {
|
const block: Record<string, any> = {}
|
||||||
name: form.name,
|
|
||||||
type: form.type,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabled
|
|
||||||
if (!form.enabled) {
|
if (!form.enabled) {
|
||||||
config.enabled = false
|
block.enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transport
|
|
||||||
if (form.useEncryption || form.useCompression) {
|
if (form.useEncryption || form.useCompression) {
|
||||||
config.transport = {}
|
block.transport = {}
|
||||||
if (form.useEncryption) config.transport.useEncryption = true
|
if (form.useEncryption) block.transport.useEncryption = true
|
||||||
if (form.useCompression) config.transport.useCompression = true
|
if (form.useCompression) block.transport.useCompression = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base fields
|
if (form.secretKey) block.secretKey = form.secretKey
|
||||||
if (form.secretKey) config.secretKey = form.secretKey
|
if (form.serverUser) block.serverUser = form.serverUser
|
||||||
if (form.serverUser) config.serverUser = form.serverUser
|
if (form.serverName) block.serverName = form.serverName
|
||||||
if (form.serverName) config.serverName = form.serverName
|
|
||||||
if (form.bindAddr && form.bindAddr !== '127.0.0.1') {
|
if (form.bindAddr && form.bindAddr !== '127.0.0.1') {
|
||||||
config.bindAddr = form.bindAddr
|
block.bindAddr = form.bindAddr
|
||||||
}
|
}
|
||||||
if (form.bindPort != null) {
|
if (form.bindPort != null) {
|
||||||
config.bindPort = form.bindPort
|
block.bindPort = form.bindPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// XTCP specific
|
|
||||||
if (form.type === 'xtcp') {
|
if (form.type === 'xtcp') {
|
||||||
if (form.protocol && form.protocol !== 'quic') {
|
if (form.protocol && form.protocol !== 'quic') {
|
||||||
config.protocol = form.protocol
|
block.protocol = form.protocol
|
||||||
}
|
}
|
||||||
if (form.keepTunnelOpen) {
|
if (form.keepTunnelOpen) {
|
||||||
config.keepTunnelOpen = true
|
block.keepTunnelOpen = true
|
||||||
}
|
}
|
||||||
if (form.maxRetriesAnHour != null) {
|
if (form.maxRetriesAnHour != null) {
|
||||||
config.maxRetriesAnHour = form.maxRetriesAnHour
|
block.maxRetriesAnHour = form.maxRetriesAnHour
|
||||||
}
|
}
|
||||||
if (form.minRetryInterval != null) {
|
if (form.minRetryInterval != null) {
|
||||||
config.minRetryInterval = form.minRetryInterval
|
block.minRetryInterval = form.minRetryInterval
|
||||||
}
|
}
|
||||||
if (form.fallbackTo) {
|
if (form.fallbackTo) {
|
||||||
config.fallbackTo = form.fallbackTo
|
block.fallbackTo = form.fallbackTo
|
||||||
}
|
}
|
||||||
if (form.fallbackTimeoutMs != null) {
|
if (form.fallbackTimeoutMs != null) {
|
||||||
config.fallbackTimeoutMs = form.fallbackTimeoutMs
|
block.fallbackTimeoutMs = form.fallbackTimeoutMs
|
||||||
}
|
}
|
||||||
if (form.natTraversalDisableAssistedAddrs) {
|
if (form.natTraversalDisableAssistedAddrs) {
|
||||||
config.natTraversal = {
|
block.natTraversal = {
|
||||||
disableAssistedAddrs: true,
|
disableAssistedAddrs: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return withStoreVisitorBlock(
|
||||||
|
{
|
||||||
|
name: form.name,
|
||||||
|
type: form.type,
|
||||||
|
},
|
||||||
|
form.type,
|
||||||
|
block,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// CONVERTERS: Store API -> Form
|
// CONVERTERS: Store API -> Form
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
export function storeProxyToForm(config: StoreProxyConfig): ProxyFormData {
|
function getStoreProxyBlock(config: ProxyDefinition): Record<string, any> {
|
||||||
const c = config.config || {}
|
switch (config.type) {
|
||||||
|
case 'tcp':
|
||||||
|
return config.tcp || {}
|
||||||
|
case 'udp':
|
||||||
|
return config.udp || {}
|
||||||
|
case 'http':
|
||||||
|
return config.http || {}
|
||||||
|
case 'https':
|
||||||
|
return config.https || {}
|
||||||
|
case 'tcpmux':
|
||||||
|
return config.tcpmux || {}
|
||||||
|
case 'stcp':
|
||||||
|
return config.stcp || {}
|
||||||
|
case 'sudp':
|
||||||
|
return config.sudp || {}
|
||||||
|
case 'xtcp':
|
||||||
|
return config.xtcp || {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function withStoreProxyBlock(
|
||||||
|
payload: ProxyDefinition,
|
||||||
|
type: ProxyType,
|
||||||
|
block: Record<string, any>,
|
||||||
|
): ProxyDefinition {
|
||||||
|
switch (type) {
|
||||||
|
case 'tcp':
|
||||||
|
payload.tcp = block
|
||||||
|
break
|
||||||
|
case 'udp':
|
||||||
|
payload.udp = block
|
||||||
|
break
|
||||||
|
case 'http':
|
||||||
|
payload.http = block
|
||||||
|
break
|
||||||
|
case 'https':
|
||||||
|
payload.https = block
|
||||||
|
break
|
||||||
|
case 'tcpmux':
|
||||||
|
payload.tcpmux = block
|
||||||
|
break
|
||||||
|
case 'stcp':
|
||||||
|
payload.stcp = block
|
||||||
|
break
|
||||||
|
case 'sudp':
|
||||||
|
payload.sudp = block
|
||||||
|
break
|
||||||
|
case 'xtcp':
|
||||||
|
payload.xtcp = block
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoreVisitorBlock(config: VisitorDefinition): Record<string, any> {
|
||||||
|
switch (config.type) {
|
||||||
|
case 'stcp':
|
||||||
|
return config.stcp || {}
|
||||||
|
case 'sudp':
|
||||||
|
return config.sudp || {}
|
||||||
|
case 'xtcp':
|
||||||
|
return config.xtcp || {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function withStoreVisitorBlock(
|
||||||
|
payload: VisitorDefinition,
|
||||||
|
type: VisitorType,
|
||||||
|
block: Record<string, any>,
|
||||||
|
): VisitorDefinition {
|
||||||
|
switch (type) {
|
||||||
|
case 'stcp':
|
||||||
|
payload.stcp = block
|
||||||
|
break
|
||||||
|
case 'sudp':
|
||||||
|
payload.sudp = block
|
||||||
|
break
|
||||||
|
case 'xtcp':
|
||||||
|
payload.xtcp = block
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storeProxyToForm(config: ProxyDefinition): ProxyFormData {
|
||||||
|
const c = getStoreProxyBlock(config)
|
||||||
const form = createDefaultProxyForm()
|
const form = createDefaultProxyForm()
|
||||||
|
|
||||||
form.name = config.name || ''
|
form.name = config.name || ''
|
||||||
form.type = (config.type as ProxyType) || 'tcp'
|
form.type = config.type || 'tcp'
|
||||||
form.enabled = c.enabled !== false
|
form.enabled = c.enabled !== false
|
||||||
|
|
||||||
// Backend
|
// Backend
|
||||||
@@ -608,13 +692,13 @@ export function storeProxyToForm(config: StoreProxyConfig): ProxyFormData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function storeVisitorToForm(
|
export function storeVisitorToForm(
|
||||||
config: StoreVisitorConfig,
|
config: VisitorDefinition,
|
||||||
): VisitorFormData {
|
): VisitorFormData {
|
||||||
const c = config.config || {}
|
const c = getStoreVisitorBlock(config)
|
||||||
const form = createDefaultVisitorForm()
|
const form = createDefaultVisitorForm()
|
||||||
|
|
||||||
form.name = config.name || ''
|
form.name = config.name || ''
|
||||||
form.type = (config.type as VisitorType) || 'stcp'
|
form.type = config.type || 'stcp'
|
||||||
form.enabled = c.enabled !== false
|
form.enabled = c.enabled !== false
|
||||||
|
|
||||||
// Transport
|
// Transport
|
||||||
|
|||||||
@@ -337,17 +337,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="visitor-card-body">
|
<div class="visitor-card-body">
|
||||||
<span v-if="visitor.config?.serverName">
|
<span v-if="getVisitorBlock(visitor)?.serverName">
|
||||||
Server: {{ visitor.config.serverName }}
|
Server: {{ getVisitorBlock(visitor)?.serverName }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="
|
||||||
visitor.config?.bindAddr || visitor.config?.bindPort != null
|
getVisitorBlock(visitor)?.bindAddr ||
|
||||||
|
getVisitorBlock(visitor)?.bindPort != null
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Bind: {{ visitor.config.bindAddr || '127.0.0.1'
|
Bind: {{ getVisitorBlock(visitor)?.bindAddr || '127.0.0.1'
|
||||||
}}<template v-if="visitor.config?.bindPort != null"
|
}}<template v-if="getVisitorBlock(visitor)?.bindPort != null"
|
||||||
>:{{ visitor.config.bindPort }}</template
|
>:{{ getVisitorBlock(visitor)?.bindPort }}</template
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -388,8 +389,8 @@ import {
|
|||||||
} from '../api/frpc'
|
} from '../api/frpc'
|
||||||
import type {
|
import type {
|
||||||
ProxyStatus,
|
ProxyStatus,
|
||||||
StoreProxyConfig,
|
ProxyDefinition,
|
||||||
StoreVisitorConfig,
|
VisitorDefinition,
|
||||||
} from '../types/proxy'
|
} from '../types/proxy'
|
||||||
import StatCard from '../components/StatCard.vue'
|
import StatCard from '../components/StatCard.vue'
|
||||||
import ProxyCard from '../components/ProxyCard.vue'
|
import ProxyCard from '../components/ProxyCard.vue'
|
||||||
@@ -398,8 +399,8 @@ const router = useRouter()
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
const status = ref<ProxyStatus[]>([])
|
const status = ref<ProxyStatus[]>([])
|
||||||
const storeProxies = ref<StoreProxyConfig[]>([])
|
const storeProxies = ref<ProxyDefinition[]>([])
|
||||||
const storeVisitors = ref<StoreVisitorConfig[]>([])
|
const storeVisitors = ref<VisitorDefinition[]>([])
|
||||||
const storeEnabled = ref(false)
|
const storeEnabled = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
@@ -463,9 +464,41 @@ const filteredStatus = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const disabledStoreProxies = computed(() => {
|
const disabledStoreProxies = computed(() => {
|
||||||
return storeProxies.value.filter((p) => p.config?.enabled === false)
|
return storeProxies.value.filter((p) => getProxyBlock(p)?.enabled === false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getProxyBlock = (proxy: ProxyDefinition) => {
|
||||||
|
switch (proxy.type) {
|
||||||
|
case 'tcp':
|
||||||
|
return proxy.tcp
|
||||||
|
case 'udp':
|
||||||
|
return proxy.udp
|
||||||
|
case 'http':
|
||||||
|
return proxy.http
|
||||||
|
case 'https':
|
||||||
|
return proxy.https
|
||||||
|
case 'tcpmux':
|
||||||
|
return proxy.tcpmux
|
||||||
|
case 'stcp':
|
||||||
|
return proxy.stcp
|
||||||
|
case 'sudp':
|
||||||
|
return proxy.sudp
|
||||||
|
case 'xtcp':
|
||||||
|
return proxy.xtcp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVisitorBlock = (visitor: VisitorDefinition) => {
|
||||||
|
switch (visitor.type) {
|
||||||
|
case 'stcp':
|
||||||
|
return visitor.stcp
|
||||||
|
case 'sudp':
|
||||||
|
return visitor.sudp
|
||||||
|
case 'xtcp':
|
||||||
|
return visitor.xtcp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const toggleTypeFilter = (type: string) => {
|
const toggleTypeFilter = (type: string) => {
|
||||||
filterType.value = filterType.value === type ? '' : type
|
filterType.value = filterType.value === type ? '' : type
|
||||||
@@ -563,11 +596,11 @@ const handleDelete = async (proxy: ProxyStatus) => {
|
|||||||
confirmAndDeleteProxy(proxy.name)
|
confirmAndDeleteProxy(proxy.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditStoreProxy = (proxy: StoreProxyConfig) => {
|
const handleEditStoreProxy = (proxy: ProxyDefinition) => {
|
||||||
router.push('/proxies/' + encodeURIComponent(proxy.name) + '/edit')
|
router.push('/proxies/' + encodeURIComponent(proxy.name) + '/edit')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteStoreProxy = async (proxy: StoreProxyConfig) => {
|
const handleDeleteStoreProxy = async (proxy: ProxyDefinition) => {
|
||||||
confirmAndDeleteProxy(proxy.name)
|
confirmAndDeleteProxy(proxy.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,7 +608,7 @@ const handleCreateVisitor = () => {
|
|||||||
router.push('/visitors/create')
|
router.push('/visitors/create')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditVisitor = (visitor: StoreVisitorConfig) => {
|
const handleEditVisitor = (visitor: VisitorDefinition) => {
|
||||||
router.push('/visitors/' + encodeURIComponent(visitor.name) + '/edit')
|
router.push('/visitors/' + encodeURIComponent(visitor.name) + '/edit')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user