simplify configuration parsing (#5372)

work around golang/go#21092 globally
This commit is contained in:
Alessandro Ros
2026-01-24 19:33:47 +01:00
committed by GitHub
parent 0d11156c6e
commit f4f795a2a6
9 changed files with 201 additions and 116 deletions
-4
View File
@@ -1,4 +0,0 @@
package conf
// AllowedOrigins is a list of allowed CORS origins.
type AllowedOrigins []string
+2 -18
View File
@@ -39,24 +39,8 @@ func (d *AuthInternalUser) UnmarshalJSON(b []byte) error {
return nil
}
// AuthInternalUsers is a list of AuthInternalUser
// AuthInternalUsers is a list of AuthInternalUser.
type AuthInternalUsers []AuthInternalUser
// UnmarshalJSON implements json.Unmarshaler.
func (s *AuthInternalUsers) UnmarshalJSON(b []byte) error {
// remove default value before loading new value
// https://github.com/golang/go/issues/21092
*s = nil
return jsonwrapper.Unmarshal(b, (*[]AuthInternalUser)(s))
}
// AuthInternalUserPermissions is a list of AuthInternalUserPermission
// AuthInternalUserPermissions is a list of AuthInternalUserPermission.
type AuthInternalUserPermissions []AuthInternalUserPermission
// UnmarshalJSON implements json.Unmarshaler.
func (s *AuthInternalUserPermissions) UnmarshalJSON(b []byte) error {
// remove default value before loading new value
// https://github.com/golang/go/issues/21092
*s = nil
return jsonwrapper.Unmarshal(b, (*[]AuthInternalUserPermission)(s))
}
+50 -51
View File
@@ -147,7 +147,6 @@ var defaultAuthInternalUsers = AuthInternalUsers{
}
// Conf is a configuration.
// WARNING: Avoid using slices directly due to https://github.com/golang/go/issues/21092
type Conf struct {
// General
LogLevel LogLevel `json:"logLevel"`
@@ -178,44 +177,44 @@ type Conf struct {
AuthJWTInHTTPQuery bool `json:"authJWTInHTTPQuery"`
// Control API
API bool `json:"api"`
APIAddress string `json:"apiAddress"`
APIEncryption bool `json:"apiEncryption"`
APIServerKey string `json:"apiServerKey"`
APIServerCert string `json:"apiServerCert"`
APIAllowOrigin *string `json:"apiAllowOrigin,omitempty"` // deprecated
APIAllowOrigins AllowedOrigins `json:"apiAllowOrigins"`
APITrustedProxies IPNetworks `json:"apiTrustedProxies"`
API bool `json:"api"`
APIAddress string `json:"apiAddress"`
APIEncryption bool `json:"apiEncryption"`
APIServerKey string `json:"apiServerKey"`
APIServerCert string `json:"apiServerCert"`
APIAllowOrigin *string `json:"apiAllowOrigin,omitempty"` // deprecated
APIAllowOrigins []string `json:"apiAllowOrigins"`
APITrustedProxies IPNetworks `json:"apiTrustedProxies"`
// Metrics
Metrics bool `json:"metrics"`
MetricsAddress string `json:"metricsAddress"`
MetricsEncryption bool `json:"metricsEncryption"`
MetricsServerKey string `json:"metricsServerKey"`
MetricsServerCert string `json:"metricsServerCert"`
MetricsAllowOrigin *string `json:"metricsAllowOrigin,omitempty"` // deprecated
MetricsAllowOrigins AllowedOrigins `json:"metricsAllowOrigins"`
MetricsTrustedProxies IPNetworks `json:"metricsTrustedProxies"`
Metrics bool `json:"metrics"`
MetricsAddress string `json:"metricsAddress"`
MetricsEncryption bool `json:"metricsEncryption"`
MetricsServerKey string `json:"metricsServerKey"`
MetricsServerCert string `json:"metricsServerCert"`
MetricsAllowOrigin *string `json:"metricsAllowOrigin,omitempty"` // deprecated
MetricsAllowOrigins []string `json:"metricsAllowOrigins"`
MetricsTrustedProxies IPNetworks `json:"metricsTrustedProxies"`
// PPROF
PPROF bool `json:"pprof"`
PPROFAddress string `json:"pprofAddress"`
PPROFEncryption bool `json:"pprofEncryption"`
PPROFServerKey string `json:"pprofServerKey"`
PPROFServerCert string `json:"pprofServerCert"`
PPROFAllowOrigin *string `json:"pprofAllowOrigin,omitempty"` // deprecated
PPROFAllowOrigins AllowedOrigins `json:"pprofAllowOrigins"`
PPROFTrustedProxies IPNetworks `json:"pprofTrustedProxies"`
PPROF bool `json:"pprof"`
PPROFAddress string `json:"pprofAddress"`
PPROFEncryption bool `json:"pprofEncryption"`
PPROFServerKey string `json:"pprofServerKey"`
PPROFServerCert string `json:"pprofServerCert"`
PPROFAllowOrigin *string `json:"pprofAllowOrigin,omitempty"` // deprecated
PPROFAllowOrigins []string `json:"pprofAllowOrigins"`
PPROFTrustedProxies IPNetworks `json:"pprofTrustedProxies"`
// Playback
Playback bool `json:"playback"`
PlaybackAddress string `json:"playbackAddress"`
PlaybackEncryption bool `json:"playbackEncryption"`
PlaybackServerKey string `json:"playbackServerKey"`
PlaybackServerCert string `json:"playbackServerCert"`
PlaybackAllowOrigin *string `json:"playbackAllowOrigin,omitempty"` // deprecated
PlaybackAllowOrigins AllowedOrigins `json:"playbackAllowOrigins"`
PlaybackTrustedProxies IPNetworks `json:"playbackTrustedProxies"`
Playback bool `json:"playback"`
PlaybackAddress string `json:"playbackAddress"`
PlaybackEncryption bool `json:"playbackEncryption"`
PlaybackServerKey string `json:"playbackServerKey"`
PlaybackServerCert string `json:"playbackServerCert"`
PlaybackAllowOrigin *string `json:"playbackAllowOrigin,omitempty"` // deprecated
PlaybackAllowOrigins []string `json:"playbackAllowOrigins"`
PlaybackTrustedProxies IPNetworks `json:"playbackTrustedProxies"`
// RTSP server
RTSP bool `json:"rtsp"`
@@ -253,23 +252,23 @@ type Conf struct {
RTMPServerCert string `json:"rtmpServerCert"`
// HLS server
HLS bool `json:"hls"`
HLSDisable *bool `json:"hlsDisable,omitempty"` // deprecated
HLSAddress string `json:"hlsAddress"`
HLSEncryption bool `json:"hlsEncryption"`
HLSServerKey string `json:"hlsServerKey"`
HLSServerCert string `json:"hlsServerCert"`
HLSAllowOrigin *string `json:"hlsAllowOrigin,omitempty"` // deprecated
HLSAllowOrigins AllowedOrigins `json:"hlsAllowOrigins"`
HLSTrustedProxies IPNetworks `json:"hlsTrustedProxies"`
HLSAlwaysRemux bool `json:"hlsAlwaysRemux"`
HLSVariant HLSVariant `json:"hlsVariant"`
HLSSegmentCount int `json:"hlsSegmentCount"`
HLSSegmentDuration Duration `json:"hlsSegmentDuration"`
HLSPartDuration Duration `json:"hlsPartDuration"`
HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"`
HLSDirectory string `json:"hlsDirectory"`
HLSMuxerCloseAfter Duration `json:"hlsMuxerCloseAfter"`
HLS bool `json:"hls"`
HLSDisable *bool `json:"hlsDisable,omitempty"` // deprecated
HLSAddress string `json:"hlsAddress"`
HLSEncryption bool `json:"hlsEncryption"`
HLSServerKey string `json:"hlsServerKey"`
HLSServerCert string `json:"hlsServerCert"`
HLSAllowOrigin *string `json:"hlsAllowOrigin,omitempty"` // deprecated
HLSAllowOrigins []string `json:"hlsAllowOrigins"`
HLSTrustedProxies IPNetworks `json:"hlsTrustedProxies"`
HLSAlwaysRemux bool `json:"hlsAlwaysRemux"`
HLSVariant HLSVariant `json:"hlsVariant"`
HLSSegmentCount int `json:"hlsSegmentCount"`
HLSSegmentDuration Duration `json:"hlsSegmentDuration"`
HLSPartDuration Duration `json:"hlsPartDuration"`
HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"`
HLSDirectory string `json:"hlsDirectory"`
HLSMuxerCloseAfter Duration `json:"hlsMuxerCloseAfter"`
// WebRTC server
WebRTC bool `json:"webrtc"`
@@ -279,7 +278,7 @@ type Conf struct {
WebRTCServerKey string `json:"webrtcServerKey"`
WebRTCServerCert string `json:"webrtcServerCert"`
WebRTCAllowOrigin *string `json:"webrtcAllowOrigin,omitempty"` // deprecated
WebRTCAllowOrigins AllowedOrigins `json:"webrtcAllowOrigins"`
WebRTCAllowOrigins []string `json:"webrtcAllowOrigins"`
WebRTCTrustedProxies IPNetworks `json:"webrtcTrustedProxies"`
WebRTCLocalUDPAddress string `json:"webrtcLocalUDPAddress"`
WebRTCLocalTCPAddress string `json:"webrtcLocalTCPAddress"`
-28
View File
@@ -770,31 +770,3 @@ func TestSampleConfFile(t *testing.T) {
require.Equal(t, conf1.Paths, conf2.Paths)
}()
}
// needed due to https://github.com/golang/go/issues/21092
func TestConfOverrideDefaultSlices(t *testing.T) {
tmpf, err := createTempFile([]byte(
"authInternalUsers:\n" +
" - user: user1\n" +
" - user: user2\n" +
"authHTTPExclude:\n" +
" - path: ''\n"))
require.NoError(t, err)
defer os.Remove(tmpf)
conf, _, err := Load(tmpf, nil, nil)
require.NoError(t, err)
require.Equal(t, AuthInternalUsers{
{
User: "user1",
},
{
User: "user2",
},
}, conf.AuthInternalUsers)
require.Equal(t, AuthInternalUserPermissions{
{},
}, conf.AuthHTTPExclude)
}
+70 -3
View File
@@ -5,18 +5,85 @@ import (
"bytes"
"encoding/json"
"io"
"reflect"
"strings"
)
// differences with respect to the standard package:
// - unknown fields cause an error
// - using existing elements of slices is prevented, fixing https://github.com/golang/go/issues/21092
// Unmarshal decodes JSON.
// It returns an error if a non-existing field is found.
func Unmarshal(buf []byte, dest any) error {
return Decode(bytes.NewReader(buf), dest)
}
// Decode decodes JSON.
// It returns an error if a non-existing field is found.
func Decode(r io.Reader, dest any) error {
d := json.NewDecoder(r)
buf, err := io.ReadAll(r)
if err != nil {
return err
}
var raw any
err = json.Unmarshal(buf, &raw)
if err != nil {
return err
}
nilExistingSlices(reflect.ValueOf(dest), raw)
d := json.NewDecoder(bytes.NewReader(buf))
d.DisallowUnknownFields()
return d.Decode(dest)
}
// nilExistingSlices recursively nils slices that are present in the JSON data.
func nilExistingSlices(v reflect.Value, jsonData any) {
if !v.IsValid() || jsonData == nil {
return
}
for v.Kind() == reflect.Pointer {
if v.IsNil() {
return
}
v = v.Elem()
}
switch v.Kind() {
case reflect.Slice:
if _, ok := jsonData.([]any); ok {
if !v.IsNil() {
v.Set(reflect.Zero(v.Type()))
}
return
}
case reflect.Struct:
jsonMap, ok := jsonData.(map[string]any)
if !ok {
return
}
vType := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := vType.Field(i)
jsonKey := fieldType.Tag.Get("json")
if jsonKey == "" || jsonKey == "-" {
continue
}
jsonKey = strings.Split(jsonKey, ",")[0]
if jsonValue, exists := jsonMap[jsonKey]; exists {
if field.Kind() == reflect.Slice && !field.IsNil() {
field.Set(reflect.Zero(field.Type()))
}
nilExistingSlices(field, jsonValue)
}
}
}
}
@@ -0,0 +1,74 @@
package jsonwrapper
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
type testStruct struct {
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
func TestUnmarshalDisallowUnknownFields(t *testing.T) {
input := strings.NewReader(`{"field1": "test", "unknownField": "value", "field2": 456}`)
var result testStruct
err := Decode(input, &result)
require.Error(t, err)
require.Contains(t, err.Error(), "unknown field")
}
func TestUnmarshalPreventSliceReuse(t *testing.T) {
t.Run("a", func(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
slice := []Person{
{Name: "John", Age: 30},
{Name: "Jane", Age: 25},
}
json := []byte(`[{"name": "Bob"}]`)
err := Unmarshal(json, &slice)
require.NoError(t, err)
require.Equal(t, []Person{{
Name: "Bob",
Age: 0,
}}, slice)
})
t.Run("b", func(t *testing.T) {
type Config struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Port int `json:"port"`
}
type Settings struct {
Configs []Config `json:"configs"`
}
settings := Settings{
Configs: []Config{
{Name: "old1", Enabled: true, Port: 8080},
{Name: "old2", Enabled: false, Port: 9090},
{Name: "old3", Enabled: true, Port: 7070},
},
}
json := []byte(`{"configs": [{"name": "new1"}, {"name": "new2"}]}`)
err := Unmarshal(json, &settings)
require.NoError(t, err)
require.Equal(t, Settings{
Configs: []Config{
{Name: "new1", Enabled: false, Port: 0},
{Name: "new2", Enabled: false, Port: 0},
},
}, settings)
})
}
-1
View File
@@ -106,7 +106,6 @@ func FindPathConf(pathConfs map[string]*Path, name string) (*Path, []string, err
}
// Path is a path configuration.
// WARNING: Avoid using slices directly due to https://github.com/golang/go/issues/21092
type Path struct {
Regexp *regexp.Regexp `json:"-"` // filled by Check()
Name string `json:"name"` // filled by Check()
+1 -11
View File
@@ -1,7 +1,5 @@
package conf
import "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
// WebRTCICEServer is a WebRTC ICE Server.
type WebRTCICEServer struct {
URL string `json:"url"`
@@ -10,13 +8,5 @@ type WebRTCICEServer struct {
ClientOnly bool `json:"clientOnly"`
}
// WebRTCICEServers is a list of WebRTCICEServer
// WebRTCICEServers is a list of WebRTCICEServer.
type WebRTCICEServers []WebRTCICEServer
// UnmarshalJSON implements json.Unmarshaler.
func (s *WebRTCICEServers) UnmarshalJSON(b []byte) error {
// remove default value before loading new value
// https://github.com/golang/go/issues/21092
*s = nil
return jsonwrapper.Unmarshal(b, (*[]WebRTCICEServer)(s))
}
+4
View File
@@ -8,6 +8,10 @@ import (
"gopkg.in/yaml.v2"
)
// differences with respect to the standard package:
// - unknown fields cause an error
// - integer map keys cause an error
func convertKeys(i any) (any, error) {
switch x := i.(type) {
case map[any]any: