Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03e17ec1dd | |||
| 892e470ba5 | |||
| f8b835c6be | |||
| 5fab25effd | |||
| d746b043c8 | |||
| beb643c666 | |||
| d039df46c0 | |||
| b7e5948b09 | |||
| e3dd072022 | |||
| b98a8bafb3 | |||
| 4b5c44b61c | |||
| 41c131d840 | |||
| 66df2e57ae | |||
| 4a6e9c189c | |||
| 2eece95ed2 | |||
| 008b07a52f | |||
| 4fd01ae552 | |||
| 8e6cf4bf4e | |||
| ace8451604 | |||
| feffc0416f | |||
| 42dfa45d52 | |||
| 706926543e | |||
| a024cefd35 | |||
| 42dcb3d2cc | |||
| ed1852f8f6 |
+4
-4
@@ -1,6 +1,4 @@
|
||||
syncthing
|
||||
!gui/syncthing
|
||||
!Godeps/_workspace/src/github.com/syncthing
|
||||
./syncthing
|
||||
syncthing.exe
|
||||
*.tar.gz
|
||||
*.zip
|
||||
@@ -11,6 +9,8 @@ files/pidx
|
||||
bin
|
||||
perfstats*.csv
|
||||
coverage.xml
|
||||
syncthing.sig
|
||||
!gui/scripts/syncthing
|
||||
syncthing.md5
|
||||
syncthing.exe.md5
|
||||
RELEASE
|
||||
deb
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# This is the official list of Syncthing authors for copyright purposes.
|
||||
|
||||
Aaron Bieber <qbit@deftly.net>
|
||||
Adam Piggott <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
|
||||
Alexander Graf <register-github@alex-graf.de>
|
||||
Andrew Dunham <andrew@du.nham.ca>
|
||||
Antony Male <antony.male@gmail.com>
|
||||
|
||||
Generated
+11
-7
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"ImportPath": "github.com/syncthing/syncthing",
|
||||
"GoVersion": "go1.5.1",
|
||||
"GoVersion": "go1.5",
|
||||
"Packages": [
|
||||
"./cmd/..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/bkaradzic/go-lz4",
|
||||
"Rev": "74ddf82598bc4745b965729e9c6a463bedd33049"
|
||||
"Rev": "d47913b1412890a261b9fefae99d72d2bf5aebd8"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/du",
|
||||
@@ -37,9 +37,13 @@
|
||||
"ImportPath": "github.com/kardianos/osext",
|
||||
"Rev": "6e7f843663477789fac7c02def0d0909e969b4e5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syncthing/protocol",
|
||||
"Rev": "388a29bbe21d8772ee4c29f4520aa8040309607d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||
"Rev": "1a9d62f03ea92815b46fcaab357cfd4df264b1a0"
|
||||
"Rev": "b743d92d3215f11c9b5ce8830fafe1f16786adf4"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/thejerf/suture",
|
||||
@@ -60,19 +64,19 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/bcrypt",
|
||||
"Rev": "81bf7719a6b7ce9b665598222362b50122dfc13b"
|
||||
"Rev": "c16968172724c0b5e8bdc6ad33f5a79443a44cd7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/blowfish",
|
||||
"Rev": "81bf7719a6b7ce9b665598222362b50122dfc13b"
|
||||
"Rev": "c16968172724c0b5e8bdc6ad33f5a79443a44cd7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/internal/iana",
|
||||
"Rev": "db8e4de5b2d6653f66aea53094624468caad15d2"
|
||||
"Rev": "66f0418ca41253f8d1a024eb9754e9441a8e79b9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/ipv6",
|
||||
"Rev": "db8e4de5b2d6653f66aea53094624468caad15d2"
|
||||
"Rev": "66f0418ca41253f8d1a024eb9754e9441a8e79b9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/text/transform",
|
||||
|
||||
+1
-2
@@ -4,6 +4,5 @@ go:
|
||||
- 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.4
|
||||
- tip
|
||||
|
||||
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Executable → Regular
+4
-4
@@ -14,10 +14,10 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoError error
|
||||
ErrGeneric = errors.New("generic error")
|
||||
ErrNoSuchFile = errors.New("no such file")
|
||||
ErrInvalid = errors.New("file is invalid")
|
||||
ErrNoError error = nil
|
||||
ErrGeneric = errors.New("generic error")
|
||||
ErrNoSuchFile = errors.New("no such file")
|
||||
ErrInvalid = errors.New("file is invalid")
|
||||
)
|
||||
|
||||
var lookupError = map[int32]error{
|
||||
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
+4
-5
@@ -1,6 +1,6 @@
|
||||
// Copyright (C) 2014 The Protocol Authors.
|
||||
|
||||
//go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||
//go:generate -command genxdr go run ../syncthing/Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||
//go:generate genxdr -o message_xdr.go message.go
|
||||
|
||||
package protocol
|
||||
@@ -8,7 +8,7 @@ package protocol
|
||||
import "fmt"
|
||||
|
||||
type IndexMessage struct {
|
||||
Folder string // max:256
|
||||
Folder string
|
||||
Files []FileInfo // max:1000000
|
||||
Flags uint32
|
||||
Options []Option // max:64
|
||||
@@ -20,7 +20,6 @@ type FileInfo struct {
|
||||
Modified int64
|
||||
Version Vector
|
||||
LocalVersion int64
|
||||
CachedSize int64 // noencode (cache only)
|
||||
Blocks []BlockInfo // max:1000000
|
||||
}
|
||||
|
||||
@@ -95,7 +94,7 @@ func (b BlockInfo) String() string {
|
||||
}
|
||||
|
||||
type RequestMessage struct {
|
||||
Folder string // max:256
|
||||
Folder string // max:64
|
||||
Name string // max:8192
|
||||
Offset int64
|
||||
Size int32
|
||||
@@ -126,7 +125,7 @@ func (o *ClusterConfigMessage) GetOption(key string) string {
|
||||
}
|
||||
|
||||
type Folder struct {
|
||||
ID string // max:256
|
||||
ID string // max:64
|
||||
Devices []Device // max:1000000
|
||||
Flags uint32
|
||||
Options []Option // max:64
|
||||
Generated
Vendored
+10
-13
@@ -41,7 +41,7 @@ IndexMessage Structure:
|
||||
|
||||
|
||||
struct IndexMessage {
|
||||
string Folder<256>;
|
||||
string Folder<>;
|
||||
FileInfo Files<1000000>;
|
||||
unsigned int Flags;
|
||||
Option Options<64>;
|
||||
@@ -74,9 +74,6 @@ func (o IndexMessage) AppendXDR(bs []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (o IndexMessage) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
if l := len(o.Folder); l > 256 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("Folder", l, 256)
|
||||
}
|
||||
xw.WriteString(o.Folder)
|
||||
if l := len(o.Files); l > 1000000 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("Files", l, 1000000)
|
||||
@@ -114,7 +111,7 @@ func (o *IndexMessage) UnmarshalXDR(bs []byte) error {
|
||||
}
|
||||
|
||||
func (o *IndexMessage) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.Folder = xr.ReadStringMax(256)
|
||||
o.Folder = xr.ReadString()
|
||||
_FilesSize := int(xr.ReadUint32())
|
||||
if _FilesSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Files", _FilesSize, 1000000)
|
||||
@@ -383,7 +380,7 @@ RequestMessage Structure:
|
||||
|
||||
|
||||
struct RequestMessage {
|
||||
string Folder<256>;
|
||||
string Folder<64>;
|
||||
string Name<8192>;
|
||||
hyper Offset;
|
||||
int Size;
|
||||
@@ -419,8 +416,8 @@ func (o RequestMessage) AppendXDR(bs []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (o RequestMessage) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
if l := len(o.Folder); l > 256 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("Folder", l, 256)
|
||||
if l := len(o.Folder); l > 64 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("Folder", l, 64)
|
||||
}
|
||||
xw.WriteString(o.Folder)
|
||||
if l := len(o.Name); l > 8192 {
|
||||
@@ -459,7 +456,7 @@ func (o *RequestMessage) UnmarshalXDR(bs []byte) error {
|
||||
}
|
||||
|
||||
func (o *RequestMessage) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.Folder = xr.ReadStringMax(256)
|
||||
o.Folder = xr.ReadStringMax(64)
|
||||
o.Name = xr.ReadStringMax(8192)
|
||||
o.Offset = int64(xr.ReadUint64())
|
||||
o.Size = int32(xr.ReadUint32())
|
||||
@@ -717,7 +714,7 @@ Folder Structure:
|
||||
|
||||
|
||||
struct Folder {
|
||||
string ID<256>;
|
||||
string ID<64>;
|
||||
Device Devices<1000000>;
|
||||
unsigned int Flags;
|
||||
Option Options<64>;
|
||||
@@ -750,8 +747,8 @@ func (o Folder) AppendXDR(bs []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (o Folder) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
if l := len(o.ID); l > 256 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("ID", l, 256)
|
||||
if l := len(o.ID); l > 64 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("ID", l, 64)
|
||||
}
|
||||
xw.WriteString(o.ID)
|
||||
if l := len(o.Devices); l > 1000000 {
|
||||
@@ -790,7 +787,7 @@ func (o *Folder) UnmarshalXDR(bs []byte) error {
|
||||
}
|
||||
|
||||
func (o *Folder) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.ID = xr.ReadStringMax(256)
|
||||
o.ID = xr.ReadStringMax(64)
|
||||
_DevicesSize := int(xr.ReadUint32())
|
||||
if _DevicesSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Devices", _DevicesSize, 1000000)
|
||||
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
+58
-54
@@ -15,10 +15,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// BlockSize is the standard ata block size (128 KiB)
|
||||
// Data block size (128 KiB)
|
||||
BlockSize = 128 << 10
|
||||
|
||||
// MaxMessageLen is the largest message size allowed on the wire. (64 MiB)
|
||||
// We reject messages larger than this when encountered on the wire. (64 MiB)
|
||||
MaxMessageLen = 64 << 20
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ const (
|
||||
messageTypeRequest = 2
|
||||
messageTypeResponse = 3
|
||||
messageTypePing = 4
|
||||
messageTypePong = 5
|
||||
messageTypeIndexUpdate = 6
|
||||
messageTypeClose = 7
|
||||
)
|
||||
@@ -70,12 +71,13 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrClosed = errors.New("connection closed")
|
||||
ErrTimeout = errors.New("read timeout")
|
||||
ErrClusterHash = fmt.Errorf("configuration error: mismatched cluster hash")
|
||||
ErrClosed = errors.New("connection closed")
|
||||
)
|
||||
|
||||
// Specific variants of empty messages...
|
||||
type pingMessage struct{ EmptyMessage }
|
||||
type pongMessage struct{ EmptyMessage }
|
||||
|
||||
type Model interface {
|
||||
// An index was received from the peer device
|
||||
@@ -144,13 +146,9 @@ type isEofer interface {
|
||||
IsEOF() bool
|
||||
}
|
||||
|
||||
const (
|
||||
// PingSendInterval is how often we make sure to send a message, by
|
||||
// triggering pings if necessary.
|
||||
PingSendInterval = 90 * time.Second
|
||||
// ReceiveTimeout is the longest we'll wait for a message from the other
|
||||
// side before closing the connection.
|
||||
ReceiveTimeout = 300 * time.Second
|
||||
var (
|
||||
PingTimeout = 30 * time.Second
|
||||
PingIdleTime = 60 * time.Second
|
||||
)
|
||||
|
||||
func NewConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, receiver Model, name string, compress Compression) Connection {
|
||||
@@ -182,8 +180,7 @@ func NewConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, receiv
|
||||
func (c *rawConnection) Start() {
|
||||
go c.readerLoop()
|
||||
go c.writerLoop()
|
||||
go c.pingSender()
|
||||
go c.pingReceiver()
|
||||
go c.pingerLoop()
|
||||
go c.idGenerator()
|
||||
}
|
||||
|
||||
@@ -281,7 +278,18 @@ func (c *rawConnection) ping() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return c.send(id, messageTypePing, nil, nil)
|
||||
rc := make(chan asyncResult, 1)
|
||||
c.awaitingMut.Lock()
|
||||
c.awaiting[id] = rc
|
||||
c.awaitingMut.Unlock()
|
||||
|
||||
ok := c.send(id, messageTypePing, nil, nil)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
res, ok := <-rc
|
||||
return ok && res.err == nil
|
||||
}
|
||||
|
||||
func (c *rawConnection) readerLoop() (err error) {
|
||||
@@ -344,7 +352,13 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
if state != stateReady {
|
||||
return fmt.Errorf("protocol error: ping message in state %d", state)
|
||||
}
|
||||
// Nothing
|
||||
c.send(hdr.msgID, messageTypePong, pongMessage{}, nil)
|
||||
|
||||
case pongMessage:
|
||||
if state != stateReady {
|
||||
return fmt.Errorf("protocol error: pong message in state %d", state)
|
||||
}
|
||||
c.handlePong(hdr.msgID)
|
||||
|
||||
case CloseMessage:
|
||||
return errors.New(msg.Reason)
|
||||
@@ -453,6 +467,9 @@ func (c *rawConnection) readMessage() (hdr header, msg encodable, err error) {
|
||||
case messageTypePing:
|
||||
msg = pingMessage{}
|
||||
|
||||
case messageTypePong:
|
||||
msg = pongMessage{}
|
||||
|
||||
case messageTypeClusterConfig:
|
||||
var cc ClusterConfigMessage
|
||||
err = cc.UnmarshalXDR(msgBuf)
|
||||
@@ -712,55 +729,42 @@ func (c *rawConnection) idGenerator() {
|
||||
}
|
||||
}
|
||||
|
||||
// The pingSender makes sure that we've sent a message within the last
|
||||
// PingSendInterval. If we already have something sent in the last
|
||||
// PingSendInterval/2, we do nothing. Otherwise we send a ping message. This
|
||||
// results in an effecting ping interval of somewhere between
|
||||
// PingSendInterval/2 and PingSendInterval.
|
||||
func (c *rawConnection) pingSender() {
|
||||
ticker := time.Tick(PingSendInterval / 2)
|
||||
|
||||
func (c *rawConnection) pingerLoop() {
|
||||
var rc = make(chan bool, 1)
|
||||
ticker := time.Tick(PingIdleTime / 2)
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
d := time.Since(c.cw.Last())
|
||||
if d < PingSendInterval/2 {
|
||||
if d := time.Since(c.cr.Last()); d < PingIdleTime {
|
||||
if debug {
|
||||
l.Debugln(c.id, "ping skipped after rd", d)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if d := time.Since(c.cw.Last()); d < PingIdleTime {
|
||||
if debug {
|
||||
l.Debugln(c.id, "ping skipped after wr", d)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln(c.id, "ping -> after", d)
|
||||
}
|
||||
c.ping()
|
||||
|
||||
case <-c.closed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The pingReciever checks that we've received a message (any message will do,
|
||||
// but we expect pings in the absence of other messages) within the last
|
||||
// ReceiveTimeout. If not, we close the connection with an ErrTimeout.
|
||||
func (c *rawConnection) pingReceiver() {
|
||||
ticker := time.Tick(ReceiveTimeout / 2)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
d := time.Since(c.cr.Last())
|
||||
if d > ReceiveTimeout {
|
||||
go func() {
|
||||
if debug {
|
||||
l.Debugln(c.id, "ping timeout", d)
|
||||
l.Debugln(c.id, "ping ->")
|
||||
}
|
||||
c.close(ErrTimeout)
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln(c.id, "last read within", d)
|
||||
rc <- c.ping()
|
||||
}()
|
||||
select {
|
||||
case ok := <-rc:
|
||||
if debug {
|
||||
l.Debugln(c.id, "<- pong")
|
||||
}
|
||||
if !ok {
|
||||
c.close(fmt.Errorf("ping failure"))
|
||||
}
|
||||
case <-time.After(PingTimeout):
|
||||
c.close(fmt.Errorf("ping timeout"))
|
||||
case <-c.closed:
|
||||
return
|
||||
}
|
||||
|
||||
case <-c.closed:
|
||||
lib/protocol/protocol_test.go → Godeps/_workspace/src/github.com/syncthing/protocol/protocol_test.go
Generated
Vendored
+97
-7
@@ -6,6 +6,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -81,6 +82,94 @@ func TestPing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPingErr(t *testing.T) {
|
||||
e := errors.New("something broke")
|
||||
|
||||
for i := 0; i < 32; i++ {
|
||||
for j := 0; j < 32; j++ {
|
||||
m0 := newTestModel()
|
||||
m1 := newTestModel()
|
||||
|
||||
ar, aw := io.Pipe()
|
||||
br, bw := io.Pipe()
|
||||
eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
|
||||
ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
|
||||
|
||||
c0 := NewConnection(c0ID, ar, ebw, m0, "name", CompressAlways).(wireFormatConnection).next.(*rawConnection)
|
||||
c0.Start()
|
||||
c1 := NewConnection(c1ID, br, eaw, m1, "name", CompressAlways)
|
||||
c1.Start()
|
||||
c0.ClusterConfig(ClusterConfigMessage{})
|
||||
c1.ClusterConfig(ClusterConfigMessage{})
|
||||
|
||||
res := c0.ping()
|
||||
if (i < 8 || j < 8) && res {
|
||||
// This should have resulted in failure, as there is no way an empty ClusterConfig plus a Ping message fits in eight bytes.
|
||||
t.Errorf("Unexpected ping success; i=%d, j=%d", i, j)
|
||||
} else if (i >= 28 && j >= 28) && !res {
|
||||
// This should have worked though, as 28 bytes is plenty for both.
|
||||
t.Errorf("Unexpected ping fail; i=%d, j=%d", i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// func TestRequestResponseErr(t *testing.T) {
|
||||
// e := errors.New("something broke")
|
||||
|
||||
// var pass bool
|
||||
// for i := 0; i < 48; i++ {
|
||||
// for j := 0; j < 38; j++ {
|
||||
// m0 := newTestModel()
|
||||
// m0.data = []byte("response data")
|
||||
// m1 := newTestModel()
|
||||
|
||||
// ar, aw := io.Pipe()
|
||||
// br, bw := io.Pipe()
|
||||
// eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
|
||||
// ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
|
||||
|
||||
// NewConnection(c0ID, ar, ebw, m0, nil)
|
||||
// c1 := NewConnection(c1ID, br, eaw, m1, nil).(wireFormatConnection).next.(*rawConnection)
|
||||
|
||||
// d, err := c1.Request("default", "tn", 1234, 5678)
|
||||
// if err == e || err == ErrClosed {
|
||||
// t.Logf("Error at %d+%d bytes", i, j)
|
||||
// if !m1.isClosed() {
|
||||
// t.Fatal("c1 not closed")
|
||||
// }
|
||||
// if !m0.isClosed() {
|
||||
// t.Fatal("c0 not closed")
|
||||
// }
|
||||
// continue
|
||||
// }
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// if string(d) != "response data" {
|
||||
// t.Fatalf("Incorrect response data %q", string(d))
|
||||
// }
|
||||
// if m0.folder != "default" {
|
||||
// t.Fatalf("Incorrect folder %q", m0.folder)
|
||||
// }
|
||||
// if m0.name != "tn" {
|
||||
// t.Fatalf("Incorrect name %q", m0.name)
|
||||
// }
|
||||
// if m0.offset != 1234 {
|
||||
// t.Fatalf("Incorrect offset %d", m0.offset)
|
||||
// }
|
||||
// if m0.size != 5678 {
|
||||
// t.Fatalf("Incorrect size %d", m0.size)
|
||||
// }
|
||||
// t.Logf("Pass at %d+%d bytes", i, j)
|
||||
// pass = true
|
||||
// }
|
||||
// }
|
||||
// if !pass {
|
||||
// t.Fatal("Never passed")
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestVersionErr(t *testing.T) {
|
||||
m0 := newTestModel()
|
||||
m1 := newTestModel()
|
||||
@@ -172,7 +261,9 @@ func TestClose(t *testing.T) {
|
||||
|
||||
func TestElementSizeExceededNested(t *testing.T) {
|
||||
m := ClusterConfigMessage{
|
||||
ClientName: "longstringlongstringlongstringinglongstringlongstringlonlongstringlongstringlon",
|
||||
Folders: []Folder{
|
||||
{ID: "longstringlongstringlongstringinglongstringlongstringlonlongstringlongstringlon"},
|
||||
},
|
||||
}
|
||||
_, err := m.EncodeXDR(ioutil.Discard)
|
||||
if err == nil {
|
||||
@@ -187,12 +278,11 @@ func TestMarshalIndexMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
f := func(m1 IndexMessage) bool {
|
||||
for i, f := range m1.Files {
|
||||
m1.Files[i].CachedSize = 0
|
||||
for j := range f.Blocks {
|
||||
f.Blocks[j].Offset = 0
|
||||
if len(f.Blocks[j].Hash) == 0 {
|
||||
f.Blocks[j].Hash = nil
|
||||
for _, f := range m1.Files {
|
||||
for i := range f.Blocks {
|
||||
f.Blocks[i].Offset = 0
|
||||
if len(f.Blocks[i].Hash) == 0 {
|
||||
f.Blocks[i].Hash = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+31
-31
@@ -4,7 +4,7 @@ package protocol
|
||||
|
||||
// The Vector type represents a version vector. The zero value is a usable
|
||||
// version vector. The vector has slice semantics and some operations on it
|
||||
// are "append-like" in that they may return the same vector modified, or v
|
||||
// are "append-like" in that they may return the same vector modified, or a
|
||||
// new allocated Vector with the modified contents.
|
||||
type Vector []Counter
|
||||
|
||||
@@ -33,43 +33,43 @@ func (v Vector) Update(ID uint64) Vector {
|
||||
return nv
|
||||
}
|
||||
}
|
||||
// Append a new index
|
||||
// Append a new new index
|
||||
return append(v, Counter{ID, 1})
|
||||
}
|
||||
|
||||
// Merge returns the vector containing the maximum indexes from v and b. If it
|
||||
// is possible, the vector v is updated and returned. If it is not, a copy
|
||||
// Merge returns the vector containing the maximum indexes from a and b. If it
|
||||
// is possible, the vector a is updated and returned. If it is not, a copy
|
||||
// will be created, updated and returned.
|
||||
func (v Vector) Merge(b Vector) Vector {
|
||||
var vi, bi int
|
||||
func (a Vector) Merge(b Vector) Vector {
|
||||
var ai, bi int
|
||||
for bi < len(b) {
|
||||
if vi == len(v) {
|
||||
// We've reach the end of v, all that remains are appends
|
||||
return append(v, b[bi:]...)
|
||||
if ai == len(a) {
|
||||
// We've reach the end of a, all that remains are appends
|
||||
return append(a, b[bi:]...)
|
||||
}
|
||||
|
||||
if v[vi].ID > b[bi].ID {
|
||||
if a[ai].ID > b[bi].ID {
|
||||
// The index from b should be inserted here
|
||||
n := make(Vector, len(v)+1)
|
||||
copy(n, v[:vi])
|
||||
n[vi] = b[bi]
|
||||
copy(n[vi+1:], v[vi:])
|
||||
v = n
|
||||
n := make(Vector, len(a)+1)
|
||||
copy(n, a[:ai])
|
||||
n[ai] = b[bi]
|
||||
copy(n[ai+1:], a[ai:])
|
||||
a = n
|
||||
}
|
||||
|
||||
if v[vi].ID == b[bi].ID {
|
||||
if val := b[bi].Value; val > v[vi].Value {
|
||||
v[vi].Value = val
|
||||
if a[ai].ID == b[bi].ID {
|
||||
if v := b[bi].Value; v > a[ai].Value {
|
||||
a[ai].Value = v
|
||||
}
|
||||
}
|
||||
|
||||
if bi < len(b) && v[vi].ID == b[bi].ID {
|
||||
if bi < len(b) && a[ai].ID == b[bi].ID {
|
||||
bi++
|
||||
}
|
||||
vi++
|
||||
ai++
|
||||
}
|
||||
|
||||
return v
|
||||
return a
|
||||
}
|
||||
|
||||
// Copy returns an identical vector that is not shared with v.
|
||||
@@ -80,27 +80,27 @@ func (v Vector) Copy() Vector {
|
||||
}
|
||||
|
||||
// Equal returns true when the two vectors are equivalent.
|
||||
func (v Vector) Equal(b Vector) bool {
|
||||
return v.Compare(b) == Equal
|
||||
func (a Vector) Equal(b Vector) bool {
|
||||
return a.Compare(b) == Equal
|
||||
}
|
||||
|
||||
// LesserEqual returns true when the two vectors are equivalent or v is Lesser
|
||||
// LesserEqual returns true when the two vectors are equivalent or a is Lesser
|
||||
// than b.
|
||||
func (v Vector) LesserEqual(b Vector) bool {
|
||||
comp := v.Compare(b)
|
||||
func (a Vector) LesserEqual(b Vector) bool {
|
||||
comp := a.Compare(b)
|
||||
return comp == Lesser || comp == Equal
|
||||
}
|
||||
|
||||
// GreaterEqual returns true when the two vectors are equivalent or v is Greater
|
||||
// LesserEqual returns true when the two vectors are equivalent or a is Greater
|
||||
// than b.
|
||||
func (v Vector) GreaterEqual(b Vector) bool {
|
||||
comp := v.Compare(b)
|
||||
func (a Vector) GreaterEqual(b Vector) bool {
|
||||
comp := a.Compare(b)
|
||||
return comp == Greater || comp == Equal
|
||||
}
|
||||
|
||||
// Concurrent returns true when the two vectors are concrurrent.
|
||||
func (v Vector) Concurrent(b Vector) bool {
|
||||
comp := v.Compare(b)
|
||||
func (a Vector) Concurrent(b Vector) bool {
|
||||
comp := a.Compare(b)
|
||||
return comp == ConcurrentGreater || comp == ConcurrentLesser
|
||||
}
|
||||
|
||||
Generated
Vendored
Generated
Vendored
Generated
Vendored
+3
-3
@@ -123,12 +123,12 @@ func TestMerge(t *testing.T) {
|
||||
func TestCounterValue(t *testing.T) {
|
||||
v0 := Vector{Counter{42, 1}, Counter{64, 5}}
|
||||
if v0.Counter(42) != 1 {
|
||||
t.Errorf("Counter error, %d != %d", v0.Counter(42), 1)
|
||||
t.Error("Counter error, %d != %d", v0.Counter(42), 1)
|
||||
}
|
||||
if v0.Counter(64) != 5 {
|
||||
t.Errorf("Counter error, %d != %d", v0.Counter(64), 5)
|
||||
t.Error("Counter error, %d != %d", v0.Counter(64), 5)
|
||||
}
|
||||
if v0.Counter(72) != 0 {
|
||||
t.Errorf("Counter error, %d != %d", v0.Counter(72), 0)
|
||||
t.Error("Counter error, %d != %d", v0.Counter(72), 0)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
Generated
Vendored
-1
@@ -201,7 +201,6 @@ func (p *BufferPool) String() string {
|
||||
|
||||
func (p *BufferPool) drain() {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
|
||||
+8
-2
@@ -6,8 +6,10 @@ Suture
|
||||
Suture provides Erlang-ish supervisor trees for Go. "Supervisor trees" ->
|
||||
"sutree" -> "suture" -> holds your code together when it's trying to die.
|
||||
|
||||
This library has hit maturity, and isn't expected to be changed
|
||||
radically. This can also be imported via gopkg.in/thejerf/suture.v1 .
|
||||
This is intended to be a production-quality library going into code that I
|
||||
will be very early on the phone tree to support when it goes down. However,
|
||||
it has not been deployed into something quite that serious yet. (I will
|
||||
update this statement when that changes.)
|
||||
|
||||
It is intended to deal gracefully with the real failure cases that can
|
||||
occur with supervision trees (such as burning all your CPU time endlessly
|
||||
@@ -22,6 +24,10 @@ This module is fully covered with [godoc](http://godoc.org/github.com/thejerf/su
|
||||
including an example, usage, and everything else you might expect from a
|
||||
README.md on GitHub. (DRY.)
|
||||
|
||||
This is not currently tagged with particular git tags for Go as this is
|
||||
currently considered to be alpha code. As I move this into production and
|
||||
feel more confident about it, I'll give it relevant tags.
|
||||
|
||||
Code Signing
|
||||
------------
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ rumpelsepp <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
|
||||
sciurius <jvromans@squirrel.nl>
|
||||
seehuhn <voss@seehuhn.de>
|
||||
snnd <dw@risu.io>
|
||||
simplypeachy <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
|
||||
timabell <tim@timwise.co.uk>
|
||||
tnn2 <tnn@nygren.pp.se>
|
||||
tojrobinson <tully@tojr.org>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
# Syncthing
|
||||
Syncthing
|
||||
=========
|
||||
|
||||
[](http://build.syncthing.net/job/syncthing/lastBuild/)
|
||||
[](https://ci.appveyor.com/project/calmh/syncthing)
|
||||
[](http://godoc.org/github.com/syncthing/syncthing)
|
||||
[](https://www.mozilla.org/MPL/2.0/)
|
||||
[](http://build.syncthing.net/job/syncthing/lastBuild/)
|
||||
[](http://godoc.org/github.com/syncthing/syncthing)
|
||||
[](https://www.mozilla.org/MPL/2.0/)
|
||||
|
||||
This is the Syncthing project which pursues the following goals:
|
||||
|
||||
1. Define a protocol for synchronization of a folder between a number of
|
||||
collaborating devices. This protocol should be well defined, unambiguous,
|
||||
easily understood, free to use, efficient, secure and language neutral.
|
||||
This is called the [Block Exchange Protocol][1].
|
||||
This is called the [Block Exchange
|
||||
Protocol](https://github.com/syncthing/specs/blob/master/BEPv1.md).
|
||||
|
||||
2. Provide the reference implementation to demonstrate the usability of
|
||||
said protocol. This is the `syncthing` utility. We hope that
|
||||
@@ -20,38 +21,38 @@ The two are evolving together; the protocol is not to be considered
|
||||
stable until Syncthing 1.0 is released, at which point it is locked down
|
||||
for incompatible changes.
|
||||
|
||||
## Getting Started
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
Take a look at the [getting started guide][2].
|
||||
Take a look at the [getting started
|
||||
guide](http://docs.syncthing.net/intro/getting-started.html).
|
||||
|
||||
There are a few examples for keeping Syncthing running in the background
|
||||
on your system in [the etc directory][3].
|
||||
on your system in [the etc directory](https://github.com/syncthing/syncthing/blob/master/etc).
|
||||
|
||||
There is an IRC channel, `#syncthing` on [Freenode][4], for talking directly
|
||||
There is an IRC channel, `#syncthing` on Freenode, for talking directly
|
||||
to developers and users.
|
||||
|
||||
## Building
|
||||
Building
|
||||
--------
|
||||
|
||||
Building Syncthing from source is easy, and there's a [guide][5].
|
||||
Building Syncthing from source is easy, and there's a
|
||||
[guide](http://docs.syncthing.net/dev/building.html).
|
||||
that describes it for both Unix and Windows systems.
|
||||
|
||||
## Signed Releases
|
||||
Signed Releases
|
||||
---------------
|
||||
|
||||
As of v0.10.15 and onwards, git tags and release binaries are GPG signed
|
||||
with the key D26E6ED000654A3E (see https://syncthing.net/security.html).
|
||||
For release binaries, MD5 and SHA1 checksums are calculated and signed,
|
||||
available in the md5sum.txt.asc and sha1sum.txt.asc files.
|
||||
|
||||
## Documentation
|
||||
Documentation
|
||||
=============
|
||||
|
||||
Please see the [Syncthing documentation site][6].
|
||||
Please see the [Syncthing
|
||||
documentation site](http://docs.syncthing.net/).
|
||||
|
||||
All code is licensed under the [MPLv2 License][7].
|
||||
|
||||
[1]: https://github.com/syncthing/specs/blob/master/BEPv1.md
|
||||
[2]: http://docs.syncthing.net/intro/getting-started.html
|
||||
[3]: https://github.com/syncthing/syncthing/blob/master/etc
|
||||
[4]: https://webchat.freenode.net/
|
||||
[5]: http://docs.syncthing.net/dev/building.html
|
||||
[6]: http://docs.syncthing.net/
|
||||
[7]: https://github.com/syncthing/syncthing/blob/master/LICENSE
|
||||
All code is licensed under the
|
||||
[MPLv2 License](https://github.com/syncthing/syncthing/blob/master/LICENSE).
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
version: '{branch}-{build}'
|
||||
clone_folder: C:\src\github.com\syncthing\syncthing
|
||||
init:
|
||||
- go version
|
||||
environment:
|
||||
GOPATH: C:\
|
||||
build_script:
|
||||
- go run build.go zip
|
||||
test_script:
|
||||
- go run build.go test
|
||||
artifacts:
|
||||
- path: '*.zip'
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -214,7 +215,7 @@ func build(pkg string, tags []string) {
|
||||
binary += ".exe"
|
||||
}
|
||||
|
||||
rmr(binary)
|
||||
rmr(binary, binary+".md5")
|
||||
args := []string{"build", "-ldflags", ldflags()}
|
||||
if len(tags) > 0 {
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
@@ -225,6 +226,13 @@ func build(pkg string, tags []string) {
|
||||
args = append(args, pkg)
|
||||
setBuildEnv()
|
||||
runPrint("go", args...)
|
||||
|
||||
// Create an md5 checksum of the binary, to be included in the archive for
|
||||
// automatic upgrades.
|
||||
err := md5File(binary)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func buildTar() {
|
||||
@@ -241,6 +249,7 @@ func buildTar() {
|
||||
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
||||
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
||||
{src: "syncthing", dst: name + "/syncthing"},
|
||||
{src: "syncthing.md5", dst: name + "/syncthing.md5"},
|
||||
}
|
||||
|
||||
for _, file := range listFiles("etc") {
|
||||
@@ -268,6 +277,7 @@ func buildZip() {
|
||||
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
||||
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
||||
{src: "syncthing.exe", dst: name + "/syncthing.exe"},
|
||||
{src: "syncthing.exe.md5", dst: name + "/syncthing.exe.md5"},
|
||||
}
|
||||
|
||||
for _, file := range listFiles("extra") {
|
||||
@@ -401,7 +411,7 @@ func assets() {
|
||||
}
|
||||
|
||||
func xdr() {
|
||||
runPrint("go", "generate", "./lib/discover", "./lib/db", "./lib/protocol")
|
||||
runPrint("go", "generate", "./lib/discover", "./lib/db")
|
||||
}
|
||||
|
||||
func translate() {
|
||||
@@ -702,6 +712,32 @@ func zipFile(out string, files []archiveFile) {
|
||||
}
|
||||
}
|
||||
|
||||
func md5File(file string) error {
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
h := md5.New()
|
||||
_, err = io.Copy(h, fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(file + ".md5")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(out, "%x\n", h.Sum(nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
func vet(pkg string) {
|
||||
bs, err := runError("go", "vet", pkg)
|
||||
if err != nil && err.Error() == "exit status 3" || bytes.Contains(bs, []byte("no such tool \"vet\"")) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/scanner"
|
||||
)
|
||||
|
||||
@@ -68,7 +68,7 @@ func main() {
|
||||
if *standardBlocks || blockSize < protocol.BlockSize {
|
||||
blockSize = protocol.BlockSize
|
||||
}
|
||||
bs, err := scanner.Blocks(fd, blockSize, fi.Size(), nil)
|
||||
bs, err := scanner.Blocks(fd, blockSize, fi.Size())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
+13
-81
@@ -7,105 +7,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
var timeout = 5 * time.Second
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
var server string
|
||||
|
||||
flag.StringVar(&server, "server", "", "Announce server (blank for default set)")
|
||||
flag.DurationVar(&timeout, "timeout", timeout, "Query timeout")
|
||||
flag.Usage = usage
|
||||
flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22026", "Announce server")
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() != 1 {
|
||||
flag.Usage()
|
||||
if len(flag.Args()) != 1 || server == "" {
|
||||
log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22026\"] <device>", os.Args[0])
|
||||
os.Exit(64)
|
||||
}
|
||||
|
||||
id, err := protocol.DeviceIDFromString(flag.Args()[0])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if server != "" {
|
||||
checkServers(id, server)
|
||||
} else {
|
||||
checkServers(id, config.DefaultDiscoveryServers...)
|
||||
discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil)
|
||||
discoverer.StartGlobal([]string{server}, 1)
|
||||
for _, addr := range discoverer.Lookup(id) {
|
||||
log.Println(addr)
|
||||
}
|
||||
}
|
||||
|
||||
type checkResult struct {
|
||||
server string
|
||||
direct []string
|
||||
relays []discover.Relay
|
||||
error
|
||||
}
|
||||
|
||||
func checkServers(deviceID protocol.DeviceID, servers ...string) {
|
||||
t0 := time.Now()
|
||||
resc := make(chan checkResult)
|
||||
for _, srv := range servers {
|
||||
srv := srv
|
||||
go func() {
|
||||
res := checkServer(deviceID, srv)
|
||||
res.server = srv
|
||||
resc <- res
|
||||
}()
|
||||
}
|
||||
|
||||
for _ = range servers {
|
||||
res := <-resc
|
||||
|
||||
u, _ := url.Parse(res.server)
|
||||
fmt.Printf("%s (%v):\n", u.Host, time.Since(t0))
|
||||
|
||||
if res.error != nil {
|
||||
fmt.Println(" " + res.error.Error())
|
||||
}
|
||||
for _, addr := range res.direct {
|
||||
fmt.Println(" address:", addr)
|
||||
}
|
||||
for _, rel := range res.relays {
|
||||
fmt.Printf(" relay: %s (%d ms)\n", rel.URL, rel.Latency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkServer(deviceID protocol.DeviceID, server string) checkResult {
|
||||
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, nil)
|
||||
if err != nil {
|
||||
return checkResult{error: err}
|
||||
}
|
||||
|
||||
res := make(chan checkResult, 1)
|
||||
|
||||
time.AfterFunc(timeout, func() {
|
||||
res <- checkResult{error: errors.New("timeout")}
|
||||
})
|
||||
|
||||
go func() {
|
||||
direct, relays, err := disco.Lookup(deviceID)
|
||||
res <- checkResult{direct: direct, relays: relays, error: err}
|
||||
}()
|
||||
|
||||
return <-res
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Printf("Usage:\n\t%s [options] <device ID>\n\nOptions:\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,8 +13,8 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/signature"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() < 1 {
|
||||
log.Println(`Usage:
|
||||
stsigtool <command>
|
||||
|
||||
Where command is one of:
|
||||
|
||||
gen
|
||||
- generate a new key pair
|
||||
|
||||
sign <privkeyfile> <datafile>
|
||||
- sign a file
|
||||
|
||||
verify <signaturefile> <datafile>
|
||||
- verify a signature, using the built in public key
|
||||
|
||||
verify <signaturefile> <datafile> <pubkeyfile>
|
||||
- verify a signature, using the specified public key file
|
||||
`)
|
||||
}
|
||||
|
||||
switch flag.Arg(0) {
|
||||
case "gen":
|
||||
gen()
|
||||
case "sign":
|
||||
sign(flag.Arg(1), flag.Arg(2))
|
||||
case "verify":
|
||||
if flag.NArg() == 4 {
|
||||
verifyWithFile(flag.Arg(1), flag.Arg(2), flag.Arg(3))
|
||||
} else {
|
||||
verifyWithKey(flag.Arg(1), flag.Arg(2), upgrade.SigningKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func gen() {
|
||||
priv, pub, err := signature.GenerateKeys()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
os.Stdout.Write(priv)
|
||||
os.Stdout.Write(pub)
|
||||
}
|
||||
|
||||
func sign(keyname, dataname string) {
|
||||
privkey, err := ioutil.ReadFile(keyname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fd, err := os.Open(dataname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
sig, err := signature.Sign(privkey, fd)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
os.Stdout.Write(sig)
|
||||
}
|
||||
|
||||
func verifyWithFile(signame, dataname, keyname string) {
|
||||
pubkey, err := ioutil.ReadFile(keyname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
verifyWithKey(signame, dataname, pubkey)
|
||||
}
|
||||
|
||||
func verifyWithKey(signame, dataname string, pubkey []byte) {
|
||||
sig, err := ioutil.ReadFile(signame)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fd, err := os.Open(dataname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
err = signature.Verify(pubkey, sig, fd)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("correct signature")
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
)
|
||||
|
||||
type addressLister struct {
|
||||
upnpSvc *upnpSvc
|
||||
cfg *config.Wrapper
|
||||
}
|
||||
|
||||
func newAddressLister(upnpSvc *upnpSvc, cfg *config.Wrapper) *addressLister {
|
||||
return &addressLister{
|
||||
upnpSvc: upnpSvc,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// ExternalAddresses returns a list of addresses that are our best guess for
|
||||
// where we are reachable from the outside. As a special case, we may return
|
||||
// one or more addresses with an empty IP address (0.0.0.0 or ::) and just
|
||||
// port number - this means that the outside address of a NAT gateway should
|
||||
// be substituted.
|
||||
func (e *addressLister) ExternalAddresses() []string {
|
||||
return e.addresses(false)
|
||||
}
|
||||
|
||||
// AllAddresses returns a list of addresses that are our best guess for where
|
||||
// we are reachable from the local network. Same conditions as
|
||||
// ExternalAddresses, but private IPv4 addresses are included.
|
||||
func (e *addressLister) AllAddresses() []string {
|
||||
return e.addresses(true)
|
||||
}
|
||||
|
||||
func (e *addressLister) addresses(includePrivateIPV4 bool) []string {
|
||||
var addrs []string
|
||||
|
||||
// Grab our listen addresses from the config. Unspecified ones are passed
|
||||
// on verbatim (to be interpreted by a global discovery server or local
|
||||
// discovery peer). Public addresses are passed on verbatim. Private
|
||||
// addresses are filtered.
|
||||
for _, addrStr := range e.cfg.Options().ListenAddress {
|
||||
addrURL, err := url.Parse(addrStr)
|
||||
if err != nil {
|
||||
l.Infoln("Listen address", addrStr, "is invalid:", err)
|
||||
continue
|
||||
}
|
||||
addr, err := net.ResolveTCPAddr("tcp", addrURL.Host)
|
||||
if err != nil {
|
||||
l.Infoln("Listen address", addrStr, "is invalid:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if addr.IP == nil || addr.IP.IsUnspecified() {
|
||||
// Address like 0.0.0.0:22000 or [::]:22000 or :22000; include as is.
|
||||
addrs = append(addrs, "tcp://"+addr.String())
|
||||
} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
|
||||
// A public address; include as is.
|
||||
addrs = append(addrs, "tcp://"+addr.String())
|
||||
} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
|
||||
// A private IPv4 address.
|
||||
addrs = append(addrs, "tcp://"+addr.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Get an external port mapping from the upnpSvc, if it has one. If so,
|
||||
// add it as another unspecified address.
|
||||
if e.upnpSvc != nil {
|
||||
if port := e.upnpSvc.ExternalPort(); port != 0 {
|
||||
addrs = append(addrs, fmt.Sprintf("tcp://:%d", port))
|
||||
}
|
||||
}
|
||||
|
||||
return addrs
|
||||
}
|
||||
|
||||
func isPublicIPv4(ip net.IP) bool {
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
// Not an IPv4 address (IPv6)
|
||||
return false
|
||||
}
|
||||
|
||||
// IsGlobalUnicast below only checks that it's not link local or
|
||||
// multicast, and we want to exclude private (NAT:ed) addresses as well.
|
||||
rfc1918 := []net.IPNet{
|
||||
{IP: net.IP{10, 0, 0, 0}, Mask: net.IPMask{255, 0, 0, 0}},
|
||||
{IP: net.IP{172, 16, 0, 0}, Mask: net.IPMask{255, 240, 0, 0}},
|
||||
{IP: net.IP{192, 168, 0, 0}, Mask: net.IPMask{255, 255, 0, 0}},
|
||||
}
|
||||
for _, n := range rfc1918 {
|
||||
if n.Contains(ip) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return ip.IsGlobalUnicast()
|
||||
}
|
||||
|
||||
func isPublicIPv6(ip net.IP) bool {
|
||||
if ip.To4() != nil {
|
||||
// Not an IPv6 address (IPv4)
|
||||
// (To16() returns a v6 mapped v4 address so can't be used to check
|
||||
// that it's an actual v6 address)
|
||||
return false
|
||||
}
|
||||
|
||||
return ip.IsGlobalUnicast()
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
// The connection service listens on TLS and dials configured unconnected
|
||||
// devices. Successful connections are handed to the model.
|
||||
type connectionSvc struct {
|
||||
*suture.Supervisor
|
||||
cfg *config.Wrapper
|
||||
myID protocol.DeviceID
|
||||
model *model.Model
|
||||
tlsCfg *tls.Config
|
||||
conns chan *tls.Conn
|
||||
}
|
||||
|
||||
func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.Model, tlsCfg *tls.Config) *connectionSvc {
|
||||
svc := &connectionSvc{
|
||||
Supervisor: suture.NewSimple("connectionSvc"),
|
||||
cfg: cfg,
|
||||
myID: myID,
|
||||
model: model,
|
||||
tlsCfg: tlsCfg,
|
||||
conns: make(chan *tls.Conn),
|
||||
}
|
||||
|
||||
// There are several moving parts here; one routine per listening address
|
||||
// to handle incoming connections, one routine to periodically attempt
|
||||
// outgoing connections, and lastly one routine to the the common handling
|
||||
// regardless of whether the connection was incoming or outgoing. It ends
|
||||
// up as in the diagram below. We embed a Supervisor to manage the
|
||||
// routines (i.e. log and restart if they crash or exit, etc).
|
||||
//
|
||||
// +-----------------+
|
||||
// Incoming | +---------------+-+ +-----------------+
|
||||
// Connections | | | | | Outgoing
|
||||
// -------------->| | svc.listen | | | Connections
|
||||
// | | (1 per listen | | svc.connect |-------------->
|
||||
// | | address) | | |
|
||||
// +-+ | | |
|
||||
// +-----------------+ +-----------------+
|
||||
// v v
|
||||
// | |
|
||||
// | |
|
||||
// +------------+-----------+
|
||||
// |
|
||||
// | svc.conns
|
||||
// v
|
||||
// +-----------------+
|
||||
// | |
|
||||
// | |
|
||||
// | svc.handle |------> model.AddConnection()
|
||||
// | |
|
||||
// | |
|
||||
// +-----------------+
|
||||
//
|
||||
// TODO: Clean shutdown, and/or handling config changes on the fly. We
|
||||
// partly do this now - new devices and addresses will be picked up, but
|
||||
// not new listen addresses and we don't support disconnecting devices
|
||||
// that are removed and so on...
|
||||
|
||||
svc.Add(serviceFunc(svc.connect))
|
||||
for _, addr := range svc.cfg.Options().ListenAddress {
|
||||
addr := addr
|
||||
listener := serviceFunc(func() {
|
||||
svc.listen(addr)
|
||||
})
|
||||
svc.Add(listener)
|
||||
}
|
||||
svc.Add(serviceFunc(svc.handle))
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (s *connectionSvc) handle() {
|
||||
next:
|
||||
for conn := range s.conns {
|
||||
cs := conn.ConnectionState()
|
||||
|
||||
// We should have negotiated the next level protocol "bep/1.0" as part
|
||||
// of the TLS handshake. Unfortunately this can't be a hard error,
|
||||
// because there are implementations out there that don't support
|
||||
// protocol negotiation (iOS for one...).
|
||||
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != bepProtocolName {
|
||||
l.Infof("Peer %s did not negotiate bep/1.0", conn.RemoteAddr())
|
||||
}
|
||||
|
||||
// We should have received exactly one certificate from the other
|
||||
// side. If we didn't, they don't have a device ID and we drop the
|
||||
// connection.
|
||||
certs := cs.PeerCertificates
|
||||
if cl := len(certs); cl != 1 {
|
||||
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, conn.RemoteAddr())
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
remoteCert := certs[0]
|
||||
remoteID := protocol.NewDeviceID(remoteCert.Raw)
|
||||
|
||||
// The device ID should not be that of ourselves. It can happen
|
||||
// though, especially in the presence of NAT hairpinning, multiple
|
||||
// clients between the same NAT gateway, and global discovery.
|
||||
if remoteID == myID {
|
||||
l.Infof("Connected to myself (%s) - should not happen", remoteID)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// We should not already be connected to the other party. TODO: This
|
||||
// could use some better handling. If the old connection is dead but
|
||||
// hasn't timed out yet we may want to drop *that* connection and keep
|
||||
// this one. But in case we are two devices connecting to each other
|
||||
// in parallel we don't want to do that or we end up with no
|
||||
// connections still established...
|
||||
if s.model.ConnectedTo(remoteID) {
|
||||
l.Infof("Connected to already connected device (%s)", remoteID)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
for deviceID, deviceCfg := range s.cfg.Devices() {
|
||||
if deviceID == remoteID {
|
||||
// Verify the name on the certificate. By default we set it to
|
||||
// "syncthing" when generating, but the user may have replaced
|
||||
// the certificate and used another name.
|
||||
certName := deviceCfg.CertName
|
||||
if certName == "" {
|
||||
certName = tlsDefaultCommonName
|
||||
}
|
||||
err := remoteCert.VerifyHostname(certName)
|
||||
if err != nil {
|
||||
// Incorrect certificate name is something the user most
|
||||
// likely wants to know about, since it's an advanced
|
||||
// config. Warn instead of Info.
|
||||
l.Warnf("Bad certificate from %s (%v): %v", remoteID, conn.RemoteAddr(), err)
|
||||
conn.Close()
|
||||
continue next
|
||||
}
|
||||
|
||||
// If rate limiting is set, and based on the address we should
|
||||
// limit the connection, then we wrap it in a limiter.
|
||||
|
||||
limit := s.shouldLimit(conn.RemoteAddr())
|
||||
|
||||
wr := io.Writer(conn)
|
||||
if limit && writeRateLimit != nil {
|
||||
wr = &limitedWriter{conn, writeRateLimit}
|
||||
}
|
||||
|
||||
rd := io.Reader(conn)
|
||||
if limit && readRateLimit != nil {
|
||||
rd = &limitedReader{conn, readRateLimit}
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
|
||||
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
|
||||
|
||||
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
||||
if debugNet {
|
||||
l.Debugf("cipher suite: %04X in lan: %t", conn.ConnectionState().CipherSuite, !limit)
|
||||
}
|
||||
|
||||
s.model.AddConnection(conn, protoConn)
|
||||
continue next
|
||||
}
|
||||
}
|
||||
|
||||
if !s.cfg.IgnoredDevice(remoteID) {
|
||||
events.Default.Log(events.DeviceRejected, map[string]string{
|
||||
"device": remoteID.String(),
|
||||
"address": conn.RemoteAddr().String(),
|
||||
})
|
||||
l.Infof("Connection from %s with unknown device ID %s", conn.RemoteAddr(), remoteID)
|
||||
} else {
|
||||
l.Infof("Connection from %s with ignored device ID %s", conn.RemoteAddr(), remoteID)
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) listen(addr string) {
|
||||
if debugNet {
|
||||
l.Debugln("listening on", addr)
|
||||
}
|
||||
|
||||
tcaddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
l.Fatalln("listen (BEP):", err)
|
||||
}
|
||||
listener, err := net.ListenTCP("tcp", tcaddr)
|
||||
if err != nil {
|
||||
l.Fatalln("listen (BEP):", err)
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
l.Warnln("Accepting connection:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if debugNet {
|
||||
l.Debugln("connect from", conn.RemoteAddr())
|
||||
}
|
||||
|
||||
tcpConn := conn.(*net.TCPConn)
|
||||
s.setTCPOptions(tcpConn)
|
||||
|
||||
tc := tls.Server(conn, s.tlsCfg)
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
l.Infoln("TLS handshake:", err)
|
||||
tc.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
s.conns <- tc
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) connect() {
|
||||
delay := time.Second
|
||||
for {
|
||||
nextDevice:
|
||||
for deviceID, deviceCfg := range s.cfg.Devices() {
|
||||
if deviceID == myID {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.model.ConnectedTo(deviceID) {
|
||||
continue
|
||||
}
|
||||
|
||||
var addrs []string
|
||||
for _, addr := range deviceCfg.Addresses {
|
||||
if addr == "dynamic" {
|
||||
if discoverer != nil {
|
||||
t := discoverer.Lookup(deviceID)
|
||||
if len(t) == 0 {
|
||||
continue
|
||||
}
|
||||
addrs = append(addrs, t...)
|
||||
}
|
||||
} else {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
|
||||
// addr is on the form "1.2.3.4"
|
||||
addr = net.JoinHostPort(addr, "22000")
|
||||
} else if err == nil && port == "" {
|
||||
// addr is on the form "1.2.3.4:"
|
||||
addr = net.JoinHostPort(host, "22000")
|
||||
}
|
||||
if debugNet {
|
||||
l.Debugln("dial", deviceCfg.DeviceID, addr)
|
||||
}
|
||||
|
||||
raddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
if debugNet {
|
||||
l.Debugln(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
conn, err := net.DialTCP("tcp", nil, raddr)
|
||||
if err != nil {
|
||||
if debugNet {
|
||||
l.Debugln(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
s.setTCPOptions(conn)
|
||||
|
||||
tc := tls.Client(conn, s.tlsCfg)
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
l.Infoln("TLS handshake:", err)
|
||||
tc.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
s.conns <- tc
|
||||
continue nextDevice
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
delay *= 2
|
||||
if maxD := time.Duration(s.cfg.Options().ReconnectIntervalS) * time.Second; delay > maxD {
|
||||
delay = maxD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (*connectionSvc) setTCPOptions(conn *net.TCPConn) {
|
||||
var err error
|
||||
if err = conn.SetLinger(0); err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
if err = conn.SetNoDelay(false); err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
if err = conn.SetKeepAlivePeriod(60 * time.Second); err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
if err = conn.SetKeepAlive(true); err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
|
||||
if s.cfg.Options().LimitBandwidthInLan {
|
||||
return true
|
||||
}
|
||||
|
||||
tcpaddr, ok := addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
for _, lan := range lans {
|
||||
if lan.Contains(tcpaddr.IP) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return !tcpaddr.IP.IsLoopback()
|
||||
}
|
||||
|
||||
func (s *connectionSvc) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
// We require a restart if a device as been removed.
|
||||
|
||||
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
|
||||
for _, dev := range to.Devices {
|
||||
newDevices[dev.DeviceID] = true
|
||||
}
|
||||
|
||||
for _, dev := range from.Devices {
|
||||
if !newDevices[dev.DeviceID] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+24
-71
@@ -25,6 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/calmh/logger"
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/auto"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
@@ -32,10 +33,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
"github.com/vitrun/qart/qr"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -58,25 +56,21 @@ type apiSvc struct {
|
||||
cfg config.GUIConfiguration
|
||||
assetDir string
|
||||
model *model.Model
|
||||
eventSub *events.BufferedSubscription
|
||||
discoverer *discover.CachingMux
|
||||
relaySvc *relay.Svc
|
||||
listener net.Listener
|
||||
fss *folderSummarySvc
|
||||
stop chan struct{}
|
||||
systemConfigMut sync.Mutex
|
||||
eventSub *events.BufferedSubscription
|
||||
}
|
||||
|
||||
func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc) (*apiSvc, error) {
|
||||
func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir string, m *model.Model, eventSub *events.BufferedSubscription) (*apiSvc, error) {
|
||||
svc := &apiSvc{
|
||||
id: id,
|
||||
cfg: cfg,
|
||||
assetDir: assetDir,
|
||||
model: m,
|
||||
eventSub: eventSub,
|
||||
discoverer: discoverer,
|
||||
relaySvc: relaySvc,
|
||||
systemConfigMut: sync.NewMutex(),
|
||||
eventSub: eventSub,
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -98,7 +92,7 @@ func (s *apiSvc) getListener(cfg config.GUIConfiguration) (net.Listener, error)
|
||||
name = tlsDefaultCommonName
|
||||
}
|
||||
|
||||
cert, err = tlsutil.NewCertificate(locations[locHTTPSCertFile], locations[locHTTPSKeyFile], name, tlsRSABits)
|
||||
cert, err = newCertificate(locations[locHTTPSCertFile], locations[locHTTPSKeyFile], name)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -126,7 +120,7 @@ func (s *apiSvc) getListener(cfg config.GUIConfiguration) (net.Listener, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listener := &tlsutil.DowngradingListener{rawListener, tlsCfg}
|
||||
listener := &DowngradingListener{rawListener, tlsCfg}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
@@ -167,6 +161,7 @@ func (s *apiSvc) Serve() {
|
||||
postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder
|
||||
postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
|
||||
postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // <body>
|
||||
postRestMux.HandleFunc("/rest/system/discovery", s.postSystemDiscovery) // device addr
|
||||
postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // <body>
|
||||
postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
|
||||
postRestMux.HandleFunc("/rest/system/ping", s.restPing) // -
|
||||
@@ -174,8 +169,6 @@ func (s *apiSvc) Serve() {
|
||||
postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart) // -
|
||||
postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown) // -
|
||||
postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade) // -
|
||||
postRestMux.HandleFunc("/rest/system/pause", s.postSystemPause) // device
|
||||
postRestMux.HandleFunc("/rest/system/resume", s.postSystemResume) // device
|
||||
|
||||
// Debug endpoints, not for general use
|
||||
getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
|
||||
@@ -632,30 +625,8 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
res["alloc"] = m.Alloc
|
||||
res["sys"] = m.Sys - m.HeapReleased
|
||||
res["tilde"] = tilde
|
||||
if cfg.Options().LocalAnnEnabled || cfg.Options().GlobalAnnEnabled {
|
||||
res["discoveryEnabled"] = true
|
||||
discoErrors := make(map[string]string)
|
||||
discoMethods := 0
|
||||
for disco, err := range s.discoverer.ChildErrors() {
|
||||
discoMethods++
|
||||
if err != nil {
|
||||
discoErrors[disco] = err.Error()
|
||||
}
|
||||
}
|
||||
res["discoveryMethods"] = discoMethods
|
||||
res["discoveryErrors"] = discoErrors
|
||||
}
|
||||
if s.relaySvc != nil {
|
||||
res["relaysEnabled"] = true
|
||||
relayClientStatus := make(map[string]bool)
|
||||
relayClientLatency := make(map[string]int)
|
||||
for _, relay := range s.relaySvc.Relays() {
|
||||
latency, ok := s.relaySvc.RelayStatus(relay)
|
||||
relayClientStatus[relay] = ok
|
||||
relayClientLatency[relay] = int(latency / time.Millisecond)
|
||||
}
|
||||
res["relayClientStatus"] = relayClientStatus
|
||||
res["relayClientLatency"] = relayClientLatency
|
||||
if cfg.Options().GlobalAnnEnabled && discoverer != nil {
|
||||
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
|
||||
}
|
||||
cpuUsageLock.RLock()
|
||||
var cpusum float64
|
||||
@@ -666,7 +637,6 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
res["cpuPercent"] = cpusum / float64(len(cpuUsagePercent)) / float64(runtime.NumCPU())
|
||||
res["pathSeparator"] = string(filepath.Separator)
|
||||
res["uptime"] = int(time.Since(startTime).Seconds())
|
||||
res["startTime"] = startTime
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
@@ -700,16 +670,25 @@ func (s *apiSvc) showGuiError(l logger.LogLevel, err string) {
|
||||
guiErrorsMut.Unlock()
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var device = qs.Get("device")
|
||||
var addr = qs.Get("addr")
|
||||
if len(device) != 0 && len(addr) != 0 && discoverer != nil {
|
||||
discoverer.Hint(device, []string{addr})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
devices := make(map[string]discover.CacheEntry)
|
||||
devices := map[string][]discover.CacheEntry{}
|
||||
|
||||
if s.discoverer != nil {
|
||||
if discoverer != nil {
|
||||
// Device ids can't be marshalled as keys so we need to manually
|
||||
// rebuild this map using strings. Discoverer may be nil if discovery
|
||||
// has not started yet.
|
||||
for device, entry := range s.discoverer.Cache() {
|
||||
devices[device.String()] = entry
|
||||
for device, entries := range discoverer.All() {
|
||||
devices[device.String()] = entries
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,7 +766,7 @@ func (s *apiSvc) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), 500)
|
||||
return
|
||||
}
|
||||
rel, err := upgrade.LatestRelease(cfg.Options().ReleasesURL, Version)
|
||||
rel, err := upgrade.LatestRelease(Version)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
@@ -830,7 +809,7 @@ func (s *apiSvc) getLang(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
rel, err := upgrade.LatestRelease(cfg.Options().ReleasesURL, Version)
|
||||
rel, err := upgrade.LatestRelease(Version)
|
||||
if err != nil {
|
||||
l.Warnln("getting latest release:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
@@ -851,32 +830,6 @@ func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemPause(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var deviceStr = qs.Get("device")
|
||||
|
||||
device, err := protocol.DeviceIDFromString(deviceStr)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
s.model.PauseDevice(device)
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemResume(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var deviceStr = qs.Get("device")
|
||||
|
||||
device, err := protocol.DeviceIDFromString(deviceStr)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
s.model.ResumeDevice(device)
|
||||
}
|
||||
|
||||
func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package connections
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -12,20 +12,13 @@ import (
|
||||
"github.com/juju/ratelimit"
|
||||
)
|
||||
|
||||
type LimitedReader struct {
|
||||
reader io.Reader
|
||||
type limitedReader struct {
|
||||
r io.Reader
|
||||
bucket *ratelimit.Bucket
|
||||
}
|
||||
|
||||
func NewReadLimiter(r io.Reader, b *ratelimit.Bucket) *LimitedReader {
|
||||
return &LimitedReader{
|
||||
reader: r,
|
||||
bucket: b,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LimitedReader) Read(buf []byte) (int, error) {
|
||||
n, err := r.reader.Read(buf)
|
||||
func (r *limitedReader) Read(buf []byte) (int, error) {
|
||||
n, err := r.r.Read(buf)
|
||||
if r.bucket != nil {
|
||||
r.bucket.Wait(int64(n))
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package connections
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -12,21 +12,14 @@ import (
|
||||
"github.com/juju/ratelimit"
|
||||
)
|
||||
|
||||
type LimitedWriter struct {
|
||||
writer io.Writer
|
||||
type limitedWriter struct {
|
||||
w io.Writer
|
||||
bucket *ratelimit.Bucket
|
||||
}
|
||||
|
||||
func NewWriteLimiter(w io.Writer, b *ratelimit.Bucket) *LimitedWriter {
|
||||
return &LimitedWriter{
|
||||
writer: w,
|
||||
bucket: b,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LimitedWriter) Write(buf []byte) (int, error) {
|
||||
func (w *limitedWriter) Write(buf []byte) (int, error) {
|
||||
if w.bucket != nil {
|
||||
w.bucket.Wait(int64(len(buf)))
|
||||
}
|
||||
return w.writer.Write(buf)
|
||||
return w.w.Write(buf)
|
||||
}
|
||||
+56
-110
@@ -27,19 +27,15 @@ import (
|
||||
|
||||
"github.com/calmh/logger"
|
||||
"github.com/juju/ratelimit"
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/connections"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay"
|
||||
"github.com/syncthing/syncthing/lib/symlinks"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
@@ -49,7 +45,7 @@ import (
|
||||
|
||||
var (
|
||||
Version = "unknown-dev"
|
||||
Codename = "Beryllium Bedbug"
|
||||
Codename = "Aluminium Ant"
|
||||
BuildEnv = "default"
|
||||
BuildStamp = "0"
|
||||
BuildDate time.Time
|
||||
@@ -69,10 +65,8 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
bepProtocolName = "bep/1.0"
|
||||
tlsDefaultCommonName = "syncthing"
|
||||
tlsRSABits = 3072
|
||||
pingEventInterval = time.Minute
|
||||
bepProtocolName = "bep/1.0"
|
||||
pingEventInterval = time.Minute
|
||||
)
|
||||
|
||||
var l = logger.DefaultLogger
|
||||
@@ -115,6 +109,7 @@ var (
|
||||
writeRateLimit *ratelimit.Bucket
|
||||
readRateLimit *ratelimit.Bucket
|
||||
stop = make(chan int)
|
||||
discoverer *discover.Discoverer
|
||||
cert tls.Certificate
|
||||
lans []*net.IPNet
|
||||
)
|
||||
@@ -203,7 +198,6 @@ var (
|
||||
logFile string
|
||||
auditEnabled bool
|
||||
verbose bool
|
||||
paused bool
|
||||
noRestart = os.Getenv("STNORESTART") != ""
|
||||
noUpgrade = os.Getenv("STNOUPGRADE") != ""
|
||||
guiAddress = os.Getenv("STGUIADDRESS") // legacy
|
||||
@@ -243,7 +237,6 @@ func main() {
|
||||
flag.StringVar(&upgradeTo, "upgrade-to", upgradeTo, "Force upgrade directly from specified URL")
|
||||
flag.BoolVar(&auditEnabled, "audit", false, "Write events to audit file")
|
||||
flag.BoolVar(&verbose, "verbose", false, "Print verbose log output")
|
||||
flag.BoolVar(&paused, "paused", false, "Start with all devices paused")
|
||||
|
||||
flag.Usage = usageFor(flag.CommandLine, usage, fmt.Sprintf(extraUsage, baseDirs["config"]))
|
||||
flag.Parse()
|
||||
@@ -300,13 +293,10 @@ func main() {
|
||||
l.Warnln("Key exists; will not overwrite.")
|
||||
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
|
||||
} else {
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName, tlsRSABits)
|
||||
if err != nil {
|
||||
l.Fatalln("Create certificate:", err)
|
||||
}
|
||||
cert, err = newCertificate(certFile, keyFile, tlsDefaultCommonName)
|
||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||
if err != nil {
|
||||
l.Fatalln("Load certificate:", err)
|
||||
l.Fatalln("load cert:", err)
|
||||
}
|
||||
if err == nil {
|
||||
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
|
||||
@@ -346,7 +336,7 @@ func main() {
|
||||
}
|
||||
|
||||
if doUpgrade || doUpgradeCheck {
|
||||
rel, err := upgrade.LatestRelease(cfg.Options().ReleasesURL, Version)
|
||||
rel, err := upgrade.LatestRelease(Version)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
}
|
||||
@@ -469,10 +459,9 @@ func syncthingMain() {
|
||||
// Ensure that that we have a certificate and key.
|
||||
cert, err := tls.LoadX509KeyPair(locations[locCertFile], locations[locKeyFile])
|
||||
if err != nil {
|
||||
l.Infof("Generating RSA key and certificate for %s...", tlsDefaultCommonName)
|
||||
cert, err = tlsutil.NewCertificate(locations[locCertFile], locations[locKeyFile], tlsDefaultCommonName, tlsRSABits)
|
||||
cert, err = newCertificate(locations[locCertFile], locations[locKeyFile], tlsDefaultCommonName)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
l.Fatalln("load cert:", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,20 +567,22 @@ func syncthingMain() {
|
||||
symlinks.Supported = false
|
||||
}
|
||||
|
||||
protocol.PingTimeout = time.Duration(opts.PingTimeoutS) * time.Second
|
||||
protocol.PingIdleTime = time.Duration(opts.PingIdleTimeS) * time.Second
|
||||
|
||||
if opts.MaxSendKbps > 0 {
|
||||
writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxSendKbps), int64(5*1000*opts.MaxSendKbps))
|
||||
}
|
||||
if opts.MaxRecvKbps > 0 {
|
||||
readRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxRecvKbps), int64(5*1000*opts.MaxRecvKbps))
|
||||
}
|
||||
|
||||
if (opts.MaxRecvKbps > 0 || opts.MaxSendKbps > 0) && !opts.LimitBandwidthInLan {
|
||||
lans, _ = osutil.GetLans()
|
||||
networks := make([]string, 0, len(lans))
|
||||
for _, lan := range lans {
|
||||
networks = append(networks, lan.String())
|
||||
}
|
||||
for _, lan := range opts.AlwaysLocalNets {
|
||||
_, ipnet, err := net.ParseCIDR(lan)
|
||||
if err != nil {
|
||||
l.Infoln("Network", lan, "is malformed:", err)
|
||||
continue
|
||||
}
|
||||
networks = append(networks, ipnet.String())
|
||||
}
|
||||
l.Infoln("Local networks:", strings.Join(networks, ", "))
|
||||
}
|
||||
|
||||
@@ -632,13 +623,7 @@ func syncthingMain() {
|
||||
m.StartDeadlockDetector(time.Duration(it) * time.Second)
|
||||
}
|
||||
} else if !IsRelease || IsBeta {
|
||||
m.StartDeadlockDetector(20 * time.Minute)
|
||||
}
|
||||
|
||||
if paused {
|
||||
for device := range cfg.Devices() {
|
||||
m.PauseDevice(device)
|
||||
}
|
||||
m.StartDeadlockDetector(20 * 60 * time.Second)
|
||||
}
|
||||
|
||||
// Clear out old indexes for other devices. Otherwise we'll start up and
|
||||
@@ -663,88 +648,32 @@ func syncthingMain() {
|
||||
|
||||
mainSvc.Add(m)
|
||||
|
||||
// GUI
|
||||
|
||||
setupGUI(mainSvc, cfg, m, apiSub)
|
||||
|
||||
// The default port we announce, possibly modified by setupUPnP next.
|
||||
|
||||
uri, err := url.Parse(opts.ListenAddress[0])
|
||||
if err != nil {
|
||||
l.Fatalf("Failed to parse listen address %s: %v", opts.ListenAddress[0], err)
|
||||
}
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||
addr, err := net.ResolveTCPAddr("tcp", opts.ListenAddress[0])
|
||||
if err != nil {
|
||||
l.Fatalln("Bad listen address:", err)
|
||||
}
|
||||
|
||||
// The externalAddr tracks our external addresses for discovery purposes.
|
||||
|
||||
var addrList *addressLister
|
||||
|
||||
// Start UPnP
|
||||
|
||||
if opts.UPnPEnabled {
|
||||
upnpSvc := newUPnPSvc(cfg, addr.Port)
|
||||
mainSvc.Add(upnpSvc)
|
||||
|
||||
// The external address tracker needs to know about the UPnP service
|
||||
// so it can check for an external mapped port.
|
||||
addrList = newAddressLister(upnpSvc, cfg)
|
||||
} else {
|
||||
addrList = newAddressLister(nil, cfg)
|
||||
}
|
||||
|
||||
// Start relay management
|
||||
|
||||
var relaySvc *relay.Svc
|
||||
if opts.RelaysEnabled && (opts.GlobalAnnEnabled || opts.RelayWithoutGlobalAnn) {
|
||||
relaySvc = relay.NewSvc(cfg, tlsCfg)
|
||||
mainSvc.Add(relaySvc)
|
||||
}
|
||||
|
||||
// Start discovery
|
||||
|
||||
cachedDiscovery := discover.NewCachingMux()
|
||||
mainSvc.Add(cachedDiscovery)
|
||||
localPort := addr.Port
|
||||
discoverer = discovery(localPort)
|
||||
|
||||
if cfg.Options().GlobalAnnEnabled {
|
||||
for _, srv := range cfg.GlobalDiscoveryServers() {
|
||||
l.Infoln("Using discovery server", srv)
|
||||
gd, err := discover.NewGlobal(srv, cert, addrList, relaySvc)
|
||||
if err != nil {
|
||||
l.Warnln("Global discovery:", err)
|
||||
continue
|
||||
}
|
||||
// Start UPnP. The UPnP service will restart global discovery if the
|
||||
// external port changes.
|
||||
|
||||
// Each global discovery server gets its results cached for five
|
||||
// minutes, and is not asked again for a minute when it's returned
|
||||
// unsuccessfully.
|
||||
cachedDiscovery.Add(gd, 5*time.Minute, time.Minute)
|
||||
}
|
||||
if opts.UPnPEnabled {
|
||||
upnpSvc := newUPnPSvc(cfg, localPort)
|
||||
mainSvc.Add(upnpSvc)
|
||||
}
|
||||
|
||||
if cfg.Options().LocalAnnEnabled {
|
||||
// v4 broadcasts
|
||||
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), addrList, relaySvc)
|
||||
if err != nil {
|
||||
l.Warnln("IPv4 local discovery:", err)
|
||||
} else {
|
||||
cachedDiscovery.Add(bcd, 0, 0)
|
||||
}
|
||||
// v6 multicasts
|
||||
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, addrList, relaySvc)
|
||||
if err != nil {
|
||||
l.Warnln("IPv6 local discovery:", err)
|
||||
} else {
|
||||
cachedDiscovery.Add(mcd, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// GUI
|
||||
|
||||
setupGUI(mainSvc, cfg, m, apiSub, cachedDiscovery, relaySvc)
|
||||
|
||||
// Start connection management
|
||||
|
||||
connectionSvc := connections.NewConnectionSvc(cfg, myID, m, tlsCfg, cachedDiscovery, relaySvc, bepProtocolName, tlsDefaultCommonName, lans)
|
||||
connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg)
|
||||
cfg.Subscribe(connectionSvc)
|
||||
mainSvc.Add(connectionSvc)
|
||||
|
||||
if cpuProfile {
|
||||
@@ -867,7 +796,7 @@ func startAuditing(mainSvc *suture.Supervisor) {
|
||||
l.Infoln("Audit log in", auditFile)
|
||||
}
|
||||
|
||||
func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc) {
|
||||
func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription) {
|
||||
opts := cfg.Options()
|
||||
guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
|
||||
|
||||
@@ -896,7 +825,7 @@ func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, a
|
||||
|
||||
urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
|
||||
l.Infoln("Starting web GUI on", urlShow)
|
||||
api, err := newAPISvc(myID, guiCfg, guiAssets, m, apiSub, discoverer, relaySvc)
|
||||
api, err := newAPISvc(myID, guiCfg, guiAssets, m, apiSub)
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot start GUI:", err)
|
||||
}
|
||||
@@ -942,7 +871,7 @@ func defaultConfig(myName string) config.Configuration {
|
||||
if err != nil {
|
||||
l.Fatalln("get free port (BEP):", err)
|
||||
}
|
||||
newCfg.Options.ListenAddress = []string{fmt.Sprintf("tcp://0.0.0.0:%d", port)}
|
||||
newCfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)}
|
||||
return newCfg
|
||||
}
|
||||
|
||||
@@ -967,6 +896,23 @@ func shutdown() {
|
||||
stop <- exitSuccess
|
||||
}
|
||||
|
||||
func discovery(extPort int) *discover.Discoverer {
|
||||
opts := cfg.Options()
|
||||
disc := discover.NewDiscoverer(myID, opts.ListenAddress)
|
||||
|
||||
if opts.LocalAnnEnabled {
|
||||
l.Infoln("Starting local discovery announcements")
|
||||
disc.StartLocal(opts.LocalAnnPort, opts.LocalAnnMCAddr)
|
||||
}
|
||||
|
||||
if opts.GlobalAnnEnabled {
|
||||
l.Infoln("Starting global discovery announcements")
|
||||
disc.StartGlobal(opts.GlobalAnnServers, uint16(extPort))
|
||||
}
|
||||
|
||||
return disc
|
||||
}
|
||||
|
||||
func ensureDir(dir string, mode int) {
|
||||
fi, err := os.Stat(dir)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -1080,7 +1026,7 @@ func autoUpgrade() {
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
rel, err := upgrade.LatestRelease(cfg.Options().ReleasesURL, Version)
|
||||
rel, err := upgrade.LatestRelease(Version)
|
||||
if err == upgrade.ErrUpgradeUnsupported {
|
||||
events.Default.Unsubscribe(sub)
|
||||
return
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/protocol"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package tlsutil
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
mr "math/rand"
|
||||
@@ -23,14 +22,17 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrIdentificationFailed = fmt.Errorf("failed to identify socket type")
|
||||
const (
|
||||
tlsRSABits = 3072
|
||||
tlsDefaultCommonName = "syncthing"
|
||||
)
|
||||
|
||||
func NewCertificate(certFile, keyFile, tlsDefaultCommonName string, tlsRSABits int) (tls.Certificate, error) {
|
||||
func newCertificate(certFile, keyFile, name string) (tls.Certificate, error) {
|
||||
l.Infof("Generating RSA key and certificate for %s...", name)
|
||||
|
||||
priv, err := rsa.GenerateKey(rand.Reader, tlsRSABits)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("generate key: %s", err)
|
||||
l.Fatalln("generate key:", err)
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
@@ -39,7 +41,7 @@ func NewCertificate(certFile, keyFile, tlsDefaultCommonName string, tlsRSABits i
|
||||
template := x509.Certificate{
|
||||
SerialNumber: new(big.Int).SetInt64(mr.Int63()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: tlsDefaultCommonName,
|
||||
CommonName: name,
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
@@ -47,38 +49,37 @@ func NewCertificate(certFile, keyFile, tlsDefaultCommonName string, tlsRSABits i
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("create cert: %s", err)
|
||||
l.Fatalln("create cert:", err)
|
||||
}
|
||||
|
||||
certOut, err := os.Create(certFile)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("save cert: %s", err)
|
||||
l.Fatalln("save cert:", err)
|
||||
}
|
||||
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("save cert: %s", err)
|
||||
l.Fatalln("save cert:", err)
|
||||
}
|
||||
err = certOut.Close()
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("save cert: %s", err)
|
||||
l.Fatalln("save cert:", err)
|
||||
}
|
||||
|
||||
keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("save key: %s", err)
|
||||
l.Fatalln("save key:", err)
|
||||
}
|
||||
err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("save key: %s", err)
|
||||
l.Fatalln("save key:", err)
|
||||
}
|
||||
err = keyOut.Close()
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("save key: %s", err)
|
||||
l.Fatalln("save key:", err)
|
||||
}
|
||||
|
||||
return tls.LoadX509KeyPair(certFile, keyFile)
|
||||
@@ -89,29 +90,15 @@ type DowngradingListener struct {
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
func (l *DowngradingListener) Accept() (net.Conn, error) {
|
||||
conn, isTLS, err := l.AcceptNoWrapTLS()
|
||||
|
||||
// We failed to identify the socket type, pretend that everything is fine,
|
||||
// and pass it to the underlying handler, and let them deal with it.
|
||||
if err == ErrIdentificationFailed {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
|
||||
if isTLS {
|
||||
return tls.Server(conn, l.TLSConfig), nil
|
||||
}
|
||||
return conn, nil
|
||||
type WrappedConnection struct {
|
||||
io.Reader
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (l *DowngradingListener) AcceptNoWrapTLS() (net.Conn, bool, error) {
|
||||
func (l *DowngradingListener) Accept() (net.Conn, error) {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
@@ -120,19 +107,20 @@ func (l *DowngradingListener) AcceptNoWrapTLS() (net.Conn, bool, error) {
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
if err != nil {
|
||||
// We hit a read error here, but the Accept() call succeeded so we must not return an error.
|
||||
// We return the connection as is with a special error which handles this
|
||||
// special case in Accept().
|
||||
return conn, false, ErrIdentificationFailed
|
||||
// We return the connection as is and let whoever tries to use it deal with the error.
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
return &UnionedConnection{br, conn}, bs[0] == 0x16, nil
|
||||
wrapper := &WrappedConnection{br, conn}
|
||||
|
||||
// 0x16 is the first byte of a TLS handshake
|
||||
if bs[0] == 0x16 {
|
||||
return tls.Server(wrapper, l.TLSConfig), nil
|
||||
}
|
||||
|
||||
return wrapper, nil
|
||||
}
|
||||
|
||||
type UnionedConnection struct {
|
||||
io.Reader
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *UnionedConnection) Read(b []byte) (n int, err error) {
|
||||
func (c *WrappedConnection) Read(b []byte) (n int, err error) {
|
||||
return c.Reader.Read(b)
|
||||
}
|
||||
+13
-27
@@ -11,30 +11,26 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/upnp"
|
||||
)
|
||||
|
||||
// The UPnP service runs a loop for discovery of IGDs (Internet Gateway
|
||||
// Devices) and setup/renewal of a port mapping.
|
||||
type upnpSvc struct {
|
||||
cfg *config.Wrapper
|
||||
localPort int
|
||||
extPort int
|
||||
extPortMut sync.Mutex
|
||||
stop chan struct{}
|
||||
cfg *config.Wrapper
|
||||
localPort int
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func newUPnPSvc(cfg *config.Wrapper, localPort int) *upnpSvc {
|
||||
return &upnpSvc{
|
||||
cfg: cfg,
|
||||
localPort: localPort,
|
||||
extPortMut: sync.NewMutex(),
|
||||
cfg: cfg,
|
||||
localPort: localPort,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *upnpSvc) Serve() {
|
||||
extPort := 0
|
||||
foundIGD := true
|
||||
s.stop = make(chan struct{})
|
||||
|
||||
@@ -42,15 +38,7 @@ func (s *upnpSvc) Serve() {
|
||||
igds := upnp.Discover(time.Duration(s.cfg.Options().UPnPTimeoutS) * time.Second)
|
||||
if len(igds) > 0 {
|
||||
foundIGD = true
|
||||
s.extPortMut.Lock()
|
||||
oldExtPort := s.extPort
|
||||
s.extPortMut.Unlock()
|
||||
|
||||
newExtPort := s.tryIGDs(igds, oldExtPort)
|
||||
|
||||
s.extPortMut.Lock()
|
||||
s.extPort = newExtPort
|
||||
s.extPortMut.Unlock()
|
||||
extPort = s.tryIGDs(igds, extPort)
|
||||
} else if foundIGD {
|
||||
// Only print a notice if we've previously found an IGD or this is
|
||||
// the first time around.
|
||||
@@ -76,13 +64,6 @@ func (s *upnpSvc) Stop() {
|
||||
close(s.stop)
|
||||
}
|
||||
|
||||
func (s *upnpSvc) ExternalPort() int {
|
||||
s.extPortMut.Lock()
|
||||
port := s.extPort
|
||||
s.extPortMut.Unlock()
|
||||
return port
|
||||
}
|
||||
|
||||
func (s *upnpSvc) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
|
||||
// Lets try all the IGDs we found and use the first one that works.
|
||||
// TODO: Use all of them, and sort out the resulting mess to the
|
||||
@@ -95,8 +76,13 @@ func (s *upnpSvc) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
|
||||
}
|
||||
|
||||
if extPort != prevExtPort {
|
||||
// External port changed; refresh the discovery announcement.
|
||||
// TODO: Don't reach out to some magic global here?
|
||||
l.Infof("New UPnP port mapping: external port %d to local port %d.", extPort, s.localPort)
|
||||
events.Default.Log(events.ExternalPortMappingChanged, map[string]int{"port": extPort})
|
||||
if s.cfg.Options().GlobalAnnEnabled {
|
||||
discoverer.StopGlobal()
|
||||
discoverer.StartGlobal(s.cfg.Options().GlobalAnnServers, uint16(extPort))
|
||||
}
|
||||
}
|
||||
if debugNet {
|
||||
l.Debugf("Created/updated UPnP port mapping for external port %d on device %s.", extPort, igd.FriendlyIdentifier())
|
||||
|
||||
@@ -10,26 +10,22 @@ import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
// Current version number of the usage report, for acceptance purposes. If
|
||||
// fields are added or changed this integer must be incremented so that users
|
||||
// are prompted for acceptance of the new report.
|
||||
const usageReportVersion = 2
|
||||
const usageReportVersion = 1
|
||||
|
||||
type usageReportingManager struct {
|
||||
model *model.Model
|
||||
@@ -81,7 +77,6 @@ func (m *usageReportingManager) String() string {
|
||||
// various places, so not part of the usageReportingSvc object.
|
||||
func reportData(m *model.Model) map[string]interface{} {
|
||||
res := make(map[string]interface{})
|
||||
res["urVersion"] = usageReportVersion
|
||||
res["uniqueID"] = cfg.Options().URUniqueID
|
||||
res["version"] = Version
|
||||
res["longVersion"] = LongVersion
|
||||
@@ -125,118 +120,10 @@ func reportData(m *model.Model) map[string]interface{} {
|
||||
if err == nil {
|
||||
res["memorySize"] = bytes / 1024 / 1024
|
||||
}
|
||||
res["numCPU"] = runtime.NumCPU()
|
||||
|
||||
var rescanIntvs []int
|
||||
folderUses := map[string]int{
|
||||
"readonly": 0,
|
||||
"ignorePerms": 0,
|
||||
"ignoreDelete": 0,
|
||||
"autoNormalize": 0,
|
||||
}
|
||||
for _, cfg := range cfg.Folders() {
|
||||
rescanIntvs = append(rescanIntvs, cfg.RescanIntervalS)
|
||||
|
||||
if cfg.ReadOnly {
|
||||
folderUses["readonly"]++
|
||||
}
|
||||
if cfg.IgnorePerms {
|
||||
folderUses["ignorePerms"]++
|
||||
}
|
||||
if cfg.IgnoreDelete {
|
||||
folderUses["ignoreDelete"]++
|
||||
}
|
||||
if cfg.AutoNormalize {
|
||||
folderUses["autoNormalize"]++
|
||||
}
|
||||
}
|
||||
sort.Ints(rescanIntvs)
|
||||
res["rescanIntvs"] = rescanIntvs
|
||||
res["folderUses"] = folderUses
|
||||
|
||||
deviceUses := map[string]int{
|
||||
"introducer": 0,
|
||||
"customCertName": 0,
|
||||
"compressAlways": 0,
|
||||
"compressMetadata": 0,
|
||||
"compressNever": 0,
|
||||
"dynamicAddr": 0,
|
||||
"staticAddr": 0,
|
||||
}
|
||||
for _, cfg := range cfg.Devices() {
|
||||
if cfg.Introducer {
|
||||
deviceUses["introducer"]++
|
||||
}
|
||||
if cfg.CertName != "" && cfg.CertName != "syncthing" {
|
||||
deviceUses["customCertName"]++
|
||||
}
|
||||
if cfg.Compression == protocol.CompressAlways {
|
||||
deviceUses["compressAlways"]++
|
||||
} else if cfg.Compression == protocol.CompressMetadata {
|
||||
deviceUses["compressMetadata"]++
|
||||
} else if cfg.Compression == protocol.CompressNever {
|
||||
deviceUses["compressNever"]++
|
||||
}
|
||||
for _, addr := range cfg.Addresses {
|
||||
if addr == "dynamic" {
|
||||
deviceUses["dynamicAddr"]++
|
||||
} else {
|
||||
deviceUses["staticAddr"]++
|
||||
}
|
||||
}
|
||||
}
|
||||
res["deviceUses"] = deviceUses
|
||||
|
||||
defaultAnnounceServersDNS, defaultAnnounceServersIP, otherAnnounceServers := 0, 0, 0
|
||||
for _, addr := range cfg.Options().GlobalAnnServers {
|
||||
if addr == "default" {
|
||||
defaultAnnounceServersDNS++
|
||||
} else if stringIn(addr, config.DefaultDiscoveryServersIP) {
|
||||
defaultAnnounceServersIP++
|
||||
} else {
|
||||
otherAnnounceServers++
|
||||
}
|
||||
}
|
||||
res["announce"] = map[string]interface{}{
|
||||
"globalEnabled": cfg.Options().GlobalAnnEnabled,
|
||||
"localEnabled": cfg.Options().LocalAnnEnabled,
|
||||
"defaultServersDNS": defaultAnnounceServersDNS,
|
||||
"defaultServersIP": defaultAnnounceServersIP,
|
||||
"otherServers": otherAnnounceServers,
|
||||
}
|
||||
|
||||
defaultRelayServers, otherRelayServers := 0, 0
|
||||
for _, addr := range cfg.Options().RelayServers {
|
||||
switch addr {
|
||||
case "dynamic+https://relays.syncthing.net":
|
||||
defaultRelayServers++
|
||||
default:
|
||||
otherRelayServers++
|
||||
}
|
||||
}
|
||||
res["relays"] = map[string]interface{}{
|
||||
"enabled": cfg.Options().RelaysEnabled,
|
||||
"defaultServers": defaultRelayServers,
|
||||
"otherServers": otherRelayServers,
|
||||
}
|
||||
|
||||
res["usesRateLimit"] = cfg.Options().MaxRecvKbps > 0 || cfg.Options().MaxSendKbps > 0
|
||||
|
||||
res["upgradeAllowedManual"] = !(upgrade.DisabledByCompilation || noUpgrade)
|
||||
res["upgradeAllowedAuto"] = !(upgrade.DisabledByCompilation || noUpgrade) && cfg.Options().AutoUpgradeIntervalH > 0
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func stringIn(needle string, haystack []string) bool {
|
||||
for _, s := range haystack {
|
||||
if needle == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type usageReportingService struct {
|
||||
model *model.Model
|
||||
stop chan struct{}
|
||||
@@ -247,21 +134,17 @@ func (s *usageReportingService) sendUsageReport() error {
|
||||
var b bytes.Buffer
|
||||
json.NewEncoder(&b).Encode(d)
|
||||
|
||||
transp := &http.Transport{}
|
||||
client := &http.Client{Transport: transp}
|
||||
var client = http.DefaultClient
|
||||
if BuildEnv == "android" {
|
||||
// This works around the lack of DNS resolution on Android... :(
|
||||
transp.Dial = func(network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, "194.126.249.13:443")
|
||||
tr := &http.Transport{
|
||||
Dial: func(network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, "194.126.249.13:443")
|
||||
},
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
if cfg.Options().URPostInsecurely {
|
||||
transp.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
_, err := client.Post(cfg.Options().URURL, "application/json", &b)
|
||||
_, err := client.Post("https://data.syncthing.net/newdata", "application/json", &b)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -271,7 +154,7 @@ func (s *usageReportingService) Serve() {
|
||||
l.Infoln("Starting usage reporting")
|
||||
defer l.Infoln("Stopping usage reporting")
|
||||
|
||||
t := time.NewTimer(time.Duration(cfg.Options().URInitialDelayS) * time.Second) // time to initial report at start
|
||||
t := time.NewTimer(10 * time.Minute) // time to initial report at start
|
||||
for {
|
||||
select {
|
||||
case <-s.stop:
|
||||
|
||||
@@ -8,7 +8,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
)
|
||||
@@ -61,7 +60,7 @@ func (s *verboseSvc) WaitForStart() {
|
||||
|
||||
func (s *verboseSvc) formatEvent(ev events.Event) string {
|
||||
switch ev.Type {
|
||||
case events.Ping, events.DownloadProgress, events.LocalIndexUpdated:
|
||||
case events.Ping, events.DownloadProgress:
|
||||
// Skip
|
||||
return ""
|
||||
|
||||
@@ -75,7 +74,7 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
||||
return fmt.Sprintf("Discovered device %v at %v", data["device"], data["addrs"])
|
||||
case events.DeviceConnected:
|
||||
data := ev.Data.(map[string]string)
|
||||
return fmt.Sprintf("Connected to device %v at %v (type %s)", data["id"], data["addr"], data["type"])
|
||||
return fmt.Sprintf("Connected to device %v at %v", data["id"], data["addr"])
|
||||
case events.DeviceDisconnected:
|
||||
data := ev.Data.(map[string]string)
|
||||
return fmt.Sprintf("Disconnected from device %v", data["id"])
|
||||
@@ -87,6 +86,9 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
||||
case events.RemoteIndexUpdated:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
return fmt.Sprintf("Device %v sent an index update for %q with %d items", data["device"], data["folder"], data["items"])
|
||||
case events.LocalIndexUpdated:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
return fmt.Sprintf("Updated index for folder %q with %v items", data["folder"], data["items"])
|
||||
|
||||
case events.DeviceRejected:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
@@ -121,35 +123,6 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
||||
delete(sum, "ignorePatterns")
|
||||
delete(sum, "stateChanged")
|
||||
return fmt.Sprintf("Summary for folder %q is %v", data["folder"], data["summary"])
|
||||
case events.FolderScanProgress:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
folder := data["folder"].(string)
|
||||
current := data["current"].(int64)
|
||||
total := data["total"].(int64)
|
||||
var pct int64
|
||||
if total > 0 {
|
||||
pct = 100 * current / total
|
||||
}
|
||||
return fmt.Sprintf("Scanning folder %q, %d%% done", folder, pct)
|
||||
|
||||
case events.DevicePaused:
|
||||
data := ev.Data.(map[string]string)
|
||||
device := data["device"]
|
||||
return fmt.Sprintf("Device %v was paused", device)
|
||||
case events.DeviceResumed:
|
||||
data := ev.Data.(map[string]string)
|
||||
device := data["device"]
|
||||
return fmt.Sprintf("Device %v was resumed", device)
|
||||
|
||||
case events.ExternalPortMappingChanged:
|
||||
data := ev.Data.(map[string]int)
|
||||
port := data["port"]
|
||||
return fmt.Sprintf("External port mapping changed; new port is %d.", port)
|
||||
case events.RelayStateChanged:
|
||||
data := ev.Data.(map[string][]string)
|
||||
newRelays := data["new"]
|
||||
return fmt.Sprintf("Relay state changed; connected relay(s) are %s.", strings.Join(newRelays, ", "))
|
||||
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %#v", ev.Type, ev)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Systemd Configuration
|
||||
|
||||
This directory contains configuration files for running Syncthing under the
|
||||
This directory contains configuration files for running syncthing under the
|
||||
"systemd" service manager on Linux both under either a systemd system service or
|
||||
systemd user service. For further documentation take a look at the [systemd
|
||||
section][1] on http://docs.syncthing.net.
|
||||
section][1] on the Github Wiki.
|
||||
|
||||
[1]: http://docs.syncthing.net/users/autostart.html#systemd
|
||||
[1]: http://docs.syncthing.net/users/autostart.html#systemd
|
||||
|
||||
@@ -41,17 +41,8 @@ identicon {
|
||||
position: relative;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
line-height: 1;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.identicon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.identicon rect {
|
||||
fill: #333;
|
||||
line-height: 1em;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@@ -66,6 +57,17 @@ identicon {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.identicon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.panel-heading .identicon {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.panel-heading .fa, .modal-header .fa {
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -75,6 +77,10 @@ identicon {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.identicon rect {
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.text-monospace {
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
@@ -249,7 +255,3 @@ ul.three-columns li, ul.two-columns li {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
.popover {
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"Edit Folder": "Edit Folder",
|
||||
"Editing": "Editing",
|
||||
"Enable UPnP": "Enable UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||
"Error": "Error",
|
||||
"External File Versioning": "External File Versioning",
|
||||
@@ -113,8 +113,6 @@
|
||||
"Override Changes": "Override Changes",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||
"Please wait": "Please wait",
|
||||
"Preview": "Preview",
|
||||
@@ -122,8 +120,6 @@
|
||||
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||
"RAM Utilization": "RAM Utilization",
|
||||
"Random": "Random",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Release Notes": "Release Notes",
|
||||
"Remove": "Remove",
|
||||
"Rescan": "Rescan",
|
||||
@@ -132,7 +128,6 @@
|
||||
"Restart": "Restart",
|
||||
"Restart Needed": "Restart Needed",
|
||||
"Restarting": "Restarting",
|
||||
"Resume": "Resume",
|
||||
"Reused": "Reused",
|
||||
"Save": "Save",
|
||||
"Scanning": "Scanning",
|
||||
@@ -212,7 +207,6 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.",
|
||||
"Yes": "Yes",
|
||||
"You must keep at least one version.": "You must keep at least one version.",
|
||||
"days": "days",
|
||||
"full documentation": "full documentation",
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"Add new folder?": "Aggiungere una nuova cartella?",
|
||||
"Address": "Indirizzo",
|
||||
"Addresses": "Indirizzi",
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Configuration": "Advanced Configuration",
|
||||
"Advanced": "Avanzato",
|
||||
"Advanced Configuration": "Configurazione avanzata",
|
||||
"All Data": "Tutti i Dati",
|
||||
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
|
||||
"Alphabetic": "Alfabetico",
|
||||
@@ -19,7 +19,7 @@
|
||||
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Qualsiasi dispositivo configurato in un introduttore verrà aggiunto anche a questo dispositivo.",
|
||||
"Automatic upgrades": "Aggiornamenti automatici",
|
||||
"Be careful!": "Be careful!",
|
||||
"Be careful!": "Fai attenzione!",
|
||||
"Bugs": "Bug",
|
||||
"CPU Utilization": "Utilizzo CPU",
|
||||
"Changelog": "Changelog",
|
||||
@@ -49,23 +49,23 @@
|
||||
"Edit Folder": "Modifica Cartella",
|
||||
"Editing": "Modifica di",
|
||||
"Enable UPnP": "Attiva UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci indirizzi separati da virgola (\"tcp://ip:porta\", \"tcp://host:porta\") oppure \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
|
||||
"Enter ignore patterns, one per line.": "Inserisci gli schemi di esclusione, uno per riga.",
|
||||
"Error": "Errore",
|
||||
"External File Versioning": "Controllo Versione Esterno",
|
||||
"Failed Items": "Failed Items",
|
||||
"Failed Items": "Elementi errati",
|
||||
"File Pull Order": "Ordine di prelievo dei file",
|
||||
"File Versioning": "Controllo Versione dei File",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Il software evita i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "I file sono spostati nella certella .stversions quando vengono sostituiti o cancellati da Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "I file sostituiti o eliminati da Syncthing vengono datati e spostati in una cartella .stversions.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "I file sono protetti dalle modifiche effettuate negli altri dispositivi, ma le modifiche effettuate in questo dispositivo verranno inviate anche al resto del cluster.",
|
||||
"Folder": "Folder",
|
||||
"Folder": "Cartella",
|
||||
"Folder ID": "ID Cartella",
|
||||
"Folder Master": "Cartella Principale",
|
||||
"Folder Path": "Percorso Cartella",
|
||||
"Folders": "Cartelle",
|
||||
"GUI": "GUI",
|
||||
"GUI": "Interfaccia grafica utente",
|
||||
"GUI Authentication Password": "Password di Autenticazione dell'Utente",
|
||||
"GUI Authentication User": "Utente dell'Interfaccia Grafica",
|
||||
"GUI Listen Addresses": "Indirizzi dell'Interfaccia Grafica",
|
||||
@@ -74,12 +74,12 @@
|
||||
"Global Discovery Server": "Server di Ricerca Globale",
|
||||
"Global State": "Stato Globale",
|
||||
"Help": "Aiuto",
|
||||
"Home page": "Home page",
|
||||
"Home page": "Pagina home",
|
||||
"Ignore": "Ignora",
|
||||
"Ignore Patterns": "Schemi Esclusione File",
|
||||
"Ignore Permissions": "Ignora Permessi",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite Velocità in Ingresso (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configurazione incorretta potrebbe danneggiare il contenuto delle cartelle e rendere Syncthing inoperativo.",
|
||||
"Introducer": "Introduttore",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversione della condizione indicata (ad es. non escludere)",
|
||||
"Keep Versions": "Versioni Mantenute",
|
||||
@@ -93,7 +93,7 @@
|
||||
"Major Upgrade": "Aggiornamento principale",
|
||||
"Maximum Age": "Durata Massima",
|
||||
"Metadata Only": "Solo i Metadati",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Minimo spazio libero su disco",
|
||||
"Move to top of queue": "Posiziona in cima alla coda",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Metacarattere multi-livello (corrisponde alle cartelle e alle sotto-cartelle)",
|
||||
"Never": "Mai",
|
||||
@@ -106,15 +106,15 @@
|
||||
"OK": "OK",
|
||||
"Off": "Disattiva",
|
||||
"Oldest First": "Prima il meno recente",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Out of Sync",
|
||||
"Options": "Opzioni",
|
||||
"Out of Sync": "Non sincronizzato",
|
||||
"Out of Sync Items": "Elementi Non Sincronizzati",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite Velocità in Uscita (KiB/s)",
|
||||
"Override Changes": "Ignora Modifiche",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Percorso della cartella nel computer locale. Verrà creata se non esiste già. Il carattere tilde (~) può essere utilizzato come scorciatoia per",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Percorso di salvataggio delle versioni (lasciare vuoto per utilizzare la cartella predefinita .stversions in questa cartella).",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "Pausa",
|
||||
"Paused": "In pausa",
|
||||
"Please consult the release notes before performing a major upgrade.": "Si prega di consultare le note di rilascio prima di eseguire un aggiornamento principale.",
|
||||
"Please wait": "Attendere prego",
|
||||
"Preview": "Anteprima",
|
||||
@@ -122,17 +122,17 @@
|
||||
"Quick guide to supported patterns": "Guida veloce agli schemi supportati",
|
||||
"RAM Utilization": "Utilizzo RAM",
|
||||
"Random": "Casuale",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "Reindirizzato tramite",
|
||||
"Relays": "Servers di reindirizzamento",
|
||||
"Release Notes": "Note di rilascio",
|
||||
"Remove": "Remove",
|
||||
"Remove": "Rimuovi",
|
||||
"Rescan": "Riscansiona",
|
||||
"Rescan All": "Riscansiona Tutto",
|
||||
"Rescan Interval": "Intervallo Scansione",
|
||||
"Restart": "Riavvia",
|
||||
"Restart Needed": "Riavvio Necessario",
|
||||
"Restarting": "Riavvio",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Riprendi",
|
||||
"Reused": "Riutilizzato",
|
||||
"Save": "Salva",
|
||||
"Scanning": "Scansione in corso",
|
||||
@@ -157,7 +157,7 @@
|
||||
"Source Code": "Codice Sorgente",
|
||||
"Staggered File Versioning": "Controllo Versione Cadenzato",
|
||||
"Start Browser": "Avvia Browser",
|
||||
"Statistics": "Statistics",
|
||||
"Statistics": "Statistiche",
|
||||
"Stopped": "Fermato",
|
||||
"Support": "Supporto",
|
||||
"Sync Protocol Listen Addresses": "Indirizzi del Protocollo di Sincronizzazione",
|
||||
@@ -180,18 +180,18 @@
|
||||
"The folder ID must be unique.": "L'ID della cartella dev'essere unico.",
|
||||
"The folder path cannot be blank.": "Il percorso della cartella non può essere vuoto.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Vengono utilizzati i seguenti intervalli temporali: per la prima ora viene mantenuta una versione ogni 30 secondi, per il primo giorno viene mantenuta una versione ogni ora, per i primi 30 giorni viene mantenuta una versione al giorno, successivamente viene mantenuta una versione ogni settimana fino al periodo massimo impostato.",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items could not be synchronized.": "Non è stato possibile sincronizzare i seguenti elementi",
|
||||
"The maximum age must be a number and cannot be blank.": "La durata massima dev'essere un numero e non può essere vuoto.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Lo spazio libero minimo su disco deve essere un numero non negativo tra 0 e 100 (inclusi)",
|
||||
"The number of days must be a number and cannot be blank.": "Il numero di giorni deve essere un numero e non può essere vuoto.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Il numero di giorni per conservare i file nel cestino. Zero significa per sempre.",
|
||||
"The number of old versions to keep, per file.": "Il numero di vecchie versioni da mantenere, per file.",
|
||||
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
|
||||
"The path cannot be blank.": "Il percorso non può essere vuoto.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Il limite di banda deve essere un numero non negativo (da 0 a infinito)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "L'intervallo di scansione deve essere un numero superiore a zero secondi.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Verranno effettuati tentativi in automatico e verranno sincronizzati quando l'errore sarà risolto.",
|
||||
"This is a major version upgrade.": "Questo è un aggiornamento di versione principale",
|
||||
"Trash Can File Versioning": "Controllo Versione con Cestino",
|
||||
"Unknown": "Sconosciuto",
|
||||
@@ -212,7 +212,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quando aggiungi una nuova cartella, ricordati che gli ID vengono utilizzati per collegare le cartelle nei dispositivi. Distinguono maiuscole e minuscole e devono corrispondere esattamente su tutti i dispositivi.",
|
||||
"Yes": "Sì",
|
||||
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
||||
"days": "days",
|
||||
"days": "giorni",
|
||||
"full documentation": "documentazione completa",
|
||||
"items": "elementi",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\"."
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
"Unknown": "Desconhecida",
|
||||
"Unshared": "Não compartilhada",
|
||||
"Unused": "Não utilizado",
|
||||
"Up to Date": "Sincronizada",
|
||||
"Up to Date": "Em sincronia",
|
||||
"Updated": "Atualizado",
|
||||
"Upgrade": "Atualização",
|
||||
"Upgrade To {%version%}": "Atualizar para {{version}}",
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Коли додаєте нову директорію, пам’ятайте, що ID цієї директорії використовується для того, щоб зв’язувати директорії разом між вузлами. Назви є чутливими до регістра та повинні співпадати точно між усіма вузлами.",
|
||||
"Yes": "Так",
|
||||
"You must keep at least one version.": "Ви повинні зберігати щонайменше одну версію.",
|
||||
"days": "days",
|
||||
"days": "днів",
|
||||
"full documentation": "повна документація",
|
||||
"items": "елементи",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хоче поділитися директорією \"{{folder}}\"."
|
||||
|
||||
+15
-46
@@ -190,25 +190,18 @@
|
||||
<!-- Folder list (top left) -->
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3 translate>Folders</h3>
|
||||
<div class="panel-group" id="folders">
|
||||
<div class="visible-xs"><h3><span translate>Folders</span></h3><hr></div>
|
||||
<div class="panel panel-default" ng-repeat="folder in folderList()">
|
||||
<div class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer">
|
||||
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id)}}%"></div>
|
||||
<div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id)}}%"></div>
|
||||
<h3 class="panel-title">
|
||||
<span class="fa fa-folder hidden-xs"></span>{{folder.id}}
|
||||
<span class="pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
|
||||
<span ng-switch-when="unknown"><span class="hidden-xs" translate>Unknown</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="stopped"><span class="hidden-xs" translate>Stopped</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="scanning">
|
||||
<span class="hidden-xs" translate>Scanning</span>
|
||||
<span class="hidden-xs" ng-if="scanPercentage(folder.id) != undefined">
|
||||
({{scanPercentage(folder.id)}}%)
|
||||
</span>
|
||||
<span class="visible-xs">◼</span>
|
||||
</span>
|
||||
<span ng-switch-when="scanning"><span class="hidden-xs" translate>Scanning</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="idle"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="syncing">
|
||||
<span class="hidden-xs" translate>Syncing</span>
|
||||
@@ -348,7 +341,7 @@
|
||||
<!-- This device -->
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3 translate>Devices</h3>
|
||||
<div class="visible-xs"><h3><span translate>Devices</span></h3><hr></div>
|
||||
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
|
||||
<div class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer">
|
||||
<h3 class="panel-title">
|
||||
@@ -379,28 +372,15 @@
|
||||
<th><span class="fa fa-fw fa-tachometer"></span> <span translate>CPU Utilization</span></th>
|
||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||
</tr>
|
||||
<tr ng-if="system.discoveryEnabled">
|
||||
<th><span class="fa fa-fw fa-map-signs"></span> <span translate>Discovery</span></th>
|
||||
<tr ng-if="system.extAnnounceOK != undefined && announceServersTotal > 0">
|
||||
<th><span class="fa fa-fw fa-bullhorn"></span> <span translate>Global Discovery</span></th>
|
||||
<td class="text-right">
|
||||
<span ng-if="discoveryFailed.length == 0" class="data text-success">
|
||||
<span>{{discoveryTotal}}/{{discoveryTotal}}</span>
|
||||
<span ng-if="announceServersFailed.length == 0" class="data text-success">
|
||||
<span>OK</span>
|
||||
</span>
|
||||
<span ng-if="discoveryFailed.length != 0" class="data" ng-class="{'text-danger': discoveryFailed.length == discoveryTotal}">
|
||||
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{discoveryFailed.join('<br>\n')}}">
|
||||
{{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="system.relaysEnabled">
|
||||
<th><span class="fa fa-fw fa-sitemap"></span> <span translate>Relays</span></th>
|
||||
<td class="text-right">
|
||||
<span ng-if="relaysFailed.length == 0" class="data text-success">
|
||||
<span>{{relaysTotal}}/{{relaysTotal}}</span>
|
||||
</span>
|
||||
<span ng-if="relaysFailed.length != 0" class="data" ng-class="{'text-danger': relaysFailed.length == relaysTotal}">
|
||||
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{relaysFailed.join('<br>\n')}}">
|
||||
{{relaysTotal-relaysFailed.length}}/{{relaysTotal}}
|
||||
<span ng-if="announceServersFailed.length != 0" class="data" ng-class="{'text-danger': announceServersFailed.length == announceServersTotal}">
|
||||
<span popover data-trigger="hover" data-placement="bottom" data-content="{{announceServersFailed.join('\n')}}">
|
||||
{{announceServersTotal-announceServersFailed.length}}/{{announceServersTotal}}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
@@ -432,7 +412,6 @@
|
||||
<span ng-switch-when="syncing">
|
||||
<span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | number:0}}%)
|
||||
</span>
|
||||
<span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">◼</span></span>
|
||||
</span>
|
||||
@@ -442,20 +421,16 @@
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr ng-if="connections[deviceCfg.deviceID].connected">
|
||||
<tr ng-if="connections[deviceCfg.deviceID]">
|
||||
<th><span class="fa fa-fw fa-cloud-download"></span> <span translate>Download Rate</span></th>
|
||||
<td class="text-right">{{connections[deviceCfg.deviceID].inbps | binary}}B/s ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr ng-if="connections[deviceCfg.deviceID].connected">
|
||||
<tr ng-if="connections[deviceCfg.deviceID]">
|
||||
<th><span class="fa fa-fw fa-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||
<td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="fa fa-fw fa-link"></span>
|
||||
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('direct') == 0" >Address</span>
|
||||
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('relay') == 0" >Relayed via</span>
|
||||
</th>
|
||||
<th><span class="fa fa-fw fa-link"></span> <span translate>Address</span></th>
|
||||
<td class="text-right">{{deviceAddr(deviceCfg)}}</td>
|
||||
</tr>
|
||||
<tr ng-if="deviceCfg.compression != 'metadata'">
|
||||
@@ -469,11 +444,11 @@
|
||||
<th><span class="fa fa-fw fa-thumbs-o-up"></span> <span translate>Introducer</span></th>
|
||||
<td translate class="text-right">Yes</td>
|
||||
</tr>
|
||||
<tr ng-if="connections[deviceCfg.deviceID].version">
|
||||
<tr ng-if="connections[deviceCfg.deviceID]">
|
||||
<th><span class="fa fa-fw fa-tag"></span> <span translate>Version</span></th>
|
||||
<td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
|
||||
</tr>
|
||||
<tr ng-if="!connections[deviceCfg.deviceID].connected">
|
||||
<tr ng-if="!connections[deviceCfg.deviceID]">
|
||||
<th><span class="fa fa-fw fa-eye"></span> <span translate>Last seen</span></th>
|
||||
<td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td>
|
||||
<td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}</td>
|
||||
@@ -490,12 +465,6 @@
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
|
||||
<span class="fa fa-pencil"></span> <span translate>Edit</span>
|
||||
</button>
|
||||
<button ng-if="!connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="pauseDevice(deviceCfg.deviceID)">
|
||||
<span class="fa fa-pause"></span> <span translate>Pause</span>
|
||||
</button>
|
||||
<button ng-if="connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="resumeDevice(deviceCfg.deviceID)">
|
||||
<span class="fa fa-play"></span> <span translate>Resume</span>
|
||||
</button>
|
||||
</span>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<div class="col-md-12">
|
||||
<ul class="list-unstyled three-columns" id="contributor-list">
|
||||
<li class="auto-generated">Aaron Bieber</li>
|
||||
<li class="auto-generated">Adam Piggott</li>
|
||||
<li class="auto-generated">Alexander Graf</li>
|
||||
<li class="auto-generated">Andrew Dunham</li>
|
||||
<li class="auto-generated">Antony Male</li>
|
||||
|
||||
@@ -63,8 +63,6 @@ angular.module('syncthing.core')
|
||||
DEVICE_DISCONNECTED: 'DeviceDisconnected', // Generated each time a connection to a device has been terminated
|
||||
DEVICE_DISCOVERED: 'DeviceDiscovered', // Emitted when a new device is discovered using local discovery
|
||||
DEVICE_REJECTED: 'DeviceRejected', // Emitted when there is a connection from a device we are not configured to talk to
|
||||
DEVICE_PAUSED: 'DevicePaused', // Emitted when a device has been paused
|
||||
DEVICE_RESUMED: 'DeviceResumed', // Emitted when a device has been resumed
|
||||
DOWNLOAD_PROGRESS: 'DownloadProgress', // Emitted during file downloads for each folder for each file
|
||||
FOLDER_COMPLETION: 'FolderCompletion', //Emitted when the local or remote contents for a folder changes
|
||||
FOLDER_REJECTED: 'FolderRejected', // Emitted when a device sends index information for a folder we do not have, or have but do not share with the device in question
|
||||
@@ -78,7 +76,6 @@ angular.module('syncthing.core')
|
||||
STARTUP_COMPLETED: 'StartupCompleted', // Emitted exactly once, when initialization is complete and Syncthing is ready to start exchanging data with other devices
|
||||
STATE_CHANGED: 'StateChanged', // Emitted when a folder changes state
|
||||
FOLDER_ERRORS: 'FolderErrors', // Emitted when a folder has errors preventing a full sync
|
||||
FOLDER_SCAN_PROGRESS: 'FolderScanProgress', // Emitted every ScanProgressIntervalS seconds, indicating how far into the scan it is at.
|
||||
|
||||
start: function() {
|
||||
$http.get(urlbase + '/events?limit=1')
|
||||
|
||||
@@ -48,7 +48,6 @@ angular.module('syncthing.core')
|
||||
$scope.failedCurrentPage = 1;
|
||||
$scope.failedCurrentFolder = undefined;
|
||||
$scope.failedPageSize = 10;
|
||||
$scope.scanProgress = {};
|
||||
|
||||
$scope.localStateTotal = {
|
||||
bytes: 0,
|
||||
@@ -164,12 +163,6 @@ angular.module('syncthing.core')
|
||||
if (data.to === 'syncing') {
|
||||
$scope.failed[data.folder] = [];
|
||||
}
|
||||
|
||||
// If a folder has started scanning, then any scan progress is
|
||||
// also obsolete.
|
||||
if (data.to === 'scanning') {
|
||||
delete $scope.scanProgress[data.folder];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -178,7 +171,7 @@ angular.module('syncthing.core')
|
||||
});
|
||||
|
||||
$scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
|
||||
$scope.connections[arg.data.id].connected = false;
|
||||
delete $scope.connections[arg.data.id];
|
||||
refreshDeviceStats();
|
||||
});
|
||||
|
||||
@@ -189,7 +182,6 @@ angular.module('syncthing.core')
|
||||
outbps: 0,
|
||||
inBytesTotal: 0,
|
||||
outBytesTotal: 0,
|
||||
type: arg.data.type,
|
||||
address: arg.data.addr
|
||||
};
|
||||
$scope.completion[arg.data.id] = {
|
||||
@@ -222,14 +214,6 @@ angular.module('syncthing.core')
|
||||
$scope.deviceRejections[arg.data.device] = arg;
|
||||
});
|
||||
|
||||
$scope.$on(Events.DEVICE_PAUSED, function (event, arg) {
|
||||
$scope.connections[arg.data.device].paused = true;
|
||||
});
|
||||
|
||||
$scope.$on(Events.DEVICE_RESUMED, function (event, arg) {
|
||||
$scope.connections[arg.data.device].paused = false;
|
||||
});
|
||||
|
||||
$scope.$on(Events.FOLDER_REJECTED, function (event, arg) {
|
||||
$scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
|
||||
});
|
||||
@@ -317,15 +301,6 @@ angular.module('syncthing.core')
|
||||
$scope.failed[data.folder] = data.errors;
|
||||
});
|
||||
|
||||
$scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) {
|
||||
var data = arg.data;
|
||||
$scope.scanProgress[data.folder] = {
|
||||
current: data.current,
|
||||
total: data.total
|
||||
};
|
||||
console.log("FolderScanProgress", data);
|
||||
});
|
||||
|
||||
$scope.emitHTTPError = function (data, status, headers, config) {
|
||||
$scope.$emit('HTTPError', {data: data, status: status, headers: headers, config: config});
|
||||
};
|
||||
@@ -377,26 +352,15 @@ angular.module('syncthing.core')
|
||||
$http.get(urlbase + '/system/status').success(function (data) {
|
||||
$scope.myID = data.myID;
|
||||
$scope.system = data;
|
||||
|
||||
$scope.discoveryTotal = data.discoveryMethods;
|
||||
var discoveryFailed = [];
|
||||
for (var disco in data.discoveryErrors) {
|
||||
if (data.discoveryErrors[disco]) {
|
||||
discoveryFailed.push(disco + ": " + data.discoveryErrors[disco]);
|
||||
$scope.announceServersTotal = data.extAnnounceOK ? Object.keys(data.extAnnounceOK).length : 0;
|
||||
var failed = [];
|
||||
for (var server in data.extAnnounceOK) {
|
||||
if (!data.extAnnounceOK[server]) {
|
||||
failed.push(server);
|
||||
}
|
||||
}
|
||||
$scope.discoveryFailed = discoveryFailed;
|
||||
$scope.announceServersFailed = failed;
|
||||
|
||||
var relaysFailed = [];
|
||||
var relaysTotal = 0;
|
||||
for (var relay in data.relayClientStatus) {
|
||||
if (!data.relayClientStatus[relay]) {
|
||||
relaysFailed.push(relay);
|
||||
}
|
||||
relaysTotal++;
|
||||
}
|
||||
$scope.relaysFailed = relaysFailed;
|
||||
$scope.relaysTotal = relaysTotal;
|
||||
|
||||
console.log("refreshSystem", data);
|
||||
}).error($scope.emitHTTPError);
|
||||
@@ -651,28 +615,12 @@ angular.module('syncthing.core')
|
||||
return Math.floor(pct);
|
||||
};
|
||||
|
||||
$scope.scanPercentage = function (folder) {
|
||||
if (!$scope.scanProgress[folder]) {
|
||||
return undefined;
|
||||
}
|
||||
var pct = 100 * $scope.scanProgress[folder].current / $scope.scanProgress[folder].total;
|
||||
return Math.floor(pct);
|
||||
}
|
||||
|
||||
$scope.deviceStatus = function (deviceCfg) {
|
||||
if ($scope.deviceFolders(deviceCfg).length === 0) {
|
||||
return 'unused';
|
||||
}
|
||||
|
||||
if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
if ($scope.connections[deviceCfg.deviceID].paused) {
|
||||
return 'paused';
|
||||
}
|
||||
|
||||
if ($scope.connections[deviceCfg.deviceID].connected) {
|
||||
if ($scope.connections[deviceCfg.deviceID]) {
|
||||
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
|
||||
return 'insync';
|
||||
} else {
|
||||
@@ -690,15 +638,7 @@ angular.module('syncthing.core')
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
|
||||
return 'info';
|
||||
}
|
||||
|
||||
if ($scope.connections[deviceCfg.deviceID].paused) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
if ($scope.connections[deviceCfg.deviceID].connected) {
|
||||
if ($scope.connections[deviceCfg.deviceID]) {
|
||||
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
|
||||
return 'success';
|
||||
} else {
|
||||
@@ -712,7 +652,7 @@ angular.module('syncthing.core')
|
||||
|
||||
$scope.deviceAddr = function (deviceCfg) {
|
||||
var conn = $scope.connections[deviceCfg.deviceID];
|
||||
if (conn && conn.connected) {
|
||||
if (conn) {
|
||||
return conn.address;
|
||||
}
|
||||
return '?';
|
||||
@@ -757,14 +697,6 @@ angular.module('syncthing.core')
|
||||
return device.deviceID.substr(0, 6);
|
||||
};
|
||||
|
||||
$scope.pauseDevice = function (device) {
|
||||
$http.post(urlbase + "/system/pause?device=" + device);
|
||||
};
|
||||
|
||||
$scope.resumeDevice = function (device) {
|
||||
$http.post(urlbase + "/system/resume?device=" + device);
|
||||
};
|
||||
|
||||
$scope.editSettings = function () {
|
||||
// Make a working copy
|
||||
$scope.tmpOptions = angular.copy($scope.config.options);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<label translate for="deviceID">Device ID</label>
|
||||
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
|
||||
<datalist id="discovery-list" ng-if="!editingExisting">
|
||||
<option ng-repeat="(id, data) in discovery" value="{{id}}" />
|
||||
<option ng-repeat="(id,address) in discovery" value="{{ id }}" />
|
||||
</datalist>
|
||||
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentDevice.deviceID}}</div>
|
||||
<p class="help-block">
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="form-group">
|
||||
<label translate for="addresses">Addresses</label>
|
||||
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
|
||||
<p translate class="help-block">Enter comma separated ("tcp://ip:port", "tcp://host:port") addresses or "dynamic" to perform automatic discovery of the address.</p>
|
||||
<p translate class="help-block">Enter comma separated "ip:port" addresses or "dynamic" to perform automatic discovery of the address.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>Compression</label>
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
<div class="col-md-12">
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
|
||||
<label for="folderID"><span translate>Folder ID</span></label>
|
||||
<input name="folderID" ng-readonly="editingExisting" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required unique-folder></input>
|
||||
<input name="folderID" ng-readonly="editingExisting" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required unique-folder ng-pattern="/^[a-zA-Z0-9-_.]{1,64}$/"></input>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Short identifier for the folder. Must be the same on all cluster devices.</span>
|
||||
<span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
|
||||
<span translate ng-if="folderEditor.folderID.$error.required && folderEditor.folderID.$dirty">The folder ID cannot be blank.</span>
|
||||
<span translate ng-if="folderEditor.folderID.$error.pattern && folderEditor.folderID.$dirty">The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty}">
|
||||
@@ -97,7 +98,7 @@
|
||||
<label translate for="trashcanClean">Clean out after</label>
|
||||
<div class="input-group">
|
||||
<input name="trashcanClean" id="trashcanClean" class="form-control text-right" type="number" ng-model="currentFolder.trashcanClean" required min="0"></input>
|
||||
<div class="input-group-addon" translate>days</div>
|
||||
<div class="input-group-addon">days</div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.trashcanClean.$valid || folderEditor.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<modal id="urPreview" status="success" icon="bar-chart" title="{{'Anonymous Usage Reporting' | translate}}" large="yes" close="yes" tabindex="-1">
|
||||
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
|
||||
<p translate translate-value-url="<a href="https://data.syncthing.net" target="_blank">https://data.syncthing.net</a>">The aggregated statistics are publicly available at {%url%}.</p>
|
||||
<form>
|
||||
<textarea class="form-control" rows="20">{{reportData | json}}</textarea>
|
||||
</form>
|
||||
<pre><small>{{reportData | json}}</small></pre>
|
||||
</modal>
|
||||
|
||||
+13
-13
File diff suppressed because one or more lines are too long
+22
-23
@@ -6,12 +6,7 @@
|
||||
|
||||
package beacon
|
||||
|
||||
import (
|
||||
"net"
|
||||
stdsync "sync"
|
||||
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
import "net"
|
||||
|
||||
type recv struct {
|
||||
data []byte
|
||||
@@ -19,30 +14,34 @@ type recv struct {
|
||||
}
|
||||
|
||||
type Interface interface {
|
||||
suture.Service
|
||||
Send(data []byte)
|
||||
Recv() ([]byte, net.Addr)
|
||||
Error() error
|
||||
}
|
||||
|
||||
type readerFrom interface {
|
||||
ReadFrom([]byte) (int, net.Addr, error)
|
||||
}
|
||||
|
||||
type errorHolder struct {
|
||||
err error
|
||||
mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
|
||||
}
|
||||
func genericReader(conn readerFrom, outbox chan<- recv) {
|
||||
bs := make([]byte, 65536)
|
||||
for {
|
||||
n, addr, err := conn.ReadFrom(bs)
|
||||
if err != nil {
|
||||
l.Warnln("multicast read:", err)
|
||||
return
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("recv %d bytes from %s", n, addr)
|
||||
}
|
||||
|
||||
func (e *errorHolder) setError(err error) {
|
||||
e.mut.Lock()
|
||||
e.err = err
|
||||
e.mut.Unlock()
|
||||
}
|
||||
|
||||
func (e *errorHolder) Error() error {
|
||||
e.mut.Lock()
|
||||
err := e.err
|
||||
e.mut.Unlock()
|
||||
return err
|
||||
c := make([]byte, n)
|
||||
copy(c, bs)
|
||||
select {
|
||||
case outbox <- recv{c, addr}:
|
||||
default:
|
||||
if debug {
|
||||
l.Debugln("dropping message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-59
@@ -19,8 +19,6 @@ type Broadcast struct {
|
||||
port int
|
||||
inbox chan []byte
|
||||
outbox chan recv
|
||||
br *broadcastReader
|
||||
bw *broadcastWriter
|
||||
}
|
||||
|
||||
func NewBroadcast(port int) *Broadcast {
|
||||
@@ -43,16 +41,14 @@ func NewBroadcast(port int) *Broadcast {
|
||||
outbox: make(chan recv, 16),
|
||||
}
|
||||
|
||||
b.br = &broadcastReader{
|
||||
b.Add(&broadcastReader{
|
||||
port: port,
|
||||
outbox: b.outbox,
|
||||
}
|
||||
b.Add(b.br)
|
||||
b.bw = &broadcastWriter{
|
||||
})
|
||||
b.Add(&broadcastWriter{
|
||||
port: port,
|
||||
inbox: b.inbox,
|
||||
}
|
||||
b.Add(b.bw)
|
||||
})
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -66,18 +62,11 @@ func (b *Broadcast) Recv() ([]byte, net.Addr) {
|
||||
return recv.data, recv.src
|
||||
}
|
||||
|
||||
func (b *Broadcast) Error() error {
|
||||
if err := b.br.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.bw.Error()
|
||||
}
|
||||
|
||||
type broadcastWriter struct {
|
||||
port int
|
||||
inbox chan []byte
|
||||
conn *net.UDPConn
|
||||
errorHolder
|
||||
port int
|
||||
inbox chan []byte
|
||||
conn *net.UDPConn
|
||||
failed bool // Have we already logged a failure reason?
|
||||
}
|
||||
|
||||
func (w *broadcastWriter) Serve() {
|
||||
@@ -89,21 +78,22 @@ func (w *broadcastWriter) Serve() {
|
||||
var err error
|
||||
w.conn, err = net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
if !w.failed {
|
||||
l.Warnln("Local discovery over IPv4 unavailable:", err)
|
||||
w.failed = true
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
}
|
||||
defer w.conn.Close()
|
||||
|
||||
w.failed = false
|
||||
|
||||
for bs := range w.inbox {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
l.Debugln("Local discovery (broadcast writer):", err)
|
||||
}
|
||||
w.setError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -124,50 +114,31 @@ func (w *broadcastWriter) Serve() {
|
||||
l.Debugln("addresses:", dsts)
|
||||
}
|
||||
|
||||
success := 0
|
||||
for _, ip := range dsts {
|
||||
dst := &net.UDPAddr{IP: ip, Port: w.port}
|
||||
|
||||
w.conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
w.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
_, err := w.conn.WriteTo(bs, dst)
|
||||
w.conn.SetWriteDeadline(time.Time{})
|
||||
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
// Write timeouts should not happen. We treat it as a fatal
|
||||
// error on the socket.
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
l.Infoln("Local discovery (broadcast writer):", err)
|
||||
w.failed = true
|
||||
return
|
||||
}
|
||||
|
||||
if err, ok := err.(net.Error); ok && err.Temporary() {
|
||||
} else if err, ok := err.(net.Error); ok && err.Temporary() {
|
||||
// A transient error. Lets hope for better luck in the future.
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
} else if err != nil {
|
||||
// Some other error that we don't expect. Bail and retry.
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
l.Infoln("Local discovery (broadcast writer):", err)
|
||||
w.failed = true
|
||||
return
|
||||
}
|
||||
|
||||
if debug {
|
||||
} else if debug {
|
||||
l.Debugf("sent %d bytes to %s", len(bs), dst)
|
||||
}
|
||||
|
||||
success++
|
||||
}
|
||||
|
||||
if success > 0 {
|
||||
w.setError(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +155,7 @@ type broadcastReader struct {
|
||||
port int
|
||||
outbox chan recv
|
||||
conn *net.UDPConn
|
||||
errorHolder
|
||||
failed bool
|
||||
}
|
||||
|
||||
func (r *broadcastReader) Serve() {
|
||||
@@ -196,10 +167,10 @@ func (r *broadcastReader) Serve() {
|
||||
var err error
|
||||
r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
if !r.failed {
|
||||
l.Warnln("Local discovery over IPv4 unavailable:", err)
|
||||
r.failed = true
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
}
|
||||
defer r.conn.Close()
|
||||
@@ -208,14 +179,14 @@ func (r *broadcastReader) Serve() {
|
||||
for {
|
||||
n, addr, err := r.conn.ReadFrom(bs)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
if !r.failed {
|
||||
l.Infoln("Local discovery (broadcast reader):", err)
|
||||
r.failed = true
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
r.setError(nil)
|
||||
r.failed = false
|
||||
|
||||
if debug {
|
||||
l.Debugf("recv %d bytes from %s", n, addr)
|
||||
|
||||
+53
-207
@@ -8,207 +8,39 @@ package beacon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/thejerf/suture"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
type Multicast struct {
|
||||
*suture.Supervisor
|
||||
conn *ipv6.PacketConn
|
||||
addr *net.UDPAddr
|
||||
inbox chan []byte
|
||||
outbox chan recv
|
||||
mr *multicastReader
|
||||
mw *multicastWriter
|
||||
intfs []net.Interface
|
||||
}
|
||||
|
||||
func NewMulticast(addr string) *Multicast {
|
||||
m := &Multicast{
|
||||
Supervisor: suture.New("multicastBeacon", suture.Spec{
|
||||
// Don't retry too frenetically: an error to open a socket or
|
||||
// whatever is usually something that is either permanent or takes
|
||||
// a while to get solved...
|
||||
FailureThreshold: 2,
|
||||
FailureBackoff: 60 * time.Second,
|
||||
// Only log restarts in debug mode.
|
||||
Log: func(line string) {
|
||||
if debug {
|
||||
l.Debugln(line)
|
||||
}
|
||||
},
|
||||
}),
|
||||
inbox: make(chan []byte),
|
||||
outbox: make(chan recv, 16),
|
||||
}
|
||||
|
||||
m.mr = &multicastReader{
|
||||
addr: addr,
|
||||
outbox: m.outbox,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
m.Add(m.mr)
|
||||
|
||||
m.mw = &multicastWriter{
|
||||
addr: addr,
|
||||
inbox: m.inbox,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
m.Add(m.mw)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Multicast) Send(data []byte) {
|
||||
m.inbox <- data
|
||||
}
|
||||
|
||||
func (m *Multicast) Recv() ([]byte, net.Addr) {
|
||||
recv := <-m.outbox
|
||||
return recv.data, recv.src
|
||||
}
|
||||
|
||||
func (m *Multicast) Error() error {
|
||||
if err := m.mr.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.mw.Error()
|
||||
}
|
||||
|
||||
type multicastWriter struct {
|
||||
addr string
|
||||
inbox <-chan []byte
|
||||
errorHolder
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (w *multicastWriter) Serve() {
|
||||
if debug {
|
||||
l.Debugln(w, "starting")
|
||||
defer l.Debugln(w, "stopping")
|
||||
}
|
||||
|
||||
gaddr, err := net.ResolveUDPAddr("udp6", w.addr)
|
||||
func NewMulticast(addr string) (*Multicast, error) {
|
||||
gaddr, err := net.ResolveUDPAddr("udp6", addr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.ListenPacket("udp6", ":0")
|
||||
conn, err := net.ListenPacket("udp6", addr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
pconn := ipv6.NewPacketConn(conn)
|
||||
|
||||
wcm := &ipv6.ControlMessage{
|
||||
HopLimit: 1,
|
||||
}
|
||||
|
||||
for bs := range w.inbox {
|
||||
intfs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
success := 0
|
||||
for _, intf := range intfs {
|
||||
wcm.IfIndex = intf.Index
|
||||
pconn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
_, err = pconn.WriteTo(bs, wcm, gaddr)
|
||||
pconn.SetWriteDeadline(time.Time{})
|
||||
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err, "on write to", gaddr, intf.Name)
|
||||
}
|
||||
w.setError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("sent %d bytes to %v on %s", len(bs), gaddr, intf.Name)
|
||||
}
|
||||
|
||||
success++
|
||||
}
|
||||
|
||||
if success > 0 {
|
||||
w.setError(nil)
|
||||
} else {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *multicastWriter) Stop() {
|
||||
close(w.stop)
|
||||
}
|
||||
|
||||
func (w *multicastWriter) String() string {
|
||||
return fmt.Sprintf("multicastWriter@%p", w)
|
||||
}
|
||||
|
||||
type multicastReader struct {
|
||||
addr string
|
||||
outbox chan<- recv
|
||||
errorHolder
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (r *multicastReader) Serve() {
|
||||
if debug {
|
||||
l.Debugln(r, "starting")
|
||||
defer l.Debugln(r, "stopping")
|
||||
}
|
||||
|
||||
gaddr, err := net.ResolveUDPAddr("udp6", r.addr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := net.ListenPacket("udp6", r.addr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
intfs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pconn := ipv6.NewPacketConn(conn)
|
||||
p := ipv6.NewPacketConn(conn)
|
||||
joined := 0
|
||||
for _, intf := range intfs {
|
||||
err := pconn.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
|
||||
err := p.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
|
||||
if debug {
|
||||
if err != nil {
|
||||
l.Debugln("IPv6 join", intf.Name, "failed:", err)
|
||||
@@ -220,43 +52,57 @@ func (r *multicastReader) Serve() {
|
||||
}
|
||||
|
||||
if joined == 0 {
|
||||
if debug {
|
||||
l.Debugln("no multicast interfaces available")
|
||||
}
|
||||
r.setError(errors.New("no multicast interfaces available"))
|
||||
return
|
||||
return nil, errors.New("no multicast interfaces available")
|
||||
}
|
||||
|
||||
bs := make([]byte, 65536)
|
||||
for {
|
||||
n, _, addr, err := pconn.ReadFrom(bs)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
continue
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("recv %d bytes from %s", n, addr)
|
||||
}
|
||||
b := &Multicast{
|
||||
conn: p,
|
||||
addr: gaddr,
|
||||
inbox: make(chan []byte),
|
||||
outbox: make(chan recv, 16),
|
||||
intfs: intfs,
|
||||
}
|
||||
|
||||
c := make([]byte, n)
|
||||
copy(c, bs)
|
||||
select {
|
||||
case r.outbox <- recv{c, addr}:
|
||||
default:
|
||||
if debug {
|
||||
l.Debugln("dropping message")
|
||||
go genericReader(ipv6ReaderAdapter{b.conn}, b.outbox)
|
||||
go b.writer()
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Multicast) Send(data []byte) {
|
||||
b.inbox <- data
|
||||
}
|
||||
|
||||
func (b *Multicast) Recv() ([]byte, net.Addr) {
|
||||
recv := <-b.outbox
|
||||
return recv.data, recv.src
|
||||
}
|
||||
|
||||
func (b *Multicast) writer() {
|
||||
wcm := &ipv6.ControlMessage{
|
||||
HopLimit: 1,
|
||||
}
|
||||
|
||||
for bs := range b.inbox {
|
||||
for _, intf := range b.intfs {
|
||||
wcm.IfIndex = intf.Index
|
||||
_, err := b.conn.WriteTo(bs, wcm, b.addr)
|
||||
if err != nil && debug {
|
||||
l.Debugln(err, "on write to", b.addr)
|
||||
} else if debug {
|
||||
l.Debugf("sent %d bytes to %v on %s", len(bs), b.addr, intf.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *multicastReader) Stop() {
|
||||
close(r.stop)
|
||||
// This makes ReadFrom on an *ipv6.PacketConn behave like ReadFrom on a
|
||||
// net.PacketConn.
|
||||
type ipv6ReaderAdapter struct {
|
||||
c *ipv6.PacketConn
|
||||
}
|
||||
|
||||
func (r *multicastReader) String() string {
|
||||
return fmt.Sprintf("multicastReader@%p", r)
|
||||
func (i ipv6ReaderAdapter) ReadFrom(bs []byte) (int, net.Addr, error) {
|
||||
n, _, src, err := i.c.ReadFrom(bs)
|
||||
return n, src, err
|
||||
}
|
||||
|
||||
+25
-109
@@ -9,7 +9,6 @@ package config
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
@@ -20,43 +19,17 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
OldestHandledVersion = 5
|
||||
CurrentVersion = 12
|
||||
CurrentVersion = 11
|
||||
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultDiscoveryServers should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default</globalAnnounceServer>. This is
|
||||
// done by the "consumer" of the configuration, as we don't want these
|
||||
// saved to the config.
|
||||
DefaultDiscoveryServers = []string{
|
||||
"https://discovery-v4-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
|
||||
"https://discovery-v4-2.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 45.55.230.38, USA
|
||||
"https://discovery-v4-3.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 128.199.95.124, Singapore
|
||||
"https://discovery-v6-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
|
||||
"https://discovery-v6-2.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 2604:a880:800:10::182:a001, USA
|
||||
"https://discovery-v6-3.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 2400:6180:0:d0::d9:d001, Singapore
|
||||
}
|
||||
|
||||
// DefaultDiscoveryServersIP is used by the usage reporting.
|
||||
// XXX: Detect Android, and use this is we still don't have working DNS?
|
||||
DefaultDiscoveryServersIP = []string{
|
||||
"https://194.126.249.5/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA",
|
||||
"https://45.55.230.38/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS",
|
||||
"https://128.199.95.124/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4",
|
||||
"https://[2001:470:28:4d6::5]/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA",
|
||||
"https://[2604:a880:800:10::182:a001]/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS",
|
||||
"https://[2400:6180:0:d0::d9:d001]/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4",
|
||||
}
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Version int `xml:"version,attr" json:"version"`
|
||||
Folders []FolderConfiguration `xml:"folder" json:"folders"`
|
||||
@@ -94,21 +67,20 @@ func (cfg Configuration) Copy() Configuration {
|
||||
}
|
||||
|
||||
type FolderConfiguration struct {
|
||||
ID string `xml:"id,attr" json:"id"`
|
||||
RawPath string `xml:"path,attr" json:"path"`
|
||||
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
|
||||
ReadOnly bool `xml:"ro,attr" json:"readOnly"`
|
||||
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
|
||||
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
|
||||
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
|
||||
MinDiskFreePct float64 `xml:"minDiskFreePct" json:"minDiskFreePct"`
|
||||
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
|
||||
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
|
||||
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
|
||||
Hashers int `xml:"hashers" json:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
|
||||
Order PullOrder `xml:"order" json:"order"`
|
||||
IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"`
|
||||
ScanProgressIntervalS int `xml:"scanProgressInterval" json:"scanProgressInterval"` // Set to a negative value to disable. Value of 0 will get replaced with value of 2 (default value)
|
||||
ID string `xml:"id,attr" json:"id"`
|
||||
RawPath string `xml:"path,attr" json:"path"`
|
||||
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
|
||||
ReadOnly bool `xml:"ro,attr" json:"readOnly"`
|
||||
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
|
||||
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
|
||||
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
|
||||
MinDiskFreePct float64 `xml:"minDiskFreePct" json:"minDiskFreePct"`
|
||||
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
|
||||
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
|
||||
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
|
||||
Hashers int `xml:"hashers" json:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
|
||||
Order PullOrder `xml:"order" json:"order"`
|
||||
IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"`
|
||||
|
||||
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
|
||||
}
|
||||
@@ -240,19 +212,15 @@ type FolderDeviceConfiguration struct {
|
||||
}
|
||||
|
||||
type OptionsConfiguration struct {
|
||||
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
|
||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
|
||||
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"0.0.0.0:22000"`
|
||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22026, udp6://announce-v6.syncthing.net:22026"`
|
||||
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
|
||||
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
|
||||
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
|
||||
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"`
|
||||
RelayServers []string `xml:"relayServer" json:"relayServers" default:"dynamic+https://relays.syncthing.net"`
|
||||
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21025"`
|
||||
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff32::5222]:21026"`
|
||||
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
|
||||
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
|
||||
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
|
||||
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
|
||||
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
|
||||
RelayWithoutGlobalAnn bool `xml:"relayWithoutGlobalAnn" json:"relayWithoutGlobalAnn" default:"false"`
|
||||
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
||||
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
||||
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
|
||||
@@ -260,9 +228,6 @@ type OptionsConfiguration struct {
|
||||
UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"10"`
|
||||
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
||||
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
|
||||
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
|
||||
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
|
||||
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
|
||||
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
|
||||
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
|
||||
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
|
||||
@@ -271,9 +236,9 @@ type OptionsConfiguration struct {
|
||||
SymlinksEnabled bool `xml:"symlinksEnabled" json:"symlinksEnabled" default:"true"`
|
||||
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
|
||||
DatabaseBlockCacheMiB int `xml:"databaseBlockCacheMiB" json:"databaseBlockCacheMiB" default:"0"`
|
||||
PingTimeoutS int `xml:"pingTimeoutS" json:"pingTimeoutS" default:"30"`
|
||||
PingIdleTimeS int `xml:"pingIdleTimeS" json:"pingIdleTimeS" default:"60"`
|
||||
MinHomeDiskFreePct float64 `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
|
||||
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"`
|
||||
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
|
||||
}
|
||||
|
||||
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
|
||||
@@ -381,9 +346,6 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
||||
cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
|
||||
|
||||
if cfg.Version < OldestHandledVersion {
|
||||
l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version)
|
||||
}
|
||||
@@ -407,9 +369,6 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
if cfg.Version == 10 {
|
||||
convertV10V11(cfg)
|
||||
}
|
||||
if cfg.Version == 11 {
|
||||
convertV11V12(cfg)
|
||||
}
|
||||
|
||||
// Hash old cleartext passwords
|
||||
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
|
||||
@@ -461,6 +420,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
cfg.Options.ReconnectIntervalS = 5
|
||||
}
|
||||
|
||||
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
||||
cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
|
||||
|
||||
if cfg.GUI.APIKey == "" {
|
||||
cfg.GUI.APIKey = randomString(32)
|
||||
}
|
||||
@@ -505,52 +467,6 @@ func convertV10V11(cfg *Configuration) {
|
||||
cfg.Version = 11
|
||||
}
|
||||
|
||||
func convertV11V12(cfg *Configuration) {
|
||||
// Change listen address schema
|
||||
for i, addr := range cfg.Options.ListenAddress {
|
||||
if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") {
|
||||
cfg.Options.ListenAddress[i] = fmt.Sprintf("tcp://%s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
for i, device := range cfg.Devices {
|
||||
for j, addr := range device.Addresses {
|
||||
if addr != "dynamic" && addr != "" {
|
||||
cfg.Devices[i].Addresses[j] = fmt.Sprintf("tcp://%s", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use new discovery server
|
||||
var newDiscoServers []string
|
||||
var useDefault bool
|
||||
for _, addr := range cfg.Options.GlobalAnnServers {
|
||||
if addr == "udp4://announce.syncthing.net:22026" {
|
||||
useDefault = true
|
||||
} else if addr == "udp6://announce-v6.syncthing.net:22026" {
|
||||
useDefault = true
|
||||
} else {
|
||||
newDiscoServers = append(newDiscoServers, addr)
|
||||
}
|
||||
}
|
||||
if useDefault {
|
||||
newDiscoServers = append(newDiscoServers, "default")
|
||||
}
|
||||
cfg.Options.GlobalAnnServers = newDiscoServers
|
||||
|
||||
// Use new multicast group
|
||||
if cfg.Options.LocalAnnMCAddr == "[ff32::5222]:21026" {
|
||||
cfg.Options.LocalAnnMCAddr = "[ff12::8384]:21027"
|
||||
}
|
||||
|
||||
// Use new local discovery port
|
||||
if cfg.Options.LocalAnnPort == 21025 {
|
||||
cfg.Options.LocalAnnPort = 21027
|
||||
}
|
||||
|
||||
cfg.Version = 12
|
||||
}
|
||||
|
||||
func convertV9V10(cfg *Configuration) {
|
||||
// Enable auto normalization on existing folders.
|
||||
for i := range cfg.Folders {
|
||||
|
||||
+19
-31
@@ -17,7 +17,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/protocol"
|
||||
)
|
||||
|
||||
var device1, device2, device3, device4 protocol.DeviceID
|
||||
@@ -31,19 +31,15 @@ func init() {
|
||||
|
||||
func TestDefaultValues(t *testing.T) {
|
||||
expected := OptionsConfiguration{
|
||||
ListenAddress: []string{"tcp://0.0.0.0:22000"},
|
||||
GlobalAnnServers: []string{"default"},
|
||||
ListenAddress: []string{"0.0.0.0:22000"},
|
||||
GlobalAnnServers: []string{"udp4://announce.syncthing.net:22026", "udp6://announce-v6.syncthing.net:22026"},
|
||||
GlobalAnnEnabled: true,
|
||||
LocalAnnEnabled: true,
|
||||
LocalAnnPort: 21027,
|
||||
LocalAnnMCAddr: "[ff12::8384]:21027",
|
||||
RelayServers: []string{"dynamic+https://relays.syncthing.net"},
|
||||
LocalAnnPort: 21025,
|
||||
LocalAnnMCAddr: "[ff32::5222]:21026",
|
||||
MaxSendKbps: 0,
|
||||
MaxRecvKbps: 0,
|
||||
ReconnectIntervalS: 60,
|
||||
RelaysEnabled: true,
|
||||
RelayReconnectIntervalM: 10,
|
||||
RelayWithoutGlobalAnn: false,
|
||||
StartBrowser: true,
|
||||
UPnPEnabled: true,
|
||||
UPnPLeaseM: 60,
|
||||
@@ -57,11 +53,9 @@ func TestDefaultValues(t *testing.T) {
|
||||
SymlinksEnabled: true,
|
||||
LimitBandwidthInLan: false,
|
||||
DatabaseBlockCacheMiB: 0,
|
||||
PingTimeoutS: 30,
|
||||
PingIdleTimeS: 60,
|
||||
MinHomeDiskFreePct: 1,
|
||||
URURL: "https://data.syncthing.net/newdata",
|
||||
URInitialDelayS: 1800,
|
||||
URPostInsecurely: false,
|
||||
ReleasesURL: "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30",
|
||||
}
|
||||
|
||||
cfg := New(device1)
|
||||
@@ -106,13 +100,13 @@ func TestDeviceConfig(t *testing.T) {
|
||||
{
|
||||
DeviceID: device1,
|
||||
Name: "node one",
|
||||
Addresses: []string{"tcp://a"},
|
||||
Addresses: []string{"a"},
|
||||
Compression: protocol.CompressMetadata,
|
||||
},
|
||||
{
|
||||
DeviceID: device4,
|
||||
Name: "node two",
|
||||
Addresses: []string{"tcp://b"},
|
||||
Addresses: []string{"b"},
|
||||
Compression: protocol.CompressMetadata,
|
||||
},
|
||||
}
|
||||
@@ -148,19 +142,15 @@ func TestNoListenAddress(t *testing.T) {
|
||||
|
||||
func TestOverriddenValues(t *testing.T) {
|
||||
expected := OptionsConfiguration{
|
||||
ListenAddress: []string{"tcp://:23000"},
|
||||
ListenAddress: []string{":23000"},
|
||||
GlobalAnnServers: []string{"udp4://syncthing.nym.se:22026"},
|
||||
GlobalAnnEnabled: false,
|
||||
LocalAnnEnabled: false,
|
||||
LocalAnnPort: 42123,
|
||||
LocalAnnMCAddr: "quux:3232",
|
||||
RelayServers: []string{"relay://123.123.123.123:1234", "relay://125.125.125.125:1255"},
|
||||
MaxSendKbps: 1234,
|
||||
MaxRecvKbps: 2341,
|
||||
ReconnectIntervalS: 6000,
|
||||
RelaysEnabled: false,
|
||||
RelayReconnectIntervalM: 20,
|
||||
RelayWithoutGlobalAnn: true,
|
||||
StartBrowser: false,
|
||||
UPnPEnabled: false,
|
||||
UPnPLeaseM: 90,
|
||||
@@ -174,11 +164,9 @@ func TestOverriddenValues(t *testing.T) {
|
||||
SymlinksEnabled: false,
|
||||
LimitBandwidthInLan: true,
|
||||
DatabaseBlockCacheMiB: 42,
|
||||
PingTimeoutS: 60,
|
||||
PingIdleTimeS: 120,
|
||||
MinHomeDiskFreePct: 5.2,
|
||||
URURL: "https://localhost/newdata",
|
||||
URInitialDelayS: 800,
|
||||
URPostInsecurely: true,
|
||||
ReleasesURL: "https://localhost/releases",
|
||||
}
|
||||
|
||||
cfg, err := Load("testdata/overridenvalues.xml", device1)
|
||||
@@ -267,15 +255,15 @@ func TestDeviceAddressesStatic(t *testing.T) {
|
||||
expected := map[protocol.DeviceID]DeviceConfiguration{
|
||||
device1: {
|
||||
DeviceID: device1,
|
||||
Addresses: []string{"tcp://192.0.2.1", "tcp://192.0.2.2"},
|
||||
Addresses: []string{"192.0.2.1", "192.0.2.2"},
|
||||
},
|
||||
device2: {
|
||||
DeviceID: device2,
|
||||
Addresses: []string{"tcp://192.0.2.3:6070", "tcp://[2001:db8::42]:4242"},
|
||||
Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"},
|
||||
},
|
||||
device3: {
|
||||
DeviceID: device3,
|
||||
Addresses: []string{"tcp://[2001:db8::44]:4444", "tcp://192.0.2.4:6090"},
|
||||
Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"},
|
||||
},
|
||||
device4: {
|
||||
DeviceID: device4,
|
||||
@@ -342,12 +330,12 @@ func TestIssue1750(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cfg.Options().ListenAddress[0] != "tcp://:23000" {
|
||||
t.Errorf("%q != %q", cfg.Options().ListenAddress[0], "tcp://:23000")
|
||||
if cfg.Options().ListenAddress[0] != ":23000" {
|
||||
t.Errorf("%q != %q", cfg.Options().ListenAddress[0], ":23000")
|
||||
}
|
||||
|
||||
if cfg.Options().ListenAddress[1] != "tcp://:23001" {
|
||||
t.Errorf("%q != %q", cfg.Options().ListenAddress[1], "tcp://:23001")
|
||||
if cfg.Options().ListenAddress[1] != ":23001" {
|
||||
t.Errorf("%q != %q", cfg.Options().ListenAddress[1], ":23001")
|
||||
}
|
||||
|
||||
if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026" {
|
||||
|
||||
+2
-9
@@ -7,15 +7,10 @@
|
||||
<localAnnounceEnabled>false</localAnnounceEnabled>
|
||||
<localAnnouncePort>42123</localAnnouncePort>
|
||||
<localAnnounceMCAddr>quux:3232</localAnnounceMCAddr>
|
||||
<relayServer>relay://123.123.123.123:1234</relayServer>
|
||||
<relayServer>relay://125.125.125.125:1255</relayServer>
|
||||
<parallelRequests>32</parallelRequests>
|
||||
<maxSendKbps>1234</maxSendKbps>
|
||||
<maxRecvKbps>2341</maxRecvKbps>
|
||||
<reconnectionIntervalS>6000</reconnectionIntervalS>
|
||||
<relaysEnabled>false</relaysEnabled>
|
||||
<relayReconnectIntervalM>20</relayReconnectIntervalM>
|
||||
<relayWithoutGlobalAnn>true</relayWithoutGlobalAnn>
|
||||
<startBrowser>false</startBrowser>
|
||||
<upnpEnabled>false</upnpEnabled>
|
||||
<upnpLeaseMinutes>90</upnpLeaseMinutes>
|
||||
@@ -29,10 +24,8 @@
|
||||
<symlinksEnabled>false</symlinksEnabled>
|
||||
<limitBandwidthInLan>true</limitBandwidthInLan>
|
||||
<databaseBlockCacheMiB>42</databaseBlockCacheMiB>
|
||||
<pingTimeoutS>60</pingTimeoutS>
|
||||
<pingIdleTimeS>120</pingIdleTimeS>
|
||||
<minHomeDiskFreePct>5.2</minHomeDiskFreePct>
|
||||
<urURL>https://localhost/newdata</urURL>
|
||||
<urInitialDelayS>800</urInitialDelayS>
|
||||
<urPostInsecurely>true</urPostInsecurely>
|
||||
<releasesURL>https://localhost/releases</releasesURL>
|
||||
</options>
|
||||
</configuration>
|
||||
|
||||
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
<configuration version="12">
|
||||
<folder id="test" path="testdata" ro="true" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
|
||||
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
|
||||
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
|
||||
<minDiskFreePct>1</minDiskFreePct>
|
||||
</folder>
|
||||
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
|
||||
<address>tcp://a</address>
|
||||
</device>
|
||||
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
|
||||
<address>tcp://b</address>
|
||||
</device>
|
||||
</configuration>
|
||||
+7
-31
@@ -9,9 +9,9 @@ package config
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
@@ -60,8 +60,10 @@ type Wrapper struct {
|
||||
deviceMap map[protocol.DeviceID]DeviceConfiguration
|
||||
folderMap map[string]FolderConfiguration
|
||||
replaces chan Configuration
|
||||
subs []Committer
|
||||
mut sync.Mutex
|
||||
|
||||
subs []Committer
|
||||
sMut sync.Mutex
|
||||
}
|
||||
|
||||
// Wrap wraps an existing Configuration structure and ties it to a file on
|
||||
@@ -71,6 +73,7 @@ func Wrap(path string, cfg Configuration) *Wrapper {
|
||||
cfg: cfg,
|
||||
path: path,
|
||||
mut: sync.NewMutex(),
|
||||
sMut: sync.NewMutex(),
|
||||
}
|
||||
w.replaces = make(chan Configuration)
|
||||
return w
|
||||
@@ -106,24 +109,9 @@ func (w *Wrapper) Stop() {
|
||||
// Subscribe registers the given handler to be called on any future
|
||||
// configuration changes.
|
||||
func (w *Wrapper) Subscribe(c Committer) {
|
||||
w.mut.Lock()
|
||||
w.sMut.Lock()
|
||||
w.subs = append(w.subs, c)
|
||||
w.mut.Unlock()
|
||||
}
|
||||
|
||||
// Unsubscribe de-registers the given handler from any future calls to
|
||||
// configuration changes
|
||||
func (w *Wrapper) Unsubscribe(c Committer) {
|
||||
w.mut.Lock()
|
||||
for i := range w.subs {
|
||||
if w.subs[i] == c {
|
||||
copy(w.subs[i:], w.subs[i+1:])
|
||||
w.subs[len(w.subs)-1] = nil
|
||||
w.subs = w.subs[:len(w.subs)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
w.mut.Unlock()
|
||||
w.sMut.Unlock()
|
||||
}
|
||||
|
||||
// Raw returns the currently wrapped Configuration object.
|
||||
@@ -314,15 +302,3 @@ func (w *Wrapper) Save() error {
|
||||
events.Default.Log(events.ConfigSaved, w.cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) GlobalDiscoveryServers() []string {
|
||||
var servers []string
|
||||
for _, srv := range w.cfg.Options.GlobalAnnServers {
|
||||
if srv == "default" {
|
||||
servers = append(servers, DefaultDiscoveryServers...)
|
||||
} else {
|
||||
servers = append(servers, srv)
|
||||
}
|
||||
}
|
||||
return uniqueStrings(servers)
|
||||
}
|
||||
|
||||
@@ -1,477 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package connections
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/juju/ratelimit"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay"
|
||||
"github.com/syncthing/syncthing/lib/relay/client"
|
||||
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
type DialerFactory func(*url.URL, *tls.Config) (*tls.Conn, error)
|
||||
type ListenerFactory func(*url.URL, *tls.Config, chan<- model.IntermediateConnection)
|
||||
|
||||
var (
|
||||
dialers = make(map[string]DialerFactory, 0)
|
||||
listeners = make(map[string]ListenerFactory, 0)
|
||||
)
|
||||
|
||||
type Model interface {
|
||||
protocol.Model
|
||||
AddConnection(conn model.Connection)
|
||||
ConnectedTo(remoteID protocol.DeviceID) bool
|
||||
IsPaused(remoteID protocol.DeviceID) bool
|
||||
}
|
||||
|
||||
// The connection service listens on TLS and dials configured unconnected
|
||||
// devices. Successful connections are handed to the model.
|
||||
type connectionSvc struct {
|
||||
*suture.Supervisor
|
||||
cfg *config.Wrapper
|
||||
myID protocol.DeviceID
|
||||
model Model
|
||||
tlsCfg *tls.Config
|
||||
discoverer discover.Finder
|
||||
conns chan model.IntermediateConnection
|
||||
relaySvc *relay.Svc
|
||||
bepProtocolName string
|
||||
tlsDefaultCommonName string
|
||||
lans []*net.IPNet
|
||||
writeRateLimit *ratelimit.Bucket
|
||||
readRateLimit *ratelimit.Bucket
|
||||
|
||||
lastRelayCheck map[protocol.DeviceID]time.Time
|
||||
|
||||
mut sync.RWMutex
|
||||
connType map[protocol.DeviceID]model.ConnectionType
|
||||
relaysEnabled bool
|
||||
}
|
||||
|
||||
func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, relaySvc *relay.Svc,
|
||||
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) suture.Service {
|
||||
svc := &connectionSvc{
|
||||
Supervisor: suture.NewSimple("connectionSvc"),
|
||||
cfg: cfg,
|
||||
myID: myID,
|
||||
model: mdl,
|
||||
tlsCfg: tlsCfg,
|
||||
discoverer: discoverer,
|
||||
relaySvc: relaySvc,
|
||||
conns: make(chan model.IntermediateConnection),
|
||||
bepProtocolName: bepProtocolName,
|
||||
tlsDefaultCommonName: tlsDefaultCommonName,
|
||||
lans: lans,
|
||||
|
||||
connType: make(map[protocol.DeviceID]model.ConnectionType),
|
||||
relaysEnabled: cfg.Options().RelaysEnabled,
|
||||
lastRelayCheck: make(map[protocol.DeviceID]time.Time),
|
||||
}
|
||||
cfg.Subscribe(svc)
|
||||
|
||||
if svc.cfg.Options().MaxSendKbps > 0 {
|
||||
svc.writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*svc.cfg.Options().MaxSendKbps), int64(5*1000*svc.cfg.Options().MaxSendKbps))
|
||||
}
|
||||
if svc.cfg.Options().MaxRecvKbps > 0 {
|
||||
svc.readRateLimit = ratelimit.NewBucketWithRate(float64(1000*svc.cfg.Options().MaxRecvKbps), int64(5*1000*svc.cfg.Options().MaxRecvKbps))
|
||||
}
|
||||
|
||||
// There are several moving parts here; one routine per listening address
|
||||
// to handle incoming connections, one routine to periodically attempt
|
||||
// outgoing connections, one routine to the the common handling
|
||||
// regardless of whether the connection was incoming or outgoing.
|
||||
// Furthermore, a relay service which handles incoming requests to connect
|
||||
// via the relays.
|
||||
//
|
||||
// TODO: Clean shutdown, and/or handling config changes on the fly. We
|
||||
// partly do this now - new devices and addresses will be picked up, but
|
||||
// not new listen addresses and we don't support disconnecting devices
|
||||
// that are removed and so on...
|
||||
|
||||
svc.Add(serviceFunc(svc.connect))
|
||||
for _, addr := range svc.cfg.Options().ListenAddress {
|
||||
uri, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse listen address:", addr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
listener, ok := listeners[uri.Scheme]
|
||||
if !ok {
|
||||
l.Infoln("Unknown listen address scheme:", uri.String())
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("listening on", uri.String())
|
||||
}
|
||||
|
||||
svc.Add(serviceFunc(func() {
|
||||
listener(uri, svc.tlsCfg, svc.conns)
|
||||
}))
|
||||
}
|
||||
svc.Add(serviceFunc(svc.handle))
|
||||
|
||||
if svc.relaySvc != nil {
|
||||
svc.Add(serviceFunc(svc.acceptRelayConns))
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (s *connectionSvc) handle() {
|
||||
next:
|
||||
for c := range s.conns {
|
||||
cs := c.Conn.ConnectionState()
|
||||
|
||||
// We should have negotiated the next level protocol "bep/1.0" as part
|
||||
// of the TLS handshake. Unfortunately this can't be a hard error,
|
||||
// because there are implementations out there that don't support
|
||||
// protocol negotiation (iOS for one...).
|
||||
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != s.bepProtocolName {
|
||||
l.Infof("Peer %s did not negotiate bep/1.0", c.Conn.RemoteAddr())
|
||||
}
|
||||
|
||||
// We should have received exactly one certificate from the other
|
||||
// side. If we didn't, they don't have a device ID and we drop the
|
||||
// connection.
|
||||
certs := cs.PeerCertificates
|
||||
if cl := len(certs); cl != 1 {
|
||||
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.Conn.RemoteAddr())
|
||||
c.Conn.Close()
|
||||
continue
|
||||
}
|
||||
remoteCert := certs[0]
|
||||
remoteID := protocol.NewDeviceID(remoteCert.Raw)
|
||||
|
||||
// The device ID should not be that of ourselves. It can happen
|
||||
// though, especially in the presence of NAT hairpinning, multiple
|
||||
// clients between the same NAT gateway, and global discovery.
|
||||
if remoteID == s.myID {
|
||||
l.Infof("Connected to myself (%s) - should not happen", remoteID)
|
||||
c.Conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// If we have a relay connection, and the new incoming connection is
|
||||
// not a relay connection, we should drop that, and prefer the this one.
|
||||
s.mut.RLock()
|
||||
ct, ok := s.connType[remoteID]
|
||||
s.mut.RUnlock()
|
||||
if ok && !ct.IsDirect() && c.Type.IsDirect() {
|
||||
if debug {
|
||||
l.Debugln("Switching connections", remoteID)
|
||||
}
|
||||
s.model.Close(remoteID, fmt.Errorf("switching connections"))
|
||||
} else if s.model.ConnectedTo(remoteID) {
|
||||
// We should not already be connected to the other party. TODO: This
|
||||
// could use some better handling. If the old connection is dead but
|
||||
// hasn't timed out yet we may want to drop *that* connection and keep
|
||||
// this one. But in case we are two devices connecting to each other
|
||||
// in parallel we don't want to do that or we end up with no
|
||||
// connections still established...
|
||||
l.Infof("Connected to already connected device (%s)", remoteID)
|
||||
c.Conn.Close()
|
||||
continue
|
||||
} else if s.model.IsPaused(remoteID) {
|
||||
l.Infof("Connection from paused device (%s)", remoteID)
|
||||
c.Conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
for deviceID, deviceCfg := range s.cfg.Devices() {
|
||||
if deviceID == remoteID {
|
||||
// Verify the name on the certificate. By default we set it to
|
||||
// "syncthing" when generating, but the user may have replaced
|
||||
// the certificate and used another name.
|
||||
certName := deviceCfg.CertName
|
||||
if certName == "" {
|
||||
certName = s.tlsDefaultCommonName
|
||||
}
|
||||
err := remoteCert.VerifyHostname(certName)
|
||||
if err != nil {
|
||||
// Incorrect certificate name is something the user most
|
||||
// likely wants to know about, since it's an advanced
|
||||
// config. Warn instead of Info.
|
||||
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.Conn.RemoteAddr(), err)
|
||||
c.Conn.Close()
|
||||
continue next
|
||||
}
|
||||
|
||||
// If rate limiting is set, and based on the address we should
|
||||
// limit the connection, then we wrap it in a limiter.
|
||||
|
||||
limit := s.shouldLimit(c.Conn.RemoteAddr())
|
||||
|
||||
wr := io.Writer(c.Conn)
|
||||
if limit && s.writeRateLimit != nil {
|
||||
wr = NewWriteLimiter(c.Conn, s.writeRateLimit)
|
||||
}
|
||||
|
||||
rd := io.Reader(c.Conn)
|
||||
if limit && s.readRateLimit != nil {
|
||||
rd = NewReadLimiter(c.Conn, s.readRateLimit)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%s-%s (%s)", c.Conn.LocalAddr(), c.Conn.RemoteAddr(), c.Type)
|
||||
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
|
||||
|
||||
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
||||
if debug {
|
||||
l.Debugf("cipher suite: %04X in lan: %t", c.Conn.ConnectionState().CipherSuite, !limit)
|
||||
}
|
||||
|
||||
s.model.AddConnection(model.Connection{
|
||||
c.Conn,
|
||||
protoConn,
|
||||
c.Type,
|
||||
})
|
||||
s.mut.Lock()
|
||||
s.connType[remoteID] = c.Type
|
||||
s.mut.Unlock()
|
||||
continue next
|
||||
}
|
||||
}
|
||||
|
||||
if !s.cfg.IgnoredDevice(remoteID) {
|
||||
events.Default.Log(events.DeviceRejected, map[string]string{
|
||||
"device": remoteID.String(),
|
||||
"address": c.Conn.RemoteAddr().String(),
|
||||
})
|
||||
}
|
||||
|
||||
l.Infof("Connection from %s (%s) with ignored device ID %s", c.Conn.RemoteAddr(), c.Type, remoteID)
|
||||
c.Conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) connect() {
|
||||
delay := time.Second
|
||||
for {
|
||||
nextDevice:
|
||||
for deviceID, deviceCfg := range s.cfg.Devices() {
|
||||
if deviceID == s.myID {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.model.IsPaused(deviceID) {
|
||||
continue
|
||||
}
|
||||
|
||||
connected := s.model.ConnectedTo(deviceID)
|
||||
|
||||
s.mut.RLock()
|
||||
ct, ok := s.connType[deviceID]
|
||||
relaysEnabled := s.relaysEnabled
|
||||
s.mut.RUnlock()
|
||||
if connected && ok && ct.IsDirect() {
|
||||
continue
|
||||
}
|
||||
|
||||
var addrs []string
|
||||
var relays []discover.Relay
|
||||
for _, addr := range deviceCfg.Addresses {
|
||||
if addr == "dynamic" {
|
||||
if s.discoverer != nil {
|
||||
if t, r, err := s.discoverer.Lookup(deviceID); err == nil {
|
||||
addrs = append(addrs, t...)
|
||||
relays = append(relays, r...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
uri, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse connection url:", addr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
dialer, ok := dialers[uri.Scheme]
|
||||
if !ok {
|
||||
l.Infoln("Unknown address schema", uri.String())
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("dial", deviceCfg.DeviceID, uri.String())
|
||||
}
|
||||
conn, err := dialer(uri, s.tlsCfg)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln("dial failed", deviceCfg.DeviceID, uri.String(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if connected {
|
||||
s.model.Close(deviceID, fmt.Errorf("switching connections"))
|
||||
}
|
||||
|
||||
s.conns <- model.IntermediateConnection{
|
||||
conn, model.ConnectionTypeDirectDial,
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
|
||||
// Only connect via relays if not already connected
|
||||
// Also, do not set lastRelayCheck time if we have no relays,
|
||||
// as otherwise when we do discover relays, we might have to
|
||||
// wait up to RelayReconnectIntervalM to connect again.
|
||||
// Also, do not try relays if we are explicitly told not to.
|
||||
if connected || len(relays) == 0 || !relaysEnabled {
|
||||
continue nextDevice
|
||||
}
|
||||
|
||||
reconIntv := time.Duration(s.cfg.Options().RelayReconnectIntervalM) * time.Minute
|
||||
if last, ok := s.lastRelayCheck[deviceID]; ok && time.Since(last) < reconIntv {
|
||||
if debug {
|
||||
l.Debugln("Skipping connecting via relay to", deviceID, "last checked at", last)
|
||||
}
|
||||
continue nextDevice
|
||||
} else if debug {
|
||||
l.Debugln("Trying relay connections to", deviceID, relays)
|
||||
}
|
||||
|
||||
s.lastRelayCheck[deviceID] = time.Now()
|
||||
|
||||
for _, addr := range relays {
|
||||
uri, err := url.Parse(addr.URL)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse relay connection url:", addr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
inv, err := client.GetInvitationFromRelay(uri, deviceID, s.tlsCfg.Certificates)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("Failed to get invitation for %s from %s: %v", deviceID, uri, err)
|
||||
}
|
||||
continue
|
||||
} else if debug {
|
||||
l.Debugln("Succesfully retrieved relay invitation", inv, "from", uri)
|
||||
}
|
||||
|
||||
conn, err := client.JoinSession(inv)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("Failed to join relay session %s: %v", inv, err)
|
||||
}
|
||||
continue
|
||||
} else if debug {
|
||||
l.Debugln("Sucessfully joined relay session", inv)
|
||||
}
|
||||
|
||||
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
|
||||
var tc *tls.Conn
|
||||
|
||||
if inv.ServerSocket {
|
||||
tc = tls.Server(conn, s.tlsCfg)
|
||||
} else {
|
||||
tc = tls.Client(conn, s.tlsCfg)
|
||||
}
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
l.Infof("TLS handshake (BEP/relay %s): %v", inv, err)
|
||||
tc.Close()
|
||||
continue
|
||||
}
|
||||
s.conns <- model.IntermediateConnection{
|
||||
tc, model.ConnectionTypeRelayDial,
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
delay *= 2
|
||||
if maxD := time.Duration(s.cfg.Options().ReconnectIntervalS) * time.Second; delay > maxD {
|
||||
delay = maxD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) acceptRelayConns() {
|
||||
for {
|
||||
conn := s.relaySvc.Accept()
|
||||
s.conns <- model.IntermediateConnection{
|
||||
Conn: conn,
|
||||
Type: model.ConnectionTypeRelayAccept,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
|
||||
if s.cfg.Options().LimitBandwidthInLan {
|
||||
return true
|
||||
}
|
||||
|
||||
tcpaddr, ok := addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
for _, lan := range s.lans {
|
||||
if lan.Contains(tcpaddr.IP) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return !tcpaddr.IP.IsLoopback()
|
||||
}
|
||||
|
||||
func (s *connectionSvc) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
s.mut.Lock()
|
||||
s.relaysEnabled = to.Options.RelaysEnabled
|
||||
s.mut.Unlock()
|
||||
|
||||
// We require a restart if a device as been removed.
|
||||
|
||||
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
|
||||
for _, dev := range to.Devices {
|
||||
newDevices[dev.DeviceID] = true
|
||||
}
|
||||
|
||||
for _, dev := range from.Devices {
|
||||
if !newDevices[dev.DeviceID] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// serviceFunc wraps a function to create a suture.Service without stop
|
||||
// functionality.
|
||||
type serviceFunc func()
|
||||
|
||||
func (f serviceFunc) Serve() { f() }
|
||||
func (f serviceFunc) Stop() {}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package connections
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dialers["tcp"] = tcpDialer
|
||||
listeners["tcp"] = tcpListener
|
||||
}
|
||||
|
||||
func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(uri.Host)
|
||||
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
|
||||
// addr is on the form "1.2.3.4"
|
||||
uri.Host = net.JoinHostPort(uri.Host, "22000")
|
||||
} else if err == nil && port == "" {
|
||||
// addr is on the form "1.2.3.4:"
|
||||
uri.Host = net.JoinHostPort(host, "22000")
|
||||
}
|
||||
|
||||
raddr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.DialTCP("tcp", nil, raddr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = osutil.SetTCPOptions(conn)
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
|
||||
tc := tls.Client(conn, tlsCfg)
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
tc.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- model.IntermediateConnection) {
|
||||
tcaddr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||
if err != nil {
|
||||
l.Fatalln("listen (BEP/tcp):", err)
|
||||
return
|
||||
}
|
||||
listener, err := net.ListenTCP("tcp", tcaddr)
|
||||
if err != nil {
|
||||
l.Fatalln("listen (BEP/tcp):", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
l.Warnln("Accepting connection (BEP/tcp):", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("connect from", conn.RemoteAddr())
|
||||
}
|
||||
|
||||
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
|
||||
tc := tls.Server(conn, tlsCfg)
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
l.Infoln("TLS handshake (BEP/tcp):", err)
|
||||
tc.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
conns <- model.IntermediateConnection{
|
||||
tc, model.ConnectionTypeDirectAccept,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package connections
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/calmh/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = strings.Contains(os.Getenv("STTRACE"), "connections") || os.Getenv("STTRACE") == "all"
|
||||
l = logger.DefaultLogger
|
||||
)
|
||||
+38
-5
@@ -16,9 +16,12 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
@@ -109,21 +112,48 @@ func (m *BlockMap) blockKey(hash []byte, file string) []byte {
|
||||
}
|
||||
|
||||
type BlockFinder struct {
|
||||
db *leveldb.DB
|
||||
db *leveldb.DB
|
||||
folders []string
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
func NewBlockFinder(db *leveldb.DB) *BlockFinder {
|
||||
func NewBlockFinder(db *leveldb.DB, cfg *config.Wrapper) *BlockFinder {
|
||||
if blockFinder != nil {
|
||||
return blockFinder
|
||||
}
|
||||
|
||||
f := &BlockFinder{
|
||||
db: db,
|
||||
db: db,
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
|
||||
f.CommitConfiguration(config.Configuration{}, cfg.Raw())
|
||||
cfg.Subscribe(f)
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// VerifyConfiguration implementes the config.Committer interface
|
||||
func (f *BlockFinder) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommitConfiguration implementes the config.Committer interface
|
||||
func (f *BlockFinder) CommitConfiguration(from, to config.Configuration) bool {
|
||||
folders := make([]string, len(to.Folders))
|
||||
for i, folder := range to.Folders {
|
||||
folders[i] = folder.ID
|
||||
}
|
||||
|
||||
sort.Strings(folders)
|
||||
|
||||
f.mut.Lock()
|
||||
f.folders = folders
|
||||
f.mut.Unlock()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *BlockFinder) String() string {
|
||||
return fmt.Sprintf("BlockFinder@%p", f)
|
||||
}
|
||||
@@ -133,7 +163,10 @@ func (f *BlockFinder) String() string {
|
||||
// they are happy with the block) or false to continue iterating for whatever
|
||||
// reason. The iterator finally returns the result, whether or not a
|
||||
// satisfying block was eventually found.
|
||||
func (f *BlockFinder) Iterate(folders []string, hash []byte, iterFn func(string, string, int32) bool) bool {
|
||||
func (f *BlockFinder) Iterate(hash []byte, iterFn func(string, string, int32) bool) bool {
|
||||
f.mut.RLock()
|
||||
folders := f.folders
|
||||
f.mut.RUnlock()
|
||||
for _, folder := range folders {
|
||||
key := toBlockKey(hash, folder, "")
|
||||
iter := f.db.NewIterator(util.BytesPrefix(key), nil)
|
||||
|
||||
+23
-16
@@ -9,7 +9,8 @@ package db
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
@@ -29,7 +30,6 @@ func genBlocks(n int) []protocol.BlockInfo {
|
||||
}
|
||||
|
||||
var f1, f2, f3 protocol.FileInfo
|
||||
var folders = []string{"folder1", "folder2"}
|
||||
|
||||
func init() {
|
||||
blocks := genBlocks(30)
|
||||
@@ -57,7 +57,16 @@ func setup() (*leveldb.DB, *BlockFinder) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return db, NewBlockFinder(db)
|
||||
|
||||
wrapper := config.Wrap("", config.Configuration{})
|
||||
wrapper.SetFolder(config.FolderConfiguration{
|
||||
ID: "folder1",
|
||||
})
|
||||
wrapper.SetFolder(config.FolderConfiguration{
|
||||
ID: "folder2",
|
||||
})
|
||||
|
||||
return db, NewBlockFinder(db, wrapper)
|
||||
}
|
||||
|
||||
func dbEmpty(db *leveldb.DB) bool {
|
||||
@@ -85,21 +94,21 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
if folder != "folder1" || file != "f1" || index != 0 {
|
||||
t.Fatal("Mismatch")
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
f.Iterate(folders, f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
f.Iterate(f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
if folder != "folder1" || file != "f2" || index != 0 {
|
||||
t.Fatal("Mismatch")
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
f.Iterate(folders, f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
f.Iterate(f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
t.Fatal("Unexpected block")
|
||||
return true
|
||||
})
|
||||
@@ -114,17 +123,17 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
t.Fatal("Unexpected block")
|
||||
return false
|
||||
})
|
||||
|
||||
f.Iterate(folders, f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
f.Iterate(f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
t.Fatal("Unexpected block")
|
||||
return false
|
||||
})
|
||||
|
||||
f.Iterate(folders, f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
f.Iterate(f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
if folder != "folder1" || file != "f3" || index != 0 {
|
||||
t.Fatal("Mismatch")
|
||||
}
|
||||
@@ -171,7 +180,7 @@ func TestBlockFinderLookup(t *testing.T) {
|
||||
}
|
||||
|
||||
counter := 0
|
||||
f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
counter++
|
||||
switch counter {
|
||||
case 1:
|
||||
@@ -187,7 +196,6 @@ func TestBlockFinderLookup(t *testing.T) {
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if counter != 2 {
|
||||
t.Fatal("Incorrect count", counter)
|
||||
}
|
||||
@@ -200,7 +208,7 @@ func TestBlockFinderLookup(t *testing.T) {
|
||||
}
|
||||
|
||||
counter = 0
|
||||
f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||
counter++
|
||||
switch counter {
|
||||
case 1:
|
||||
@@ -212,7 +220,6 @@ func TestBlockFinderLookup(t *testing.T) {
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if counter != 1 {
|
||||
t.Fatal("Incorrect count")
|
||||
}
|
||||
@@ -233,7 +240,7 @@ func TestBlockFinderFix(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !f.Iterate(folders, f1.Blocks[0].Hash, iterFn) {
|
||||
if !f.Iterate(f1.Blocks[0].Hash, iterFn) {
|
||||
t.Fatal("Block not found")
|
||||
}
|
||||
|
||||
@@ -242,11 +249,11 @@ func TestBlockFinderFix(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if f.Iterate(folders, f1.Blocks[0].Hash, iterFn) {
|
||||
if f.Iterate(f1.Blocks[0].Hash, iterFn) {
|
||||
t.Fatal("Unexpected block")
|
||||
}
|
||||
|
||||
if !f.Iterate(folders, f2.Blocks[0].Hash, iterFn) {
|
||||
if !f.Iterate(f2.Blocks[0].Hash, iterFn) {
|
||||
t.Fatal("Block not found")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import (
|
||||
"runtime"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
|
||||
+1
-1
@@ -13,8 +13,8 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
+1
-1
@@ -13,8 +13,8 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
package db
|
||||
|
||||
import "github.com/syncthing/syncthing/lib/protocol"
|
||||
import "github.com/syncthing/protocol"
|
||||
|
||||
type FileInfoTruncated struct {
|
||||
protocol.FileInfo
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
stdsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
// The CachingMux aggregates results from multiple Finders. Each Finder has
|
||||
// an associated cache time and negative cache time. The cache time sets how
|
||||
// long we cache and return successfull lookup results, the negative cache
|
||||
// time sets how long we refrain from asking about the same device ID after
|
||||
// receiving a negative answer. The value of zero disables caching (positive
|
||||
// or negative).
|
||||
type CachingMux struct {
|
||||
*suture.Supervisor
|
||||
finders []cachedFinder
|
||||
caches []*cache
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
// A cachedFinder is a Finder with associated cache timeouts.
|
||||
type cachedFinder struct {
|
||||
Finder
|
||||
cacheTime time.Duration
|
||||
negCacheTime time.Duration
|
||||
}
|
||||
|
||||
func NewCachingMux() *CachingMux {
|
||||
return &CachingMux{
|
||||
Supervisor: suture.NewSimple("discover.cachingMux"),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
// Add registers a new Finder, with associated cache timeouts.
|
||||
func (m *CachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration) {
|
||||
m.mut.Lock()
|
||||
m.finders = append(m.finders, cachedFinder{finder, cacheTime, negCacheTime})
|
||||
m.caches = append(m.caches, newCache())
|
||||
m.mut.Unlock()
|
||||
|
||||
if svc, ok := finder.(suture.Service); ok {
|
||||
m.Supervisor.Add(svc)
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup attempts to resolve the device ID using any of the added Finders,
|
||||
// while obeying the cache settings.
|
||||
func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
|
||||
m.mut.Lock()
|
||||
for i, finder := range m.finders {
|
||||
if cacheEntry, ok := m.caches[i].Get(deviceID); ok {
|
||||
// We have a cache entry. Lets see what it says.
|
||||
|
||||
if cacheEntry.found && time.Since(cacheEntry.when) < finder.cacheTime {
|
||||
// It's a positive, valid entry. Use it.
|
||||
if debug {
|
||||
l.Debugln("cached discovery entry for", deviceID, "at", finder.String())
|
||||
l.Debugln(" ", cacheEntry)
|
||||
}
|
||||
direct = append(direct, cacheEntry.Direct...)
|
||||
relays = append(relays, cacheEntry.Relays...)
|
||||
continue
|
||||
}
|
||||
|
||||
if !cacheEntry.found && time.Since(cacheEntry.when) < finder.negCacheTime {
|
||||
// It's a negative, valid entry. We should not make another
|
||||
// attempt right now.
|
||||
if debug {
|
||||
l.Debugln("negative cache entry for", deviceID, "at", finder.String())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// It's expired. Ignore and continue.
|
||||
}
|
||||
|
||||
// Perform the actual lookup and cache the result.
|
||||
if td, tr, err := finder.Lookup(deviceID); err == nil {
|
||||
if debug {
|
||||
l.Debugln("lookup for", deviceID, "at", finder.String())
|
||||
l.Debugln(" ", td)
|
||||
l.Debugln(" ", tr)
|
||||
}
|
||||
direct = append(direct, td...)
|
||||
relays = append(relays, tr...)
|
||||
m.caches[i].Set(deviceID, CacheEntry{
|
||||
Direct: td,
|
||||
Relays: tr,
|
||||
when: time.Now(),
|
||||
found: len(td)+len(tr) > 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
m.mut.Unlock()
|
||||
|
||||
if debug {
|
||||
l.Debugln("lookup results for", deviceID)
|
||||
l.Debugln(" ", direct)
|
||||
l.Debugln(" ", relays)
|
||||
}
|
||||
|
||||
return direct, relays, nil
|
||||
}
|
||||
|
||||
func (m *CachingMux) String() string {
|
||||
return "discovery cache"
|
||||
}
|
||||
|
||||
func (m *CachingMux) Error() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CachingMux) ChildErrors() map[string]error {
|
||||
m.mut.Lock()
|
||||
children := make(map[string]error, len(m.finders))
|
||||
for _, f := range m.finders {
|
||||
children[f.String()] = f.Error()
|
||||
}
|
||||
m.mut.Unlock()
|
||||
return children
|
||||
}
|
||||
|
||||
func (m *CachingMux) Cache() map[protocol.DeviceID]CacheEntry {
|
||||
// Res will be the "total" cache, i.e. the union of our cache and all our
|
||||
// children's caches.
|
||||
res := make(map[protocol.DeviceID]CacheEntry)
|
||||
|
||||
m.mut.Lock()
|
||||
for i := range m.finders {
|
||||
// Each finder[i] has a corresponding cache at cache[i]. Go through it
|
||||
// and populate the total, if it's newer than what's already in there.
|
||||
// We skip any negative cache entries.
|
||||
for k, v := range m.caches[i].Cache() {
|
||||
if v.found && v.when.After(res[k].when) {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Then ask the finder itself for it's cache and do the same. If this
|
||||
// finder is a global discovery client, it will have no cache. If it's
|
||||
// a local discovery client, this will be it's current state.
|
||||
for k, v := range m.finders[i].Cache() {
|
||||
if v.found && v.when.After(res[k].when) {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
m.mut.Unlock()
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// A cache can be embedded wherever useful
|
||||
|
||||
type cache struct {
|
||||
entries map[protocol.DeviceID]CacheEntry
|
||||
mut stdsync.Mutex
|
||||
}
|
||||
|
||||
func newCache() *cache {
|
||||
return &cache{
|
||||
entries: make(map[protocol.DeviceID]CacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cache) Set(id protocol.DeviceID, ce CacheEntry) {
|
||||
c.mut.Lock()
|
||||
c.entries[id] = ce
|
||||
c.mut.Unlock()
|
||||
}
|
||||
|
||||
func (c *cache) Get(id protocol.DeviceID) (CacheEntry, bool) {
|
||||
c.mut.Lock()
|
||||
ce, ok := c.entries[id]
|
||||
c.mut.Unlock()
|
||||
return ce, ok
|
||||
}
|
||||
|
||||
func (c *cache) Cache() map[protocol.DeviceID]CacheEntry {
|
||||
c.mut.Lock()
|
||||
m := make(map[protocol.DeviceID]CacheEntry, len(c.entries))
|
||||
for k, v := range c.entries {
|
||||
m[k] = v
|
||||
}
|
||||
c.mut.Unlock()
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
)
|
||||
|
||||
type Factory func(*url.URL, *Announce) (Client, error)
|
||||
|
||||
var (
|
||||
factories = make(map[string]Factory)
|
||||
DefaultErrorRetryInternval = 60 * time.Second
|
||||
DefaultGlobalBroadcastInterval = 1800 * time.Second
|
||||
)
|
||||
|
||||
func Register(proto string, factory Factory) {
|
||||
factories[proto] = factory
|
||||
}
|
||||
|
||||
func New(addr string, pkt *Announce) (Client, error) {
|
||||
uri, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
factory, ok := factories[uri.Scheme]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Unsupported scheme: %s", uri.Scheme)
|
||||
}
|
||||
client, err := factory(uri, pkt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Lookup(device protocol.DeviceID) []string
|
||||
StatusOK() bool
|
||||
Address() string
|
||||
Stop()
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
var device protocol.DeviceID
|
||||
|
||||
func init() {
|
||||
device, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
|
||||
}
|
||||
|
||||
func TestUDP4Success(t *testing.T) {
|
||||
conn, err := net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
port := conn.LocalAddr().(*net.UDPAddr).Port
|
||||
|
||||
address := fmt.Sprintf("udp4://127.0.0.1:%d", port)
|
||||
pkt := &Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{
|
||||
device[:],
|
||||
[]Address{{
|
||||
IP: net.IPv4(123, 123, 123, 123),
|
||||
Port: 1234,
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
client, err := New(address, pkt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
udpclient := client.(*UDPClient)
|
||||
if udpclient.errorRetryInterval != DefaultErrorRetryInternval {
|
||||
t.Fatal("Incorrect retry interval")
|
||||
}
|
||||
|
||||
if udpclient.listenAddress.IP != nil || udpclient.listenAddress.Port != 0 {
|
||||
t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
|
||||
}
|
||||
|
||||
if client.Address() != address {
|
||||
t.Fatal("Incorrect address")
|
||||
}
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
|
||||
// First announcement
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, err = conn.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Announcement verification
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
|
||||
_, addr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reply to it.
|
||||
_, err = conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We should get nothing else
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, err = conn.Read(buf)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
|
||||
// Status should be ok
|
||||
if !client.StatusOK() {
|
||||
t.Fatal("Wrong status")
|
||||
}
|
||||
|
||||
// Do a lookup in a separate routine
|
||||
addrs := []string{}
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
addrs = client.Lookup(device)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Receive the lookup and reply
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, addr, err = conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
|
||||
|
||||
// Wait for the lookup to arrive, verify that the number of answers is correct
|
||||
wg.Wait()
|
||||
|
||||
if len(addrs) != 1 || addrs[0] != "123.123.123.123:1234" {
|
||||
t.Fatal("Wrong number of answers")
|
||||
}
|
||||
|
||||
client.Stop()
|
||||
}
|
||||
|
||||
func TestUDP4Failure(t *testing.T) {
|
||||
conn, err := net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
port := conn.LocalAddr().(*net.UDPAddr).Port
|
||||
|
||||
address := fmt.Sprintf("udp4://127.0.0.1:%d/?listenaddress=127.0.0.1&retry=5", port)
|
||||
|
||||
pkt := &Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{
|
||||
device[:],
|
||||
[]Address{{
|
||||
IP: net.IPv4(123, 123, 123, 123),
|
||||
Port: 1234,
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
client, err := New(address, pkt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
udpclient := client.(*UDPClient)
|
||||
if udpclient.errorRetryInterval != time.Second*5 {
|
||||
t.Fatal("Incorrect retry interval")
|
||||
}
|
||||
|
||||
if !udpclient.listenAddress.IP.Equal(net.IPv4(127, 0, 0, 1)) || udpclient.listenAddress.Port != 0 {
|
||||
t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
|
||||
}
|
||||
|
||||
if client.Address() != address {
|
||||
t.Fatal("Incorrect address")
|
||||
}
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
|
||||
// First announcement
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, err = conn.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Announcement verification
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
|
||||
_, _, err = conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Don't reply
|
||||
// We should get nothing else
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, err = conn.Read(buf)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
|
||||
// Status should be failure
|
||||
if client.StatusOK() {
|
||||
t.Fatal("Wrong status")
|
||||
}
|
||||
|
||||
// Do a lookup in a separate routine
|
||||
addrs := []string{}
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
addrs = client.Lookup(device)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Receive the lookup and don't reply
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, _, err = conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for the lookup to timeout, verify that the number of answers is none
|
||||
wg.Wait()
|
||||
|
||||
if len(addrs) != 0 {
|
||||
t.Fatal("Wrong number of answers")
|
||||
}
|
||||
|
||||
client.Stop()
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, proto := range []string{"udp", "udp4", "udp6"} {
|
||||
Register(proto, func(uri *url.URL, pkt *Announce) (Client, error) {
|
||||
c := &UDPClient{
|
||||
wg: sync.NewWaitGroup(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
err := c.Start(uri, pkt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type UDPClient struct {
|
||||
url *url.URL
|
||||
|
||||
id protocol.DeviceID
|
||||
|
||||
stop chan struct{}
|
||||
wg sync.WaitGroup
|
||||
listenAddress *net.UDPAddr
|
||||
|
||||
globalBroadcastInterval time.Duration
|
||||
errorRetryInterval time.Duration
|
||||
|
||||
status bool
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
func (d *UDPClient) Start(uri *url.URL, pkt *Announce) error {
|
||||
d.url = uri
|
||||
d.id = protocol.DeviceIDFromBytes(pkt.This.ID)
|
||||
d.stop = make(chan struct{})
|
||||
|
||||
params := uri.Query()
|
||||
// The address must not have a port, as otherwise both announce and lookup
|
||||
// sockets would try to bind to the same port.
|
||||
addr, err := net.ResolveUDPAddr(d.url.Scheme, params.Get("listenaddress")+":0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.listenAddress = addr
|
||||
|
||||
broadcastSeconds, err := strconv.ParseUint(params.Get("broadcast"), 0, 0)
|
||||
if err != nil {
|
||||
d.globalBroadcastInterval = DefaultGlobalBroadcastInterval
|
||||
} else {
|
||||
d.globalBroadcastInterval = time.Duration(broadcastSeconds) * time.Second
|
||||
}
|
||||
|
||||
retrySeconds, err := strconv.ParseUint(params.Get("retry"), 0, 0)
|
||||
if err != nil {
|
||||
d.errorRetryInterval = DefaultErrorRetryInternval
|
||||
} else {
|
||||
d.errorRetryInterval = time.Duration(retrySeconds) * time.Second
|
||||
}
|
||||
|
||||
d.wg.Add(1)
|
||||
go d.broadcast(pkt.MustMarshalXDR())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *UDPClient) broadcast(pkt []byte) {
|
||||
defer d.wg.Done()
|
||||
|
||||
conn, err := net.ListenUDP(d.url.Scheme, d.listenAddress)
|
||||
for err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: broadcast listen: %v; trying again in %v", d.url, err, d.errorRetryInterval)
|
||||
}
|
||||
select {
|
||||
case <-d.stop:
|
||||
return
|
||||
case <-time.After(d.errorRetryInterval):
|
||||
}
|
||||
conn, err = net.ListenUDP(d.url.Scheme, d.listenAddress)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
remote, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
||||
for err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: broadcast resolve: %v; trying again in %v", d.url, err, d.errorRetryInterval)
|
||||
}
|
||||
select {
|
||||
case <-d.stop:
|
||||
return
|
||||
case <-time.After(d.errorRetryInterval):
|
||||
}
|
||||
remote, err = net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(0)
|
||||
for {
|
||||
select {
|
||||
case <-d.stop:
|
||||
return
|
||||
|
||||
case <-timer.C:
|
||||
var ok bool
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover %s: broadcast: Sending self announcement to %v", d.url, remote)
|
||||
}
|
||||
|
||||
_, err := conn.WriteTo(pkt, remote)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: broadcast: Failed to send self announcement: %s", d.url, err)
|
||||
}
|
||||
ok = false
|
||||
} else {
|
||||
// Verify that the announce server responds positively for our device ID
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
res := d.Lookup(d.id)
|
||||
if debug {
|
||||
l.Debugf("discover %s: broadcast: Self-lookup returned: %v", d.url, res)
|
||||
}
|
||||
ok = len(res) > 0
|
||||
}
|
||||
|
||||
d.mut.Lock()
|
||||
d.status = ok
|
||||
d.mut.Unlock()
|
||||
|
||||
if ok {
|
||||
timer.Reset(d.globalBroadcastInterval)
|
||||
} else {
|
||||
timer.Reset(d.errorRetryInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
||||
extIP, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
conn, err := net.DialUDP(d.url.Scheme, d.listenAddress, extIP)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := Query{QueryMagic, device[:]}.MustMarshalXDR()
|
||||
_, err = conn.Write(buf)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
buf = make([]byte, 2048)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
// Expected if the server doesn't know about requested device ID
|
||||
return nil
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var pkt Announce
|
||||
err = pkt.UnmarshalXDR(buf[:n])
|
||||
if err != nil && err != io.EOF {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s\n%s", d.url, device, err, hex.Dump(buf[:n]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var addrs []string
|
||||
for _, a := range pkt.This.Addresses {
|
||||
deviceAddr := net.JoinHostPort(net.IP(a.IP).String(), strconv.Itoa(int(a.Port)))
|
||||
addrs = append(addrs, deviceAddr)
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s) result: %v", d.url, device, addrs)
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
func (d *UDPClient) Stop() {
|
||||
if d.stop != nil {
|
||||
close(d.stop)
|
||||
d.wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *UDPClient) StatusOK() bool {
|
||||
d.mut.RLock()
|
||||
defer d.mut.RUnlock()
|
||||
return d.status
|
||||
}
|
||||
|
||||
func (d *UDPClient) Address() string {
|
||||
return d.url.String()
|
||||
}
|
||||
+406
-30
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
@@ -7,48 +7,424 @@
|
||||
package discover
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/thejerf/suture"
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/beacon"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
// A Finder provides lookup services of some kind.
|
||||
type Finder interface {
|
||||
Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error)
|
||||
Error() error
|
||||
String() string
|
||||
Cache() map[protocol.DeviceID]CacheEntry
|
||||
type Discoverer struct {
|
||||
myID protocol.DeviceID
|
||||
listenAddrs []string
|
||||
localBcastIntv time.Duration
|
||||
localBcastStart time.Time
|
||||
cacheLifetime time.Duration
|
||||
negCacheCutoff time.Duration
|
||||
beacons []beacon.Interface
|
||||
extPort uint16
|
||||
localBcastTick <-chan time.Time
|
||||
forcedBcastTick chan time.Time
|
||||
|
||||
registryLock sync.RWMutex
|
||||
registry map[protocol.DeviceID][]CacheEntry
|
||||
lastLookup map[protocol.DeviceID]time.Time
|
||||
|
||||
clients []Client
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
type CacheEntry struct {
|
||||
Direct []string `json:"direct"`
|
||||
Relays []Relay `json:"relays"`
|
||||
when time.Time // When did we get the result
|
||||
found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)?
|
||||
Address string
|
||||
Seen time.Time
|
||||
}
|
||||
|
||||
// A FinderService is a Finder that has background activity and must be run as
|
||||
// a suture.Service.
|
||||
type FinderService interface {
|
||||
Finder
|
||||
suture.Service
|
||||
var (
|
||||
ErrIncorrectMagic = errors.New("incorrect magic number")
|
||||
)
|
||||
|
||||
func NewDiscoverer(id protocol.DeviceID, addresses []string) *Discoverer {
|
||||
return &Discoverer{
|
||||
myID: id,
|
||||
listenAddrs: addresses,
|
||||
localBcastIntv: 30 * time.Second,
|
||||
cacheLifetime: 5 * time.Minute,
|
||||
negCacheCutoff: 3 * time.Minute,
|
||||
registry: make(map[protocol.DeviceID][]CacheEntry),
|
||||
lastLookup: make(map[protocol.DeviceID]time.Time),
|
||||
registryLock: sync.NewRWMutex(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
type FinderMux interface {
|
||||
Finder
|
||||
ChildStatus() map[string]error
|
||||
func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
|
||||
if localPort > 0 {
|
||||
d.startLocalIPv4Broadcasts(localPort)
|
||||
}
|
||||
|
||||
if len(localMCAddr) > 0 {
|
||||
d.startLocalIPv6Multicasts(localMCAddr)
|
||||
}
|
||||
|
||||
if len(d.beacons) == 0 {
|
||||
l.Warnln("Local discovery unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
d.localBcastTick = time.Tick(d.localBcastIntv)
|
||||
d.forcedBcastTick = make(chan time.Time)
|
||||
d.localBcastStart = time.Now()
|
||||
go d.sendLocalAnnouncements()
|
||||
}
|
||||
|
||||
// The RelayStatusProvider answers questions about current relay status.
|
||||
type RelayStatusProvider interface {
|
||||
Relays() []string
|
||||
RelayStatus(uri string) (time.Duration, bool)
|
||||
func (d *Discoverer) startLocalIPv4Broadcasts(localPort int) {
|
||||
bb := beacon.NewBroadcast(localPort)
|
||||
d.beacons = append(d.beacons, bb)
|
||||
go d.recvAnnouncements(bb)
|
||||
bb.ServeBackground()
|
||||
}
|
||||
|
||||
// The AddressLister answers questions about what addresses we are listening
|
||||
// on.
|
||||
type AddressLister interface {
|
||||
ExternalAddresses() []string
|
||||
AllAddresses() []string
|
||||
func (d *Discoverer) startLocalIPv6Multicasts(localMCAddr string) {
|
||||
mb, err := beacon.NewMulticast(localMCAddr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln("beacon.NewMulticast:", err)
|
||||
}
|
||||
l.Infoln("Local discovery over IPv6 unavailable")
|
||||
return
|
||||
}
|
||||
d.beacons = append(d.beacons, mb)
|
||||
go d.recvAnnouncements(mb)
|
||||
}
|
||||
|
||||
func (d *Discoverer) StartGlobal(servers []string, extPort uint16) {
|
||||
d.mut.Lock()
|
||||
defer d.mut.Unlock()
|
||||
|
||||
if len(d.clients) > 0 {
|
||||
d.stopGlobal()
|
||||
}
|
||||
|
||||
d.extPort = extPort
|
||||
pkt := d.announcementPkt()
|
||||
wg := sync.NewWaitGroup()
|
||||
clients := make(chan Client, len(servers))
|
||||
for _, address := range servers {
|
||||
wg.Add(1)
|
||||
go func(addr string) {
|
||||
defer wg.Done()
|
||||
client, err := New(addr, pkt)
|
||||
if err != nil {
|
||||
l.Infoln("Error creating discovery client", addr, err)
|
||||
return
|
||||
}
|
||||
clients <- client
|
||||
}(address)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(clients)
|
||||
|
||||
for client := range clients {
|
||||
d.clients = append(d.clients, client)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) StopGlobal() {
|
||||
d.mut.Lock()
|
||||
defer d.mut.Unlock()
|
||||
d.stopGlobal()
|
||||
}
|
||||
|
||||
func (d *Discoverer) stopGlobal() {
|
||||
for _, client := range d.clients {
|
||||
client.Stop()
|
||||
}
|
||||
d.clients = []Client{}
|
||||
}
|
||||
|
||||
func (d *Discoverer) ExtAnnounceOK() map[string]bool {
|
||||
d.mut.RLock()
|
||||
defer d.mut.RUnlock()
|
||||
|
||||
ret := make(map[string]bool)
|
||||
for _, client := range d.clients {
|
||||
ret[client.Address()] = client.StatusOK()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
|
||||
d.registryLock.RLock()
|
||||
cached := d.filterCached(d.registry[device])
|
||||
lastLookup := d.lastLookup[device]
|
||||
d.registryLock.RUnlock()
|
||||
|
||||
d.mut.RLock()
|
||||
defer d.mut.RUnlock()
|
||||
|
||||
if len(cached) > 0 {
|
||||
// There are cached address entries.
|
||||
addrs := make([]string, len(cached))
|
||||
for i := range cached {
|
||||
addrs[i] = cached[i].Address
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
if time.Since(lastLookup) < d.negCacheCutoff {
|
||||
// We have recently tried to lookup this address and failed. Lets
|
||||
// chill for a while.
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(d.clients) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
|
||||
// Only perform external lookups if we have at least one external
|
||||
// server client and one local announcement interval has passed. This is
|
||||
// to avoid finding local peers on their remote address at startup.
|
||||
results := make(chan []string, len(d.clients))
|
||||
wg := sync.NewWaitGroup()
|
||||
for _, client := range d.clients {
|
||||
wg.Add(1)
|
||||
go func(c Client) {
|
||||
defer wg.Done()
|
||||
results <- c.Lookup(device)
|
||||
}(client)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
cached := []CacheEntry{}
|
||||
seen := make(map[string]struct{})
|
||||
now := time.Now()
|
||||
|
||||
var addrs []string
|
||||
for result := range results {
|
||||
for _, addr := range result {
|
||||
_, ok := seen[addr]
|
||||
if !ok {
|
||||
cached = append(cached, CacheEntry{
|
||||
Address: addr,
|
||||
Seen: now,
|
||||
})
|
||||
seen[addr] = struct{}{}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.registryLock.Lock()
|
||||
d.registry[device] = cached
|
||||
d.lastLookup[device] = time.Now()
|
||||
d.registryLock.Unlock()
|
||||
|
||||
return addrs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Discoverer) Hint(device string, addrs []string) {
|
||||
resAddrs := resolveAddrs(addrs)
|
||||
var id protocol.DeviceID
|
||||
id.UnmarshalText([]byte(device))
|
||||
d.registerDevice(nil, Device{
|
||||
Addresses: resAddrs,
|
||||
ID: id[:],
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry {
|
||||
d.registryLock.RLock()
|
||||
devices := make(map[protocol.DeviceID][]CacheEntry, len(d.registry))
|
||||
for device, addrs := range d.registry {
|
||||
addrsCopy := make([]CacheEntry, len(addrs))
|
||||
copy(addrsCopy, addrs)
|
||||
devices[device] = addrsCopy
|
||||
}
|
||||
d.registryLock.RUnlock()
|
||||
return devices
|
||||
}
|
||||
|
||||
func (d *Discoverer) announcementPkt() *Announce {
|
||||
var addrs []Address
|
||||
if d.extPort != 0 {
|
||||
addrs = []Address{{Port: d.extPort}}
|
||||
} else {
|
||||
for _, astr := range d.listenAddrs {
|
||||
addr, err := net.ResolveTCPAddr("tcp", astr)
|
||||
if err != nil {
|
||||
l.Warnln("discover: %v: not announcing %s", err, astr)
|
||||
continue
|
||||
} else if debug {
|
||||
l.Debugf("discover: resolved %s as %#v", astr, addr)
|
||||
}
|
||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
||||
addrs = append(addrs, Address{Port: uint16(addr.Port)})
|
||||
} else if bs := addr.IP.To4(); bs != nil {
|
||||
addrs = append(addrs, Address{IP: bs, Port: uint16(addr.Port)})
|
||||
} else if bs := addr.IP.To16(); bs != nil {
|
||||
addrs = append(addrs, Address{IP: bs, Port: uint16(addr.Port)})
|
||||
}
|
||||
}
|
||||
}
|
||||
return &Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{d.myID[:], addrs},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) sendLocalAnnouncements() {
|
||||
var addrs = resolveAddrs(d.listenAddrs)
|
||||
|
||||
var pkt = Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{d.myID[:], addrs},
|
||||
}
|
||||
msg := pkt.MustMarshalXDR()
|
||||
|
||||
for {
|
||||
for _, b := range d.beacons {
|
||||
b.Send(msg)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-d.localBcastTick:
|
||||
case <-d.forcedBcastTick:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) recvAnnouncements(b beacon.Interface) {
|
||||
for {
|
||||
buf, addr := b.Recv()
|
||||
|
||||
var pkt Announce
|
||||
err := pkt.UnmarshalXDR(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
if debug {
|
||||
l.Debugf("discover: Failed to unmarshal local announcement from %s:\n%s", addr, hex.Dump(buf))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
|
||||
}
|
||||
|
||||
var newDevice bool
|
||||
if bytes.Compare(pkt.This.ID, d.myID[:]) != 0 {
|
||||
newDevice = d.registerDevice(addr, pkt.This)
|
||||
}
|
||||
|
||||
if newDevice {
|
||||
select {
|
||||
case d.forcedBcastTick <- time.Now():
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
|
||||
var id protocol.DeviceID
|
||||
copy(id[:], device.ID)
|
||||
|
||||
d.registryLock.Lock()
|
||||
defer d.registryLock.Unlock()
|
||||
|
||||
current := d.filterCached(d.registry[id])
|
||||
|
||||
orig := current
|
||||
|
||||
for _, a := range device.Addresses {
|
||||
var deviceAddr string
|
||||
if len(a.IP) > 0 {
|
||||
deviceAddr = net.JoinHostPort(net.IP(a.IP).String(), strconv.Itoa(int(a.Port)))
|
||||
} else if addr != nil {
|
||||
ua := addr.(*net.UDPAddr)
|
||||
ua.Port = int(a.Port)
|
||||
deviceAddr = ua.String()
|
||||
}
|
||||
for i := range current {
|
||||
if current[i].Address == deviceAddr {
|
||||
current[i].Seen = time.Now()
|
||||
goto done
|
||||
}
|
||||
}
|
||||
current = append(current, CacheEntry{
|
||||
Address: deviceAddr,
|
||||
Seen: time.Now(),
|
||||
})
|
||||
done:
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover: Caching %s addresses: %v", id, current)
|
||||
}
|
||||
|
||||
d.registry[id] = current
|
||||
|
||||
if len(current) > len(orig) {
|
||||
addrs := make([]string, len(current))
|
||||
for i := range current {
|
||||
addrs[i] = current[i].Address
|
||||
}
|
||||
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
|
||||
"device": id.String(),
|
||||
"addrs": addrs,
|
||||
})
|
||||
}
|
||||
|
||||
return len(current) > len(orig)
|
||||
}
|
||||
|
||||
func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry {
|
||||
for i := 0; i < len(c); {
|
||||
if ago := time.Since(c[i].Seen); ago > d.cacheLifetime {
|
||||
if debug {
|
||||
l.Debugf("discover: Removing cached address %s - seen %v ago", c[i].Address, ago)
|
||||
}
|
||||
c[i] = c[len(c)-1]
|
||||
c = c[:len(c)-1]
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func addrToAddr(addr *net.TCPAddr) Address {
|
||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
||||
return Address{Port: uint16(addr.Port)}
|
||||
} else if bs := addr.IP.To4(); bs != nil {
|
||||
return Address{IP: bs, Port: uint16(addr.Port)}
|
||||
} else if bs := addr.IP.To16(); bs != nil {
|
||||
return Address{IP: bs, Port: uint16(addr.Port)}
|
||||
}
|
||||
return Address{}
|
||||
}
|
||||
|
||||
func resolveAddrs(addrs []string) []Address {
|
||||
var raddrs []Address
|
||||
for _, addrStr := range addrs {
|
||||
addrRes, err := net.ResolveTCPAddr("tcp", addrStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addr := addrToAddr(addrRes)
|
||||
if len(addr.IP) > 0 {
|
||||
raddrs = append(raddrs, addr)
|
||||
} else {
|
||||
raddrs = append(raddrs, Address{Port: addr.Port})
|
||||
}
|
||||
}
|
||||
return raddrs
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
)
|
||||
|
||||
type DummyClient struct {
|
||||
url *url.URL
|
||||
lookups []protocol.DeviceID
|
||||
lookupRet []string
|
||||
stops int
|
||||
statusRet bool
|
||||
statusChecks int
|
||||
}
|
||||
|
||||
func (c *DummyClient) Lookup(device protocol.DeviceID) []string {
|
||||
c.lookups = append(c.lookups, device)
|
||||
return c.lookupRet
|
||||
}
|
||||
|
||||
func (c *DummyClient) StatusOK() bool {
|
||||
c.statusChecks++
|
||||
return c.statusRet
|
||||
}
|
||||
|
||||
func (c *DummyClient) Stop() {
|
||||
c.stops++
|
||||
}
|
||||
|
||||
func (c *DummyClient) Address() string {
|
||||
return c.url.String()
|
||||
}
|
||||
|
||||
func TestGlobalDiscovery(t *testing.T) {
|
||||
c1 := &DummyClient{
|
||||
statusRet: false,
|
||||
lookupRet: []string{"test.com:1234"},
|
||||
}
|
||||
|
||||
c2 := &DummyClient{
|
||||
statusRet: true,
|
||||
lookupRet: []string{},
|
||||
}
|
||||
|
||||
c3 := &DummyClient{
|
||||
statusRet: true,
|
||||
lookupRet: []string{"best.com:2345"},
|
||||
}
|
||||
|
||||
clients := []*DummyClient{c1, c2}
|
||||
|
||||
Register("test1", func(uri *url.URL, pkt *Announce) (Client, error) {
|
||||
c := clients[0]
|
||||
clients = clients[1:]
|
||||
c.url = uri
|
||||
return c, nil
|
||||
})
|
||||
|
||||
Register("test2", func(uri *url.URL, pkt *Announce) (Client, error) {
|
||||
c3.url = uri
|
||||
return c3, nil
|
||||
})
|
||||
|
||||
d := NewDiscoverer(device, []string{})
|
||||
d.localBcastStart = time.Time{}
|
||||
servers := []string{
|
||||
"test1://123.123.123.123:1234",
|
||||
"test1://23.23.23.23:234",
|
||||
"test2://234.234.234.234.2345",
|
||||
}
|
||||
d.StartGlobal(servers, 1234)
|
||||
|
||||
if len(d.clients) != 3 {
|
||||
t.Fatal("Wrong number of clients")
|
||||
}
|
||||
|
||||
status := d.ExtAnnounceOK()
|
||||
|
||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
||||
if status[c.url.String()] != c.statusRet || c.statusChecks != 1 {
|
||||
t.Fatal("Wrong status")
|
||||
}
|
||||
}
|
||||
|
||||
addrs := d.Lookup(device)
|
||||
if len(addrs) != 2 {
|
||||
t.Fatal("Wrong number of addresses", addrs)
|
||||
}
|
||||
|
||||
for _, addr := range []string{"test.com:1234", "best.com:2345"} {
|
||||
found := false
|
||||
for _, laddr := range addrs {
|
||||
if laddr == addr {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("Couldn't find", addr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
||||
if len(c.lookups) != 1 || c.lookups[0] != device {
|
||||
t.Fatal("Wrong lookups")
|
||||
}
|
||||
}
|
||||
|
||||
addrs = d.Lookup(device)
|
||||
if len(addrs) != 2 {
|
||||
t.Fatal("Wrong number of addresses", addrs)
|
||||
}
|
||||
|
||||
// Answer should be cached, so number of lookups should have not increased
|
||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
||||
if len(c.lookups) != 1 || c.lookups[0] != device {
|
||||
t.Fatal("Wrong lookups")
|
||||
}
|
||||
}
|
||||
|
||||
d.StopGlobal()
|
||||
|
||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
||||
if c.stops != 1 {
|
||||
t.Fatal("Wrong number of stops")
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-69
@@ -1,75 +1,8 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
/*
|
||||
Package discover implements the local and global device discovery protocols.
|
||||
|
||||
Global Discovery
|
||||
================
|
||||
|
||||
Announcements
|
||||
-------------
|
||||
|
||||
A device should announce itself at startup. It does this by an HTTPS POST to
|
||||
the announce server URL (with the path usually being "/", but this is of
|
||||
course up to the discovery server). The POST has a JSON payload listing direct
|
||||
connection addresses (if any) and relay addresses (if any).
|
||||
|
||||
{
|
||||
direct: ["tcp://192.0.2.45:22000", "tcp://:22202"],
|
||||
relays: [{"url": "relay://192.0.2.99:22028", "latency": 142}]
|
||||
}
|
||||
|
||||
It's OK for either of the "direct" or "relays" fields to be either the empty
|
||||
list ([]), null, or missing entirely. An announcment with both fields missing
|
||||
or empty is however not useful...
|
||||
|
||||
Any empty or unspecified IP addresses (i.e. addresses like tcp://:22000,
|
||||
tcp://0.0.0.0:22000, tcp://[::]:22000) are interpreted as referring to the
|
||||
source IP address of the announcement.
|
||||
|
||||
The device ID of the announcing device is not part of the announcement.
|
||||
Instead, the server requires that the client perform certificate
|
||||
authentication. The device ID is deduced from the presented certificate.
|
||||
|
||||
The server response is empty, with code 200 (OK) on success. If no certificate
|
||||
was presented, status 403 (Forbidden) is returned. If the posted data doesn't
|
||||
conform to the expected format, 400 (Bad Request) is returned.
|
||||
|
||||
In successfull responses, the server may return a "Reannounce-After" header
|
||||
containing the number of seconds after which the client should perform a new
|
||||
announcement.
|
||||
|
||||
In error responses, the server may return a "Retry-After" header containing
|
||||
the number of seconds after which the client should retry.
|
||||
|
||||
Performing announcements significantly more often than indicated by the
|
||||
Reannounce-After or Retry-After headers may result in the client being
|
||||
throttled. In such cases the server may respond with status code 429 (Too Many
|
||||
Requests).
|
||||
|
||||
Queries
|
||||
=======
|
||||
|
||||
Queries are performed as HTTPS GET requests to the announce server URL. The
|
||||
requested device ID is passed as the query parameter "device", in canonical
|
||||
string form, i.e. https://announce.syncthing.net/?device=ABC12345-....
|
||||
|
||||
Successfull responses will have status code 200 (OK) and carry a JSON payload
|
||||
of the same format as the announcement above. The response will not contain
|
||||
empty or unspecified addresses.
|
||||
|
||||
If the "device" query parameter is missing or malformed, the status code 400
|
||||
(Bad Request) is returned.
|
||||
|
||||
If the device ID is of a valid format but not found in the registry, 404 (Not
|
||||
Found) is returned.
|
||||
|
||||
If the client has exceeded a rate limit, the server may respond with 429 (Too
|
||||
Many Requests).
|
||||
|
||||
*/
|
||||
// Package discover implements the device discovery protocol.
|
||||
package discover
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
stdsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type globalClient struct {
|
||||
server string
|
||||
addrList AddressLister
|
||||
relayStat RelayStatusProvider
|
||||
announceClient httpClient
|
||||
queryClient httpClient
|
||||
noAnnounce bool
|
||||
stop chan struct{}
|
||||
errorHolder
|
||||
}
|
||||
|
||||
type httpClient interface {
|
||||
Get(url string) (*http.Response, error)
|
||||
Post(url, ctype string, data io.Reader) (*http.Response, error)
|
||||
}
|
||||
|
||||
const (
|
||||
defaultReannounceInterval = 30 * time.Minute
|
||||
announceErrorRetryInterval = 5 * time.Minute
|
||||
)
|
||||
|
||||
type announcement struct {
|
||||
Direct []string `json:"direct"`
|
||||
Relays []Relay `json:"relays"`
|
||||
}
|
||||
|
||||
type serverOptions struct {
|
||||
insecure bool // don't check certificate
|
||||
noAnnounce bool // don't announce
|
||||
id string // expected server device ID
|
||||
}
|
||||
|
||||
func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
|
||||
server, opts, err := parseOptions(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var devID protocol.DeviceID
|
||||
if opts.id != "" {
|
||||
devID, err = protocol.DeviceIDFromString(opts.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// The http.Client used for announcements. It needs to have our
|
||||
// certificate to prove our identity, and may or may not verify the server
|
||||
// certificate depending on the insecure setting.
|
||||
var announceClient httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: opts.insecure,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
},
|
||||
},
|
||||
}
|
||||
if opts.id != "" {
|
||||
announceClient = newIDCheckingHTTPClient(announceClient, devID)
|
||||
}
|
||||
|
||||
// The http.Client used for queries. We don't need to present our
|
||||
// certificate here, so lets not include it. May be insecure if requested.
|
||||
var queryClient httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: opts.insecure,
|
||||
},
|
||||
},
|
||||
}
|
||||
if opts.id != "" {
|
||||
queryClient = newIDCheckingHTTPClient(queryClient, devID)
|
||||
}
|
||||
|
||||
cl := &globalClient{
|
||||
server: server,
|
||||
addrList: addrList,
|
||||
relayStat: relayStat,
|
||||
announceClient: announceClient,
|
||||
queryClient: queryClient,
|
||||
noAnnounce: opts.noAnnounce,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
cl.setError(errors.New("not announced"))
|
||||
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
// Lookup returns the list of addresses where the given device is available;
|
||||
// direct, and via relays.
|
||||
func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
|
||||
qURL, err := url.Parse(c.server)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
q := qURL.Query()
|
||||
q.Set("device", device.String())
|
||||
qURL.RawQuery = q.Encode()
|
||||
|
||||
resp, err := c.queryClient.Get(qURL.String())
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln("globalClient.Lookup", qURL.String(), err)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
resp.Body.Close()
|
||||
if debug {
|
||||
l.Debugln("globalClient.Lookup", qURL.String(), resp.Status)
|
||||
}
|
||||
return nil, nil, errors.New(resp.Status)
|
||||
}
|
||||
|
||||
// TODO: Handle 429 and Retry-After?
|
||||
|
||||
var ann announcement
|
||||
err = json.NewDecoder(resp.Body).Decode(&ann)
|
||||
resp.Body.Close()
|
||||
return ann.Direct, ann.Relays, err
|
||||
}
|
||||
|
||||
func (c *globalClient) String() string {
|
||||
return "global@" + c.server
|
||||
}
|
||||
|
||||
func (c *globalClient) Serve() {
|
||||
if c.noAnnounce {
|
||||
// We're configured to not do announcements, only lookups. To maintain
|
||||
// the same interface, we just pause here if Serve() is run.
|
||||
<-c.stop
|
||||
return
|
||||
}
|
||||
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
|
||||
eventSub := events.Default.Subscribe(events.ExternalPortMappingChanged | events.RelayStateChanged)
|
||||
defer events.Default.Unsubscribe(eventSub)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-eventSub.C():
|
||||
c.sendAnnouncement(timer)
|
||||
|
||||
case <-timer.C:
|
||||
c.sendAnnouncement(timer)
|
||||
|
||||
case <-c.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *globalClient) sendAnnouncement(timer *time.Timer) {
|
||||
|
||||
var ann announcement
|
||||
if c.addrList != nil {
|
||||
ann.Direct = c.addrList.ExternalAddresses()
|
||||
}
|
||||
|
||||
if c.relayStat != nil {
|
||||
for _, relay := range c.relayStat.Relays() {
|
||||
latency, ok := c.relayStat.RelayStatus(relay)
|
||||
if ok {
|
||||
ann.Relays = append(ann.Relays, Relay{
|
||||
URL: relay,
|
||||
Latency: int32(latency / time.Millisecond),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ann.Direct)+len(ann.Relays) == 0 {
|
||||
c.setError(errors.New("nothing to announce"))
|
||||
if debug {
|
||||
l.Debugln("Nothing to announce")
|
||||
}
|
||||
timer.Reset(announceErrorRetryInterval)
|
||||
return
|
||||
}
|
||||
|
||||
// The marshal doesn't fail, I promise.
|
||||
postData, _ := json.Marshal(ann)
|
||||
|
||||
if debug {
|
||||
l.Debugf("Announcement: %s", postData)
|
||||
}
|
||||
|
||||
resp, err := c.announceClient.Post(c.server, "application/json", bytes.NewReader(postData))
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln("announce POST:", err)
|
||||
}
|
||||
c.setError(err)
|
||||
timer.Reset(announceErrorRetryInterval)
|
||||
return
|
||||
}
|
||||
if debug {
|
||||
l.Debugln("announce POST:", resp.Status)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
if debug {
|
||||
l.Debugln("announce POST:", resp.Status)
|
||||
}
|
||||
c.setError(errors.New(resp.Status))
|
||||
|
||||
if h := resp.Header.Get("Retry-After"); h != "" {
|
||||
// The server has a recommendation on when we should
|
||||
// retry. Follow it.
|
||||
if secs, err := strconv.Atoi(h); err == nil && secs > 0 {
|
||||
if debug {
|
||||
l.Debugln("announce Retry-After:", secs, err)
|
||||
}
|
||||
timer.Reset(time.Duration(secs) * time.Second)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
timer.Reset(announceErrorRetryInterval)
|
||||
return
|
||||
}
|
||||
|
||||
c.setError(nil)
|
||||
|
||||
if h := resp.Header.Get("Reannounce-After"); h != "" {
|
||||
// The server has a recommendation on when we should
|
||||
// reannounce. Follow it.
|
||||
if secs, err := strconv.Atoi(h); err == nil && secs > 0 {
|
||||
if debug {
|
||||
l.Debugln("announce Reannounce-After:", secs, err)
|
||||
}
|
||||
timer.Reset(time.Duration(secs) * time.Second)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
timer.Reset(defaultReannounceInterval)
|
||||
}
|
||||
|
||||
func (c *globalClient) Stop() {
|
||||
close(c.stop)
|
||||
}
|
||||
|
||||
func (c *globalClient) Cache() map[protocol.DeviceID]CacheEntry {
|
||||
// The globalClient doesn't do caching
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseOptions parses and strips away any ?query=val options, setting the
|
||||
// corresponding field in the serverOptions struct. Unknown query options are
|
||||
// ignored and removed.
|
||||
func parseOptions(dsn string) (server string, opts serverOptions, err error) {
|
||||
p, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return "", serverOptions{}, err
|
||||
}
|
||||
|
||||
// Grab known options from the query string
|
||||
q := p.Query()
|
||||
opts.id = q.Get("id")
|
||||
opts.insecure = opts.id != "" || queryBool(q, "insecure")
|
||||
opts.noAnnounce = queryBool(q, "noannounce")
|
||||
|
||||
// Check for disallowed combinations
|
||||
if p.Scheme == "http" {
|
||||
if !opts.insecure {
|
||||
return "", serverOptions{}, errors.New("http without insecure not supported")
|
||||
}
|
||||
if !opts.noAnnounce {
|
||||
return "", serverOptions{}, errors.New("http without noannounce not supported")
|
||||
}
|
||||
} else if p.Scheme != "https" {
|
||||
return "", serverOptions{}, errors.New("unsupported scheme " + p.Scheme)
|
||||
}
|
||||
|
||||
// Remove the query string
|
||||
p.RawQuery = ""
|
||||
server = p.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// queryBool returns the query parameter parsed as a boolean. An empty value
|
||||
// ("?foo") is considered true, as is any value string except false
|
||||
// ("?foo=false").
|
||||
func queryBool(q url.Values, key string) bool {
|
||||
if _, ok := q[key]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return q.Get(key) != "false"
|
||||
}
|
||||
|
||||
type idCheckingHTTPClient struct {
|
||||
httpClient
|
||||
id protocol.DeviceID
|
||||
}
|
||||
|
||||
func newIDCheckingHTTPClient(client httpClient, id protocol.DeviceID) *idCheckingHTTPClient {
|
||||
return &idCheckingHTTPClient{
|
||||
httpClient: client,
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *idCheckingHTTPClient) check(resp *http.Response) error {
|
||||
if resp.TLS == nil {
|
||||
return errors.New("security: not TLS")
|
||||
}
|
||||
|
||||
if len(resp.TLS.PeerCertificates) == 0 {
|
||||
return errors.New("security: no certificates")
|
||||
}
|
||||
|
||||
id := protocol.NewDeviceID(resp.TLS.PeerCertificates[0].Raw)
|
||||
if !id.Equals(c.id) {
|
||||
return errors.New("security: incorrect device id")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *idCheckingHTTPClient) Get(url string) (*http.Response, error) {
|
||||
resp, err := c.httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.check(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *idCheckingHTTPClient) Post(url, ctype string, data io.Reader) (*http.Response, error) {
|
||||
resp, err := c.httpClient.Post(url, ctype, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.check(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type errorHolder struct {
|
||||
err error
|
||||
mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
|
||||
}
|
||||
|
||||
func (e *errorHolder) setError(err error) {
|
||||
e.mut.Lock()
|
||||
e.err = err
|
||||
e.mut.Unlock()
|
||||
}
|
||||
|
||||
func (e *errorHolder) Error() error {
|
||||
e.mut.Lock()
|
||||
err := e.err
|
||||
e.mut.Unlock()
|
||||
return err
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
)
|
||||
|
||||
func TestParseOptions(t *testing.T) {
|
||||
testcases := []struct {
|
||||
in string
|
||||
out string
|
||||
opts serverOptions
|
||||
}{
|
||||
{"https://example.com/", "https://example.com/", serverOptions{}},
|
||||
{"https://example.com/?insecure", "https://example.com/", serverOptions{insecure: true}},
|
||||
{"https://example.com/?insecure=true", "https://example.com/", serverOptions{insecure: true}},
|
||||
{"https://example.com/?insecure=yes", "https://example.com/", serverOptions{insecure: true}},
|
||||
{"https://example.com/?insecure=false&noannounce", "https://example.com/", serverOptions{noAnnounce: true}},
|
||||
{"https://example.com/?id=abc", "https://example.com/", serverOptions{id: "abc", insecure: true}},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
res, opts, err := parseOptions(tc.in)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected err %v for %v", err, tc.in)
|
||||
continue
|
||||
}
|
||||
if res != tc.out {
|
||||
t.Errorf("Incorrect server, %v!= %v for %v", res, tc.out, tc.in)
|
||||
}
|
||||
if opts != tc.opts {
|
||||
t.Errorf("Incorrect options, %v!= %v for %v", opts, tc.opts, tc.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalOverHTTP(t *testing.T) {
|
||||
// HTTP works for queries, but is obviously insecure and we can't do
|
||||
// announces over it (as we don't present a certificate). As such, http://
|
||||
// is only allowed in combination with the "insecure" and "noannounce"
|
||||
// parameters.
|
||||
|
||||
if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil, nil); err == nil {
|
||||
t.Fatal("http is not allowed without insecure and noannounce")
|
||||
}
|
||||
|
||||
if _, err := NewGlobal("http://192.0.2.42/?insecure", tls.Certificate{}, nil, nil); err == nil {
|
||||
t.Fatal("http is not allowed without noannounce")
|
||||
}
|
||||
|
||||
if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil, nil); err == nil {
|
||||
t.Fatal("http is not allowed without insecure")
|
||||
}
|
||||
|
||||
// Now lets check that lookups work over HTTP, given the correct options.
|
||||
|
||||
list, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer list.Close()
|
||||
|
||||
s := new(fakeDiscoveryServer)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handler)
|
||||
go http.Serve(list, mux)
|
||||
|
||||
direct, relays, err := testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
|
||||
t.Errorf("incorrect direct list: %+v", direct)
|
||||
}
|
||||
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
|
||||
t.Errorf("incorrect relays list: %+v", direct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalOverHTTPS(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "syncthing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate a server certificate, using fewer bits than usual to hurry the
|
||||
// process along a bit.
|
||||
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
list, err := tls.Listen("tcp4", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer list.Close()
|
||||
|
||||
s := new(fakeDiscoveryServer)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handler)
|
||||
go http.Serve(list, mux)
|
||||
|
||||
// With default options the lookup code expects the server certificate to
|
||||
// check out according to the usual CA chains etc. That won't be the case
|
||||
// here so we expect the lookup to fail.
|
||||
|
||||
url := "https://" + list.Addr().String()
|
||||
if _, _, err := testLookup(url); err == nil {
|
||||
t.Fatalf("unexpected nil error when we should have got a certificate error")
|
||||
}
|
||||
|
||||
// With "insecure" set, whatever certificate is on the other side should
|
||||
// be accepted.
|
||||
|
||||
url = "https://" + list.Addr().String() + "?insecure"
|
||||
if direct, relays, err := testLookup(url); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
} else {
|
||||
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
|
||||
t.Errorf("incorrect direct list: %+v", direct)
|
||||
}
|
||||
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
|
||||
t.Errorf("incorrect relays list: %+v", direct)
|
||||
}
|
||||
}
|
||||
|
||||
// With "id" set to something incorrect, the checks should fail again.
|
||||
|
||||
url = "https://" + list.Addr().String() + "?id=" + protocol.LocalDeviceID.String()
|
||||
if _, _, err := testLookup(url); err == nil {
|
||||
t.Fatalf("unexpected nil error for incorrect discovery server ID")
|
||||
}
|
||||
|
||||
// With the correct device ID, the check should pass and we should get a
|
||||
// lookup response.
|
||||
|
||||
id := protocol.NewDeviceID(cert.Certificate[0])
|
||||
url = "https://" + list.Addr().String() + "?id=" + id.String()
|
||||
if direct, relays, err := testLookup(url); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
} else {
|
||||
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
|
||||
t.Errorf("incorrect direct list: %+v", direct)
|
||||
}
|
||||
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
|
||||
t.Errorf("incorrect relays list: %+v", direct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalAnnounce(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "syncthing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate a server certificate, using fewer bits than usual to hurry the
|
||||
// process along a bit.
|
||||
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
list, err := tls.Listen("tcp4", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer list.Close()
|
||||
|
||||
s := new(fakeDiscoveryServer)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handler)
|
||||
go http.Serve(list, mux)
|
||||
|
||||
url := "https://" + list.Addr().String() + "?insecure"
|
||||
disco, err := NewGlobal(url, cert, new(fakeAddressLister), new(fakeRelayStatus))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go disco.Serve()
|
||||
defer disco.Stop()
|
||||
|
||||
// The discovery thing should attempt an announcement immediately. We wait
|
||||
// for it to succeed, a while.
|
||||
t0 := time.Now()
|
||||
for err := disco.Error(); err != nil; err = disco.Error() {
|
||||
if time.Since(t0) > 10*time.Second {
|
||||
t.Fatal("announce failed:", err)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(s.announce), "tcp://0.0.0.0:22000") {
|
||||
t.Errorf("announce missing direct address: %s", s.announce)
|
||||
}
|
||||
if !strings.Contains(string(s.announce), "relay://192.0.2.42:443") {
|
||||
t.Errorf("announce missing relay address: %s", s.announce)
|
||||
}
|
||||
}
|
||||
|
||||
func testLookup(url string) ([]string, []Relay, error) {
|
||||
disco, err := NewGlobal(url, tls.Certificate{}, nil, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
go disco.Serve()
|
||||
defer disco.Stop()
|
||||
|
||||
return disco.Lookup(protocol.LocalDeviceID)
|
||||
}
|
||||
|
||||
type fakeDiscoveryServer struct {
|
||||
announce []byte
|
||||
}
|
||||
|
||||
func (s *fakeDiscoveryServer) handler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
s.announce, _ = ioutil.ReadAll(r.Body)
|
||||
w.WriteHeader(204)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"direct":["tcp://192.0.2.42::22000"], "relays":[{"url": "relay://192.0.2.43:443", "latency": 42}]}`))
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAddressLister struct{}
|
||||
|
||||
func (f *fakeAddressLister) ExternalAddresses() []string {
|
||||
return []string{"tcp://0.0.0.0:22000"}
|
||||
}
|
||||
func (f *fakeAddressLister) AllAddresses() []string {
|
||||
return []string{"tcp://0.0.0.0:22000", "tcp://192.168.0.1:22000"}
|
||||
}
|
||||
|
||||
type fakeRelayStatus struct{}
|
||||
|
||||
func (f *fakeRelayStatus) Relays() []string {
|
||||
return []string{"relay://192.0.2.42:443"}
|
||||
}
|
||||
func (f *fakeRelayStatus) RelayStatus(uri string) (time.Duration, bool) {
|
||||
return 42 * time.Millisecond, true
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/beacon"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
type localClient struct {
|
||||
*suture.Supervisor
|
||||
myID protocol.DeviceID
|
||||
addrList AddressLister
|
||||
relayStat RelayStatusProvider
|
||||
name string
|
||||
|
||||
beacon beacon.Interface
|
||||
localBcastStart time.Time
|
||||
localBcastTick <-chan time.Time
|
||||
forcedBcastTick chan time.Time
|
||||
|
||||
*cache
|
||||
}
|
||||
|
||||
const (
|
||||
BroadcastInterval = 30 * time.Second
|
||||
CacheLifeTime = 3 * BroadcastInterval
|
||||
)
|
||||
|
||||
var (
|
||||
ErrIncorrectMagic = errors.New("incorrect magic number")
|
||||
)
|
||||
|
||||
func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
|
||||
c := &localClient{
|
||||
Supervisor: suture.NewSimple("local"),
|
||||
myID: id,
|
||||
addrList: addrList,
|
||||
relayStat: relayStat,
|
||||
localBcastTick: time.Tick(BroadcastInterval),
|
||||
forcedBcastTick: make(chan time.Time),
|
||||
localBcastStart: time.Now(),
|
||||
cache: newCache(),
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(host) == 0 {
|
||||
// A broadcast client
|
||||
c.name = "IPv4 local"
|
||||
bcPort, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.startLocalIPv4Broadcasts(bcPort)
|
||||
} else {
|
||||
// A multicast client
|
||||
c.name = "IPv6 local"
|
||||
c.startLocalIPv6Multicasts(addr)
|
||||
}
|
||||
|
||||
go c.sendLocalAnnouncements()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *localClient) startLocalIPv4Broadcasts(localPort int) {
|
||||
c.beacon = beacon.NewBroadcast(localPort)
|
||||
c.Add(c.beacon)
|
||||
go c.recvAnnouncements(c.beacon)
|
||||
}
|
||||
|
||||
func (c *localClient) startLocalIPv6Multicasts(localMCAddr string) {
|
||||
c.beacon = beacon.NewMulticast(localMCAddr)
|
||||
c.Add(c.beacon)
|
||||
go c.recvAnnouncements(c.beacon)
|
||||
}
|
||||
|
||||
// Lookup returns a list of addresses the device is available at. Local
|
||||
// discovery never returns relays.
|
||||
func (c *localClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
|
||||
if cache, ok := c.Get(device); ok {
|
||||
if time.Since(cache.when) < CacheLifeTime {
|
||||
direct = cache.Direct
|
||||
relays = cache.Relays
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *localClient) String() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c *localClient) Error() error {
|
||||
return c.beacon.Error()
|
||||
}
|
||||
|
||||
func (c *localClient) announcementPkt() Announce {
|
||||
var addrs []Address
|
||||
for _, addr := range c.addrList.AllAddresses() {
|
||||
addrs = append(addrs, Address{
|
||||
URL: addr,
|
||||
})
|
||||
}
|
||||
|
||||
var relays []Relay
|
||||
for _, relay := range c.relayStat.Relays() {
|
||||
latency, ok := c.relayStat.RelayStatus(relay)
|
||||
if ok {
|
||||
relays = append(relays, Relay{
|
||||
URL: relay,
|
||||
Latency: int32(latency / time.Millisecond),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{
|
||||
ID: c.myID[:],
|
||||
Addresses: addrs,
|
||||
Relays: relays,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *localClient) sendLocalAnnouncements() {
|
||||
var pkt = c.announcementPkt()
|
||||
msg := pkt.MustMarshalXDR()
|
||||
|
||||
for {
|
||||
c.beacon.Send(msg)
|
||||
|
||||
select {
|
||||
case <-c.localBcastTick:
|
||||
case <-c.forcedBcastTick:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *localClient) recvAnnouncements(b beacon.Interface) {
|
||||
for {
|
||||
buf, addr := b.Recv()
|
||||
|
||||
var pkt Announce
|
||||
err := pkt.UnmarshalXDR(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
if debug {
|
||||
l.Debugf("discover: Failed to unmarshal local announcement from %s:\n%s", addr, hex.Dump(buf))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
|
||||
}
|
||||
|
||||
var newDevice bool
|
||||
if bytes.Compare(pkt.This.ID, c.myID[:]) != 0 {
|
||||
newDevice = c.registerDevice(addr, pkt.This)
|
||||
}
|
||||
|
||||
if newDevice {
|
||||
select {
|
||||
case c.forcedBcastTick <- time.Now():
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *localClient) registerDevice(src net.Addr, device Device) bool {
|
||||
var id protocol.DeviceID
|
||||
copy(id[:], device.ID)
|
||||
|
||||
// Remember whether we already had a valid cache entry for this device.
|
||||
|
||||
ce, existsAlready := c.Get(id)
|
||||
isNewDevice := !existsAlready || time.Since(ce.when) > CacheLifeTime
|
||||
|
||||
// Any empty or unspecified addresses should be set to the source address
|
||||
// of the announcement. We also skip any addresses we can't parse.
|
||||
|
||||
var validAddresses []string
|
||||
for _, addr := range device.Addresses {
|
||||
u, err := url.Parse(addr.URL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", u.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(tcpAddr.IP) == 0 || tcpAddr.IP.IsUnspecified() {
|
||||
host, _, err := net.SplitHostPort(src.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
u.Host = net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port))
|
||||
validAddresses = append(validAddresses, u.String())
|
||||
} else {
|
||||
validAddresses = append(validAddresses, addr.URL)
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(id, CacheEntry{
|
||||
Direct: validAddresses,
|
||||
Relays: device.Relays,
|
||||
when: time.Now(),
|
||||
found: true,
|
||||
})
|
||||
|
||||
if isNewDevice {
|
||||
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
|
||||
"device": id.String(),
|
||||
"addrs": device.Addresses,
|
||||
"relays": device.Relays,
|
||||
})
|
||||
}
|
||||
|
||||
return isNewDevice
|
||||
}
|
||||
|
||||
func addrToAddr(addr *net.TCPAddr) string {
|
||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
||||
return fmt.Sprintf(":%c", addr.Port)
|
||||
} else if bs := addr.IP.To4(); bs != nil {
|
||||
return net.JoinHostPort(bs.String(), strconv.Itoa(addr.Port))
|
||||
} else if bs := addr.IP.To16(); bs != nil {
|
||||
return net.JoinHostPort(bs.String(), strconv.Itoa(addr.Port))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveAddrs(addrs []string) []string {
|
||||
var raddrs []string
|
||||
for _, addrStr := range addrs {
|
||||
uri, err := url.Parse(addrStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addrRes, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addr := addrToAddr(addrRes)
|
||||
if len(addr) > 0 {
|
||||
uri.Host = addr
|
||||
raddrs = append(raddrs, uri.String())
|
||||
}
|
||||
}
|
||||
return raddrs
|
||||
}
|
||||
@@ -5,14 +5,20 @@
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||
//go:generate genxdr -o localpackets_xdr.go localpackets.go
|
||||
//go:generate genxdr -o packets_xdr.go packets.go
|
||||
|
||||
package discover
|
||||
|
||||
const (
|
||||
AnnouncementMagic = 0x9D79BC40
|
||||
AnnouncementMagic = 0x9D79BC39
|
||||
QueryMagic = 0x2CA856F5
|
||||
)
|
||||
|
||||
type Query struct {
|
||||
Magic uint32
|
||||
DeviceID []byte // max:32
|
||||
}
|
||||
|
||||
type Announce struct {
|
||||
Magic uint32
|
||||
This Device
|
||||
@@ -22,14 +28,9 @@ type Announce struct {
|
||||
type Device struct {
|
||||
ID []byte // max:32
|
||||
Addresses []Address // max:16
|
||||
Relays []Relay // max:16
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
URL string // max:2083
|
||||
}
|
||||
|
||||
type Relay struct {
|
||||
URL string `json:"url"` // max:2083
|
||||
Latency int32 `json:"latency"`
|
||||
IP []byte // max:16
|
||||
Port uint16
|
||||
}
|
||||
@@ -13,6 +13,80 @@ import (
|
||||
|
||||
/*
|
||||
|
||||
Query Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Magic |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Device ID |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Device ID (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct Query {
|
||||
unsigned int Magic;
|
||||
opaque DeviceID<32>;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o Query) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.EncodeXDRInto(xw)
|
||||
}
|
||||
|
||||
func (o Query) MarshalXDR() ([]byte, error) {
|
||||
return o.AppendXDR(make([]byte, 0, 128))
|
||||
}
|
||||
|
||||
func (o Query) MustMarshalXDR() []byte {
|
||||
bs, err := o.MarshalXDR()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (o Query) AppendXDR(bs []byte) ([]byte, error) {
|
||||
var aw = xdr.AppendWriter(bs)
|
||||
var xw = xdr.NewWriter(&aw)
|
||||
_, err := o.EncodeXDRInto(xw)
|
||||
return []byte(aw), err
|
||||
}
|
||||
|
||||
func (o Query) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
xw.WriteUint32(o.Magic)
|
||||
if l := len(o.DeviceID); l > 32 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("DeviceID", l, 32)
|
||||
}
|
||||
xw.WriteBytes(o.DeviceID)
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
|
||||
func (o *Query) DecodeXDR(r io.Reader) error {
|
||||
xr := xdr.NewReader(r)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *Query) UnmarshalXDR(bs []byte) error {
|
||||
var br = bytes.NewReader(bs)
|
||||
var xr = xdr.NewReader(br)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *Query) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.Magic = xr.ReadUint32()
|
||||
o.DeviceID = xr.ReadBytesMax(32)
|
||||
return xr.Error()
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Announce Structure:
|
||||
|
||||
0 1 2 3
|
||||
@@ -130,18 +204,11 @@ Device Structure:
|
||||
\ Zero or more Address Structures \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Number of Relays |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Zero or more Relay Structures \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct Device {
|
||||
opaque ID<32>;
|
||||
Address Addresses<16>;
|
||||
Relay Relays<16>;
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -185,16 +252,6 @@ func (o Device) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
return xw.Tot(), err
|
||||
}
|
||||
}
|
||||
if l := len(o.Relays); l > 16 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("Relays", l, 16)
|
||||
}
|
||||
xw.WriteUint32(uint32(len(o.Relays)))
|
||||
for i := range o.Relays {
|
||||
_, err := o.Relays[i].EncodeXDRInto(xw)
|
||||
if err != nil {
|
||||
return xw.Tot(), err
|
||||
}
|
||||
}
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
|
||||
@@ -222,17 +279,6 @@ func (o *Device) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
for i := range o.Addresses {
|
||||
(&o.Addresses[i]).DecodeXDRFrom(xr)
|
||||
}
|
||||
_RelaysSize := int(xr.ReadUint32())
|
||||
if _RelaysSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16)
|
||||
}
|
||||
if _RelaysSize > 16 {
|
||||
return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16)
|
||||
}
|
||||
o.Relays = make([]Relay, _RelaysSize)
|
||||
for i := range o.Relays {
|
||||
(&o.Relays[i]).DecodeXDRFrom(xr)
|
||||
}
|
||||
return xr.Error()
|
||||
}
|
||||
|
||||
@@ -243,16 +289,19 @@ Address Structure:
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of URL |
|
||||
| Length of IP |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ URL (variable length) \
|
||||
\ IP (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| 0x0000 | Port |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct Address {
|
||||
string URL<2083>;
|
||||
opaque IP<16>;
|
||||
unsigned int Port;
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -282,10 +331,11 @@ func (o Address) AppendXDR(bs []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (o Address) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
if l := len(o.URL); l > 2083 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("URL", l, 2083)
|
||||
if l := len(o.IP); l > 16 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("IP", l, 16)
|
||||
}
|
||||
xw.WriteString(o.URL)
|
||||
xw.WriteBytes(o.IP)
|
||||
xw.WriteUint16(o.Port)
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
|
||||
@@ -301,80 +351,7 @@ func (o *Address) UnmarshalXDR(bs []byte) error {
|
||||
}
|
||||
|
||||
func (o *Address) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.URL = xr.ReadStringMax(2083)
|
||||
return xr.Error()
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Relay Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of URL |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ URL (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Latency |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct Relay {
|
||||
string URL<2083>;
|
||||
int Latency;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o Relay) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.EncodeXDRInto(xw)
|
||||
}
|
||||
|
||||
func (o Relay) MarshalXDR() ([]byte, error) {
|
||||
return o.AppendXDR(make([]byte, 0, 128))
|
||||
}
|
||||
|
||||
func (o Relay) MustMarshalXDR() []byte {
|
||||
bs, err := o.MarshalXDR()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (o Relay) AppendXDR(bs []byte) ([]byte, error) {
|
||||
var aw = xdr.AppendWriter(bs)
|
||||
var xw = xdr.NewWriter(&aw)
|
||||
_, err := o.EncodeXDRInto(xw)
|
||||
return []byte(aw), err
|
||||
}
|
||||
|
||||
func (o Relay) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
if l := len(o.URL); l > 2083 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("URL", l, 2083)
|
||||
}
|
||||
xw.WriteString(o.URL)
|
||||
xw.WriteUint32(uint32(o.Latency))
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
|
||||
func (o *Relay) DecodeXDR(r io.Reader) error {
|
||||
xr := xdr.NewReader(r)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *Relay) UnmarshalXDR(bs []byte) error {
|
||||
var br = bytes.NewReader(bs)
|
||||
var xr = xdr.NewReader(br)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *Relay) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.URL = xr.ReadStringMax(2083)
|
||||
o.Latency = int32(xr.ReadUint32())
|
||||
o.IP = xr.ReadBytesMax(16)
|
||||
o.Port = xr.ReadUint16()
|
||||
return xr.Error()
|
||||
}
|
||||
+12
-23
@@ -25,8 +25,6 @@ const (
|
||||
DeviceConnected
|
||||
DeviceDisconnected
|
||||
DeviceRejected
|
||||
DevicePaused
|
||||
DeviceResumed
|
||||
LocalIndexUpdated
|
||||
RemoteIndexUpdated
|
||||
ItemStarted
|
||||
@@ -38,9 +36,6 @@ const (
|
||||
FolderSummary
|
||||
FolderCompletion
|
||||
FolderErrors
|
||||
FolderScanProgress
|
||||
ExternalPortMappingChanged
|
||||
RelayStateChanged
|
||||
|
||||
AllEvents = (1 << iota) - 1
|
||||
)
|
||||
@@ -83,16 +78,6 @@ func (t EventType) String() string {
|
||||
return "FolderCompletion"
|
||||
case FolderErrors:
|
||||
return "FolderErrors"
|
||||
case DevicePaused:
|
||||
return "DevicePaused"
|
||||
case DeviceResumed:
|
||||
return "DeviceResumed"
|
||||
case FolderScanProgress:
|
||||
return "FolderScanProgress"
|
||||
case ExternalPortMappingChanged:
|
||||
return "ExternalPortMappingChanged"
|
||||
case RelayStateChanged:
|
||||
return "RelayStateChanged"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -105,7 +90,7 @@ func (t EventType) MarshalText() ([]byte, error) {
|
||||
const BufferSize = 64
|
||||
|
||||
type Logger struct {
|
||||
subs map[int]*Subscription
|
||||
subs []*Subscription
|
||||
nextID int
|
||||
mutex sync.Mutex
|
||||
}
|
||||
@@ -119,7 +104,6 @@ type Event struct {
|
||||
|
||||
type Subscription struct {
|
||||
mask EventType
|
||||
id int
|
||||
events chan Event
|
||||
timeout *time.Timer
|
||||
}
|
||||
@@ -133,7 +117,6 @@ var (
|
||||
|
||||
func NewLogger() *Logger {
|
||||
return &Logger{
|
||||
subs: make(map[int]*Subscription),
|
||||
mutex: sync.NewMutex(),
|
||||
}
|
||||
}
|
||||
@@ -143,13 +126,13 @@ func (l *Logger) Log(t EventType, data interface{}) {
|
||||
if debug {
|
||||
dl.Debugln("log", l.nextID, t.String(), data)
|
||||
}
|
||||
l.nextID++
|
||||
e := Event{
|
||||
ID: l.nextID,
|
||||
Time: time.Now(),
|
||||
Type: t,
|
||||
Data: data,
|
||||
}
|
||||
l.nextID++
|
||||
for _, s := range l.subs {
|
||||
if s.mask&t != 0 {
|
||||
select {
|
||||
@@ -169,12 +152,10 @@ func (l *Logger) Subscribe(mask EventType) *Subscription {
|
||||
}
|
||||
s := &Subscription{
|
||||
mask: mask,
|
||||
id: l.nextID,
|
||||
events: make(chan Event, BufferSize),
|
||||
timeout: time.NewTimer(0),
|
||||
}
|
||||
l.nextID++
|
||||
l.subs[s.id] = s
|
||||
l.subs = append(l.subs, s)
|
||||
l.mutex.Unlock()
|
||||
return s
|
||||
}
|
||||
@@ -184,7 +165,15 @@ func (l *Logger) Unsubscribe(s *Subscription) {
|
||||
if debug {
|
||||
dl.Debugln("unsubscribe")
|
||||
}
|
||||
delete(l.subs, s.id)
|
||||
for i, ss := range l.subs {
|
||||
if s == ss {
|
||||
last := len(l.subs) - 1
|
||||
l.subs[i] = l.subs[last]
|
||||
l.subs[last] = nil
|
||||
l.subs = l.subs[:last]
|
||||
break
|
||||
}
|
||||
}
|
||||
close(s.events)
|
||||
l.mutex.Unlock()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user