Skip to content

Commit d476c5d

Browse files
committed
Avoid unknown fields on config
This will reduce headaches when adding the wrong fields to the config. Signed-off-by: Antonio Navarro Perez <[email protected]>
1 parent c8a4b6a commit d476c5d

File tree

9 files changed

+172
-140
lines changed

9 files changed

+172
-140
lines changed

cmd/ipfs/daemon.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
utilmain "github.com/ipfs/kubo/cmd/ipfs/util"
2020
oldcmds "github.com/ipfs/kubo/commands"
2121
config "github.com/ipfs/kubo/config"
22-
cserial "github.com/ipfs/kubo/config/serialize"
2322
"github.com/ipfs/kubo/core"
2423
commands "github.com/ipfs/kubo/core/commands"
2524
"github.com/ipfs/kubo/core/coreapi"
@@ -252,7 +251,7 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment
252251
var conf *config.Config
253252

254253
if cfgLocation != "" {
255-
if conf, err = cserial.Load(cfgLocation); err != nil {
254+
if conf, err = config.Load(cfgLocation); err != nil {
256255
return err
257256
}
258257
}

config/config.go

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ package config
44
import (
55
"bytes"
66
"encoding/json"
7+
"errors"
78
"fmt"
9+
"io"
810
"os"
911
"path/filepath"
1012
"strings"
1113

14+
"github.com/facebookgo/atomicfile"
1215
"github.com/mitchellh/go-homedir"
1316
)
1417

18+
// ErrNotInitialized is returned when we fail to read the config because the
19+
// repo doesn't exist.
20+
var ErrNotInitialized = errors.New("ipfs not initialized, please run 'ipfs init'")
21+
1522
// Config is used to load ipfs config files.
1623
type Config struct {
1724
Identity Identity // local node's peer identity
@@ -101,35 +108,36 @@ func HumanOutput(value interface{}) ([]byte, error) {
101108
if ok {
102109
return []byte(strings.Trim(s, "\n")), nil
103110
}
104-
return Marshal(value)
105-
}
106111

107-
// Marshal configuration with JSON
108-
func Marshal(value interface{}) ([]byte, error) {
109-
// need to prettyprint, hence MarshalIndent, instead of Encoder
110-
return json.MarshalIndent(value, "", " ")
112+
buf := new(bytes.Buffer)
113+
if err := encodeConfigFile(buf, value); err != nil {
114+
return nil, err
115+
}
116+
117+
return buf.Bytes(), nil
111118
}
112119

113120
func FromMap(v map[string]interface{}) (*Config, error) {
114121
buf := new(bytes.Buffer)
115-
if err := json.NewEncoder(buf).Encode(v); err != nil {
122+
123+
if err := encodeConfigFile(buf, v); err != nil {
116124
return nil, err
117125
}
118126
var conf Config
119-
if err := json.NewDecoder(buf).Decode(&conf); err != nil {
120-
return nil, fmt.Errorf("failure to decode config: %s", err)
127+
if err := decodeConfigFile(buf, &conf); err != nil {
128+
return nil, err
121129
}
122130
return &conf, nil
123131
}
124132

125133
func ToMap(conf *Config) (map[string]interface{}, error) {
126134
buf := new(bytes.Buffer)
127-
if err := json.NewEncoder(buf).Encode(conf); err != nil {
135+
if err := encodeConfigFile(buf, conf); err != nil {
128136
return nil, err
129137
}
130138
var m map[string]interface{}
131-
if err := json.NewDecoder(buf).Decode(&m); err != nil {
132-
return nil, fmt.Errorf("failure to decode config: %s", err)
139+
if err := decodeConfigFile(buf, &m); err != nil {
140+
return nil, err
133141
}
134142
return m, nil
135143
}
@@ -139,13 +147,74 @@ func (c *Config) Clone() (*Config, error) {
139147
var newConfig Config
140148
var buf bytes.Buffer
141149

142-
if err := json.NewEncoder(&buf).Encode(c); err != nil {
143-
return nil, fmt.Errorf("failure to encode config: %s", err)
150+
if err := encodeConfigFile(&buf, c); err != nil {
151+
return nil, err
144152
}
145-
146-
if err := json.NewDecoder(&buf).Decode(&newConfig); err != nil {
147-
return nil, fmt.Errorf("failure to decode config: %s", err)
153+
if err := decodeConfigFile(&buf, &newConfig); err != nil {
154+
return nil, err
148155
}
149156

150157
return &newConfig, nil
151158
}
159+
160+
// ReadConfigFile reads the config from `filename` into `cfg`.
161+
func ReadConfigFile(filename string, cfg interface{}) error {
162+
f, err := os.Open(filename)
163+
if err != nil {
164+
if os.IsNotExist(err) {
165+
err = ErrNotInitialized
166+
}
167+
return err
168+
}
169+
defer f.Close()
170+
171+
return decodeConfigFile(f, cfg)
172+
}
173+
174+
func decodeConfigFile(r io.Reader, cfg interface{}) error {
175+
dec := json.NewDecoder(r)
176+
if os.Getenv("IPFS_CONFIG_TOLERANT_MODE") == "" {
177+
dec.DisallowUnknownFields()
178+
}
179+
180+
if err := dec.Decode(cfg); err != nil {
181+
return fmt.Errorf("failure to decode config: %s", err)
182+
}
183+
184+
return nil
185+
}
186+
187+
// WriteConfigFile writes the config from `cfg` into `filename`.
188+
func WriteConfigFile(filename string, cfg interface{}) error {
189+
err := os.MkdirAll(filepath.Dir(filename), 0755)
190+
if err != nil {
191+
return err
192+
}
193+
194+
f, err := atomicfile.New(filename, 0600)
195+
if err != nil {
196+
return err
197+
}
198+
defer f.Close()
199+
200+
return encodeConfigFile(f, cfg)
201+
}
202+
203+
// encodeConfigFile encodes configuration with JSON
204+
func encodeConfigFile(w io.Writer, value interface{}) error {
205+
enc := json.NewEncoder(w)
206+
enc.SetIndent("", " ")
207+
208+
return enc.Encode(value)
209+
}
210+
211+
// Load reads given file and returns the read config, or error.
212+
func Load(filename string) (*Config, error) {
213+
var cfg Config
214+
err := ReadConfigFile(filename, &cfg)
215+
if err != nil {
216+
return nil, err
217+
}
218+
219+
return &cfg, err
220+
}

config/config_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package config
22

33
import (
4+
"os"
5+
"runtime"
46
"testing"
57
)
68

@@ -27,3 +29,64 @@ func TestClone(t *testing.T) {
2729
t.Fatal("HTTP headers not preserved")
2830
}
2931
}
32+
33+
func TestConfig(t *testing.T) {
34+
const filename = ".ipfsconfig"
35+
cfgWritten := new(Config)
36+
cfgWritten.Identity.PeerID = "faketest"
37+
38+
err := WriteConfigFile(filename, cfgWritten)
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
cfgRead, err := Load(filename)
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
if cfgWritten.Identity.PeerID != cfgRead.Identity.PeerID {
47+
t.Fatal()
48+
}
49+
st, err := os.Stat(filename)
50+
if err != nil {
51+
t.Fatalf("cannot stat config file: %v", err)
52+
}
53+
54+
if runtime.GOOS != "windows" { // see https://golang.org/src/os/types_windows.go
55+
if g := st.Mode().Perm(); g&0117 != 0 {
56+
t.Fatalf("config file should not be executable or accessible to world: %v", g)
57+
}
58+
}
59+
}
60+
61+
func TestConfigUnknownField(t *testing.T) {
62+
const filename = ".ipfsconfig"
63+
64+
badConfig := map[string]string{
65+
"BadField": "Value",
66+
}
67+
68+
err := WriteConfigFile(filename, badConfig)
69+
if err != nil {
70+
t.Fatal(err)
71+
}
72+
73+
_, err = Load(filename)
74+
if err == nil {
75+
t.Fatal("load must fail")
76+
}
77+
78+
if err.Error() != "failure to decode config: json: unknown field \"BadField\"" {
79+
t.Fatal("unexpected error:", err)
80+
}
81+
82+
mapOut := make(map[string]string)
83+
84+
err = ReadConfigFile(filename, &mapOut)
85+
if err != nil {
86+
t.Fatal(err)
87+
}
88+
89+
if mapOut["BadField"] != "Value" {
90+
t.Fatal(err)
91+
}
92+
}

config/serialize/serialize.go

Lines changed: 0 additions & 72 deletions
This file was deleted.

config/serialize/serialize_test.go

Lines changed: 0 additions & 37 deletions
This file was deleted.

docs/environment-variables.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ $ ipfs resolve -r /ipns/dnslink-test2.example.com
114114
/ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am
115115
```
116116

117+
## `IPFS_CONFIG_TOLERANT_MODE`
118+
119+
Disables strict config valiadtion to allow unsupported fields on JSON config.
120+
121+
Default: false
122+
123+
117124
## `LIBP2P_TCP_REUSEPORT`
118125

119126
Kubo tries to reuse the same source port for all connections to improve NAT

plugin/loader/loader.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"strings"
1010

1111
config "github.com/ipfs/kubo/config"
12-
cserialize "github.com/ipfs/kubo/config/serialize"
1312
"github.com/ipld/go-ipld-prime/multicodec"
1413

1514
"github.com/ipfs/kubo/core"
@@ -97,9 +96,9 @@ type PluginLoader struct {
9796
func NewPluginLoader(repo string) (*PluginLoader, error) {
9897
loader := &PluginLoader{plugins: make(map[string]plugin.Plugin, len(preloadPlugins)), repo: repo}
9998
if repo != "" {
100-
cfg, err := cserialize.Load(filepath.Join(repo, config.DefaultConfigFile))
99+
cfg, err := config.Load(filepath.Join(repo, config.DefaultConfigFile))
101100
switch err {
102-
case cserialize.ErrNotInitialized:
101+
case config.ErrNotInitialized:
103102
case nil:
104103
loader.config = cfg.Plugins
105104
default:

0 commit comments

Comments
 (0)