Skip to content

Commit

Permalink
Avoid unknown fields on config
Browse files Browse the repository at this point in the history
This will reduce headaches when adding the wrong fields to the config.

Signed-off-by: Antonio Navarro Perez <[email protected]>
  • Loading branch information
ajnavarro committed Dec 2, 2022
1 parent c8a4b6a commit d476c5d
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 140 deletions.
3 changes: 1 addition & 2 deletions cmd/ipfs/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
utilmain "github.com/ipfs/kubo/cmd/ipfs/util"
oldcmds "github.com/ipfs/kubo/commands"
config "github.com/ipfs/kubo/config"
cserial "github.com/ipfs/kubo/config/serialize"
"github.com/ipfs/kubo/core"
commands "github.com/ipfs/kubo/core/commands"
"github.com/ipfs/kubo/core/coreapi"
Expand Down Expand Up @@ -252,7 +251,7 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment
var conf *config.Config

if cfgLocation != "" {
if conf, err = cserial.Load(cfgLocation); err != nil {
if conf, err = config.Load(cfgLocation); err != nil {
return err
}
}
Expand Down
103 changes: 86 additions & 17 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ package config
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/facebookgo/atomicfile"
"github.com/mitchellh/go-homedir"
)

// ErrNotInitialized is returned when we fail to read the config because the
// repo doesn't exist.
var ErrNotInitialized = errors.New("ipfs not initialized, please run 'ipfs init'")

