mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 19:08:58 +00:00
348 lines
8.5 KiB
Go
348 lines
8.5 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
_ "net/http/pprof"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/calmh/ini"
|
|
"github.com/calmh/syncthing/discover"
|
|
flags "github.com/calmh/syncthing/github.com/jessevdk/go-flags"
|
|
"github.com/calmh/syncthing/protocol"
|
|
)
|
|
|
|
type Options struct {
|
|
ConfDir string `short:"c" long:"cfg" description:"Configuration directory" default:"~/.syncthing" value-name:"DIR"`
|
|
Listen string `short:"l" long:"listen" description:"Listen address" default:":22000" value-name:"ADDR"`
|
|
ReadOnly bool `short:"r" long:"ro" description:"Repository is read only"`
|
|
Delete bool `short:"d" long:"delete" description:"Delete files deleted from cluster"`
|
|
NoSymlinks bool `long:"no-symlinks" description:"Don't follow first level symlinks in the repo"`
|
|
Discovery DiscoveryOptions `group:"Discovery Options"`
|
|
Advanced AdvancedOptions `group:"Advanced Options"`
|
|
Debug DebugOptions `group:"Debugging Options"`
|
|
}
|
|
|
|
type DebugOptions struct {
|
|
TraceFile bool `long:"trace-file"`
|
|
TraceNet bool `long:"trace-net"`
|
|
TraceIdx bool `long:"trace-idx"`
|
|
Profiler string `long:"profiler" value-name:"ADDR"`
|
|
}
|
|
|
|
type DiscoveryOptions struct {
|
|
ExternalServer string `long:"ext-server" description:"External discovery server" value-name:"NAME" default:"syncthing.nym.se"`
|
|
ExternalPort int `short:"e" long:"ext-port" description:"External listen port" value-name:"PORT" default:"22000"`
|
|
NoExternalDiscovery bool `short:"n" long:"no-ext-announce" description:"Do not announce presence externally"`
|
|
NoLocalDiscovery bool `short:"N" long:"no-local-announce" description:"Do not announce presence locally"`
|
|
}
|
|
|
|
type AdvancedOptions struct {
|
|
RequestsInFlight int `long:"reqs-in-flight" description:"Parallell in flight requests per file" default:"8" value-name:"REQS"`
|
|
FilesInFlight int `long:"files-in-flight" description:"Parallell in flight file pulls" default:"4" value-name:"FILES"`
|
|
ScanInterval time.Duration `long:"scan-intv" description:"Repository scan interval" default:"60s" value-name:"INTV"`
|
|
ConnInterval time.Duration `long:"conn-intv" description:"Node reconnect interval" default:"60s" value-name:"INTV"`
|
|
}
|
|
|
|
var opts Options
|
|
var Version string = "unknown-dev"
|
|
|
|
const (
|
|
confDirName = ".syncthing"
|
|
confFileName = "syncthing.ini"
|
|
)
|
|
|
|
var (
|
|
config ini.Config
|
|
nodeAddrs = make(map[string][]string)
|
|
)
|
|
|
|
// Options
|
|
var (
|
|
ConfDir = path.Join(getHomeDir(), confDirName)
|
|
)
|
|
|
|
func main() {
|
|
// Useful for debugging; to be adjusted.
|
|
log.SetFlags(log.Ltime | log.Lshortfile)
|
|
|
|
_, err := flags.Parse(&opts)
|
|
if err != nil {
|
|
os.Exit(0)
|
|
}
|
|
if strings.HasPrefix(opts.ConfDir, "~/") {
|
|
opts.ConfDir = strings.Replace(opts.ConfDir, "~", getHomeDir(), 1)
|
|
}
|
|
|
|
infoln("Version", Version)
|
|
|
|
// Ensure that our home directory exists and that we have a certificate and key.
|
|
|
|
ensureDir(ConfDir, 0700)
|
|
cert, err := loadCert(ConfDir)
|
|
if err != nil {
|
|
newCertificate(ConfDir)
|
|
cert, err = loadCert(ConfDir)
|
|
fatalErr(err)
|
|
}
|
|
|
|
myID := string(certId(cert.Certificate[0]))
|
|
infoln("My ID:", myID)
|
|
|
|
if opts.Debug.Profiler != "" {
|
|
go func() {
|
|
err := http.ListenAndServe(opts.Debug.Profiler, nil)
|
|
if err != nil {
|
|
warnln(err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// The TLS configuration is used for both the listening socket and outgoing
|
|
// connections.
|
|
|
|
cfg := &tls.Config{
|
|
ClientAuth: tls.RequestClientCert,
|
|
ServerName: "syncthing",
|
|
NextProtos: []string{"bep/1.0"},
|
|
InsecureSkipVerify: true,
|
|
Certificates: []tls.Certificate{cert},
|
|
}
|
|
|
|
// Load the configuration file, if it exists.
|
|
|
|
cf, err := os.Open(path.Join(ConfDir, confFileName))
|
|
if err != nil {
|
|
fatalln("No config file")
|
|
config = ini.Config{}
|
|
}
|
|
config = ini.Parse(cf)
|
|
cf.Close()
|
|
|
|
var dir = config.Get("repository", "dir")
|
|
|
|
// Create a map of desired node connections based on the configuration file
|
|
// directives.
|
|
|
|
for nodeID, addrs := range config.OptionMap("nodes") {
|
|
addrs := strings.Fields(addrs)
|
|
nodeAddrs[nodeID] = addrs
|
|
}
|
|
|
|
ensureDir(dir, -1)
|
|
m := NewModel(dir)
|
|
|
|
// Walk the repository and update the local model before establishing any
|
|
// connections to other nodes.
|
|
|
|
infoln("Initial repository scan in progress")
|
|
loadIndex(m)
|
|
updateLocalModel(m)
|
|
|
|
// Routine to listen for incoming connections
|
|
infoln("Listening for incoming connections")
|
|
go listen(myID, opts.Listen, m, cfg)
|
|
|
|
// Routine to connect out to configured nodes
|
|
infoln("Attempting to connect to other nodes")
|
|
go connect(myID, opts.Listen, nodeAddrs, m, cfg)
|
|
|
|
// Routine to pull blocks from other nodes to synchronize the local
|
|
// repository. Does not run when we are in read only (publish only) mode.
|
|
if !opts.ReadOnly {
|
|
infoln("Cleaning out incomplete synchronizations")
|
|
CleanTempFiles(dir)
|
|
okln("Ready to synchronize")
|
|
m.Start()
|
|
}
|
|
|
|
// Periodically scan the repository and update the local model.
|
|
// XXX: Should use some fsnotify mechanism.
|
|
go func() {
|
|
for {
|
|
time.Sleep(opts.Advanced.ScanInterval)
|
|
updateLocalModel(m)
|
|
}
|
|
}()
|
|
|
|
select {}
|
|
}
|
|
|
|
func listen(myID string, addr string, m *Model, cfg *tls.Config) {
|
|
l, err := tls.Listen("tcp", addr, cfg)
|
|
fatalErr(err)
|
|
|
|
listen:
|
|
for {
|
|
conn, err := l.Accept()
|
|
if err != nil {
|
|
warnln(err)
|
|
continue
|
|
}
|
|
|
|
if opts.Debug.TraceNet {
|
|
debugln("NET: Connect from", conn.RemoteAddr())
|
|
}
|
|
|
|
tc := conn.(*tls.Conn)
|
|
err = tc.Handshake()
|
|
if err != nil {
|
|
warnln(err)
|
|
tc.Close()
|
|
continue
|
|
}
|
|
|
|
remoteID := certId(tc.ConnectionState().PeerCertificates[0].Raw)
|
|
|
|
if remoteID == myID {
|
|
warnf("Connect from myself (%s) - should not happen", remoteID)
|
|
conn.Close()
|
|
continue
|
|
}
|
|
|
|
if m.ConnectedTo(remoteID) {
|
|
warnf("Connect from connected node (%s)", remoteID)
|
|
}
|
|
|
|
for nodeID := range nodeAddrs {
|
|
if nodeID == remoteID {
|
|
nc := protocol.NewConnection(remoteID, conn, conn, m)
|
|
m.AddNode(nc)
|
|
okln("Connected to nodeID", remoteID, "(in)")
|
|
continue listen
|
|
}
|
|
}
|
|
conn.Close()
|
|
}
|
|
}
|
|
|
|
func connect(myID string, addr string, nodeAddrs map[string][]string, m *Model, cfg *tls.Config) {
|
|
_, portstr, err := net.SplitHostPort(addr)
|
|
fatalErr(err)
|
|
port, _ := strconv.Atoi(portstr)
|
|
|
|
if opts.Discovery.NoLocalDiscovery {
|
|
port = -1
|
|
} else {
|
|
infoln("Sending local discovery announcements")
|
|
}
|
|
|
|
if opts.Discovery.NoExternalDiscovery {
|
|
opts.Discovery.ExternalPort = -1
|
|
} else {
|
|
infoln("Sending external discovery announcements")
|
|
}
|
|
|
|
disc, err := discover.NewDiscoverer(myID, port, opts.Discovery.ExternalPort, opts.Discovery.ExternalServer)
|
|
|
|
if err != nil {
|
|
warnf("No discovery possible (%v)", err)
|
|
}
|
|
|
|
for {
|
|
nextNode:
|
|
for nodeID, addrs := range nodeAddrs {
|
|
if nodeID == myID {
|
|
continue
|
|
}
|
|
if m.ConnectedTo(nodeID) {
|
|
continue
|
|
}
|
|
for _, addr := range addrs {
|
|
if addr == "dynamic" {
|
|
var ok bool
|
|
if disc != nil {
|
|
addr, ok = disc.Lookup(nodeID)
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if opts.Debug.TraceNet {
|
|
debugln("NET: Dial", nodeID, addr)
|
|
}
|
|
conn, err := tls.Dial("tcp", addr, cfg)
|
|
if err != nil {
|
|
if opts.Debug.TraceNet {
|
|
debugln("NET:", err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
remoteID := certId(conn.ConnectionState().PeerCertificates[0].Raw)
|
|
if remoteID != nodeID {
|
|
warnln("Unexpected nodeID", remoteID, "!=", nodeID)
|
|
conn.Close()
|
|
continue
|
|
}
|
|
|
|
nc := protocol.NewConnection(nodeID, conn, conn, m)
|
|
m.AddNode(nc)
|
|
okln("Connected to node", remoteID, "(out)")
|
|
continue nextNode
|
|
}
|
|
}
|
|
|
|
time.Sleep(opts.Advanced.ConnInterval)
|
|
}
|
|
}
|
|
|
|
func updateLocalModel(m *Model) {
|
|
files := Walk(m.Dir(), m, !opts.NoSymlinks)
|
|
m.ReplaceLocal(files)
|
|
saveIndex(m)
|
|
}
|
|
|
|
func saveIndex(m *Model) {
|
|
fname := fmt.Sprintf("%x.idx", sha1.Sum([]byte(m.Dir())))
|
|
idxf, err := os.Create(path.Join(ConfDir, fname))
|
|
if err != nil {
|
|
return
|
|
}
|
|
protocol.WriteIndex(idxf, m.ProtocolIndex())
|
|
idxf.Close()
|
|
}
|
|
|
|
func loadIndex(m *Model) {
|
|
fname := fmt.Sprintf("%x.idx", sha1.Sum([]byte(m.Dir())))
|
|
idxf, err := os.Open(path.Join(ConfDir, fname))
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer idxf.Close()
|
|
|
|
idx, err := protocol.ReadIndex(idxf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
m.SeedIndex(idx)
|
|
}
|
|
|
|
func ensureDir(dir string, mode int) {
|
|
fi, err := os.Stat(dir)
|
|
if os.IsNotExist(err) {
|
|
err := os.MkdirAll(dir, 0700)
|
|
fatalErr(err)
|
|
} else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
|
|
err := os.Chmod(dir, os.FileMode(mode))
|
|
fatalErr(err)
|
|
}
|
|
}
|
|
|
|
func getHomeDir() string {
|
|
home := os.Getenv("HOME")
|
|
if home == "" {
|
|
fatalln("No home directory?")
|
|
}
|
|
return home
|
|
}
|