|
@@ -105,6 +105,7 @@ type Model struct {
|
|
|
|
|
|
pmut sync.RWMutex // protects the below
|
|
|
conn map[protocol.DeviceID]connections.Connection
|
|
|
+ connRequestLimiters map[protocol.DeviceID]*byteSemaphore
|
|
|
closed map[protocol.DeviceID]chan struct{}
|
|
|
helloMessages map[protocol.DeviceID]protocol.HelloResult
|
|
|
deviceDownloads map[protocol.DeviceID]*deviceDownloadState
|
|
@@ -158,6 +159,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersi
|
|
|
folderRunnerTokens: make(map[string][]suture.ServiceToken),
|
|
|
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
|
|
conn: make(map[protocol.DeviceID]connections.Connection),
|
|
|
+ connRequestLimiters: make(map[protocol.DeviceID]*byteSemaphore),
|
|
|
closed: make(map[protocol.DeviceID]chan struct{}),
|
|
|
helloMessages: make(map[protocol.DeviceID]protocol.HelloResult),
|
|
|
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
|
|
@@ -1281,6 +1283,7 @@ func (m *Model) Closed(conn protocol.Connection, err error) {
|
|
|
m.progressEmitter.temporaryIndexUnsubscribe(conn)
|
|
|
}
|
|
|
delete(m.conn, device)
|
|
|
+ delete(m.connRequestLimiters, device)
|
|
|
delete(m.helloMessages, device)
|
|
|
delete(m.deviceDownloads, device)
|
|
|
delete(m.remotePausedFolders, device)
|
|
@@ -1314,19 +1317,40 @@ func (m *Model) closeLocked(device protocol.DeviceID) {
|
|
|
closeRawConn(conn)
|
|
|
}
|
|
|
|
|
|
-// 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, offset int64, hash []byte, weakHash uint32, fromTemporary bool, buf []byte) error {
|
|
|
- if offset < 0 {
|
|
|
- return protocol.ErrInvalid
|
|
|
+// Implements protocol.RequestResponse
|
|
|
+type requestResponse struct {
|
|
|
+ data []byte
|
|
|
+ closed chan struct{}
|
|
|
+ once stdsync.Once
|
|
|
+}
|
|
|
+
|
|
|
+func newRequestResponse(size int) *requestResponse {
|
|
|
+ return &requestResponse{
|
|
|
+ data: protocol.BufferPool.Get(size),
|
|
|
+ closed: make(chan struct{}),
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- if cfg, ok := m.cfg.Folder(folder); !ok || !cfg.SharedWith(deviceID) {
|
|
|
- l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder)
|
|
|
- return protocol.ErrNoSuchFile
|
|
|
- } else if cfg.Paused {
|
|
|
- l.Debugf("Request from %s for file %s in paused folder %q", deviceID, name, folder)
|
|
|
- return protocol.ErrInvalid
|
|
|
+func (r *requestResponse) Data() []byte {
|
|
|
+ return r.data
|
|
|
+}
|
|
|
+
|
|
|
+func (r *requestResponse) Close() {
|
|
|
+ r.once.Do(func() {
|
|
|
+ protocol.BufferPool.Put(r.data)
|
|
|
+ close(r.closed)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func (r *requestResponse) Wait() {
|
|
|
+ <-r.closed
|
|
|
+}
|
|
|
+
|
|
|
+// 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) {
|
|
|
+ if size < 0 || offset < 0 {
|
|
|
+ return nil, protocol.ErrInvalid
|
|
|
}
|
|
|
|
|
|
m.fmut.RLock()
|
|
@@ -1337,35 +1361,69 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
|
|
// The folder might be already unpaused in the config, but not yet
|
|
|
// in the model.
|
|
|
l.Debugf("Request from %s for file %s in unstarted folder %q", deviceID, name, folder)
|
|
|
- return protocol.ErrInvalid
|
|
|
+ return nil, protocol.ErrInvalid
|
|
|
+ }
|
|
|
+
|
|
|
+ if !folderCfg.SharedWith(deviceID) {
|
|
|
+ l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder)
|
|
|
+ return nil, protocol.ErrNoSuchFile
|
|
|
+ }
|
|
|
+ if folderCfg.Paused {
|
|
|
+ l.Debugf("Request from %s for file %s in paused folder %q", deviceID, name, folder)
|
|
|
+ return nil, protocol.ErrInvalid
|
|
|
}
|
|
|
|
|
|
// Make sure the path is valid and in canonical form
|
|
|
- var err error
|
|
|
if name, err = fs.Canonicalize(name); err != nil {
|
|
|
l.Debugf("Request from %s in folder %q for invalid filename %s", deviceID, folder, name)
|
|
|
- return protocol.ErrInvalid
|
|
|
+ return nil, protocol.ErrInvalid
|
|
|
}
|
|
|
|
|
|
if deviceID != protocol.LocalDeviceID {
|
|
|
- l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d t=%v", m, deviceID, folder, name, offset, len(buf), fromTemporary)
|
|
|
+ l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d t=%v", m, deviceID, folder, name, offset, size, fromTemporary)
|
|
|
}
|
|
|
|
|
|
- folderFs := folderCfg.Filesystem()
|
|
|
-
|
|
|
if fs.IsInternal(name) {
|
|
|
- l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
|
|
- return protocol.ErrNoSuchFile
|
|
|
+ l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
|
|
|
+ return nil, protocol.ErrNoSuchFile
|
|
|
}
|
|
|
|
|
|
if folderIgnores.Match(name).IsIgnored() {
|
|
|
- l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
|
|
- return protocol.ErrNoSuchFile
|
|
|
+ l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
|
|
|
+ return nil, protocol.ErrNoSuchFile
|
|
|
}
|
|
|
|
|
|
+ folderFs := folderCfg.Filesystem()
|
|
|
+
|
|
|
if err := osutil.TraversesSymlink(folderFs, filepath.Dir(name)); err != nil {
|
|
|
- l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, len(buf))
|
|
|
- return protocol.ErrNoSuchFile
|
|
|
+ l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
|
|
+ return nil, protocol.ErrNoSuchFile
|
|
|
+ }
|
|
|
+
|
|
|
+ // Restrict parallel requests by connection/device
|
|
|
+
|
|
|
+ m.pmut.RLock()
|
|
|
+ limiter := m.connRequestLimiters[deviceID]
|
|
|
+ m.pmut.RUnlock()
|
|
|
+
|
|
|
+ if limiter != nil {
|
|
|
+ limiter.take(int(size))
|
|
|
+ }
|
|
|
+
|
|
|
+ // The requestResponse releases the bytes to the limiter when its Close method is called.
|
|
|
+ res := newRequestResponse(int(size))
|
|
|
+ defer func() {
|
|
|
+ // Close it ourselves if it isn't returned due to an error
|
|
|
+ if err != nil {
|
|
|
+ res.Close()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ if limiter != nil {
|
|
|
+ go func() {
|
|
|
+ res.Wait()
|
|
|
+ limiter.give(int(size))
|
|
|
+ }()
|
|
|
}
|
|
|
|
|
|
// Only check temp files if the flag is set, and if we are set to advertise
|
|
@@ -1376,11 +1434,12 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
|
|
if info, err := folderFs.Lstat(tempFn); err != nil || !info.IsRegular() {
|
|
|
// Reject reads for anything that doesn't exist or is something
|
|
|
// other than a regular file.
|
|
|
- return protocol.ErrNoSuchFile
|
|
|
+ l.Debugf("%v REQ(in) failed stating temp file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
|
|
+ return nil, protocol.ErrNoSuchFile
|
|
|
}
|
|
|
- err := readOffsetIntoBuf(folderFs, tempFn, offset, buf)
|
|
|
- if err == nil && scanner.Validate(buf, hash, weakHash) {
|
|
|
- return nil
|
|
|
+ err := readOffsetIntoBuf(folderFs, tempFn, offset, res.data)
|
|
|
+ if err == nil && scanner.Validate(res.data, hash, weakHash) {
|
|
|
+ return res, nil
|
|
|
}
|
|
|
// Fall through to reading from a non-temp file, just incase the temp
|
|
|
// file has finished downloading.
|
|
@@ -1389,21 +1448,25 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
|
|
if info, err := folderFs.Lstat(name); err != nil || !info.IsRegular() {
|
|
|
// Reject reads for anything that doesn't exist or is something
|
|
|
// other than a regular file.
|
|
|
- return protocol.ErrNoSuchFile
|
|
|
+ l.Debugf("%v REQ(in) failed stating file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
|
|
+ return nil, protocol.ErrNoSuchFile
|
|
|
}
|
|
|
|
|
|
- if err = readOffsetIntoBuf(folderFs, name, offset, buf); fs.IsNotExist(err) {
|
|
|
- return protocol.ErrNoSuchFile
|
|
|
+ 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 != nil {
|
|
|
- return protocol.ErrGeneric
|
|
|
+ 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(buf, hash, weakHash) {
|
|
|
- m.recheckFile(deviceID, folderFs, folder, name, int(offset)/len(buf), hash)
|
|
|
- return protocol.ErrNoSuchFile
|
|
|
+ if !scanner.Validate(res.data, hash, weakHash) {
|
|
|
+ m.recheckFile(deviceID, folderFs, folder, name, int(offset)/int(size), hash)
|
|
|
+ l.Debugf("%v REQ(in) failed validating data (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
|
|
+ return nil, protocol.ErrNoSuchFile
|
|
|
}
|
|
|
|
|
|
- return nil
|
|
|
+ return res, nil
|
|
|
}
|
|
|
|
|
|
func (m *Model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem, folder, name string, blockIndex int, hash []byte) {
|
|
@@ -1598,6 +1661,11 @@ func (m *Model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
|
|
|
// folder changes.
|
|
|
func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloResult) {
|
|
|
deviceID := conn.ID()
|
|
|
+ device, ok := m.cfg.Device(deviceID)
|
|
|
+ if !ok {
|
|
|
+ l.Infoln("Trying to add connection to unknown device")
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
m.pmut.Lock()
|
|
|
if oldConn, ok := m.conn[deviceID]; ok {
|
|
@@ -1617,6 +1685,13 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
|
|
|
m.conn[deviceID] = conn
|
|
|
m.closed[deviceID] = make(chan struct{})
|
|
|
m.deviceDownloads[deviceID] = newDeviceDownloadState()
|
|
|
+ // 0: default, <0: no limiting
|
|
|
+ switch {
|
|
|
+ case device.MaxRequestKiB > 0:
|
|
|
+ m.connRequestLimiters[deviceID] = newByteSemaphore(1024 * device.MaxRequestKiB)
|
|
|
+ case device.MaxRequestKiB == 0:
|
|
|
+ m.connRequestLimiters[deviceID] = newByteSemaphore(1024 * defaultPullerPendingKiB)
|
|
|
+ }
|
|
|
|
|
|
m.helloMessages[deviceID] = hello
|
|
|
|
|
@@ -1644,8 +1719,7 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
|
|
|
cm := m.generateClusterConfig(deviceID)
|
|
|
conn.ClusterConfig(cm)
|
|
|
|
|
|
- device, ok := m.cfg.Devices()[deviceID]
|
|
|
- if ok && (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
|
|
|
+ if (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
|
|
|
device.Name = hello.DeviceName
|
|
|
m.cfg.SetDevice(device)
|
|
|
m.cfg.Save()
|