mirror of
https://github.com/fatedier/frp.git
synced 2026-03-20 08:49:16 +08:00
refactor: restructure API packages into client/http and server/http with typed proxy/visitor models (#5193)
This commit is contained in:
@@ -16,7 +16,6 @@ package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -33,6 +32,7 @@ import (
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
"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
|
||||
jsonBytes, err := json.Marshal(temp)
|
||||
jsonBytes, err := jsonx.Marshal(temp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
||||
decoder.DisallowUnknownFields()
|
||||
return decoder.Decode(target)
|
||||
return decodeJSONContent(jsonBytes, target, true)
|
||||
}
|
||||
|
||||
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.
|
||||
// Now it supports json, yaml and toml format.
|
||||
func LoadConfigure(b []byte, c any, strict bool) error {
|
||||
return v1.WithDisallowUnknownFields(strict, func() error {
|
||||
var tomlObj any
|
||||
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
|
||||
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
||||
var err error
|
||||
b, err = json.Marshal(&tomlObj)
|
||||
if err != nil {
|
||||
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)
|
||||
var tomlObj any
|
||||
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
|
||||
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
||||
var err error
|
||||
b, err = jsonx.Marshal(&tomlObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 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
|
||||
if strict {
|
||||
// In strict mode, always use our custom handler to support YAML merge
|
||||
return parseYAMLWithDotFieldsHandling(b, c)
|
||||
}
|
||||
// Non-strict mode, parse normally
|
||||
return yaml.Unmarshal(b, c)
|
||||
})
|
||||
// Handle YAML content
|
||||
if strict {
|
||||
// In strict mode, always use our custom handler to support YAML merge
|
||||
return parseYAMLWithDotFieldsHandling(b, c)
|
||||
}
|
||||
// Non-strict mode, parse normally
|
||||
return yaml.Unmarshal(b, c)
|
||||
}
|
||||
|
||||
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
||||
|
||||
@@ -189,6 +189,31 @@ unixPath = "/tmp/uds.sock"
|
||||
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
|
||||
// even in strict mode by properly handling dot-prefixed fields
|
||||
func TestYAMLMergeInStrictMode(t *testing.T) {
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
)
|
||||
|
||||
type StoreSourceConfig struct {
|
||||
@@ -74,36 +74,44 @@ func (s *StoreSource) loadFromFileUnlocked() error {
|
||||
return err
|
||||
}
|
||||
|
||||
var stored storeData
|
||||
if err := v1.WithDisallowUnknownFields(false, func() error {
|
||||
return json.Unmarshal(data, &stored)
|
||||
}); err != nil {
|
||||
type rawStoreData struct {
|
||||
Proxies []jsonx.RawMessage `json:"proxies,omitempty"`
|
||||
Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
|
||||
}
|
||||
stored := rawStoreData{}
|
||||
if err := jsonx.Unmarshal(data, &stored); err != nil {
|
||||
return fmt.Errorf("failed to parse JSON: %w", err)
|
||||
}
|
||||
|
||||
s.proxies = make(map[string]v1.ProxyConfigurer)
|
||||
s.visitors = make(map[string]v1.VisitorConfigurer)
|
||||
|
||||
for _, tp := range stored.Proxies {
|
||||
if tp.ProxyConfigurer != nil {
|
||||
proxyCfg := tp.ProxyConfigurer
|
||||
name := proxyCfg.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
s.proxies[name] = proxyCfg
|
||||
for i, proxyData := range stored.Proxies {
|
||||
proxyCfg, err := v1.DecodeProxyConfigurerJSON(proxyData, v1.DecodeOptions{
|
||||
DisallowUnknownFields: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode proxy at index %d: %w", i, err)
|
||||
}
|
||||
name := proxyCfg.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
s.proxies[name] = proxyCfg
|
||||
}
|
||||
|
||||
for _, tv := range stored.Visitors {
|
||||
if tv.VisitorConfigurer != nil {
|
||||
visitorCfg := tv.VisitorConfigurer
|
||||
name := visitorCfg.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
s.visitors[name] = visitorCfg
|
||||
for i, visitorData := range stored.Visitors {
|
||||
visitorCfg, err := v1.DecodeVisitorConfigurerJSON(visitorData, v1.DecodeOptions{
|
||||
DisallowUnknownFields: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode visitor at index %d: %w", i, err)
|
||||
}
|
||||
name := visitorCfg.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
s.visitors[name] = visitorCfg
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -122,7 +130,7 @@ func (s *StoreSource) saveToFileUnlocked() error {
|
||||
stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(stored, "", " ")
|
||||
data, err := jsonx.MarshalIndent(stored, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -23,27 +22,9 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
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) {
|
||||
require := require.New(t)
|
||||
|
||||
@@ -99,7 +80,7 @@ func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}},
|
||||
Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}},
|
||||
}
|
||||
data, err := json.Marshal(stored)
|
||||
data, err := jsonx.Marshal(stored)
|
||||
require.NoError(err)
|
||||
err = os.WriteFile(path, data, 0o600)
|
||||
require.NoError(err)
|
||||
@@ -117,12 +98,9 @@ func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||
}
|
||||
|
||||
func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) {
|
||||
func TestStoreSource_LoadFromFile_UnknownFieldsAreIgnored(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
restore := setDisallowUnknownFieldsForStoreTest(t, true)
|
||||
t.Cleanup(restore)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "store.json")
|
||||
raw := []byte(`{
|
||||
"proxies": [
|
||||
@@ -140,5 +118,4 @@ func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t
|
||||
|
||||
require.NotNil(storeSource.GetProxy("proxy1"))
|
||||
require.NotNil(storeSource.GetVisitor("visitor1"))
|
||||
require.True(getDisallowUnknownFieldsForStoreTest())
|
||||
}
|
||||
|
||||
@@ -16,35 +16,10 @@ package v1
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"sync"
|
||||
|
||||
"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
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
)
|
||||
|
||||
@@ -202,35 +199,18 @@ type TypedProxyConfig struct {
|
||||
}
|
||||
|
||||
func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 4 && string(b) == "null" {
|
||||
return errors.New("type is required")
|
||||
}
|
||||
|
||||
typeStruct := struct {
|
||||
Type string `json:"type"`
|
||||
}{}
|
||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
||||
configurer, err := DecodeProxyConfigurerJSON(b, DecodeOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type = typeStruct.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.Type = configurer.GetBaseConfig().Type
|
||||
c.ProxyConfigurer = configurer
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(c.ProxyConfigurer)
|
||||
return jsonx.Marshal(c.ProxyConfigurer)
|
||||
}
|
||||
|
||||
type ProxyConfigurer interface {
|
||||
|
||||
@@ -15,14 +15,11 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
)
|
||||
|
||||
@@ -71,42 +68,16 @@ func (c TypedClientPluginOptions) Clone() TypedClientPluginOptions {
|
||||
}
|
||||
|
||||
func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 4 && string(b) == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
typeStruct := struct {
|
||||
Type string `json:"type"`
|
||||
}{}
|
||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
||||
decoded, err := DecodeClientPluginOptionsJSON(b, DecodeOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
*c = decoded
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(c.ClientPluginOptions)
|
||||
return jsonx.Marshal(c.ClientPluginOptions)
|
||||
}
|
||||
|
||||
type HTTP2HTTPSPluginOptions struct {
|
||||
|
||||
@@ -15,12 +15,9 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
)
|
||||
|
||||
@@ -93,35 +90,18 @@ type TypedVisitorConfig struct {
|
||||
}
|
||||
|
||||
func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 4 && string(b) == "null" {
|
||||
return errors.New("type is required")
|
||||
}
|
||||
|
||||
typeStruct := struct {
|
||||
Type string `json:"type"`
|
||||
}{}
|
||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
||||
configurer, err := DecodeVisitorConfigurerJSON(b, DecodeOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type = typeStruct.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.Type = configurer.GetBaseConfig().Type
|
||||
c.VisitorConfigurer = configurer
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(c.VisitorConfigurer)
|
||||
return jsonx.Marshal(c.VisitorConfigurer)
|
||||
}
|
||||
|
||||
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
||||
|
||||
@@ -15,11 +15,9 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -49,42 +47,16 @@ func (c TypedVisitorPluginOptions) Clone() TypedVisitorPluginOptions {
|
||||
}
|
||||
|
||||
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 4 && string(b) == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
typeStruct := struct {
|
||||
Type string `json:"type"`
|
||||
}{}
|
||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
||||
decoded, err := DecodeVisitorPluginOptionsJSON(b, DecodeOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
*c = decoded
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(c.VisitorPluginOptions)
|
||||
return jsonx.Marshal(c.VisitorPluginOptions)
|
||||
}
|
||||
|
||||
type VirtualNetVisitorPluginOptions struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatedier/frp/client/api"
|
||||
"github.com/fatedier/frp/client/http/model"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ func (c *Client) SetAuth(user, pwd string) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -41,7 +41,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allStatus := make(api.StatusResp)
|
||||
allStatus := make(model.StatusResp)
|
||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -64,7 +64,7 @@ func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allStatus := make(api.StatusResp)
|
||||
allStatus := make(model.StatusResp)
|
||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user