@@ -83,7 +83,7 @@ func (f *fakeConnection) IndexUpdate(ctx context.Context, folder string, fs []pr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeConnection) Request(ctx context.Context, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
func (f *fakeConnection) Request(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
if f.requestFn != nil {
|
||||
|
||||
+45
-18
@@ -459,7 +459,8 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
scanCtx, scanCancel := context.WithCancel(f.ctx)
|
||||
defer scanCancel()
|
||||
mtimefs := f.fset.MtimeFS()
|
||||
fchan := scanner.Walk(scanCtx, scanner.Config{
|
||||
|
||||
scanConfig := scanner.Config{
|
||||
Folder: f.ID,
|
||||
Subs: subDirs,
|
||||
Matcher: f.ignores,
|
||||
@@ -474,7 +475,13 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
LocalFlags: f.localFlags,
|
||||
ModTimeWindow: f.modTimeWindow,
|
||||
EventLogger: f.evLogger,
|
||||
})
|
||||
}
|
||||
var fchan chan scanner.ScanResult
|
||||
if f.Type == config.FolderTypeReceiveEncrypted {
|
||||
fchan = scanner.WalkWithoutHashing(scanCtx, scanConfig)
|
||||
} else {
|
||||
fchan = scanner.Walk(scanCtx, scanConfig)
|
||||
}
|
||||
|
||||
batch := newFileInfoBatch(func(fs []protocol.FileInfo) error {
|
||||
if err := f.getHealthErrorWithoutIgnores(); err != nil {
|
||||
@@ -485,13 +492,19 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
// Schedule a pull after scanning, but only if we actually detected any
|
||||
// changes.
|
||||
changes := 0
|
||||
defer func() {
|
||||
if changes > 0 {
|
||||
f.SchedulePull()
|
||||
}
|
||||
}()
|
||||
|
||||
var batchAppend func(protocol.FileInfo, *db.Snapshot)
|
||||
// Resolve items which are identical with the global state.
|
||||
if f.localFlags&protocol.FlagLocalReceiveOnly == 0 {
|
||||
batchAppend = func(fi protocol.FileInfo, _ *db.Snapshot) {
|
||||
batch.append(fi)
|
||||
}
|
||||
} else {
|
||||
switch f.Type {
|
||||
case config.FolderTypeReceiveOnly:
|
||||
batchAppend = func(fi protocol.FileInfo, snap *db.Snapshot) {
|
||||
switch gf, ok := snap.GetGlobal(fi.Name); {
|
||||
case !ok:
|
||||
@@ -509,16 +522,28 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
}
|
||||
batch.append(fi)
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule a pull after scanning, but only if we actually detected any
|
||||
// changes.
|
||||
changes := 0
|
||||
defer func() {
|
||||
if changes > 0 {
|
||||
f.SchedulePull()
|
||||
case config.FolderTypeReceiveEncrypted:
|
||||
batchAppend = func(fi protocol.FileInfo, _ *db.Snapshot) {
|
||||
// This is a "virtual" parent directory of encrypted files.
|
||||
// We don't track it, but check if anything still exists
|
||||
// within and delete it otherwise.
|
||||
if fi.IsDirectory() && protocol.IsEncryptedParent(fi.Name) {
|
||||
if names, err := mtimefs.DirNames(fi.Name); err == nil && len(names) == 0 {
|
||||
mtimefs.Remove(fi.Name)
|
||||
}
|
||||
changes--
|
||||
return
|
||||
}
|
||||
// Any local change must not be sent as index entry to
|
||||
// remotes and show up as an error in the UI.
|
||||
fi.LocalFlags = protocol.FlagLocalReceiveOnly
|
||||
batch.append(fi)
|
||||
}
|
||||
}()
|
||||
default:
|
||||
batchAppend = func(fi protocol.FileInfo, _ *db.Snapshot) {
|
||||
batch.append(fi)
|
||||
}
|
||||
}
|
||||
|
||||
f.clearScanErrors(subDirs)
|
||||
alreadyUsed := make(map[string]struct{})
|
||||
@@ -540,7 +565,9 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
batchAppend(res.File, snap)
|
||||
changes++
|
||||
|
||||
if f.localFlags&protocol.FlagLocalReceiveOnly == 0 {
|
||||
switch f.Type {
|
||||
case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted:
|
||||
default:
|
||||
if nf, ok := f.findRename(snap, mtimefs, res.File, alreadyUsed); ok {
|
||||
batchAppend(nf, snap)
|
||||
changes++
|
||||
@@ -648,7 +675,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
l.Debugln("marking file as deleted", nf)
|
||||
batchAppend(nf, snap)
|
||||
changes++
|
||||
case file.IsDeleted() && file.IsReceiveOnlyChanged() && f.localFlags&protocol.FlagLocalReceiveOnly != 0 && len(snap.Availability(file.Name)) == 0:
|
||||
case file.IsDeleted() && file.IsReceiveOnlyChanged() && f.Type == config.FolderTypeReceiveOnly && len(snap.Availability(file.Name)) == 0:
|
||||
file.Version = protocol.Vector{}
|
||||
file.LocalFlags &^= protocol.FlagLocalReceiveOnly
|
||||
l.Debugln("marking deleted item that doesn't exist anywhere as not receive-only", file)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/ignore"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/versioner"
|
||||
)
|
||||
|
||||
func init() {
|
||||
folderFactories[config.FolderTypeReceiveEncrypted] = newReceiveEncryptedFolder
|
||||
}
|
||||
|
||||
type receiveEncryptedFolder struct {
|
||||
*sendReceiveFolder
|
||||
}
|
||||
|
||||
func newReceiveEncryptedFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem, evLogger events.Logger, ioLimiter *byteSemaphore) service {
|
||||
return &receiveEncryptedFolder{newSendReceiveFolder(model, fset, ignores, cfg, ver, fs, evLogger, ioLimiter).(*sendReceiveFolder)}
|
||||
}
|
||||
|
||||
func (f *receiveEncryptedFolder) Revert() {
|
||||
f.doInSync(func() error { f.revert(); return nil })
|
||||
}
|
||||
|
||||
func (f *receiveEncryptedFolder) revert() {
|
||||
l.Infof("Reverting unexpected items in folder %v (receive-encrypted)", f.Description())
|
||||
|
||||
f.setState(FolderScanning)
|
||||
defer f.setState(FolderIdle)
|
||||
|
||||
batch := newFileInfoBatch(func(fs []protocol.FileInfo) error {
|
||||
f.updateLocalsFromScanning(fs)
|
||||
return nil
|
||||
})
|
||||
|
||||
snap := f.fset.Snapshot()
|
||||
defer snap.Release()
|
||||
var iterErr error
|
||||
var dirs []string
|
||||
snap.WithHaveTruncated(protocol.LocalDeviceID, func(intf protocol.FileIntf) bool {
|
||||
if iterErr = batch.flushIfFull(); iterErr != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
fit := intf.(db.FileInfoTruncated)
|
||||
if !fit.IsReceiveOnlyChanged() || intf.IsDeleted() {
|
||||
return true
|
||||
}
|
||||
|
||||
if fit.IsDirectory() {
|
||||
dirs = append(dirs, fit.Name)
|
||||
return true
|
||||
}
|
||||
|
||||
if err := f.inWritableDir(f.fs.Remove, fit.Name); err != nil && !fs.IsNotExist(err) {
|
||||
f.newScanError(fit.Name, fmt.Errorf("deleting unexpected item: %w", err))
|
||||
}
|
||||
|
||||
fi := fit.ConvertToDeletedFileInfo(f.shortID)
|
||||
// Set version to zero, such that we pull the global version in case
|
||||
// this is a valid filename that was erroneously changed locally.
|
||||
// Should already be zero from scanning, but lets be safe.
|
||||
fi.Version = protocol.Vector{}
|
||||
// Purposely not removing FlagLocalReceiveOnly as the deleted
|
||||
// item should still not be sent in index updates. However being
|
||||
// deleted, it will not show up as an unexpected file in the UI
|
||||
// anymore.
|
||||
batch.append(fi)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
f.revertHandleDirs(dirs, snap)
|
||||
|
||||
if iterErr == nil {
|
||||
iterErr = batch.flush()
|
||||
}
|
||||
if iterErr != nil {
|
||||
l.Infoln("Failed to delete unexpected items:", iterErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *receiveEncryptedFolder) revertHandleDirs(dirs []string, snap *db.Snapshot) {
|
||||
if len(dirs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
scanChan := make(chan string)
|
||||
go f.pullScannerRoutine(scanChan)
|
||||
defer close(scanChan)
|
||||
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(dirs)))
|
||||
for _, dir := range dirs {
|
||||
if err := f.deleteDirOnDisk(dir, snap, scanChan); err != nil {
|
||||
f.newScanError(dir, fmt.Errorf("deleting unexpected dir: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -686,16 +686,22 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
|
||||
// user can then clean up as they like...
|
||||
// This can also occur if an entire tree structure was deleted, but only
|
||||
// a leave has been scanned.
|
||||
//
|
||||
// And if this is an encrypted folder:
|
||||
// Encrypted files have made-up filenames with two synthetic parent
|
||||
// directories which don't have any meaning. Create those if necessary.
|
||||
if _, err := f.fs.Lstat(parent); !fs.IsNotExist(err) {
|
||||
l.Debugf("%v parent not missing %v", f, file)
|
||||
return true
|
||||
}
|
||||
l.Debugf("%v resurrecting parent directory of %v", f, file)
|
||||
l.Debugf("%v creating parent directory of %v", f, file)
|
||||
if err := f.fs.MkdirAll(parent, 0755); err != nil {
|
||||
f.newPullError(file, errors.Wrap(err, "resurrecting parent dir"))
|
||||
f.newPullError(file, errors.Wrap(err, "creating parent dir"))
|
||||
return false
|
||||
}
|
||||
scanChan <- parent
|
||||
if f.Type != config.FolderTypeReceiveEncrypted {
|
||||
scanChan <- parent
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1248,38 +1254,11 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
|
||||
continue
|
||||
}
|
||||
|
||||
f.model.progressEmitter.Register(state.sharedPullerState)
|
||||
|
||||
var file fs.File
|
||||
var weakHashFinder *weakhash.Finder
|
||||
|
||||
blocksPercentChanged := 0
|
||||
if tot := len(state.file.Blocks); tot > 0 {
|
||||
blocksPercentChanged = (tot - state.have) * 100 / tot
|
||||
if f.Type != config.FolderTypeReceiveEncrypted {
|
||||
f.model.progressEmitter.Register(state.sharedPullerState)
|
||||
}
|
||||
|
||||
if blocksPercentChanged >= f.WeakHashThresholdPct {
|
||||
hashesToFind := make([]uint32, 0, len(state.blocks))
|
||||
for _, block := range state.blocks {
|
||||
if block.WeakHash != 0 {
|
||||
hashesToFind = append(hashesToFind, block.WeakHash)
|
||||
}
|
||||
}
|
||||
|
||||
if len(hashesToFind) > 0 {
|
||||
file, err = f.fs.Open(state.file.Name)
|
||||
if err == nil {
|
||||
weakHashFinder, err = weakhash.NewFinder(f.ctx, file, state.file.BlockSize(), hashesToFind)
|
||||
if err != nil {
|
||||
l.Debugln("weak hasher", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
l.Debugf("not weak hashing %s. file did not contain any weak hashes", state.file.Name)
|
||||
}
|
||||
} else {
|
||||
l.Debugf("not weak hashing %s. not enough changed %.02f < %d", state.file.Name, blocksPercentChanged, f.WeakHashThresholdPct)
|
||||
}
|
||||
weakHashFinder, file := f.initWeakHashFinder(state)
|
||||
|
||||
blocks:
|
||||
for _, block := range state.blocks {
|
||||
@@ -1305,25 +1284,28 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
|
||||
|
||||
buf = protocol.BufferPool.Upgrade(buf, int(block.Size))
|
||||
|
||||
found, err := weakHashFinder.Iterate(block.WeakHash, buf, func(offset int64) bool {
|
||||
if verifyBuffer(buf, block) != nil {
|
||||
return true
|
||||
}
|
||||
var found bool
|
||||
if f.Type != config.FolderTypeReceiveEncrypted {
|
||||
found, err = weakHashFinder.Iterate(block.WeakHash, buf, func(offset int64) bool {
|
||||
if f.verifyBuffer(buf, block) != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
err = f.limitedWriteAt(dstFd, buf, block.Offset)
|
||||
err = f.limitedWriteAt(dstFd, buf, block.Offset)
|
||||
if err != nil {
|
||||
state.fail(errors.Wrap(err, "dst write"))
|
||||
}
|
||||
if offset == block.Offset {
|
||||
state.copiedFromOrigin()
|
||||
} else {
|
||||
state.copiedFromOriginShifted()
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
state.fail(errors.Wrap(err, "dst write"))
|
||||
l.Debugln("weak hasher iter", err)
|
||||
}
|
||||
if offset == block.Offset {
|
||||
state.copiedFromOrigin()
|
||||
} else {
|
||||
state.copiedFromOriginShifted()
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
l.Debugln("weak hasher iter", err)
|
||||
}
|
||||
|
||||
if !found {
|
||||
@@ -1341,9 +1323,14 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
|
||||
return false
|
||||
}
|
||||
|
||||
if err := verifyBuffer(buf, block); err != nil {
|
||||
l.Debugln("Finder failed to verify buffer", err)
|
||||
return false
|
||||
// Hash is not SHA256 as it's an encrypted hash token. In that
|
||||
// case we can't verify the block integrity so we'll take it on
|
||||
// trust. (The other side can and will verify.)
|
||||
if f.Type != config.FolderTypeReceiveEncrypted {
|
||||
if err := f.verifyBuffer(buf, block); err != nil {
|
||||
l.Debugln("Finder failed to verify buffer", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if f.CopyRangeMethod != fs.CopyRangeMethodStandard {
|
||||
@@ -1390,7 +1377,49 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
|
||||
}
|
||||
}
|
||||
|
||||
func verifyBuffer(buf []byte, block protocol.BlockInfo) error {
|
||||
func (f *sendReceiveFolder) initWeakHashFinder(state copyBlocksState) (*weakhash.Finder, fs.File) {
|
||||
if f.Type == config.FolderTypeReceiveEncrypted {
|
||||
l.Debugln("not weak hashing due to folder type", f.Type)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
blocksPercentChanged := 0
|
||||
if tot := len(state.file.Blocks); tot > 0 {
|
||||
blocksPercentChanged = (tot - state.have) * 100 / tot
|
||||
}
|
||||
|
||||
if blocksPercentChanged < f.WeakHashThresholdPct {
|
||||
l.Debugf("not weak hashing %s. not enough changed %.02f < %d", state.file.Name, blocksPercentChanged, f.WeakHashThresholdPct)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
hashesToFind := make([]uint32, 0, len(state.blocks))
|
||||
for _, block := range state.blocks {
|
||||
if block.WeakHash != 0 {
|
||||
hashesToFind = append(hashesToFind, block.WeakHash)
|
||||
}
|
||||
}
|
||||
|
||||
if len(hashesToFind) == 0 {
|
||||
l.Debugf("not weak hashing %s. file did not contain any weak hashes", state.file.Name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
file, err := f.fs.Open(state.file.Name)
|
||||
if err != nil {
|
||||
l.Debugln("weak hasher", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
weakHashFinder, err := weakhash.NewFinder(f.ctx, file, state.file.BlockSize(), hashesToFind)
|
||||
if err != nil {
|
||||
l.Debugln("weak hasher", err)
|
||||
return nil, file
|
||||
}
|
||||
return weakHashFinder, file
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) verifyBuffer(buf []byte, block protocol.BlockInfo) error {
|
||||
if len(buf) != int(block.Size) {
|
||||
return fmt.Errorf("length mismatch %d != %d", len(buf), block.Size)
|
||||
}
|
||||
@@ -1487,7 +1516,8 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
|
||||
// leastBusy can select another device when someone else asks.
|
||||
activity.using(selected)
|
||||
var buf []byte
|
||||
buf, lastError = f.model.requestGlobal(f.ctx, selected.ID, f.folderID, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, state.block.WeakHash, selected.FromTemporary)
|
||||
blockNo := int(state.block.Offset / int64(state.file.BlockSize()))
|
||||
buf, lastError = f.model.requestGlobal(f.ctx, selected.ID, f.folderID, state.file.Name, blockNo, state.block.Offset, int(state.block.Size), state.block.Hash, state.block.WeakHash, selected.FromTemporary)
|
||||
activity.done(selected)
|
||||
if lastError != nil {
|
||||
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "returned error:", lastError)
|
||||
@@ -1496,7 +1526,13 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
|
||||
|
||||
// Verify that the received block matches the desired hash, if not
|
||||
// try pulling it from another device.
|
||||
lastError = verifyBuffer(buf, state.block)
|
||||
// For receive-only folders, the hash is not SHA256 as it's an
|
||||
// encrypted hash token. In that case we can't verify the block
|
||||
// integrity so we'll take it on trust. (The other side can and
|
||||
// will verify.)
|
||||
if f.Type != config.FolderTypeReceiveEncrypted {
|
||||
lastError = f.verifyBuffer(buf, state.block)
|
||||
}
|
||||
if lastError != nil {
|
||||
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "hash mismatch")
|
||||
continue
|
||||
@@ -1595,7 +1631,9 @@ func (f *sendReceiveFolder) finisherRoutine(snap *db.Snapshot, in <-chan *shared
|
||||
blockStatsMut.Unlock()
|
||||
}
|
||||
|
||||
f.model.progressEmitter.Deregister(state)
|
||||
if f.Type != config.FolderTypeReceiveEncrypted {
|
||||
f.model.progressEmitter.Deregister(state)
|
||||
}
|
||||
|
||||
f.evLogger.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
|
||||
@@ -123,9 +123,9 @@ func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, e
|
||||
}
|
||||
res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"], res["needTotalItems"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
|
||||
|
||||
if haveFcfg && fcfg.Type == config.FolderTypeReceiveOnly {
|
||||
if haveFcfg && (fcfg.Type == config.FolderTypeReceiveOnly || fcfg.Type == config.FolderTypeReceiveEncrypted) {
|
||||
// Add statistics for things that have changed locally in a receive
|
||||
// only folder.
|
||||
// only or receive encrypted folder.
|
||||
res["receiveOnlyChangedFiles"] = ro.Files
|
||||
res["receiveOnlyChangedDirectories"] = ro.Directories
|
||||
res["receiveOnlyChangedSymlinks"] = ro.Symlinks
|
||||
|
||||
@@ -23,15 +23,17 @@ import (
|
||||
|
||||
type indexSender struct {
|
||||
suture.Service
|
||||
conn protocol.Connection
|
||||
folder string
|
||||
fset *db.FileSet
|
||||
prevSequence int64
|
||||
evLogger events.Logger
|
||||
connClosed chan struct{}
|
||||
token suture.ServiceToken
|
||||
pauseChan chan struct{}
|
||||
resumeChan chan *db.FileSet
|
||||
conn protocol.Connection
|
||||
folder string
|
||||
folderIsReceiveEncrypted bool
|
||||
dev string
|
||||
fset *db.FileSet
|
||||
prevSequence int64
|
||||
evLogger events.Logger
|
||||
connClosed chan struct{}
|
||||
token suture.ServiceToken
|
||||
pauseChan chan struct{}
|
||||
resumeChan chan *db.FileSet
|
||||
}
|
||||
|
||||
func (s *indexSender) serve(ctx context.Context) {
|
||||
@@ -169,6 +171,13 @@ func (s *indexSender) sendIndexTo(ctx context.Context) error {
|
||||
|
||||
f = fi.(protocol.FileInfo)
|
||||
|
||||
// If this is a folder receiving encrypted files only, we
|
||||
// mustn't ever send locally changed file infos. Those aren't
|
||||
// encrypted and thus would be a protocol error at the remote.
|
||||
if s.folderIsReceiveEncrypted && fi.IsReceiveOnlyChanged() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Mark the file as invalid if any of the local bad stuff flags are set.
|
||||
f.RawInvalid = f.IsInvalid()
|
||||
// If the file is marked LocalReceive (i.e., changed locally on a
|
||||
|
||||
+365
-125
@@ -11,6 +11,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -131,15 +132,17 @@ type model struct {
|
||||
folderIOLimiter *byteSemaphore
|
||||
|
||||
// fields protected by fmut
|
||||
fmut sync.RWMutex
|
||||
folderCfgs map[string]config.FolderConfiguration // folder -> cfg
|
||||
folderFiles map[string]*db.FileSet // folder -> files
|
||||
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
|
||||
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
|
||||
folderRunners map[string]service // folder -> puller or scanner
|
||||
folderRunnerToken map[string]suture.ServiceToken // folder -> token for folder runner
|
||||
folderRestartMuts syncMutexMap // folder -> restart mutex
|
||||
folderVersioners map[string]versioner.Versioner // folder -> versioner (may be nil)
|
||||
fmut sync.RWMutex
|
||||
folderCfgs map[string]config.FolderConfiguration // folder -> cfg
|
||||
folderFiles map[string]*db.FileSet // folder -> files
|
||||
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
|
||||
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
|
||||
folderRunners map[string]service // folder -> puller or scanner
|
||||
folderRunnerToken map[string]suture.ServiceToken // folder -> token for folder runner
|
||||
folderRestartMuts syncMutexMap // folder -> restart mutex
|
||||
folderVersioners map[string]versioner.Versioner // folder -> versioner (may be nil)
|
||||
folderEncryptionPasswordTokens map[string][]byte // folder -> encryption token (may be missing, and only for encryption type folders)
|
||||
folderEncryptionFailures map[string]map[protocol.DeviceID]error // folder -> device -> error regarding encryption consistency (may be missing)
|
||||
|
||||
// fields protected by pmut
|
||||
pmut sync.RWMutex
|
||||
@@ -171,11 +174,18 @@ var (
|
||||
errNetworkNotAllowed = errors.New("network not allowed")
|
||||
errNoVersioner = errors.New("folder has no versioner")
|
||||
// errors about why a connection is closed
|
||||
errIgnoredFolderRemoved = errors.New("folder no longer ignored")
|
||||
errReplacingConnection = errors.New("replacing connection")
|
||||
errStopped = errors.New("Syncthing is being stopped")
|
||||
errMissingRemoteInClusterConfig = errors.New("remote device missing in cluster config")
|
||||
errMissingLocalInClusterConfig = errors.New("local device missing in cluster config")
|
||||
errIgnoredFolderRemoved = errors.New("folder no longer ignored")
|
||||
errReplacingConnection = errors.New("replacing connection")
|
||||
errStopped = errors.New("Syncthing is being stopped")
|
||||
errEncryptionInvConfigLocal = errors.New("can't encrypt data for a device when the folder type is receiveEncrypted")
|
||||
errEncryptionInvConfigRemote = errors.New("remote has encrypted data and encrypts that data for us - this is impossible")
|
||||
errEncryptionNotEncryptedLocal = errors.New("folder is announced as encrypted, but not configured thus")
|
||||
errEncryptionNotEncryptedRemote = errors.New("folder is configured to be encrypted but not announced thus")
|
||||
errEncryptionNotEncryptedUntrusted = errors.New("device is untrusted, but configured to receive not encrypted data")
|
||||
errEncryptionPassword = errors.New("different encryption passwords used")
|
||||
errEncryptionReceivedToken = errors.New("resetting connection to send info on new encrypted folder (new cluster config)")
|
||||
errMissingRemoteInClusterConfig = errors.New("remote device missing in cluster config")
|
||||
errMissingLocalInClusterConfig = errors.New("local device missing in cluster config")
|
||||
)
|
||||
|
||||
// NewModel creates and starts a new model. The model starts in read-only mode,
|
||||
@@ -207,14 +217,16 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
|
||||
folderIOLimiter: newByteSemaphore(cfg.Options().MaxFolderConcurrency()),
|
||||
|
||||
// fields protected by fmut
|
||||
fmut: sync.NewRWMutex(),
|
||||
folderCfgs: make(map[string]config.FolderConfiguration),
|
||||
folderFiles: make(map[string]*db.FileSet),
|
||||
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
|
||||
folderIgnores: make(map[string]*ignore.Matcher),
|
||||
folderRunners: make(map[string]service),
|
||||
folderRunnerToken: make(map[string]suture.ServiceToken),
|
||||
folderVersioners: make(map[string]versioner.Versioner),
|
||||
fmut: sync.NewRWMutex(),
|
||||
folderCfgs: make(map[string]config.FolderConfiguration),
|
||||
folderFiles: make(map[string]*db.FileSet),
|
||||
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
|
||||
folderIgnores: make(map[string]*ignore.Matcher),
|
||||
folderRunners: make(map[string]service),
|
||||
folderRunnerToken: make(map[string]suture.ServiceToken),
|
||||
folderVersioners: make(map[string]versioner.Versioner),
|
||||
folderEncryptionPasswordTokens: make(map[string][]byte),
|
||||
folderEncryptionFailures: make(map[string]map[protocol.DeviceID]error),
|
||||
|
||||
// fields protected by pmut
|
||||
pmut: sync.NewRWMutex(),
|
||||
@@ -339,6 +351,14 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
|
||||
|
||||
ffs := fset.MtimeFS()
|
||||
|
||||
if cfg.Type == config.FolderTypeReceiveEncrypted {
|
||||
if encryptionToken, err := readEncryptionToken(cfg); err == nil {
|
||||
m.folderEncryptionPasswordTokens[folder] = encryptionToken
|
||||
} else if !fs.IsNotExist(err) {
|
||||
l.Warnf("Failed to read encryption token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// These are our metadata files, and they should always be hidden.
|
||||
_ = ffs.Hide(config.DefaultMarkerName)
|
||||
_ = ffs.Hide(".stversions")
|
||||
@@ -1026,8 +1046,6 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
// Also, collect a list of folders we do share, and if he's interested in
|
||||
// temporary indexes, subscribe the connection.
|
||||
|
||||
tempIndexFolders := make([]string, 0, len(cm.Folders))
|
||||
|
||||
m.pmut.RLock()
|
||||
indexSenderRegistry, ok := m.indexSenders[deviceID]
|
||||
m.pmut.RUnlock()
|
||||
@@ -1042,11 +1060,37 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
return errDeviceUnknown
|
||||
}
|
||||
|
||||
// Assemble the device information from the connected device about
|
||||
// themselves and us for all folders.
|
||||
ccDeviceInfos := make(map[string]*indexSenderStartInfo, len(cm.Folders))
|
||||
for _, folder := range cm.Folders {
|
||||
info := &indexSenderStartInfo{}
|
||||
for _, dev := range folder.Devices {
|
||||
if dev.ID == m.id {
|
||||
info.local = dev
|
||||
} else if dev.ID == deviceID {
|
||||
info.remote = dev
|
||||
}
|
||||
if info.local.ID != protocol.EmptyDeviceID && info.remote.ID != protocol.EmptyDeviceID {
|
||||
break
|
||||
}
|
||||
}
|
||||
if info.remote.ID == protocol.EmptyDeviceID {
|
||||
l.Infof("Device %v sent cluster-config without the device info for the remote on folder %v", deviceID, folder.Description())
|
||||
return errMissingRemoteInClusterConfig
|
||||
}
|
||||
if info.local.ID == protocol.EmptyDeviceID {
|
||||
l.Infof("Device %v sent cluster-config without the device info for us locally on folder %v", deviceID, folder.Description())
|
||||
return errMissingLocalInClusterConfig
|
||||
}
|
||||
ccDeviceInfos[folder.ID] = info
|
||||
}
|
||||
|
||||
// Needs to happen outside of the fmut, as can cause CommitConfiguration
|
||||
if deviceCfg.AutoAcceptFolders {
|
||||
changedFolders := make([]config.FolderConfiguration, 0, len(cm.Folders))
|
||||
for _, folder := range cm.Folders {
|
||||
if fcfg, fchanged := m.handleAutoAccepts(deviceCfg, folder); fchanged {
|
||||
if fcfg, fchanged := m.handleAutoAccepts(deviceID, folder, ccDeviceInfos[folder.ID]); fchanged {
|
||||
changedFolders = append(changedFolders, fcfg)
|
||||
}
|
||||
}
|
||||
@@ -1061,91 +1105,16 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
}
|
||||
}
|
||||
|
||||
paused := make(map[string]struct{}, len(cm.Folders))
|
||||
seenFolders := make(map[string]struct{}, len(cm.Folders))
|
||||
for _, folder := range cm.Folders {
|
||||
seenFolders[folder.ID] = struct{}{}
|
||||
|
||||
cfg, ok := m.cfg.Folder(folder.ID)
|
||||
if !ok || !cfg.SharedWith(deviceID) {
|
||||
indexSenderRegistry.remove(folder.ID)
|
||||
if deviceCfg.IgnoredFolder(folder.ID) {
|
||||
l.Infof("Ignoring folder %s from device %s since we are configured to", folder.Description(), deviceID)
|
||||
continue
|
||||
}
|
||||
m.cfg.AddOrUpdatePendingFolder(folder.ID, folder.Label, deviceID)
|
||||
changed = true
|
||||
m.evLogger.Log(events.FolderRejected, map[string]string{
|
||||
"folder": folder.ID,
|
||||
"folderLabel": folder.Label,
|
||||
"device": deviceID.String(),
|
||||
})
|
||||
l.Infof("Unexpected folder %s sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.Description(), deviceID)
|
||||
continue
|
||||
}
|
||||
|
||||
deviceInfos := &indexSenderStartInfo{}
|
||||
for _, dev := range folder.Devices {
|
||||
if dev.ID == m.id {
|
||||
deviceInfos.local = dev
|
||||
} else if dev.ID == deviceID {
|
||||
deviceInfos.remote = dev
|
||||
}
|
||||
if deviceInfos.local.ID != protocol.EmptyDeviceID && deviceInfos.remote.ID != protocol.EmptyDeviceID {
|
||||
break
|
||||
}
|
||||
}
|
||||
if deviceInfos.remote.ID == protocol.EmptyDeviceID {
|
||||
l.Infof("Device %v sent cluster-config without the device info for the remote on folder %v", deviceID, folder.Description())
|
||||
return errMissingRemoteInClusterConfig
|
||||
}
|
||||
if deviceInfos.local.ID == protocol.EmptyDeviceID {
|
||||
l.Infof("Device %v sent cluster-config without the device info for us locally on folder %v", deviceID, folder.Description())
|
||||
return errMissingLocalInClusterConfig
|
||||
}
|
||||
|
||||
if folder.Paused {
|
||||
indexSenderRegistry.remove(folder.ID)
|
||||
paused[cfg.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if cfg.Paused {
|
||||
indexSenderRegistry.addPaused(cfg, deviceInfos)
|
||||
continue
|
||||
}
|
||||
|
||||
m.fmut.RLock()
|
||||
fs, ok := m.folderFiles[folder.ID]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
// Shouldn't happen because !cfg.Paused, but might happen
|
||||
// if the folder is about to be unpaused, but not yet.
|
||||
continue
|
||||
}
|
||||
|
||||
if !folder.DisableTempIndexes {
|
||||
tempIndexFolders = append(tempIndexFolders, folder.ID)
|
||||
}
|
||||
|
||||
indexSenderRegistry.add(cfg, fs, deviceInfos)
|
||||
|
||||
// We might already have files that we need to pull so let the
|
||||
// folder runner know that it should recheck the index data.
|
||||
m.fmut.RLock()
|
||||
if runner := m.folderRunners[folder.ID]; runner != nil {
|
||||
defer runner.SchedulePull()
|
||||
}
|
||||
m.fmut.RUnlock()
|
||||
changedHere, tempIndexFolders, paused, err := m.ccHandleFolders(cm.Folders, deviceCfg, ccDeviceInfos, indexSenderRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indexSenderRegistry.removeAllExcept(seenFolders)
|
||||
changed = changed || changedHere
|
||||
|
||||
m.pmut.Lock()
|
||||
m.remotePausedFolders[deviceID] = paused
|
||||
m.pmut.Unlock()
|
||||
|
||||
// This breaks if we send multiple CM messages during the same connection.
|
||||
if len(tempIndexFolders) > 0 {
|
||||
m.pmut.RLock()
|
||||
conn, ok := m.conn[deviceID]
|
||||
@@ -1184,6 +1153,212 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.DeviceConfiguration, ccDeviceInfos map[string]*indexSenderStartInfo, indexSenders *indexSenderRegistry) (bool, []string, map[string]struct{}, error) {
|
||||
var changed bool
|
||||
var folderDevice config.FolderDeviceConfiguration
|
||||
tempIndexFolders := make([]string, 0, len(folders))
|
||||
paused := make(map[string]struct{}, len(folders))
|
||||
seenFolders := make(map[string]struct{}, len(folders))
|
||||
deviceID := deviceCfg.DeviceID
|
||||
for _, folder := range folders {
|
||||
seenFolders[folder.ID] = struct{}{}
|
||||
|
||||
cfg, ok := m.cfg.Folder(folder.ID)
|
||||
if ok {
|
||||
folderDevice, ok = cfg.Device(deviceID)
|
||||
}
|
||||
if !ok {
|
||||
indexSenders.remove(folder.ID)
|
||||
if deviceCfg.IgnoredFolder(folder.ID) {
|
||||
l.Infof("Ignoring folder %s from device %s since we are configured to", folder.Description(), deviceID)
|
||||
continue
|
||||
}
|
||||
m.cfg.AddOrUpdatePendingFolder(folder.ID, folder.Label, deviceID)
|
||||
changed = true
|
||||
m.evLogger.Log(events.FolderRejected, map[string]string{
|
||||
"folder": folder.ID,
|
||||
"folderLabel": folder.Label,
|
||||
"device": deviceID.String(),
|
||||
})
|
||||
l.Infof("Unexpected folder %s sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.Description(), deviceID)
|
||||
continue
|
||||
}
|
||||
|
||||
if folder.Paused {
|
||||
indexSenders.remove(folder.ID)
|
||||
paused[cfg.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if cfg.Paused {
|
||||
indexSenders.addPaused(cfg, ccDeviceInfos[folder.ID])
|
||||
continue
|
||||
}
|
||||
|
||||
m.fmut.RLock()
|
||||
fs, ok := m.folderFiles[folder.ID]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
// Shouldn't happen because !cfg.Paused, but might happen
|
||||
// if the folder is about to be unpaused, but not yet.
|
||||
l.Debugln("ccH: no fset", folder.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := m.ccCheckEncryption(cfg, folderDevice, ccDeviceInfos[folder.ID], deviceCfg.Untrusted); err != nil {
|
||||
sameError := false
|
||||
if devs, ok := m.folderEncryptionFailures[folder.ID]; ok {
|
||||
sameError = devs[deviceID] == err
|
||||
} else {
|
||||
m.folderEncryptionFailures[folder.ID] = make(map[protocol.DeviceID]error)
|
||||
}
|
||||
m.folderEncryptionFailures[folder.ID][deviceID] = err
|
||||
msg := fmt.Sprintf("Failure checking encryption consistency with device %v for folder %v: %v", deviceID, cfg.Description(), err)
|
||||
if sameError || err == errEncryptionReceivedToken {
|
||||
l.Debugln(msg)
|
||||
} else {
|
||||
l.Warnln(msg)
|
||||
}
|
||||
|
||||
return changed, tempIndexFolders, paused, err
|
||||
}
|
||||
if devErrs, ok := m.folderEncryptionFailures[folder.ID]; ok {
|
||||
if len(devErrs) == 1 {
|
||||
delete(m.folderEncryptionFailures, folder.ID)
|
||||
} else {
|
||||
delete(m.folderEncryptionFailures[folder.ID], deviceID)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle indexes
|
||||
|
||||
if !folder.DisableTempIndexes {
|
||||
tempIndexFolders = append(tempIndexFolders, folder.ID)
|
||||
}
|
||||
|
||||
indexSenders.add(cfg, fs, ccDeviceInfos[folder.ID])
|
||||
|
||||
// We might already have files that we need to pull so let the
|
||||
// folder runner know that it should recheck the index data.
|
||||
m.fmut.RLock()
|
||||
if runner := m.folderRunners[folder.ID]; runner != nil {
|
||||
defer runner.SchedulePull()
|
||||
}
|
||||
m.fmut.RUnlock()
|
||||
}
|
||||
|
||||
indexSenders.removeAllExcept(seenFolders)
|
||||
|
||||
return changed, tempIndexFolders, paused, nil
|
||||
}
|
||||
|
||||
func (m *model) ccCheckEncryption(fcfg config.FolderConfiguration, folderDevice config.FolderDeviceConfiguration, ccDeviceInfos *indexSenderStartInfo, deviceUntrusted bool) error {
|
||||
hasTokenRemote := len(ccDeviceInfos.remote.EncryptionPasswordToken) > 0
|
||||
hasTokenLocal := len(ccDeviceInfos.local.EncryptionPasswordToken) > 0
|
||||
isEncryptedRemote := folderDevice.EncryptionPassword != ""
|
||||
isEncryptedLocal := fcfg.Type == config.FolderTypeReceiveEncrypted
|
||||
|
||||
if !isEncryptedRemote && !isEncryptedLocal && deviceUntrusted {
|
||||
return errEncryptionNotEncryptedUntrusted
|
||||
}
|
||||
|
||||
if !(hasTokenRemote || hasTokenLocal || isEncryptedRemote || isEncryptedLocal) {
|
||||
// Noone cares about encryption here
|
||||
return nil
|
||||
}
|
||||
|
||||
if isEncryptedRemote && isEncryptedLocal {
|
||||
// Should never happen, but config racyness and be safe.
|
||||
return errEncryptionInvConfigLocal
|
||||
}
|
||||
|
||||
if hasTokenRemote && hasTokenLocal {
|
||||
return errEncryptionInvConfigRemote
|
||||
}
|
||||
|
||||
if !(hasTokenRemote || hasTokenLocal) {
|
||||
return errEncryptionNotEncryptedRemote
|
||||
}
|
||||
|
||||
if !(isEncryptedRemote || isEncryptedLocal) {
|
||||
return errEncryptionNotEncryptedLocal
|
||||
}
|
||||
|
||||
if isEncryptedRemote {
|
||||
passwordToken := protocol.PasswordToken(fcfg.ID, folderDevice.EncryptionPassword)
|
||||
match := false
|
||||
if hasTokenLocal {
|
||||
match = bytes.Equal(passwordToken, ccDeviceInfos.local.EncryptionPasswordToken)
|
||||
} else {
|
||||
// hasTokenRemote == true
|
||||
match = bytes.Equal(passwordToken, ccDeviceInfos.remote.EncryptionPasswordToken)
|
||||
}
|
||||
if !match {
|
||||
return errEncryptionPassword
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isEncryptedLocal == true
|
||||
|
||||
var ccToken []byte
|
||||
if hasTokenLocal {
|
||||
ccToken = ccDeviceInfos.local.EncryptionPasswordToken
|
||||
} else {
|
||||
// hasTokenRemote == true
|
||||
ccToken = ccDeviceInfos.remote.EncryptionPasswordToken
|
||||
}
|
||||
m.fmut.RLock()
|
||||
token, ok := m.folderEncryptionPasswordTokens[fcfg.ID]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
var err error
|
||||
token, err = readEncryptionToken(fcfg)
|
||||
if err != nil && !fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
m.fmut.Lock()
|
||||
m.folderEncryptionPasswordTokens[fcfg.ID] = token
|
||||
m.fmut.Unlock()
|
||||
} else {
|
||||
if err := writeEncryptionToken(ccToken, fcfg); err != nil {
|
||||
return err
|
||||
}
|
||||
m.fmut.Lock()
|
||||
m.folderEncryptionPasswordTokens[fcfg.ID] = ccToken
|
||||
m.fmut.Unlock()
|
||||
// We can only announce ourselfs once we have the token,
|
||||
// thus we need to resend CCs now that we have it.
|
||||
m.resendClusterConfig(fcfg.DeviceIDs())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(token, ccToken) {
|
||||
return errEncryptionPassword
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *model) resendClusterConfig(ids []protocol.DeviceID) {
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
ccConns := make([]protocol.Connection, 0, len(ids))
|
||||
m.pmut.RLock()
|
||||
for _, id := range ids {
|
||||
if conn, ok := m.conn[id]; ok {
|
||||
ccConns = append(ccConns, conn)
|
||||
}
|
||||
}
|
||||
m.pmut.RUnlock()
|
||||
// Generating cluster-configs acquires fmut -> must happen outside of pmut.
|
||||
for _, conn := range ccConns {
|
||||
cm := m.generateClusterConfig(conn.ID())
|
||||
go conn.ClusterConfig(cm)
|
||||
}
|
||||
}
|
||||
|
||||
// handleIntroductions handles adding devices/folders that are shared by an introducer device
|
||||
func (m *model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig) (map[string]config.FolderConfiguration, map[protocol.DeviceID]config.DeviceConfiguration, folderDeviceSet, bool) {
|
||||
changed := false
|
||||
@@ -1295,7 +1470,7 @@ func (m *model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
|
||||
|
||||
// handleAutoAccepts handles adding and sharing folders for devices that have
|
||||
// AutoAcceptFolders set to true.
|
||||
func (m *model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder protocol.Folder) (config.FolderConfiguration, bool) {
|
||||
func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Folder, ccDeviceInfos *indexSenderStartInfo) (config.FolderConfiguration, bool) {
|
||||
if cfg, ok := m.cfg.Folder(folder.ID); !ok {
|
||||
defaultPath := m.cfg.Options().DefaultFolderPath
|
||||
defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath)
|
||||
@@ -1310,25 +1485,40 @@ func (m *model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder p
|
||||
|
||||
fcfg := config.NewFolderConfiguration(m.id, folder.ID, folder.Label, fs.FilesystemTypeBasic, filepath.Join(defaultPath, path))
|
||||
fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{
|
||||
DeviceID: deviceCfg.DeviceID,
|
||||
DeviceID: deviceID,
|
||||
})
|
||||
|
||||
l.Infof("Auto-accepted %s folder %s at path %s", deviceCfg.DeviceID, folder.Description(), fcfg.Path)
|
||||
if len(ccDeviceInfos.remote.EncryptionPasswordToken) > 0 || len(ccDeviceInfos.local.EncryptionPasswordToken) > 0 {
|
||||
fcfg.Type = config.FolderTypeReceiveEncrypted
|
||||
}
|
||||
|
||||
l.Infof("Auto-accepted %s folder %s at path %s", deviceID, folder.Description(), fcfg.Path)
|
||||
return fcfg, true
|
||||
}
|
||||
l.Infof("Failed to auto-accept folder %s from %s due to path conflict", folder.Description(), deviceCfg.DeviceID)
|
||||
l.Infof("Failed to auto-accept folder %s from %s due to path conflict", folder.Description(), deviceID)
|
||||
return config.FolderConfiguration{}, false
|
||||
} else {
|
||||
for _, device := range cfg.DeviceIDs() {
|
||||
if device == deviceCfg.DeviceID {
|
||||
if device == deviceID {
|
||||
// Already shared nothing todo.
|
||||
return config.FolderConfiguration{}, false
|
||||
}
|
||||
}
|
||||
if cfg.Type == config.FolderTypeReceiveEncrypted {
|
||||
if len(ccDeviceInfos.remote.EncryptionPasswordToken) == 0 && len(ccDeviceInfos.local.EncryptionPasswordToken) == 0 {
|
||||
l.Infof("Failed to auto-accept device %s on existing folder %s as the remote wants to send us unencrypted data, but the folder type is receive-encrypted", folder.Description(), deviceID)
|
||||
return config.FolderConfiguration{}, false
|
||||
}
|
||||
} else {
|
||||
if len(ccDeviceInfos.remote.EncryptionPasswordToken) > 0 || len(ccDeviceInfos.local.EncryptionPasswordToken) > 0 {
|
||||
l.Infof("Failed to auto-accept device %s on existing folder %s as the remote wants to send us encrypted data, but the folder type is not receive-encrypted", folder.Description(), deviceID)
|
||||
return config.FolderConfiguration{}, false
|
||||
}
|
||||
}
|
||||
cfg.Devices = append(cfg.Devices, config.FolderDeviceConfiguration{
|
||||
DeviceID: deviceCfg.DeviceID,
|
||||
DeviceID: deviceID,
|
||||
})
|
||||
l.Infof("Shared %s with %s due to auto-accept", folder.ID, deviceCfg.DeviceID)
|
||||
l.Infof("Shared %s with %s due to auto-accept", folder.ID, deviceID)
|
||||
return cfg, true
|
||||
}
|
||||
}
|
||||
@@ -1422,7 +1612,7 @@ func (r *requestResponse) Wait() {
|
||||
|
||||
// Request returns the specified data segment by reading it from local disk.
|
||||
// Implements the protocol.Model interface.
|
||||
func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (out protocol.RequestResponse, err error) {
|
||||
func (m *model) Request(deviceID protocol.DeviceID, folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (out protocol.RequestResponse, err error) {
|
||||
if size < 0 || offset < 0 {
|
||||
return nil, protocol.ErrInvalid
|
||||
}
|
||||
@@ -1520,12 +1710,15 @@ func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size in
|
||||
if err := readOffsetIntoBuf(folderFs, name, offset, res.data); fs.IsNotExist(err) {
|
||||
l.Debugf("%v REQ(in) file doesn't exist: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
} else if err == io.EOF && len(hash) == 0 {
|
||||
// Read beyond end of file when we can't verify the hash -- this is
|
||||
// a padded read for an encrypted file. It's fine.
|
||||
} else if err != nil {
|
||||
l.Debugf("%v REQ(in) failed reading file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrGeneric
|
||||
}
|
||||
|
||||
if !scanner.Validate(res.data, hash, weakHash) {
|
||||
if len(hash) > 0 && !scanner.Validate(res.data, hash, weakHash) {
|
||||
m.recheckFile(deviceID, folder, name, offset, hash, weakHash)
|
||||
l.Debugf("%v REQ(in) failed validating data: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
@@ -1862,7 +2055,7 @@ func (m *model) deviceWasSeen(deviceID protocol.DeviceID) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
m.pmut.RLock()
|
||||
nc, ok := m.conn[deviceID]
|
||||
m.pmut.RUnlock()
|
||||
@@ -1871,9 +2064,9 @@ func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, f
|
||||
return nil, fmt.Errorf("requestGlobal: no such device: %s", deviceID)
|
||||
}
|
||||
|
||||
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x wh=%x ft=%t", m, deviceID, folder, name, offset, size, hash, weakHash, fromTemporary)
|
||||
l.Debugf("%v REQ(out): %s: %q / %q b=%d o=%d s=%d h=%x wh=%x ft=%t", m, deviceID, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
|
||||
|
||||
return nc.Request(ctx, folder, name, offset, size, hash, weakHash, fromTemporary)
|
||||
return nc.Request(ctx, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
|
||||
}
|
||||
|
||||
func (m *model) ScanFolders() map[string]error {
|
||||
@@ -1974,6 +2167,17 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
|
||||
continue
|
||||
}
|
||||
|
||||
var encryptionToken []byte
|
||||
var hasEncryptionToken bool
|
||||
if folderCfg.Type == config.FolderTypeReceiveEncrypted {
|
||||
if encryptionToken, hasEncryptionToken = m.folderEncryptionPasswordTokens[folderCfg.ID]; !hasEncryptionToken {
|
||||
// We haven't gotten a token for us yet and without
|
||||
// one the other side can't validate us - pretend
|
||||
// we don't have the folder yet.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
protocolFolder := protocol.Folder{
|
||||
ID: folderCfg.ID,
|
||||
Label: folderCfg.Label,
|
||||
@@ -2001,6 +2205,12 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
|
||||
Introducer: deviceCfg.Introducer,
|
||||
}
|
||||
|
||||
if deviceCfg.DeviceID == m.id && hasEncryptionToken {
|
||||
protocolDevice.EncryptionPasswordToken = encryptionToken
|
||||
} else if device.EncryptionPassword != "" {
|
||||
protocolDevice.EncryptionPasswordToken = protocol.PasswordToken(folderCfg.ID, device.EncryptionPassword)
|
||||
}
|
||||
|
||||
if fs != nil {
|
||||
if deviceCfg.DeviceID == m.id {
|
||||
protocolDevice.IndexID = fs.IndexID(protocol.LocalDeviceID)
|
||||
@@ -2375,18 +2585,13 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
|
||||
go conn.Close(errDeviceRemoved)
|
||||
}
|
||||
}
|
||||
ccConns := make([]protocol.Connection, 0, len(clusterConfigDevices))
|
||||
for id := range clusterConfigDevices {
|
||||
if conn, ok := m.conn[id]; ok {
|
||||
ccConns = append(ccConns, conn)
|
||||
}
|
||||
}
|
||||
m.pmut.RUnlock()
|
||||
// Generating cluster-configs acquires fmut -> must happen outside of pmut.
|
||||
for _, conn := range ccConns {
|
||||
cm := m.generateClusterConfig(conn.ID())
|
||||
go conn.ClusterConfig(cm)
|
||||
ids := make([]protocol.DeviceID, 0, len(clusterConfigDevices))
|
||||
for id := range clusterConfigDevices {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
m.resendClusterConfig(ids)
|
||||
|
||||
m.globalRequestLimiter.setCapacity(1024 * to.Options.MaxConcurrentIncomingRequestKiB())
|
||||
m.folderIOLimiter.setCapacity(to.Options.MaxFolderConcurrency())
|
||||
@@ -2600,3 +2805,38 @@ func addDeviceIDsToMap(m map[protocol.DeviceID]struct{}, s []protocol.DeviceID)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func encryptionTokenPath(cfg config.FolderConfiguration) string {
|
||||
return filepath.Join(cfg.MarkerName, "syncthing-encryption_password_token")
|
||||
}
|
||||
|
||||
type storedEncryptionToken struct {
|
||||
FolderID string
|
||||
Token []byte
|
||||
}
|
||||
|
||||
func readEncryptionToken(cfg config.FolderConfiguration) ([]byte, error) {
|
||||
fd, err := cfg.Filesystem().Open(encryptionTokenPath(cfg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
var stored storedEncryptionToken
|
||||
if err := json.NewDecoder(fd).Decode(&stored); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stored.Token, nil
|
||||
}
|
||||
|
||||
func writeEncryptionToken(token []byte, cfg config.FolderConfiguration) error {
|
||||
tokenName := encryptionTokenPath(cfg)
|
||||
fd, err := cfg.Filesystem().OpenFile(tokenName, fs.OptReadWrite|fs.OptCreate, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
return json.NewEncoder(fd).Encode(storedEncryptionToken{
|
||||
FolderID: cfg.ID,
|
||||
Token: token,
|
||||
})
|
||||
}
|
||||
|
||||
+309
-129
@@ -132,12 +132,35 @@ func newState(cfg config.Configuration) *model {
|
||||
return m
|
||||
}
|
||||
|
||||
func createClusterConfig(remote protocol.DeviceID, ids ...string) protocol.ClusterConfig {
|
||||
cc := protocol.ClusterConfig{
|
||||
Folders: make([]protocol.Folder, len(ids)),
|
||||
}
|
||||
for i, id := range ids {
|
||||
cc.Folders[i] = protocol.Folder{
|
||||
ID: id,
|
||||
Label: id,
|
||||
}
|
||||
}
|
||||
return addFolderDevicesToClusterConfig(cc, remote)
|
||||
}
|
||||
|
||||
func addFolderDevicesToClusterConfig(cc protocol.ClusterConfig, remote protocol.DeviceID) protocol.ClusterConfig {
|
||||
for i := range cc.Folders {
|
||||
cc.Folders[i].Devices = []protocol.Device{
|
||||
{ID: myID},
|
||||
{ID: remote},
|
||||
}
|
||||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
func TestRequest(t *testing.T) {
|
||||
m := setupModel(defaultCfgWrapper)
|
||||
defer cleanupModel(m)
|
||||
|
||||
// Existing, shared file
|
||||
res, err := m.Request(device1, "default", "foo", 6, 0, nil, 0, false)
|
||||
res, err := m.Request(device1, "default", "foo", 0, 6, 0, nil, 0, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -147,33 +170,37 @@ func TestRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
// Existing, nonshared file
|
||||
_, err = m.Request(device2, "default", "foo", 6, 0, nil, 0, false)
|
||||
_, err = m.Request(device2, "default", "foo", 0, 6, 0, nil, 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Nonexistent file
|
||||
_, err = m.Request(device1, "default", "nonexistent", 6, 0, nil, 0, false)
|
||||
_, err = m.Request(device1, "default", "nonexistent", 0, 6, 0, nil, 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Shared folder, but disallowed file name
|
||||
_, err = m.Request(device1, "default", "../walk.go", 6, 0, nil, 0, false)
|
||||
_, err = m.Request(device1, "default", "../walk.go", 0, 6, 0, nil, 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Negative offset
|
||||
_, err = m.Request(device1, "default", "foo", -4, 0, nil, 0, false)
|
||||
_, err = m.Request(device1, "default", "foo", 0, -4, 0, nil, 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Larger block than available
|
||||
_, err = m.Request(device1, "default", "foo", 42, 0, nil, 0, false)
|
||||
_, err = m.Request(device1, "default", "foo", 0, 42, 0, []byte("hash necessary but not checked"), 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
t.Error("Unexpected nil error on read past end of file")
|
||||
}
|
||||
_, err = m.Request(device1, "default", "foo", 0, 42, 0, nil, 0, false)
|
||||
if err != nil {
|
||||
t.Error("Unexpected error when large read should be permitted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +286,7 @@ func BenchmarkRequestOut(b *testing.B) {
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
data, err := m.requestGlobal(context.Background(), device1, "default", files[i%n].Name, 0, 32, nil, 0, false)
|
||||
data, err := m.requestGlobal(context.Background(), device1, "default", files[i%n].Name, 0, 0, 32, nil, 0, false)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
@@ -283,7 +310,7 @@ func BenchmarkRequestInSingleFile(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := m.Request(device1, "default", "request/for/a/file/in/a/couple/of/dirs/128k", 128<<10, 0, nil, 0, false); err != nil {
|
||||
if _, err := m.Request(device1, "default", "request/for/a/file/in/a/couple/of/dirs/128k", 0, 128<<10, 0, nil, 0, false); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -854,14 +881,7 @@ func TestIssue5063(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
addAndVerify := func(id string) {
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id))
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
|
||||
t.Error("expected shared", id)
|
||||
}
|
||||
@@ -904,14 +924,7 @@ func TestAutoAcceptRejected(t *testing.T) {
|
||||
defer cleanupModel(m)
|
||||
id := srand.String(8)
|
||||
defer os.RemoveAll(id)
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id))
|
||||
|
||||
if cfg, ok := m.cfg.Folder(id); ok && cfg.SharedWith(device1) {
|
||||
t.Error("unexpected shared", id)
|
||||
@@ -924,14 +937,7 @@ func TestAutoAcceptNewFolder(t *testing.T) {
|
||||
defer cleanupModel(m)
|
||||
id := srand.String(8)
|
||||
defer os.RemoveAll(id)
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id))
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
|
||||
t.Error("expected shared", id)
|
||||
}
|
||||
@@ -942,28 +948,14 @@ func TestAutoAcceptNewFolderFromTwoDevices(t *testing.T) {
|
||||
defer cleanupModel(m)
|
||||
id := srand.String(8)
|
||||
defer os.RemoveAll(id)
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id))
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
|
||||
t.Error("expected shared", id)
|
||||
}
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || fcfg.SharedWith(device2) {
|
||||
t.Error("unexpected expected shared", id)
|
||||
}
|
||||
m.ClusterConfig(device2, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device2, createClusterConfig(device2, id))
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device2) {
|
||||
t.Error("expected shared", id)
|
||||
}
|
||||
@@ -976,28 +968,14 @@ func TestAutoAcceptNewFolderFromOnlyOneDevice(t *testing.T) {
|
||||
id := srand.String(8)
|
||||
defer os.RemoveAll(id)
|
||||
defer cleanupModel(m)
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id))
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
|
||||
t.Error("expected shared", id)
|
||||
}
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || fcfg.SharedWith(device2) {
|
||||
t.Error("unexpected expected shared", id)
|
||||
}
|
||||
m.ClusterConfig(device2, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device2, createClusterConfig(device2, id))
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || fcfg.SharedWith(device2) {
|
||||
t.Error("unexpected shared", id)
|
||||
}
|
||||
@@ -1053,18 +1031,7 @@ func TestAutoAcceptMultipleFolders(t *testing.T) {
|
||||
defer os.RemoveAll(id2)
|
||||
m := newState(defaultAutoAcceptCfg)
|
||||
defer cleanupModel(m)
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id1,
|
||||
Label: id1,
|
||||
},
|
||||
{
|
||||
ID: id2,
|
||||
Label: id2,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id1, id2))
|
||||
if fcfg, ok := m.cfg.Folder(id1); !ok || !fcfg.SharedWith(device1) {
|
||||
t.Error("expected shared", id1)
|
||||
}
|
||||
@@ -1092,14 +1059,7 @@ func TestAutoAcceptExistingFolder(t *testing.T) {
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || fcfg.SharedWith(device1) {
|
||||
t.Error("missing folder, or shared", id)
|
||||
}
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id))
|
||||
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) || fcfg.Path != idOther {
|
||||
t.Error("missing folder, or unshared, or path changed", id)
|
||||
@@ -1125,18 +1085,7 @@ func TestAutoAcceptNewAndExistingFolder(t *testing.T) {
|
||||
if fcfg, ok := m.cfg.Folder(id1); !ok || fcfg.SharedWith(device1) {
|
||||
t.Error("missing folder, or shared", id1)
|
||||
}
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id1,
|
||||
Label: id1,
|
||||
},
|
||||
{
|
||||
ID: id2,
|
||||
Label: id2,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id1, id2))
|
||||
|
||||
for i, id := range []string{id1, id2} {
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
|
||||
@@ -1166,14 +1115,7 @@ func TestAutoAcceptAlreadyShared(t *testing.T) {
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
|
||||
t.Error("missing folder, or not shared", id)
|
||||
}
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id))
|
||||
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
|
||||
t.Error("missing folder, or not shared", id)
|
||||
@@ -1212,14 +1154,14 @@ func TestAutoAcceptPrefersLabel(t *testing.T) {
|
||||
defer os.RemoveAll(id)
|
||||
defer os.RemoveAll(label)
|
||||
defer cleanupModel(m)
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
m.ClusterConfig(device1, addFolderDevicesToClusterConfig(protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: label,
|
||||
},
|
||||
},
|
||||
})
|
||||
}, device1))
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) || !strings.HasSuffix(fcfg.Path, label) {
|
||||
t.Error("expected shared, or wrong path", id, label, fcfg.Path)
|
||||
}
|
||||
@@ -1237,14 +1179,14 @@ func TestAutoAcceptFallsBackToID(t *testing.T) {
|
||||
defer os.RemoveAll(label)
|
||||
defer os.RemoveAll(id)
|
||||
defer cleanupModel(m)
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
m.ClusterConfig(device1, addFolderDevicesToClusterConfig(protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: label,
|
||||
},
|
||||
},
|
||||
})
|
||||
}, device1))
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) || !strings.HasSuffix(fcfg.Path, id) {
|
||||
t.Error("expected shared, or wrong path", id, label, fcfg.Path)
|
||||
}
|
||||
@@ -1276,14 +1218,7 @@ func TestAutoAcceptPausedWhenFolderConfigChanged(t *testing.T) {
|
||||
t.Fatal("folder running?")
|
||||
}
|
||||
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id))
|
||||
m.generateClusterConfig(device1)
|
||||
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok {
|
||||
@@ -1333,14 +1268,7 @@ func TestAutoAcceptPausedWhenFolderConfigNotChanged(t *testing.T) {
|
||||
t.Fatal("folder running?")
|
||||
}
|
||||
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: id,
|
||||
Label: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
m.ClusterConfig(device1, createClusterConfig(device1, id))
|
||||
m.generateClusterConfig(device1)
|
||||
|
||||
if fcfg, ok := m.cfg.Folder(id); !ok {
|
||||
@@ -1361,6 +1289,113 @@ func TestAutoAcceptPausedWhenFolderConfigNotChanged(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoAcceptEnc(t *testing.T) {
|
||||
tcfg := defaultAutoAcceptCfg.Copy()
|
||||
m := newState(tcfg)
|
||||
defer cleanupModel(m)
|
||||
|
||||
id := srand.String(8)
|
||||
defer os.RemoveAll(id)
|
||||
|
||||
token := []byte("token")
|
||||
basicCC := func() protocol.ClusterConfig {
|
||||
return protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{{
|
||||
ID: id,
|
||||
Label: id,
|
||||
}}}
|
||||
}
|
||||
|
||||
// Earlier tests might cause the connection to get closed, thus ClusterConfig
|
||||
// would panic.
|
||||
clusterConfig := func(deviceID protocol.DeviceID, cm protocol.ClusterConfig) {
|
||||
m.AddConnection(&fakeConnection{id: deviceID, model: m}, protocol.Hello{})
|
||||
m.ClusterConfig(deviceID, cm)
|
||||
}
|
||||
|
||||
clusterConfig(device1, basicCC())
|
||||
if _, ok := m.cfg.Folder(id); ok {
|
||||
t.Fatal("unexpected added")
|
||||
}
|
||||
cc := basicCC()
|
||||
cc.Folders[0].Devices = []protocol.Device{{ID: device1}}
|
||||
clusterConfig(device1, cc)
|
||||
if _, ok := m.cfg.Folder(id); ok {
|
||||
t.Fatal("unexpected added")
|
||||
}
|
||||
cc = basicCC()
|
||||
cc.Folders[0].Devices = []protocol.Device{{ID: myID}}
|
||||
clusterConfig(device1, cc)
|
||||
if _, ok := m.cfg.Folder(id); ok {
|
||||
t.Fatal("unexpected added")
|
||||
}
|
||||
|
||||
// New folder, encrypted -> add as enc
|
||||
|
||||
cc = createClusterConfig(device1, id)
|
||||
cc.Folders[0].Devices[1].EncryptionPasswordToken = token
|
||||
clusterConfig(device1, cc)
|
||||
if cfg, ok := m.cfg.Folder(id); !ok {
|
||||
t.Fatal("unexpected unadded")
|
||||
} else {
|
||||
if !cfg.SharedWith(device1) {
|
||||
t.Fatal("unexpected unshared")
|
||||
}
|
||||
if cfg.Type != config.FolderTypeReceiveEncrypted {
|
||||
t.Fatal("Folder not added as receiveEncrypted")
|
||||
}
|
||||
}
|
||||
|
||||
// New device, unencrypted on encrypted folder -> reject
|
||||
|
||||
clusterConfig(device2, createClusterConfig(device2, id))
|
||||
if cfg, _ := m.cfg.Folder(id); cfg.SharedWith(device2) {
|
||||
t.Fatal("unexpected shared")
|
||||
}
|
||||
|
||||
// New device, encrypted on encrypted folder -> share
|
||||
|
||||
cc = createClusterConfig(device2, id)
|
||||
cc.Folders[0].Devices[1].EncryptionPasswordToken = token
|
||||
clusterConfig(device2, cc)
|
||||
if cfg, _ := m.cfg.Folder(id); !cfg.SharedWith(device2) {
|
||||
t.Fatal("unexpected unshared")
|
||||
}
|
||||
|
||||
// New folder, no encrypted -> add "normal"
|
||||
|
||||
id = srand.String(8)
|
||||
defer os.RemoveAll(id)
|
||||
|
||||
clusterConfig(device1, createClusterConfig(device1, id))
|
||||
if cfg, ok := m.cfg.Folder(id); !ok {
|
||||
t.Fatal("unexpected unadded")
|
||||
} else {
|
||||
if !cfg.SharedWith(device1) {
|
||||
t.Fatal("unexpected unshared")
|
||||
}
|
||||
if cfg.Type != config.FolderTypeSendReceive {
|
||||
t.Fatal("Folder not added as send-receive")
|
||||
}
|
||||
}
|
||||
|
||||
// New device, encrypted on unencrypted folder -> reject
|
||||
|
||||
cc = createClusterConfig(device2, id)
|
||||
cc.Folders[0].Devices[1].EncryptionPasswordToken = token
|
||||
clusterConfig(device2, cc)
|
||||
if cfg, _ := m.cfg.Folder(id); cfg.SharedWith(device2) {
|
||||
t.Fatal("unexpected shared")
|
||||
}
|
||||
|
||||
// New device, unencrypted on unencrypted folder -> share
|
||||
|
||||
clusterConfig(device2, createClusterConfig(device2, id))
|
||||
if cfg, _ := m.cfg.Folder(id); !cfg.SharedWith(device2) {
|
||||
t.Fatal("unexpected unshared")
|
||||
}
|
||||
}
|
||||
|
||||
func changeIgnores(t *testing.T, m *model, expected []string) {
|
||||
arrEqual := func(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
@@ -3220,14 +3255,14 @@ func TestRequestLimit(t *testing.T) {
|
||||
|
||||
file := "tmpfile"
|
||||
befReq := time.Now()
|
||||
first, err := m.Request(device1, "default", file, 2000, 0, nil, 0, false)
|
||||
first, err := m.Request(device1, "default", file, 0, 2000, 0, nil, 0, false)
|
||||
if err != nil {
|
||||
t.Fatalf("First request failed: %v", err)
|
||||
}
|
||||
reqDur := time.Since(befReq)
|
||||
returned := make(chan struct{})
|
||||
go func() {
|
||||
second, err := m.Request(device1, "default", file, 2000, 0, nil, 0, false)
|
||||
second, err := m.Request(device1, "default", file, 0, 2000, 0, nil, 0, false)
|
||||
if err != nil {
|
||||
t.Errorf("Second request failed: %v", err)
|
||||
}
|
||||
@@ -3829,7 +3864,10 @@ func TestClusterConfigOnFolderAdd(t *testing.T) {
|
||||
fcfg := testFolderConfigTmp()
|
||||
fcfg.ID = "second"
|
||||
fcfg.Label = "second"
|
||||
fcfg.Devices = []config.FolderDeviceConfiguration{{device2, protocol.EmptyDeviceID}}
|
||||
fcfg.Devices = []config.FolderDeviceConfiguration{{
|
||||
DeviceID: device2,
|
||||
IntroducedBy: protocol.EmptyDeviceID,
|
||||
}}
|
||||
if w, err := cfg.SetFolder(fcfg); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
@@ -3841,7 +3879,10 @@ func TestClusterConfigOnFolderAdd(t *testing.T) {
|
||||
func TestClusterConfigOnFolderShare(t *testing.T) {
|
||||
testConfigChangeTriggersClusterConfigs(t, true, true, nil, func(cfg config.Wrapper) {
|
||||
fcfg := cfg.FolderList()[0]
|
||||
fcfg.Devices = []config.FolderDeviceConfiguration{{device2, protocol.EmptyDeviceID}}
|
||||
fcfg.Devices = []config.FolderDeviceConfiguration{{
|
||||
DeviceID: device2,
|
||||
IntroducedBy: protocol.EmptyDeviceID,
|
||||
}}
|
||||
if w, err := cfg.SetFolder(fcfg); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
@@ -4161,6 +4202,145 @@ func TestNeedMetaAfterIndexReset(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCcCheckEncryption(t *testing.T) {
|
||||
w, fcfg := tmpDefaultWrapper()
|
||||
m := setupModel(w)
|
||||
m.Stop()
|
||||
defer cleanupModel(m)
|
||||
|
||||
pw := "foo"
|
||||
token := protocol.PasswordToken(fcfg.ID, pw)
|
||||
m.folderEncryptionPasswordTokens[fcfg.ID] = token
|
||||
|
||||
testCases := []struct {
|
||||
tokenRemote, tokenLocal []byte
|
||||
isEncryptedRemote, isEncryptedLocal bool
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
tokenRemote: token,
|
||||
tokenLocal: token,
|
||||
expectedErr: errEncryptionInvConfigRemote,
|
||||
},
|
||||
{
|
||||
isEncryptedRemote: true,
|
||||
isEncryptedLocal: true,
|
||||
expectedErr: errEncryptionInvConfigLocal,
|
||||
},
|
||||
{
|
||||
tokenRemote: token,
|
||||
tokenLocal: nil,
|
||||
isEncryptedRemote: false,
|
||||
isEncryptedLocal: false,
|
||||
expectedErr: errEncryptionNotEncryptedLocal,
|
||||
},
|
||||
{
|
||||
tokenRemote: token,
|
||||
tokenLocal: nil,
|
||||
isEncryptedRemote: true,
|
||||
isEncryptedLocal: false,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
tokenRemote: token,
|
||||
tokenLocal: nil,
|
||||
isEncryptedRemote: false,
|
||||
isEncryptedLocal: true,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
tokenRemote: nil,
|
||||
tokenLocal: token,
|
||||
isEncryptedRemote: true,
|
||||
isEncryptedLocal: false,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
tokenRemote: nil,
|
||||
tokenLocal: token,
|
||||
isEncryptedRemote: false,
|
||||
isEncryptedLocal: true,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
tokenRemote: nil,
|
||||
tokenLocal: token,
|
||||
isEncryptedRemote: false,
|
||||
isEncryptedLocal: false,
|
||||
expectedErr: errEncryptionNotEncryptedLocal,
|
||||
},
|
||||
{
|
||||
tokenRemote: nil,
|
||||
tokenLocal: nil,
|
||||
isEncryptedRemote: true,
|
||||
isEncryptedLocal: false,
|
||||
expectedErr: errEncryptionNotEncryptedRemote,
|
||||
},
|
||||
{
|
||||
tokenRemote: nil,
|
||||
tokenLocal: nil,
|
||||
isEncryptedRemote: false,
|
||||
isEncryptedLocal: true,
|
||||
expectedErr: errEncryptionNotEncryptedRemote,
|
||||
},
|
||||
{
|
||||
tokenRemote: nil,
|
||||
tokenLocal: nil,
|
||||
isEncryptedRemote: false,
|
||||
isEncryptedLocal: false,
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
tfcfg := fcfg.Copy()
|
||||
if tc.isEncryptedLocal {
|
||||
tfcfg.Type = config.FolderTypeReceiveEncrypted
|
||||
m.folderEncryptionPasswordTokens[fcfg.ID] = token
|
||||
}
|
||||
dcfg := config.FolderDeviceConfiguration{DeviceID: device1}
|
||||
if tc.isEncryptedRemote {
|
||||
dcfg.EncryptionPassword = pw
|
||||
}
|
||||
|
||||
deviceInfos := &indexSenderStartInfo{
|
||||
remote: protocol.Device{ID: device1, EncryptionPasswordToken: tc.tokenRemote},
|
||||
local: protocol.Device{ID: myID, EncryptionPasswordToken: tc.tokenLocal},
|
||||
}
|
||||
err := m.ccCheckEncryption(tfcfg, dcfg, deviceInfos, false)
|
||||
if err != tc.expectedErr {
|
||||
t.Errorf("Testcase %v: Expected error %v, got %v", i, tc.expectedErr, err)
|
||||
}
|
||||
|
||||
if tc.expectedErr == nil {
|
||||
err := m.ccCheckEncryption(tfcfg, dcfg, deviceInfos, true)
|
||||
if tc.isEncryptedRemote || tc.isEncryptedLocal {
|
||||
if err != nil {
|
||||
t.Errorf("Testcase %v: Expected no error, got %v", i, err)
|
||||
}
|
||||
} else {
|
||||
if err != errEncryptionNotEncryptedUntrusted {
|
||||
t.Errorf("Testcase %v: Expected error %v, got %v", i, errEncryptionNotEncryptedUntrusted, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || (!tc.isEncryptedRemote && !tc.isEncryptedLocal) {
|
||||
continue
|
||||
}
|
||||
|
||||
if tc.isEncryptedLocal {
|
||||
m.folderEncryptionPasswordTokens[fcfg.ID] = []byte("notAMatch")
|
||||
} else {
|
||||
dcfg.EncryptionPassword = "notAMatch"
|
||||
}
|
||||
err = m.ccCheckEncryption(tfcfg, dcfg, deviceInfos, false)
|
||||
if err != errEncryptionPassword {
|
||||
t.Errorf("Testcase %v: Expected error %v, got %v", i, errEncryptionPassword, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func equalStringsInAnyOrder(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
|
||||
@@ -101,7 +101,7 @@ func TestSymlinkTraversalRead(t *testing.T) {
|
||||
<-done
|
||||
|
||||
// Request a file by traversing the symlink
|
||||
res, err := m.Request(device1, "default", "symlink/requests_test.go", 10, 0, nil, 0, false)
|
||||
res, err := m.Request(device1, "default", "symlink/requests_test.go", 0, 10, 0, nil, 0, false)
|
||||
if err == nil || res != nil {
|
||||
t.Error("Managed to traverse symlink")
|
||||
}
|
||||
@@ -499,7 +499,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
|
||||
t.Fatalf("unexpected weak hash: %d != 103547413", f.Blocks[0].WeakHash)
|
||||
}
|
||||
|
||||
res, err := m.Request(device1, "default", "foo", int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
|
||||
res, err := m.Request(device1, "default", "foo", 0, int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -513,7 +513,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
|
||||
|
||||
must(t, writeFile(tfs, "foo", payload, 0777))
|
||||
|
||||
_, err = m.Request(device1, "default", "foo", int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
|
||||
_, err = m.Request(device1, "default", "foo", 0, int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
|
||||
if err == nil {
|
||||
t.Fatalf("expected failure")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -131,7 +132,7 @@ func (s *sharedPullerState) tempFile() (*lockedWriterAt, error) {
|
||||
return s.writer, nil
|
||||
}
|
||||
|
||||
if err := inWritableDir(s.tempFileInWritableDir, s.fs, s.tempName, s.ignorePerms); err != nil {
|
||||
if err := s.addWriterLocked(); err != nil {
|
||||
s.failLocked(err)
|
||||
return nil, err
|
||||
}
|
||||
@@ -139,6 +140,10 @@ func (s *sharedPullerState) tempFile() (*lockedWriterAt, error) {
|
||||
return s.writer, nil
|
||||
}
|
||||
|
||||
func (s *sharedPullerState) addWriterLocked() error {
|
||||
return inWritableDir(s.tempFileInWritableDir, s.fs, s.tempName, s.ignorePerms)
|
||||
}
|
||||
|
||||
// tempFileInWritableDir should only be called from tempFile.
|
||||
func (s *sharedPullerState) tempFileInWritableDir(_ string) error {
|
||||
// The permissions to use for the temporary file should be those of the
|
||||
@@ -184,9 +189,14 @@ func (s *sharedPullerState) tempFileInWritableDir(_ string) error {
|
||||
// Don't truncate symlink files, as that will mean that the path will
|
||||
// contain a bunch of nulls.
|
||||
if s.sparse && !s.file.IsSymlink() {
|
||||
size := s.file.Size
|
||||
// Trailer added to encrypted files
|
||||
if len(s.file.Encrypted) > 0 {
|
||||
size += int64(s.file.ProtoSize() + 4)
|
||||
}
|
||||
// Truncate sets the size of the file. This creates a sparse file or a
|
||||
// space reservation, depending on the underlying filesystem.
|
||||
if err := fd.Truncate(s.file.Size); err != nil {
|
||||
if err := fd.Truncate(size); err != nil {
|
||||
// The truncate call failed. That can happen in some cases when
|
||||
// space reservation isn't possible or over some network
|
||||
// filesystems... This generally doesn't matter.
|
||||
@@ -305,6 +315,13 @@ func (s *sharedPullerState) finalClose() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(s.file.Encrypted) > 0 {
|
||||
if err := s.finalizeEncrypted(); err != nil && s.err == nil {
|
||||
// This is our error as we weren't errored before.
|
||||
s.err = err
|
||||
}
|
||||
}
|
||||
|
||||
if s.writer != nil {
|
||||
if err := s.writer.SyncClose(s.fsync); err != nil && s.err == nil {
|
||||
// This is our error as we weren't errored before.
|
||||
@@ -324,6 +341,34 @@ func (s *sharedPullerState) finalClose() (bool, error) {
|
||||
return true, s.err
|
||||
}
|
||||
|
||||
// finalizeEncrypted adds a trailer to the encrypted file containing the
|
||||
// serialized FileInfo and the length of that FileInfo. When initializing a
|
||||
// folder from encrypted data we can extract this FileInfo from the end of
|
||||
// the file and regain the original metadata.
|
||||
func (s *sharedPullerState) finalizeEncrypted() error {
|
||||
size := s.file.ProtoSize()
|
||||
bs := make([]byte, 4+size)
|
||||
n, err := s.file.MarshalTo(bs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
binary.BigEndian.PutUint32(bs[n:], uint32(n))
|
||||
bs = bs[:n+4]
|
||||
|
||||
if s.writer == nil {
|
||||
if err := s.addWriterLocked(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := s.writer.WriteAt(bs, s.file.Size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.file.Size += int64(len(bs))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Progress returns the momentarily progress for the puller
|
||||
func (s *sharedPullerState) Progress() *pullerProgress {
|
||||
s.mut.RLock()
|
||||
|
||||
@@ -49,6 +49,7 @@ func init() {
|
||||
defaultCfg = defaultCfgWrapper.RawCopy()
|
||||
|
||||
defaultAutoAcceptCfg = config.Configuration{
|
||||
Version: config.CurrentVersion,
|
||||
Devices: []config.DeviceConfiguration{
|
||||
{
|
||||
DeviceID: myID, // self
|
||||
|
||||
Reference in New Issue
Block a user