// Config is used to load ipfs config files.
type Config struct {
Identity Identity // local node's peer identity
Expand Down Expand Up @@ -101,35 +108,36 @@ func HumanOutput(value interface{}) ([]byte, error) {
if ok {
return []byte(strings.Trim(s, "\n")), nil
}
return Marshal(value)
}

// Marshal configuration with JSON
func Marshal(value interface{}) ([]byte, error) {
// need to prettyprint, hence MarshalIndent, instead of Encoder
return json.MarshalIndent(value, "", " ")
buf := new(bytes.Buffer)
if err := encodeConfigFile(buf, value); err != nil {
return nil, err
}

return buf.Bytes(), nil
}

func FromMap(v map[string]interface{}) (*Config, error) {
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(v); err != nil {

if err := encodeConfigFile(buf, v); err != nil {
return nil, err
}
var conf Config
if err := json.NewDecoder(buf).Decode(&conf); err != nil {
return nil, fmt.Errorf("failure to decode config: %s", err)
if err := decodeConfigFile(buf, &conf); err != nil {
return nil, err
}
return &conf, nil
}

func ToMap(conf *Config) (map[string]interface{}, error) {
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(conf); err != nil {
if err := encodeConfigFile(buf, conf); err != nil {
return nil, err
}
var m map[string]interface{}
if err := json.NewDecoder(buf).Decode(&m); err != nil {
return nil, fmt.Errorf("failure to decode config: %s", err)
if err := decodeConfigFile(buf, &m); err != nil {
return nil, err
}
return m, nil
}
Expand All @@ -139,13 +147,74 @@ func (c *Config) Clone() (*Config, error) {
var newConfig Config
var buf bytes.Buffer

if err := json.NewEncoder(&buf).Encode(c); err != nil {
return nil, fmt.Errorf("failure to encode config: %s", err)
if err := encodeConfigFile(&buf, c); err != nil {
return nil, err
}

if err := json.NewDecoder(&buf).Decode(&newConfig); err != nil {
return nil, fmt.Errorf("failure to decode config: %s", err)
if err := decodeConfigFile(&buf, &newConfig); err != nil {
return nil, err
}

return &newConfig, nil
}

// ReadConfigFile reads the config from `filename` into `cfg`.
func ReadConfigFile(filename string, cfg interface{}) error {
f, err := os.Open(filename)
if err != nil {
if os.IsNotExist(err) {
err = ErrNotInitialized
}
return err
}
defer f.Close()

return decodeConfigFile(f, cfg)
}

func decodeConfigFile(r io.Reader, cfg interface{}) error {
dec := json.NewDecoder(r)
if os.Getenv("IPFS_CONFIG_TOLERANT_MODE") == "" {
dec.DisallowUnknownFields()
}

if err := dec.Decode(cfg); err != nil {
return fmt.Errorf("failure to decode config: %s", err)
}

return nil
}

// WriteConfigFile writes the config from `cfg` into `filename`.
func WriteConfigFile(filename string, cfg interface{}) error {
err := os.MkdirAll(filepath.Dir(filename), 0755)
if err != nil {
return err
}

f, err := atomicfile.New(filename, 0600)
if err != nil {
return err
}
defer f.Close()

return encodeConfigFile(f, cfg)
}

// encodeConfigFile encodes configuration with JSON
func encodeConfigFile(w io.Writer, value interface{}) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")

return enc.Encode(value)
}

// Load reads given file and returns the read config, or error.
func Load(filename string) (*Config, error) {
var cfg Config
err := ReadConfigFile(filename, &cfg)
if err != nil {
return nil, err
}

return &cfg, err
}
63 changes: 63 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config

import (
"os"
"runtime"
"testing"
)

Expand All @@ -27,3 +29,64 @@ func TestClone(t *testing.T) {
t.Fatal("HTTP headers not preserved")
}
}

func TestConfig(t *testing.T) {
const filename = ".ipfsconfig"
cfgWritten := new(Config)
cfgWritten.Identity.PeerID = "faketest"

err := WriteConfigFile(filename, cfgWritten)
if err != nil {
t.Fatal(err)
}
cfgRead, err := Load(filename)
if err != nil {
t.Fatal(err)
}
if cfgWritten.Identity.PeerID != cfgRead.Identity.PeerID {
t.Fatal()
}
st, err := os.Stat(filename)
if err != nil {
t.Fatalf("cannot stat config file: %v", err)
}

if runtime.GOOS != "windows" { // see https://golang.org/src/os/types_windows.go
if g := st.Mode().Perm(); g&0117 != 0 {
t.Fatalf("config file should not be executable or accessible to world: %v", g)
}
}
}

func TestConfigUnknownField(t *testing.T) {
const filename = ".ipfsconfig"

badConfig := map[string]string{
"BadField": "Value",
}

err := WriteConfigFile(filename, badConfig)
if err != nil {
t.Fatal(err)
}

_, err = Load(filename)
if err == nil {
t.Fatal("load must fail")
}

if err.Error() != "failure to decode config: json: unknown field \"BadField\"" {
t.Fatal("unexpected error:", err)
}

mapOut := make(map[string]string)

err = ReadConfigFile(filename, &mapOut)
if err != nil {
t.Fatal(err)
}

if mapOut["BadField"] != "Value" {
t.Fatal(err)
}
}
72 changes: 0 additions & 72 deletions config/serialize/serialize.go

This file was deleted.

37 changes: 0 additions & 37 deletions config/serialize/serialize_test.go

This file was deleted.

7 changes: 7 additions & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ $ ipfs resolve -r /ipns/dnslink-test2.example.com
/ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am
```

## `IPFS_CONFIG_TOLERANT_MODE`

Disables strict config valiadtion to allow unsupported fields on JSON config.

Default: false


## `LIBP2P_TCP_REUSEPORT`

Kubo tries to reuse the same source port for all connections to improve NAT
Expand Down
5 changes: 2 additions & 3 deletions plugin/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"strings"

config "github.com/ipfs/kubo/config"
cserialize "github.com/ipfs/kubo/config/serialize"
"github.com/ipld/go-ipld-prime/multicodec"

"github.com/ipfs/kubo/core"
Expand Down Expand Up @@ -97,9 +96,9 @@ type PluginLoader struct {
func NewPluginLoader(repo string) (*PluginLoader, error) {
loader := &PluginLoader{plugins: make(map[string]plugin.Plugin, len(preloadPlugins)), repo: repo}
if repo != "" {
cfg, err := cserialize.Load(filepath.Join(repo, config.DefaultConfigFile))
cfg, err := config.Load(filepath.Join(repo, config.DefaultConfigFile))
switch err {
case cserialize.ErrNotInitialized:
case config.ErrNotInitialized:
case nil:
loader.config = cfg.Plugins
default:
Expand Down
Loading

0 comments on commit d476c5d

Please sign in to comment.