// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). // All rights reserved. Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // Package config implements reading and writing of the syncthing configuration file. package config import ( "encoding/xml" "fmt" "io" "os" "reflect" "sort" "strconv" "code.google.com/p/go.crypto/bcrypt" "github.com/syncthing/syncthing/logger" "github.com/syncthing/syncthing/protocol" ) var l = logger.DefaultLogger type Configuration struct { Version int `xml:"version,attr" default:"3"` Repositories []RepositoryConfiguration `xml:"repository"` Nodes []NodeConfiguration `xml:"node"` GUI GUIConfiguration `xml:"gui"` Options OptionsConfiguration `xml:"options"` XMLName xml.Name `xml:"configuration" json:"-"` } type RepositoryConfiguration struct { ID string `xml:"id,attr"` Directory string `xml:"directory,attr"` Nodes []RepositoryNodeConfiguration `xml:"node"` ReadOnly bool `xml:"ro,attr"` RescanIntervalS int `xml:"rescanIntervalS,attr" default:"60"` IgnorePerms bool `xml:"ignorePerms,attr"` Invalid string `xml:"-"` // Set at runtime when there is an error, not saved Versioning VersioningConfiguration `xml:"versioning"` nodeIDs []protocol.NodeID } type VersioningConfiguration struct { Type string `xml:"type,attr"` Params map[string]string } type InternalVersioningConfiguration struct { Type string `xml:"type,attr,omitempty"` Params []InternalParam `xml:"param"` } type InternalParam struct { Key string `xml:"key,attr"` Val string `xml:"val,attr"` } func (c *VersioningConfiguration) MarshalXML(e *xml.Encoder, start xml.StartElement) error { var tmp InternalVersioningConfiguration tmp.Type = c.Type for k, v := range c.Params { tmp.Params = append(tmp.Params, InternalParam{k, v}) } return e.EncodeElement(tmp, start) } func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var tmp InternalVersioningConfiguration err := d.DecodeElement(&tmp, &start) if err != nil { return err } c.Type = tmp.Type c.Params = make(map[string]string, len(tmp.Params)) for _, p := range tmp.Params { c.Params[p.Key] = p.Val } return nil } func (r *RepositoryConfiguration) NodeIDs() []protocol.NodeID { if r.nodeIDs == nil { for _, n := range r.Nodes { r.nodeIDs = append(r.nodeIDs, n.NodeID) } } return r.nodeIDs } type NodeConfiguration struct { NodeID protocol.NodeID `xml:"id,attr"` Name string `xml:"name,attr,omitempty"` Addresses []string `xml:"address,omitempty"` Compression bool `xml:"compression,attr"` CertName string `xml:"certName,attr,omitempty"` } type RepositoryNodeConfiguration struct { NodeID protocol.NodeID `xml:"id,attr"` Deprecated_Name string `xml:"name,attr,omitempty" json:"-"` Deprecated_Addresses []string `xml:"address,omitempty" json:"-"` } type OptionsConfiguration struct { ListenAddress []string `xml:"listenAddress" default:"0.0.0.0:22000"` GlobalAnnServer string `xml:"globalAnnounceServer" default:"announce.syncthing.net:22026"` GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true"` LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true"` LocalAnnPort int `xml:"localAnnouncePort" default:"21025"` LocalAnnMCAddr string `xml:"localAnnounceMCAddr" default:"[ff32::5222]:21026"` ParallelRequests int `xml:"parallelRequests" default:"16"` MaxSendKbps int `xml:"maxSendKbps"` ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60"` StartBrowser bool `xml:"startBrowser" default:"true"` UPnPEnabled bool `xml:"upnpEnabled" default:"true"` UPnPLease int `xml:"upnpLeaseMinutes" default:"0"` UPnPRenewal int `xml:"upnpRenewalMinutes" default:"30"` URAccepted int `xml:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently) Deprecated_RescanIntervalS int `xml:"rescanIntervalS,omitempty" json:"-"` Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"` Deprecated_URDeclined bool `xml:"urDeclined,omitempty" json:"-"` Deprecated_ReadOnly bool `xml:"readOnly,omitempty" json:"-"` Deprecated_GUIEnabled bool `xml:"guiEnabled,omitempty" json:"-"` Deprecated_GUIAddress string `xml:"guiAddress,omitempty" json:"-"` } type GUIConfiguration struct { Enabled bool `xml:"enabled,attr" default:"true"` Address string `xml:"address" default:"127.0.0.1:8080"` User string `xml:"user,omitempty"` Password string `xml:"password,omitempty"` UseTLS bool `xml:"tls,attr"` APIKey string `xml:"apikey,omitempty"` } func (cfg *Configuration) NodeMap() map[protocol.NodeID]NodeConfiguration { m := make(map[protocol.NodeID]NodeConfiguration, len(cfg.Nodes)) for _, n := range cfg.Nodes { m[n.NodeID] = n } return m } func (cfg *Configuration) GetNodeConfiguration(nodeid protocol.NodeID) *NodeConfiguration { for i, node := range cfg.Nodes { if node.NodeID == nodeid { return &cfg.Nodes[i] } } return nil } func (cfg *Configuration) RepoMap() map[string]RepositoryConfiguration { m := make(map[string]RepositoryConfiguration, len(cfg.Repositories)) for _, r := range cfg.Repositories { m[r.ID] = r } return m } func setDefaults(data interface{}) error { s := reflect.ValueOf(data).Elem() t := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) tag := t.Field(i).Tag v := tag.Get("default") if len(v) > 0 { switch f.Interface().(type) { case string: f.SetString(v) case int: i, err := strconv.ParseInt(v, 10, 64) if err != nil { return err } f.SetInt(i) case bool: f.SetBool(v == "true") case []string: // We don't do anything with string slices here. Any default // we set will be appended to by the XML decoder, so we fill // those after decoding. default: panic(f.Type()) } } } return nil } // fillNilSlices sets default value on slices that are still nil. func fillNilSlices(data interface{}) error { s := reflect.ValueOf(data).Elem() t := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) tag := t.Field(i).Tag v := tag.Get("default") if len(v) > 0 { switch f.Interface().(type) { case []string: if f.IsNil() { rv := reflect.MakeSlice(reflect.TypeOf([]string{}), 1, 1) rv.Index(0).SetString(v) f.Set(rv) } } } } return nil } func Save(wr io.Writer, cfg Configuration) error { e := xml.NewEncoder(wr) e.Indent("", " ") err := e.Encode(cfg) if err != nil { return err } _, err = wr.Write([]byte("\n")) return err } func uniqueStrings(ss []string) []string { var m = make(map[string]bool, len(ss)) for _, s := range ss { m[s] = true } var us = make([]string, 0, len(m)) for k := range m { us = append(us, k) } return us } func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) { var cfg Configuration setDefaults(&cfg) setDefaults(&cfg.Options) setDefaults(&cfg.GUI) var err error if rd != nil { err = xml.NewDecoder(rd).Decode(&cfg) } fillNilSlices(&cfg.Options) cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress) // Initialize an empty slice for repositories if the config has none if cfg.Repositories == nil { cfg.Repositories = []RepositoryConfiguration{} } // Check for missing, bad or duplicate repository ID:s var seenRepos = map[string]*RepositoryConfiguration{} var uniqueCounter int for i := range cfg.Repositories { repo := &cfg.Repositories[i] if len(repo.Directory) == 0 { repo.Invalid = "no directory configured" continue } if repo.ID == "" { repo.ID = "default" } if seen, ok := seenRepos[repo.ID]; ok { l.Warnf("Multiple repositories with ID %q; disabling", repo.ID) seen.Invalid = "duplicate repository ID" if seen.ID == repo.ID { uniqueCounter++ seen.ID = fmt.Sprintf("%s~%d", repo.ID, uniqueCounter) } repo.Invalid = "duplicate repository ID" uniqueCounter++ repo.ID = fmt.Sprintf("%s~%d", repo.ID, uniqueCounter) } else { seenRepos[repo.ID] = repo } } if cfg.Options.Deprecated_URDeclined { cfg.Options.URAccepted = -1 } cfg.Options.Deprecated_URDeclined = false cfg.Options.Deprecated_UREnabled = false // Upgrade to v2 configuration if appropriate if cfg.Version == 1 { convertV1V2(&cfg) } // Upgrade to v3 configuration if appropriate if cfg.Version == 2 { convertV2V3(&cfg) } // Upgrade to v4 configuration if appropriate if cfg.Version == 3 { convertV3V4(&cfg) } // Hash old cleartext passwords if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' { hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0) if err != nil { l.Warnln("bcrypting password:", err) } else { cfg.GUI.Password = string(hash) } } // Build a list of available nodes existingNodes := make(map[protocol.NodeID]bool) existingNodes[myID] = true for _, node := range cfg.Nodes { existingNodes[node.NodeID] = true } // Ensure this node is present in all relevant places me := cfg.GetNodeConfiguration(myID) if me == nil { myName, _ := os.Hostname() cfg.Nodes = append(cfg.Nodes, NodeConfiguration{ NodeID: myID, Name: myName, }) } sort.Sort(NodeConfigurationList(cfg.Nodes)) // Ensure that any loose nodes are not present in the wrong places // Ensure that there are no duplicate nodes for i := range cfg.Repositories { cfg.Repositories[i].Nodes = ensureNodePresent(cfg.Repositories[i].Nodes, myID) cfg.Repositories[i].Nodes = ensureExistingNodes(cfg.Repositories[i].Nodes, existingNodes) cfg.Repositories[i].Nodes = ensureNoDuplicates(cfg.Repositories[i].Nodes) sort.Sort(RepositoryNodeConfigurationList(cfg.Repositories[i].Nodes)) } // An empty address list is equivalent to a single "dynamic" entry for i := range cfg.Nodes { n := &cfg.Nodes[i] if len(n.Addresses) == 0 || len(n.Addresses) == 1 && n.Addresses[0] == "" { n.Addresses = []string{"dynamic"} } } return cfg, err } func convertV3V4(cfg *Configuration) { // In previous versions, rescan interval was common for each repository. // From now, it can be set independently. We have to make sure, that after upgrade // the individual rescan interval will be defined for every existing repository. for i := range cfg.Repositories { cfg.Repositories[i].RescanIntervalS = cfg.Options.Deprecated_RescanIntervalS } cfg.Options.Deprecated_RescanIntervalS = 0 // In previous versions, repositories held full node configurations. // Since that's the only place where node configs were in V1, we still have // to define the deprecated fields to be able to upgrade from V1 to V4. for i, repo := range cfg.Repositories { for j := range repo.Nodes { rncfg := cfg.Repositories[i].Nodes[j] rncfg.Deprecated_Name = "" rncfg.Deprecated_Addresses = nil } } cfg.Version = 4 } func convertV2V3(cfg *Configuration) { // In previous versions, compression was always on. When upgrading, enable // compression on all existing new. New nodes will get compression on by // default by the GUI. for i := range cfg.Nodes { cfg.Nodes[i].Compression = true } // The global discovery format and port number changed in v0.9. Having the // default announce server but old port number is guaranteed to be legacy. if cfg.Options.GlobalAnnServer == "announce.syncthing.net:22025" { cfg.Options.GlobalAnnServer = "announce.syncthing.net:22026" } cfg.Version = 3 } func convertV1V2(cfg *Configuration) { // Collect the list of nodes. // Replace node configs inside repositories with only a reference to the nide ID. // Set all repositories to read only if the global read only flag is set. var nodes = map[string]RepositoryNodeConfiguration{} for i, repo := range cfg.Repositories { cfg.Repositories[i].ReadOnly = cfg.Options.Deprecated_ReadOnly for j, node := range repo.Nodes { id := node.NodeID.String() if _, ok := nodes[id]; !ok { nodes[id] = node } cfg.Repositories[i].Nodes[j] = RepositoryNodeConfiguration{NodeID: node.NodeID} } } cfg.Options.Deprecated_ReadOnly = false // Set and sort the list of nodes. for _, node := range nodes { cfg.Nodes = append(cfg.Nodes, NodeConfiguration{ NodeID: node.NodeID, Name: node.Deprecated_Name, Addresses: node.Deprecated_Addresses, }) } sort.Sort(NodeConfigurationList(cfg.Nodes)) // GUI cfg.GUI.Address = cfg.Options.Deprecated_GUIAddress cfg.GUI.Enabled = cfg.Options.Deprecated_GUIEnabled cfg.Options.Deprecated_GUIEnabled = false cfg.Options.Deprecated_GUIAddress = "" cfg.Version = 2 } type NodeConfigurationList []NodeConfiguration func (l NodeConfigurationList) Less(a, b int) bool { return l[a].NodeID.Compare(l[b].NodeID) == -1 } func (l NodeConfigurationList) Swap(a, b int) { l[a], l[b] = l[b], l[a] } func (l NodeConfigurationList) Len() int { return len(l) } type RepositoryNodeConfigurationList []RepositoryNodeConfiguration func (l RepositoryNodeConfigurationList) Less(a, b int) bool { return l[a].NodeID.Compare(l[b].NodeID) == -1 } func (l RepositoryNodeConfigurationList) Swap(a, b int) { l[a], l[b] = l[b], l[a] } func (l RepositoryNodeConfigurationList) Len() int { return len(l) } func ensureNodePresent(nodes []RepositoryNodeConfiguration, myID protocol.NodeID) []RepositoryNodeConfiguration { for _, node := range nodes { if node.NodeID.Equals(myID) { return nodes } } nodes = append(nodes, RepositoryNodeConfiguration{ NodeID: myID, }) return nodes } func ensureExistingNodes(nodes []RepositoryNodeConfiguration, existingNodes map[protocol.NodeID]bool) []RepositoryNodeConfiguration { count := len(nodes) i := 0 loop: for i < count { if _, ok := existingNodes[nodes[i].NodeID]; !ok { nodes[i] = nodes[count-1] count-- continue loop } i++ } return nodes[0:count] } func ensureNoDuplicates(nodes []RepositoryNodeConfiguration) []RepositoryNodeConfiguration { count := len(nodes) i := 0 seenNodes := make(map[protocol.NodeID]bool) loop: for i < count { id := nodes[i].NodeID if _, ok := seenNodes[id]; ok { nodes[i] = nodes[count-1] count-- continue loop } seenNodes[id] = true i++ } return nodes[0:count] }