Don't validate requests against the database

This commit is contained in:
Jakob Borg 2015-10-13 22:59:31 +09:00
parent bd2888fc3b
commit 8435a8678e

View File

@ -39,12 +39,10 @@ import (
// How many files to send in each Index/IndexUpdate message. // How many files to send in each Index/IndexUpdate message.
const ( const (
indexTargetSize = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed) indexTargetSize = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
indexPerFileSize = 250 // Each FileInfo is approximately this big, in bytes, excluding BlockInfos indexPerFileSize = 250 // Each FileInfo is approximately this big, in bytes, excluding BlockInfos
indexPerBlockSize = 40 // Each BlockInfo is approximately this big indexPerBlockSize = 40 // Each BlockInfo is approximately this big
indexBatchSize = 1000 // Either way, don't include more files than this indexBatchSize = 1000 // Either way, don't include more files than this
reqValidationTime = time.Hour // How long to cache validation entries for Request messages
reqValidationCacheSize = 1000 // How many entries to aim for in the validation cache size
) )
type service interface { type service interface {
@ -91,9 +89,6 @@ type Model struct {
deviceVer map[protocol.DeviceID]string deviceVer map[protocol.DeviceID]string
devicePaused map[protocol.DeviceID]bool devicePaused map[protocol.DeviceID]bool
pmut sync.RWMutex // protects the above pmut sync.RWMutex // protects the above
reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist
rvmut sync.RWMutex // protects reqValidationCache
} }
var ( var (
@ -110,32 +105,30 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
l.Debugln(line) l.Debugln(line)
}, },
}), }),
cfg: cfg, cfg: cfg,
db: ldb, db: ldb,
finder: db.NewBlockFinder(ldb), finder: db.NewBlockFinder(ldb),
progressEmitter: NewProgressEmitter(cfg), progressEmitter: NewProgressEmitter(cfg),
id: id, id: id,
shortID: id.Short(), shortID: id.Short(),
cacheIgnoredFiles: cfg.Options().CacheIgnoredFiles, cacheIgnoredFiles: cfg.Options().CacheIgnoredFiles,
deviceName: deviceName, deviceName: deviceName,
clientName: clientName, clientName: clientName,
clientVersion: clientVersion, clientVersion: clientVersion,
folderCfgs: make(map[string]config.FolderConfiguration), folderCfgs: make(map[string]config.FolderConfiguration),
folderFiles: make(map[string]*db.FileSet), folderFiles: make(map[string]*db.FileSet),
folderDevices: make(map[string][]protocol.DeviceID), folderDevices: make(map[string][]protocol.DeviceID),
deviceFolders: make(map[protocol.DeviceID][]string), deviceFolders: make(map[protocol.DeviceID][]string),
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference), deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
folderIgnores: make(map[string]*ignore.Matcher), folderIgnores: make(map[string]*ignore.Matcher),
folderRunners: make(map[string]service), folderRunners: make(map[string]service),
folderStatRefs: make(map[string]*stats.FolderStatisticsReference), folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
conn: make(map[protocol.DeviceID]Connection), conn: make(map[protocol.DeviceID]Connection),
deviceVer: make(map[protocol.DeviceID]string), deviceVer: make(map[protocol.DeviceID]string),
devicePaused: make(map[protocol.DeviceID]bool), devicePaused: make(map[protocol.DeviceID]bool),
reqValidationCache: make(map[string]time.Time),
fmut: sync.NewRWMutex(), fmut: sync.NewRWMutex(),
pmut: sync.NewRWMutex(), pmut: sync.NewRWMutex(),
rvmut: sync.NewRWMutex(),
} }
if cfg.Options().ProgressUpdateIntervalS > -1 { if cfg.Options().ProgressUpdateIntervalS > -1 {
go m.progressEmitter.Serve() go m.progressEmitter.Serve()
@ -732,80 +725,48 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
return fmt.Errorf("protocol error: unknown flags 0x%x in Request message", flags) return fmt.Errorf("protocol error: unknown flags 0x%x in Request message", flags)
} }
// Verify that the requested file exists in the local model. We only need
// to validate this file if we haven't done so recently, so we keep a
// cache of successfull results. "Recently" can be quite a long time, as
// we remove validation cache entries when we detect local changes. If
// we're out of sync here and the file actually doesn't exist any more, or
// has shrunk or something, then we'll anyway get a read error that we
// pass on to the other side.
m.rvmut.RLock()
validated := m.reqValidationCache[folder+"/"+name]
m.rvmut.RUnlock()
if time.Since(validated) > reqValidationTime {
m.fmut.RLock()
folderFiles, ok := m.folderFiles[folder]
m.fmut.RUnlock()
if !ok {
l.Warnf("Request from %s for file %s in nonexistent folder %q", deviceID, name, folder)
return protocol.ErrNoSuchFile
}
// This call is really expensive for large files, as we load the full
// block list which may be megabytes and megabytes of data to allocate
// space for, read, and deserialize.
lf, ok := folderFiles.Get(protocol.LocalDeviceID, name)
if !ok {
return protocol.ErrNoSuchFile
}
if lf.IsInvalid() || lf.IsDeleted() {
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d; invalid: %v", m, deviceID, folder, name, offset, len(buf), lf)
return protocol.ErrInvalid
}
if offset > lf.Size() {
l.Debugf("%v REQ(in; nonexistent): %s: %q o=%d s=%d", m, deviceID, name, offset, len(buf))
return protocol.ErrNoSuchFile
}
m.rvmut.Lock()
m.reqValidationCache[folder+"/"+name] = time.Now()
if len(m.reqValidationCache) > reqValidationCacheSize {
// Don't let the cache grow infinitely
for name, validated := range m.reqValidationCache {
if time.Since(validated) > time.Minute {
delete(m.reqValidationCache, name)
}
}
if len(m.reqValidationCache) > reqValidationCacheSize*9/10 {
// The first clean didn't help much, we're still over 90%
// full; we may have synced a lot of files lately. Prune the
// cache more aggressively by removing every other item so we
// don't get stuck doing useless cache cleaning.
i := 0
for name := range m.reqValidationCache {
if i%2 == 0 {
delete(m.reqValidationCache, name)
}
i++
}
}
}
m.rvmut.Unlock()
}
if deviceID != protocol.LocalDeviceID { if deviceID != protocol.LocalDeviceID {
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf)) l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
} }
m.fmut.RLock() m.fmut.RLock()
fn := filepath.Join(m.folderCfgs[folder].Path(), name) folderPath := m.folderCfgs[folder].Path()
folderIgnores := m.folderIgnores[folder]
m.fmut.RUnlock() m.fmut.RUnlock()
// filepath.Join() returns a filepath.Clean()ed path, which (quoting the
// docs for clarity here):
//
// Clean returns the shortest path name equivalent to path by purely lexical
// processing. It applies the following rules iteratively until no further
// processing can be done:
//
// 1. Replace multiple Separator elements with a single one.
// 2. Eliminate each . path name element (the current directory).
// 3. Eliminate each inner .. path name element (the parent directory)
// along with the non-.. element that precedes it.
// 4. Eliminate .. elements that begin a rooted path:
// that is, replace "/.." by "/" at the beginning of a path,
// assuming Separator is '/'.
fn := filepath.Join(folderPath, name)
if !strings.HasPrefix(fn, folderPath) {
// Request tries to escape!
l.Debugf("%v Invalid REQ(in) tries to escape: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
return fmt.Errorf("invalid filename")
}
if folderIgnores != nil {
// "rn" becomes the relative name of the file within the folder. This is
// different than the original "name" parameter in that it's been
// cleaned from any possible funny business.
if rn, err := filepath.Rel(folderPath, fn); err != nil {
return err
} else if folderIgnores.Match(rn) {
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
return fmt.Errorf("invalid filename")
}
}
var reader io.ReaderAt var reader io.ReaderAt
var err error var err error
if info, err := os.Lstat(fn); err == nil && info.Mode()&os.ModeSymlink != 0 { if info, err := os.Lstat(fn); err == nil && info.Mode()&os.ModeSymlink != 0 {
@ -1121,12 +1082,6 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
m.fmut.RUnlock() m.fmut.RUnlock()
files.Update(protocol.LocalDeviceID, fs) files.Update(protocol.LocalDeviceID, fs)
m.rvmut.Lock()
for _, f := range fs {
delete(m.reqValidationCache, folder+"/"+f.Name)
}
m.rvmut.Unlock()
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{ events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
"folder": folder, "folder": folder,
"items": len(fs), "items": len(fs),