Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Avoid unknown fields on config #9438

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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