common.go 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  1. // Package common defines code shared among file transfer packages and protocols
  2. package common
  3. import (
  4. "context"
  5. "errors"
  6. "fmt"
  7. "net"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "os/exec"
  12. "path/filepath"
  13. "strconv"
  14. "strings"
  15. "sync"
  16. "sync/atomic"
  17. "time"
  18. "github.com/pires/go-proxyproto"
  19. "github.com/drakkan/sftpgo/v2/dataprovider"
  20. "github.com/drakkan/sftpgo/v2/httpclient"
  21. "github.com/drakkan/sftpgo/v2/logger"
  22. "github.com/drakkan/sftpgo/v2/metric"
  23. "github.com/drakkan/sftpgo/v2/util"
  24. "github.com/drakkan/sftpgo/v2/vfs"
  25. )
  26. // constants
  27. const (
  28. logSender = "common"
  29. uploadLogSender = "Upload"
  30. downloadLogSender = "Download"
  31. renameLogSender = "Rename"
  32. rmdirLogSender = "Rmdir"
  33. mkdirLogSender = "Mkdir"
  34. symlinkLogSender = "Symlink"
  35. removeLogSender = "Remove"
  36. chownLogSender = "Chown"
  37. chmodLogSender = "Chmod"
  38. chtimesLogSender = "Chtimes"
  39. truncateLogSender = "Truncate"
  40. operationDownload = "download"
  41. operationUpload = "upload"
  42. operationDelete = "delete"
  43. // Pre-download action name
  44. OperationPreDownload = "pre-download"
  45. // Pre-upload action name
  46. OperationPreUpload = "pre-upload"
  47. operationPreDelete = "pre-delete"
  48. operationRename = "rename"
  49. operationMkdir = "mkdir"
  50. operationRmdir = "rmdir"
  51. // SSH command action name
  52. OperationSSHCmd = "ssh_cmd"
  53. chtimesFormat = "2006-01-02T15:04:05" // YYYY-MM-DDTHH:MM:SS
  54. idleTimeoutCheckInterval = 3 * time.Minute
  55. periodicTimeoutCheckInterval = 1 * time.Minute
  56. )
  57. // Stat flags
  58. const (
  59. StatAttrUIDGID = 1
  60. StatAttrPerms = 2
  61. StatAttrTimes = 4
  62. StatAttrSize = 8
  63. )
  64. // Transfer types
  65. const (
  66. TransferUpload = iota
  67. TransferDownload
  68. )
  69. // Supported protocols
  70. const (
  71. ProtocolSFTP = "SFTP"
  72. ProtocolSCP = "SCP"
  73. ProtocolSSH = "SSH"
  74. ProtocolFTP = "FTP"
  75. ProtocolWebDAV = "DAV"
  76. ProtocolHTTP = "HTTP"
  77. ProtocolHTTPShare = "HTTPShare"
  78. ProtocolDataRetention = "DataRetention"
  79. ProtocolOIDC = "OIDC"
  80. )
  81. // Upload modes
  82. const (
  83. UploadModeStandard = iota
  84. UploadModeAtomic
  85. UploadModeAtomicWithResume
  86. )
  87. func init() {
  88. Connections.clients = clientsMap{
  89. clients: make(map[string]int),
  90. }
  91. }
  92. // errors definitions
  93. var (
  94. ErrPermissionDenied = errors.New("permission denied")
  95. ErrNotExist = errors.New("no such file or directory")
  96. ErrOpUnsupported = errors.New("operation unsupported")
  97. ErrGenericFailure = errors.New("failure")
  98. ErrQuotaExceeded = errors.New("denying write due to space limit")
  99. ErrReadQuotaExceeded = errors.New("denying read due to quota limit")
  100. ErrSkipPermissionsCheck = errors.New("permission check skipped")
  101. ErrConnectionDenied = errors.New("you are not allowed to connect")
  102. ErrNoBinding = errors.New("no binding configured")
  103. ErrCrtRevoked = errors.New("your certificate has been revoked")
  104. ErrNoCredentials = errors.New("no credential provided")
  105. ErrInternalFailure = errors.New("internal failure")
  106. ErrTransferAborted = errors.New("transfer aborted")
  107. errNoTransfer = errors.New("requested transfer not found")
  108. errTransferMismatch = errors.New("transfer mismatch")
  109. )
  110. var (
  111. // Config is the configuration for the supported protocols
  112. Config Configuration
  113. // Connections is the list of active connections
  114. Connections ActiveConnections
  115. // QuotaScans is the list of active quota scans
  116. QuotaScans ActiveScans
  117. transfersChecker TransfersChecker
  118. periodicTimeoutTicker *time.Ticker
  119. periodicTimeoutTickerDone chan bool
  120. supportedProtocols = []string{ProtocolSFTP, ProtocolSCP, ProtocolSSH, ProtocolFTP, ProtocolWebDAV,
  121. ProtocolHTTP, ProtocolHTTPShare, ProtocolOIDC}
  122. disconnHookProtocols = []string{ProtocolSFTP, ProtocolSCP, ProtocolSSH, ProtocolFTP}
  123. // the map key is the protocol, for each protocol we can have multiple rate limiters
  124. rateLimiters map[string][]*rateLimiter
  125. )
  126. // Initialize sets the common configuration
  127. func Initialize(c Configuration, isShared int) error {
  128. Config = c
  129. Config.idleLoginTimeout = 2 * time.Minute
  130. Config.idleTimeoutAsDuration = time.Duration(Config.IdleTimeout) * time.Minute
  131. startPeriodicTimeoutTicker(periodicTimeoutCheckInterval)
  132. Config.defender = nil
  133. rateLimiters = make(map[string][]*rateLimiter)
  134. for _, rlCfg := range c.RateLimitersConfig {
  135. if rlCfg.isEnabled() {
  136. if err := rlCfg.validate(); err != nil {
  137. return fmt.Errorf("rate limiters initialization error: %v", err)
  138. }
  139. allowList, err := util.ParseAllowedIPAndRanges(rlCfg.AllowList)
  140. if err != nil {
  141. return fmt.Errorf("unable to parse rate limiter allow list %v: %v", rlCfg.AllowList, err)
  142. }
  143. rateLimiter := rlCfg.getLimiter()
  144. rateLimiter.allowList = allowList
  145. for _, protocol := range rlCfg.Protocols {
  146. rateLimiters[protocol] = append(rateLimiters[protocol], rateLimiter)
  147. }
  148. }
  149. }
  150. if c.DefenderConfig.Enabled {
  151. if !util.IsStringInSlice(c.DefenderConfig.Driver, supportedDefenderDrivers) {
  152. return fmt.Errorf("unsupported defender driver %#v", c.DefenderConfig.Driver)
  153. }
  154. var defender Defender
  155. var err error
  156. switch c.DefenderConfig.Driver {
  157. case DefenderDriverProvider:
  158. defender, err = newDBDefender(&c.DefenderConfig)
  159. default:
  160. defender, err = newInMemoryDefender(&c.DefenderConfig)
  161. }
  162. if err != nil {
  163. return fmt.Errorf("defender initialization error: %v", err)
  164. }
  165. logger.Info(logSender, "", "defender initialized with config %+v", c.DefenderConfig)
  166. Config.defender = defender
  167. }
  168. vfs.SetTempPath(c.TempPath)
  169. dataprovider.SetTempPath(c.TempPath)
  170. transfersChecker = getTransfersChecker(isShared)
  171. return nil
  172. }
  173. // LimitRate blocks until all the configured rate limiters
  174. // allow one event to happen.
  175. // It returns an error if the time to wait exceeds the max
  176. // allowed delay
  177. func LimitRate(protocol, ip string) (time.Duration, error) {
  178. for _, limiter := range rateLimiters[protocol] {
  179. if delay, err := limiter.Wait(ip); err != nil {
  180. logger.Debug(logSender, "", "protocol %v ip %v: %v", protocol, ip, err)
  181. return delay, err
  182. }
  183. }
  184. return 0, nil
  185. }
  186. // ReloadDefender reloads the defender's block and safe lists
  187. func ReloadDefender() error {
  188. if Config.defender == nil {
  189. return nil
  190. }
  191. return Config.defender.Reload()
  192. }
  193. // IsBanned returns true if the specified IP address is banned
  194. func IsBanned(ip string) bool {
  195. if Config.defender == nil {
  196. return false
  197. }
  198. return Config.defender.IsBanned(ip)
  199. }
  200. // GetDefenderBanTime returns the ban time for the given IP
  201. // or nil if the IP is not banned or the defender is disabled
  202. func GetDefenderBanTime(ip string) (*time.Time, error) {
  203. if Config.defender == nil {
  204. return nil, nil
  205. }
  206. return Config.defender.GetBanTime(ip)
  207. }
  208. // GetDefenderHosts returns hosts that are banned or for which some violations have been detected
  209. func GetDefenderHosts() ([]dataprovider.DefenderEntry, error) {
  210. if Config.defender == nil {
  211. return nil, nil
  212. }
  213. return Config.defender.GetHosts()
  214. }
  215. // GetDefenderHost returns a defender host by ip, if any
  216. func GetDefenderHost(ip string) (dataprovider.DefenderEntry, error) {
  217. if Config.defender == nil {
  218. return dataprovider.DefenderEntry{}, errors.New("defender is disabled")
  219. }
  220. return Config.defender.GetHost(ip)
  221. }
  222. // DeleteDefenderHost removes the specified IP address from the defender lists
  223. func DeleteDefenderHost(ip string) bool {
  224. if Config.defender == nil {
  225. return false
  226. }
  227. return Config.defender.DeleteHost(ip)
  228. }
  229. // GetDefenderScore returns the score for the given IP
  230. func GetDefenderScore(ip string) (int, error) {
  231. if Config.defender == nil {
  232. return 0, nil
  233. }
  234. return Config.defender.GetScore(ip)
  235. }
  236. // AddDefenderEvent adds the specified defender event for the given IP
  237. func AddDefenderEvent(ip string, event HostEvent) {
  238. if Config.defender == nil {
  239. return
  240. }
  241. Config.defender.AddEvent(ip, event)
  242. }
  243. // the ticker cannot be started/stopped from multiple goroutines
  244. func startPeriodicTimeoutTicker(duration time.Duration) {
  245. stopPeriodicTimeoutTicker()
  246. periodicTimeoutTicker = time.NewTicker(duration)
  247. periodicTimeoutTickerDone = make(chan bool)
  248. go func() {
  249. counter := int64(0)
  250. ratio := idleTimeoutCheckInterval / periodicTimeoutCheckInterval
  251. for {
  252. select {
  253. case <-periodicTimeoutTickerDone:
  254. return
  255. case <-periodicTimeoutTicker.C:
  256. counter++
  257. if Config.IdleTimeout > 0 && counter >= int64(ratio) {
  258. counter = 0
  259. Connections.checkIdles()
  260. }
  261. go Connections.checkTransfers()
  262. }
  263. }
  264. }()
  265. }
  266. func stopPeriodicTimeoutTicker() {
  267. if periodicTimeoutTicker != nil {
  268. periodicTimeoutTicker.Stop()
  269. periodicTimeoutTickerDone <- true
  270. periodicTimeoutTicker = nil
  271. }
  272. }
  273. // ActiveTransfer defines the interface for the current active transfers
  274. type ActiveTransfer interface {
  275. GetID() int64
  276. GetType() int
  277. GetSize() int64
  278. GetDownloadedSize() int64
  279. GetUploadedSize() int64
  280. GetVirtualPath() string
  281. GetStartTime() time.Time
  282. SignalClose(err error)
  283. Truncate(fsPath string, size int64) (int64, error)
  284. GetRealFsPath(fsPath string) string
  285. SetTimes(fsPath string, atime time.Time, mtime time.Time) bool
  286. GetTruncatedSize() int64
  287. HasSizeLimit() bool
  288. }
  289. // ActiveConnection defines the interface for the current active connections
  290. type ActiveConnection interface {
  291. GetID() string
  292. GetUsername() string
  293. GetLocalAddress() string
  294. GetRemoteAddress() string
  295. GetClientVersion() string
  296. GetProtocol() string
  297. GetConnectionTime() time.Time
  298. GetLastActivity() time.Time
  299. GetCommand() string
  300. Disconnect() error
  301. AddTransfer(t ActiveTransfer)
  302. RemoveTransfer(t ActiveTransfer)
  303. GetTransfers() []ConnectionTransfer
  304. SignalTransferClose(transferID int64, err error)
  305. CloseFS() error
  306. }
  307. // StatAttributes defines the attributes for set stat commands
  308. type StatAttributes struct {
  309. Mode os.FileMode
  310. Atime time.Time
  311. Mtime time.Time
  312. UID int
  313. GID int
  314. Flags int
  315. Size int64
  316. }
  317. // ConnectionTransfer defines the trasfer details to expose
  318. type ConnectionTransfer struct {
  319. ID int64 `json:"-"`
  320. OperationType string `json:"operation_type"`
  321. StartTime int64 `json:"start_time"`
  322. Size int64 `json:"size"`
  323. VirtualPath string `json:"path"`
  324. HasSizeLimit bool `json:"-"`
  325. ULSize int64 `json:"-"`
  326. DLSize int64 `json:"-"`
  327. }
  328. func (t *ConnectionTransfer) getConnectionTransferAsString() string {
  329. result := ""
  330. switch t.OperationType {
  331. case operationUpload:
  332. result += "UL "
  333. case operationDownload:
  334. result += "DL "
  335. }
  336. result += fmt.Sprintf("%#v ", t.VirtualPath)
  337. if t.Size > 0 {
  338. elapsed := time.Since(util.GetTimeFromMsecSinceEpoch(t.StartTime))
  339. speed := float64(t.Size) / float64(util.GetTimeAsMsSinceEpoch(time.Now())-t.StartTime)
  340. result += fmt.Sprintf("Size: %#v Elapsed: %#v Speed: \"%.1f KB/s\"", util.ByteCountIEC(t.Size),
  341. util.GetDurationAsString(elapsed), speed)
  342. }
  343. return result
  344. }
  345. // Configuration defines configuration parameters common to all supported protocols
  346. type Configuration struct {
  347. // Maximum idle timeout as minutes. If a client is idle for a time that exceeds this setting it will be disconnected.
  348. // 0 means disabled
  349. IdleTimeout int `json:"idle_timeout" mapstructure:"idle_timeout"`
  350. // UploadMode 0 means standard, the files are uploaded directly to the requested path.
  351. // 1 means atomic: the files are uploaded to a temporary path and renamed to the requested path
  352. // when the client ends the upload. Atomic mode avoid problems such as a web server that
  353. // serves partial files when the files are being uploaded.
  354. // In atomic mode if there is an upload error the temporary file is deleted and so the requested
  355. // upload path will not contain a partial file.
  356. // 2 means atomic with resume support: as atomic but if there is an upload error the temporary
  357. // file is renamed to the requested path and not deleted, this way a client can reconnect and resume
  358. // the upload.
  359. UploadMode int `json:"upload_mode" mapstructure:"upload_mode"`
  360. // Actions to execute for SFTP file operations and SSH commands
  361. Actions ProtocolActions `json:"actions" mapstructure:"actions"`
  362. // SetstatMode 0 means "normal mode": requests for changing permissions and owner/group are executed.
  363. // 1 means "ignore mode": requests for changing permissions and owner/group are silently ignored.
  364. // 2 means "ignore mode for cloud fs": requests for changing permissions and owner/group are
  365. // silently ignored for cloud based filesystem such as S3, GCS, Azure Blob. Requests for changing
  366. // modification times are ignored for cloud based filesystem if they are not supported.
  367. SetstatMode int `json:"setstat_mode" mapstructure:"setstat_mode"`
  368. // TempPath defines the path for temporary files such as those used for atomic uploads or file pipes.
  369. // If you set this option you must make sure that the defined path exists, is accessible for writing
  370. // by the user running SFTPGo, and is on the same filesystem as the users home directories otherwise
  371. // the renaming for atomic uploads will become a copy and therefore may take a long time.
  372. // The temporary files are not namespaced. The default is generally fine. Leave empty for the default.
  373. TempPath string `json:"temp_path" mapstructure:"temp_path"`
  374. // Support for HAProxy PROXY protocol.
  375. // If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable
  376. // the proxy protocol. It provides a convenient way to safely transport connection information
  377. // such as a client's address across multiple layers of NAT or TCP proxies to get the real
  378. // client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported.
  379. // - 0 means disabled
  380. // - 1 means proxy protocol enabled. Proxy header will be used and requests without proxy header will be accepted.
  381. // - 2 means proxy protocol required. Proxy header will be used and requests without proxy header will be rejected.
  382. // If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too,
  383. // for example for HAProxy add "send-proxy" or "send-proxy-v2" to each server configuration line.
  384. ProxyProtocol int `json:"proxy_protocol" mapstructure:"proxy_protocol"`
  385. // List of IP addresses and IP ranges allowed to send the proxy header.
  386. // If proxy protocol is set to 1 and we receive a proxy header from an IP that is not in the list then the
  387. // connection will be accepted and the header will be ignored.
  388. // If proxy protocol is set to 2 and we receive a proxy header from an IP that is not in the list then the
  389. // connection will be rejected.
  390. ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
  391. // Absolute path to an external program or an HTTP URL to invoke as soon as SFTPGo starts.
  392. // If you define an HTTP URL it will be invoked using a `GET` request.
  393. // Please note that SFTPGo services may not yet be available when this hook is run.
  394. // Leave empty do disable.
  395. StartupHook string `json:"startup_hook" mapstructure:"startup_hook"`
  396. // Absolute path to an external program or an HTTP URL to invoke after a user connects
  397. // and before he tries to login. It allows you to reject the connection based on the source
  398. // ip address. Leave empty do disable.
  399. PostConnectHook string `json:"post_connect_hook" mapstructure:"post_connect_hook"`
  400. // Absolute path to an external program or an HTTP URL to invoke after an SSH/FTP connection ends.
  401. // Leave empty do disable.
  402. PostDisconnectHook string `json:"post_disconnect_hook" mapstructure:"post_disconnect_hook"`
  403. // Absolute path to an external program or an HTTP URL to invoke after a data retention check completes.
  404. // Leave empty do disable.
  405. DataRetentionHook string `json:"data_retention_hook" mapstructure:"data_retention_hook"`
  406. // Maximum number of concurrent client connections. 0 means unlimited
  407. MaxTotalConnections int `json:"max_total_connections" mapstructure:"max_total_connections"`
  408. // Maximum number of concurrent client connections from the same host (IP). 0 means unlimited
  409. MaxPerHostConnections int `json:"max_per_host_connections" mapstructure:"max_per_host_connections"`
  410. // Defender configuration
  411. DefenderConfig DefenderConfig `json:"defender" mapstructure:"defender"`
  412. // Rate limiter configurations
  413. RateLimitersConfig []RateLimiterConfig `json:"rate_limiters" mapstructure:"rate_limiters"`
  414. idleTimeoutAsDuration time.Duration
  415. idleLoginTimeout time.Duration
  416. defender Defender
  417. }
  418. // IsAtomicUploadEnabled returns true if atomic upload is enabled
  419. func (c *Configuration) IsAtomicUploadEnabled() bool {
  420. return c.UploadMode == UploadModeAtomic || c.UploadMode == UploadModeAtomicWithResume
  421. }
  422. // GetProxyListener returns a wrapper for the given listener that supports the
  423. // HAProxy Proxy Protocol
  424. func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Listener, error) {
  425. var err error
  426. if c.ProxyProtocol > 0 {
  427. var policyFunc func(upstream net.Addr) (proxyproto.Policy, error)
  428. if c.ProxyProtocol == 1 && len(c.ProxyAllowed) > 0 {
  429. policyFunc, err = proxyproto.LaxWhiteListPolicy(c.ProxyAllowed)
  430. if err != nil {
  431. return nil, err
  432. }
  433. }
  434. if c.ProxyProtocol == 2 {
  435. if len(c.ProxyAllowed) == 0 {
  436. policyFunc = func(upstream net.Addr) (proxyproto.Policy, error) {
  437. return proxyproto.REQUIRE, nil
  438. }
  439. } else {
  440. policyFunc, err = proxyproto.StrictWhiteListPolicy(c.ProxyAllowed)
  441. if err != nil {
  442. return nil, err
  443. }
  444. }
  445. }
  446. return &proxyproto.Listener{
  447. Listener: listener,
  448. Policy: policyFunc,
  449. ReadHeaderTimeout: 5 * time.Second,
  450. }, nil
  451. }
  452. return nil, errors.New("proxy protocol not configured")
  453. }
  454. // ExecuteStartupHook runs the startup hook if defined
  455. func (c *Configuration) ExecuteStartupHook() error {
  456. if c.StartupHook == "" {
  457. return nil
  458. }
  459. if strings.HasPrefix(c.StartupHook, "http") {
  460. var url *url.URL
  461. url, err := url.Parse(c.StartupHook)
  462. if err != nil {
  463. logger.Warn(logSender, "", "Invalid startup hook %#v: %v", c.StartupHook, err)
  464. return err
  465. }
  466. startTime := time.Now()
  467. resp, err := httpclient.RetryableGet(url.String())
  468. if err != nil {
  469. logger.Warn(logSender, "", "Error executing startup hook: %v", err)
  470. return err
  471. }
  472. defer resp.Body.Close()
  473. logger.Debug(logSender, "", "Startup hook executed, elapsed: %v, response code: %v", time.Since(startTime), resp.StatusCode)
  474. return nil
  475. }
  476. if !filepath.IsAbs(c.StartupHook) {
  477. err := fmt.Errorf("invalid startup hook %#v", c.StartupHook)
  478. logger.Warn(logSender, "", "Invalid startup hook %#v", c.StartupHook)
  479. return err
  480. }
  481. startTime := time.Now()
  482. ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
  483. defer cancel()
  484. cmd := exec.CommandContext(ctx, c.StartupHook)
  485. err := cmd.Run()
  486. logger.Debug(logSender, "", "Startup hook executed, elapsed: %v, error: %v", time.Since(startTime), err)
  487. return nil
  488. }
  489. func (c *Configuration) executePostDisconnectHook(remoteAddr, protocol, username, connID string, connectionTime time.Time) {
  490. ipAddr := util.GetIPFromRemoteAddress(remoteAddr)
  491. connDuration := int64(time.Since(connectionTime) / time.Millisecond)
  492. if strings.HasPrefix(c.PostDisconnectHook, "http") {
  493. var url *url.URL
  494. url, err := url.Parse(c.PostDisconnectHook)
  495. if err != nil {
  496. logger.Warn(protocol, connID, "Invalid post disconnect hook %#v: %v", c.PostDisconnectHook, err)
  497. return
  498. }
  499. q := url.Query()
  500. q.Add("ip", ipAddr)
  501. q.Add("protocol", protocol)
  502. q.Add("username", username)
  503. q.Add("connection_duration", strconv.FormatInt(connDuration, 10))
  504. url.RawQuery = q.Encode()
  505. startTime := time.Now()
  506. resp, err := httpclient.RetryableGet(url.String())
  507. respCode := 0
  508. if err == nil {
  509. respCode = resp.StatusCode
  510. resp.Body.Close()
  511. }
  512. logger.Debug(protocol, connID, "Post disconnect hook response code: %v, elapsed: %v, err: %v",
  513. respCode, time.Since(startTime), err)
  514. return
  515. }
  516. if !filepath.IsAbs(c.PostDisconnectHook) {
  517. logger.Debug(protocol, connID, "invalid post disconnect hook %#v", c.PostDisconnectHook)
  518. return
  519. }
  520. ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
  521. defer cancel()
  522. startTime := time.Now()
  523. cmd := exec.CommandContext(ctx, c.PostDisconnectHook)
  524. cmd.Env = append(os.Environ(),
  525. fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr),
  526. fmt.Sprintf("SFTPGO_CONNECTION_USERNAME=%v", username),
  527. fmt.Sprintf("SFTPGO_CONNECTION_DURATION=%v", connDuration),
  528. fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol))
  529. err := cmd.Run()
  530. logger.Debug(protocol, connID, "Post disconnect hook executed, elapsed: %v error: %v", time.Since(startTime), err)
  531. }
  532. func (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username, connID string, connectionTime time.Time) {
  533. if c.PostDisconnectHook == "" {
  534. return
  535. }
  536. if !util.IsStringInSlice(protocol, disconnHookProtocols) {
  537. return
  538. }
  539. go c.executePostDisconnectHook(remoteAddr, protocol, username, connID, connectionTime)
  540. }
  541. // ExecutePostConnectHook executes the post connect hook if defined
  542. func (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error {
  543. if c.PostConnectHook == "" {
  544. return nil
  545. }
  546. if strings.HasPrefix(c.PostConnectHook, "http") {
  547. var url *url.URL
  548. url, err := url.Parse(c.PostConnectHook)
  549. if err != nil {
  550. logger.Warn(protocol, "", "Login from ip %#v denied, invalid post connect hook %#v: %v",
  551. ipAddr, c.PostConnectHook, err)
  552. return err
  553. }
  554. q := url.Query()
  555. q.Add("ip", ipAddr)
  556. q.Add("protocol", protocol)
  557. url.RawQuery = q.Encode()
  558. resp, err := httpclient.RetryableGet(url.String())
  559. if err != nil {
  560. logger.Warn(protocol, "", "Login from ip %#v denied, error executing post connect hook: %v", ipAddr, err)
  561. return err
  562. }
  563. defer resp.Body.Close()
  564. if resp.StatusCode != http.StatusOK {
  565. logger.Warn(protocol, "", "Login from ip %#v denied, post connect hook response code: %v", ipAddr, resp.StatusCode)
  566. return errUnexpectedHTTResponse
  567. }
  568. return nil
  569. }
  570. if !filepath.IsAbs(c.PostConnectHook) {
  571. err := fmt.Errorf("invalid post connect hook %#v", c.PostConnectHook)
  572. logger.Warn(protocol, "", "Login from ip %#v denied: %v", ipAddr, err)
  573. return err
  574. }
  575. ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
  576. defer cancel()
  577. cmd := exec.CommandContext(ctx, c.PostConnectHook)
  578. cmd.Env = append(os.Environ(),
  579. fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr),
  580. fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol))
  581. err := cmd.Run()
  582. if err != nil {
  583. logger.Warn(protocol, "", "Login from ip %#v denied, connect hook error: %v", ipAddr, err)
  584. }
  585. return err
  586. }
  587. // SSHConnection defines an ssh connection.
  588. // Each SSH connection can open several channels for SFTP or SSH commands
  589. type SSHConnection struct {
  590. id string
  591. conn net.Conn
  592. lastActivity int64
  593. }
  594. // NewSSHConnection returns a new SSHConnection
  595. func NewSSHConnection(id string, conn net.Conn) *SSHConnection {
  596. return &SSHConnection{
  597. id: id,
  598. conn: conn,
  599. lastActivity: time.Now().UnixNano(),
  600. }
  601. }
  602. // GetID returns the ID for this SSHConnection
  603. func (c *SSHConnection) GetID() string {
  604. return c.id
  605. }
  606. // UpdateLastActivity updates last activity for this connection
  607. func (c *SSHConnection) UpdateLastActivity() {
  608. atomic.StoreInt64(&c.lastActivity, time.Now().UnixNano())
  609. }
  610. // GetLastActivity returns the last connection activity
  611. func (c *SSHConnection) GetLastActivity() time.Time {
  612. return time.Unix(0, atomic.LoadInt64(&c.lastActivity))
  613. }
  614. // Close closes the underlying network connection
  615. func (c *SSHConnection) Close() error {
  616. return c.conn.Close()
  617. }
  618. // ActiveConnections holds the currect active connections with the associated transfers
  619. type ActiveConnections struct {
  620. // clients contains both authenticated and estabilished connections and the ones waiting
  621. // for authentication
  622. clients clientsMap
  623. transfersCheckStatus int32
  624. sync.RWMutex
  625. connections []ActiveConnection
  626. sshConnections []*SSHConnection
  627. }
  628. // GetActiveSessions returns the number of active sessions for the given username.
  629. // We return the open sessions for any protocol
  630. func (conns *ActiveConnections) GetActiveSessions(username string) int {
  631. conns.RLock()
  632. defer conns.RUnlock()
  633. numSessions := 0
  634. for _, c := range conns.connections {
  635. if c.GetUsername() == username {
  636. numSessions++
  637. }
  638. }
  639. return numSessions
  640. }
  641. // Add adds a new connection to the active ones
  642. func (conns *ActiveConnections) Add(c ActiveConnection) {
  643. conns.Lock()
  644. defer conns.Unlock()
  645. conns.connections = append(conns.connections, c)
  646. metric.UpdateActiveConnectionsSize(len(conns.connections))
  647. logger.Debug(c.GetProtocol(), c.GetID(), "connection added, local address %#v, remote address %#v, num open connections: %v",
  648. c.GetLocalAddress(), c.GetRemoteAddress(), len(conns.connections))
  649. }
  650. // Swap replaces an existing connection with the given one.
  651. // This method is useful if you have to change some connection details
  652. // for example for FTP is used to update the connection once the user
  653. // authenticates
  654. func (conns *ActiveConnections) Swap(c ActiveConnection) error {
  655. conns.Lock()
  656. defer conns.Unlock()
  657. for idx, conn := range conns.connections {
  658. if conn.GetID() == c.GetID() {
  659. err := conn.CloseFS()
  660. conns.connections[idx] = c
  661. logger.Debug(logSender, c.GetID(), "connection swapped, close fs error: %v", err)
  662. conn = nil
  663. return nil
  664. }
  665. }
  666. return errors.New("connection to swap not found")
  667. }
  668. // Remove removes a connection from the active ones
  669. func (conns *ActiveConnections) Remove(connectionID string) {
  670. conns.Lock()
  671. defer conns.Unlock()
  672. for idx, conn := range conns.connections {
  673. if conn.GetID() == connectionID {
  674. err := conn.CloseFS()
  675. lastIdx := len(conns.connections) - 1
  676. conns.connections[idx] = conns.connections[lastIdx]
  677. conns.connections[lastIdx] = nil
  678. conns.connections = conns.connections[:lastIdx]
  679. metric.UpdateActiveConnectionsSize(lastIdx)
  680. logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, local address %#v, remote address %#v close fs error: %v, num open connections: %v",
  681. conn.GetLocalAddress(), conn.GetRemoteAddress(), err, lastIdx)
  682. Config.checkPostDisconnectHook(conn.GetRemoteAddress(), conn.GetProtocol(), conn.GetUsername(),
  683. conn.GetID(), conn.GetConnectionTime())
  684. return
  685. }
  686. }
  687. logger.Warn(logSender, "", "connection id %#v to remove not found!", connectionID)
  688. }
  689. // Close closes an active connection.
  690. // It returns true on success
  691. func (conns *ActiveConnections) Close(connectionID string) bool {
  692. conns.RLock()
  693. result := false
  694. for _, c := range conns.connections {
  695. if c.GetID() == connectionID {
  696. defer func(conn ActiveConnection) {
  697. err := conn.Disconnect()
  698. logger.Debug(conn.GetProtocol(), conn.GetID(), "close connection requested, close err: %v", err)
  699. }(c)
  700. result = true
  701. break
  702. }
  703. }
  704. conns.RUnlock()
  705. return result
  706. }
  707. // AddSSHConnection adds a new ssh connection to the active ones
  708. func (conns *ActiveConnections) AddSSHConnection(c *SSHConnection) {
  709. conns.Lock()
  710. defer conns.Unlock()
  711. conns.sshConnections = append(conns.sshConnections, c)
  712. logger.Debug(logSender, c.GetID(), "ssh connection added, num open connections: %v", len(conns.sshConnections))
  713. }
  714. // RemoveSSHConnection removes a connection from the active ones
  715. func (conns *ActiveConnections) RemoveSSHConnection(connectionID string) {
  716. conns.Lock()
  717. defer conns.Unlock()
  718. for idx, conn := range conns.sshConnections {
  719. if conn.GetID() == connectionID {
  720. lastIdx := len(conns.sshConnections) - 1
  721. conns.sshConnections[idx] = conns.sshConnections[lastIdx]
  722. conns.sshConnections[lastIdx] = nil
  723. conns.sshConnections = conns.sshConnections[:lastIdx]
  724. logger.Debug(logSender, conn.GetID(), "ssh connection removed, num open ssh connections: %v", lastIdx)
  725. return
  726. }
  727. }
  728. logger.Warn(logSender, "", "ssh connection to remove with id %#v not found!", connectionID)
  729. }
  730. func (conns *ActiveConnections) checkIdles() {
  731. conns.RLock()
  732. for _, sshConn := range conns.sshConnections {
  733. idleTime := time.Since(sshConn.GetLastActivity())
  734. if idleTime > Config.idleTimeoutAsDuration {
  735. // we close the an ssh connection if it has no active connections associated
  736. idToMatch := fmt.Sprintf("_%v_", sshConn.GetID())
  737. toClose := true
  738. for _, conn := range conns.connections {
  739. if strings.Contains(conn.GetID(), idToMatch) {
  740. toClose = false
  741. break
  742. }
  743. }
  744. if toClose {
  745. defer func(c *SSHConnection) {
  746. err := c.Close()
  747. logger.Debug(logSender, c.GetID(), "close idle SSH connection, idle time: %v, close err: %v",
  748. time.Since(c.GetLastActivity()), err)
  749. }(sshConn)
  750. }
  751. }
  752. }
  753. for _, c := range conns.connections {
  754. idleTime := time.Since(c.GetLastActivity())
  755. isUnauthenticatedFTPUser := (c.GetProtocol() == ProtocolFTP && c.GetUsername() == "")
  756. if idleTime > Config.idleTimeoutAsDuration || (isUnauthenticatedFTPUser && idleTime > Config.idleLoginTimeout) {
  757. defer func(conn ActiveConnection, isFTPNoAuth bool) {
  758. err := conn.Disconnect()
  759. logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %v, username: %#v close err: %v",
  760. time.Since(conn.GetLastActivity()), conn.GetUsername(), err)
  761. if isFTPNoAuth {
  762. ip := util.GetIPFromRemoteAddress(c.GetRemoteAddress())
  763. logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, c.GetProtocol(), "client idle")
  764. metric.AddNoAuthTryed()
  765. AddDefenderEvent(ip, HostEventNoLoginTried)
  766. dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTryed, ip, c.GetProtocol(),
  767. dataprovider.ErrNoAuthTryed)
  768. }
  769. }(c, isUnauthenticatedFTPUser)
  770. }
  771. }
  772. conns.RUnlock()
  773. }
  774. func (conns *ActiveConnections) checkTransfers() {
  775. if atomic.LoadInt32(&conns.transfersCheckStatus) == 1 {
  776. logger.Warn(logSender, "", "the previous transfer check is still running, skipping execution")
  777. return
  778. }
  779. atomic.StoreInt32(&conns.transfersCheckStatus, 1)
  780. defer atomic.StoreInt32(&conns.transfersCheckStatus, 0)
  781. conns.RLock()
  782. if len(conns.connections) < 2 {
  783. conns.RUnlock()
  784. return
  785. }
  786. var wg sync.WaitGroup
  787. logger.Debug(logSender, "", "start concurrent transfers check")
  788. // update the current size for transfers to monitors
  789. for _, c := range conns.connections {
  790. for _, t := range c.GetTransfers() {
  791. if t.HasSizeLimit {
  792. wg.Add(1)
  793. go func(transfer ConnectionTransfer, connID string) {
  794. defer wg.Done()
  795. transfersChecker.UpdateTransferCurrentSizes(transfer.ULSize, transfer.DLSize, transfer.ID, connID)
  796. }(t, c.GetID())
  797. }
  798. }
  799. }
  800. conns.RUnlock()
  801. logger.Debug(logSender, "", "waiting for the update of the transfers current size")
  802. wg.Wait()
  803. logger.Debug(logSender, "", "getting overquota transfers")
  804. overquotaTransfers := transfersChecker.GetOverquotaTransfers()
  805. logger.Debug(logSender, "", "number of overquota transfers: %v", len(overquotaTransfers))
  806. if len(overquotaTransfers) == 0 {
  807. return
  808. }
  809. conns.RLock()
  810. defer conns.RUnlock()
  811. for _, c := range conns.connections {
  812. for _, overquotaTransfer := range overquotaTransfers {
  813. if c.GetID() == overquotaTransfer.ConnID {
  814. logger.Info(logSender, c.GetID(), "user %#v is overquota, try to close transfer id %v",
  815. c.GetUsername(), overquotaTransfer.TransferID)
  816. var err error
  817. if overquotaTransfer.TransferType == TransferDownload {
  818. err = getReadQuotaExceededError(c.GetProtocol())
  819. } else {
  820. err = getQuotaExceededError(c.GetProtocol())
  821. }
  822. c.SignalTransferClose(overquotaTransfer.TransferID, err)
  823. }
  824. }
  825. }
  826. logger.Debug(logSender, "", "transfers check completed")
  827. }
  828. // AddClientConnection stores a new client connection
  829. func (conns *ActiveConnections) AddClientConnection(ipAddr string) {
  830. conns.clients.add(ipAddr)
  831. }
  832. // RemoveClientConnection removes a disconnected client from the tracked ones
  833. func (conns *ActiveConnections) RemoveClientConnection(ipAddr string) {
  834. conns.clients.remove(ipAddr)
  835. }
  836. // GetClientConnections returns the total number of client connections
  837. func (conns *ActiveConnections) GetClientConnections() int32 {
  838. return conns.clients.getTotal()
  839. }
  840. // IsNewConnectionAllowed returns false if the maximum number of concurrent allowed connections is exceeded
  841. func (conns *ActiveConnections) IsNewConnectionAllowed(ipAddr string) bool {
  842. if Config.MaxTotalConnections == 0 && Config.MaxPerHostConnections == 0 {
  843. return true
  844. }
  845. if Config.MaxPerHostConnections > 0 {
  846. if total := conns.clients.getTotalFrom(ipAddr); total > Config.MaxPerHostConnections {
  847. logger.Debug(logSender, "", "active connections from %v %v/%v", ipAddr, total, Config.MaxPerHostConnections)
  848. AddDefenderEvent(ipAddr, HostEventLimitExceeded)
  849. return false
  850. }
  851. }
  852. if Config.MaxTotalConnections > 0 {
  853. if total := conns.clients.getTotal(); total > int32(Config.MaxTotalConnections) {
  854. logger.Debug(logSender, "", "active client connections %v/%v", total, Config.MaxTotalConnections)
  855. return false
  856. }
  857. // on a single SFTP connection we could have multiple SFTP channels or commands
  858. // so we check the estabilished connections too
  859. conns.RLock()
  860. defer conns.RUnlock()
  861. return len(conns.connections) < Config.MaxTotalConnections
  862. }
  863. return true
  864. }
  865. // GetStats returns stats for active connections
  866. func (conns *ActiveConnections) GetStats() []ConnectionStatus {
  867. conns.RLock()
  868. defer conns.RUnlock()
  869. stats := make([]ConnectionStatus, 0, len(conns.connections))
  870. for _, c := range conns.connections {
  871. stat := ConnectionStatus{
  872. Username: c.GetUsername(),
  873. ConnectionID: c.GetID(),
  874. ClientVersion: c.GetClientVersion(),
  875. RemoteAddress: c.GetRemoteAddress(),
  876. ConnectionTime: util.GetTimeAsMsSinceEpoch(c.GetConnectionTime()),
  877. LastActivity: util.GetTimeAsMsSinceEpoch(c.GetLastActivity()),
  878. Protocol: c.GetProtocol(),
  879. Command: c.GetCommand(),
  880. Transfers: c.GetTransfers(),
  881. }
  882. stats = append(stats, stat)
  883. }
  884. return stats
  885. }
  886. // ConnectionStatus returns the status for an active connection
  887. type ConnectionStatus struct {
  888. // Logged in username
  889. Username string `json:"username"`
  890. // Unique identifier for the connection
  891. ConnectionID string `json:"connection_id"`
  892. // client's version string
  893. ClientVersion string `json:"client_version,omitempty"`
  894. // Remote address for this connection
  895. RemoteAddress string `json:"remote_address"`
  896. // Connection time as unix timestamp in milliseconds
  897. ConnectionTime int64 `json:"connection_time"`
  898. // Last activity as unix timestamp in milliseconds
  899. LastActivity int64 `json:"last_activity"`
  900. // Protocol for this connection
  901. Protocol string `json:"protocol"`
  902. // active uploads/downloads
  903. Transfers []ConnectionTransfer `json:"active_transfers,omitempty"`
  904. // SSH command or WebDAV method
  905. Command string `json:"command,omitempty"`
  906. }
  907. // GetConnectionDuration returns the connection duration as string
  908. func (c *ConnectionStatus) GetConnectionDuration() string {
  909. elapsed := time.Since(util.GetTimeFromMsecSinceEpoch(c.ConnectionTime))
  910. return util.GetDurationAsString(elapsed)
  911. }
  912. // GetConnectionInfo returns connection info.
  913. // Protocol,Client Version and RemoteAddress are returned.
  914. func (c *ConnectionStatus) GetConnectionInfo() string {
  915. var result strings.Builder
  916. result.WriteString(fmt.Sprintf("%v. Client: %#v From: %#v", c.Protocol, c.ClientVersion, c.RemoteAddress))
  917. if c.Command == "" {
  918. return result.String()
  919. }
  920. switch c.Protocol {
  921. case ProtocolSSH, ProtocolFTP:
  922. result.WriteString(fmt.Sprintf(". Command: %#v", c.Command))
  923. case ProtocolWebDAV:
  924. result.WriteString(fmt.Sprintf(". Method: %#v", c.Command))
  925. }
  926. return result.String()
  927. }
  928. // GetTransfersAsString returns the active transfers as string
  929. func (c *ConnectionStatus) GetTransfersAsString() string {
  930. result := ""
  931. for _, t := range c.Transfers {
  932. if result != "" {
  933. result += ". "
  934. }
  935. result += t.getConnectionTransferAsString()
  936. }
  937. return result
  938. }
  939. // ActiveQuotaScan defines an active quota scan for a user home dir
  940. type ActiveQuotaScan struct {
  941. // Username to which the quota scan refers
  942. Username string `json:"username"`
  943. // quota scan start time as unix timestamp in milliseconds
  944. StartTime int64 `json:"start_time"`
  945. }
  946. // ActiveVirtualFolderQuotaScan defines an active quota scan for a virtual folder
  947. type ActiveVirtualFolderQuotaScan struct {
  948. // folder name to which the quota scan refers
  949. Name string `json:"name"`
  950. // quota scan start time as unix timestamp in milliseconds
  951. StartTime int64 `json:"start_time"`
  952. }
  953. // ActiveScans holds the active quota scans
  954. type ActiveScans struct {
  955. sync.RWMutex
  956. UserScans []ActiveQuotaScan
  957. FolderScans []ActiveVirtualFolderQuotaScan
  958. }
  959. // GetUsersQuotaScans returns the active quota scans for users home directories
  960. func (s *ActiveScans) GetUsersQuotaScans() []ActiveQuotaScan {
  961. s.RLock()
  962. defer s.RUnlock()
  963. scans := make([]ActiveQuotaScan, len(s.UserScans))
  964. copy(scans, s.UserScans)
  965. return scans
  966. }
  967. // AddUserQuotaScan adds a user to the ones with active quota scans.
  968. // Returns false if the user has a quota scan already running
  969. func (s *ActiveScans) AddUserQuotaScan(username string) bool {
  970. s.Lock()
  971. defer s.Unlock()
  972. for _, scan := range s.UserScans {
  973. if scan.Username == username {
  974. return false
  975. }
  976. }
  977. s.UserScans = append(s.UserScans, ActiveQuotaScan{
  978. Username: username,
  979. StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
  980. })
  981. return true
  982. }
  983. // RemoveUserQuotaScan removes a user from the ones with active quota scans.
  984. // Returns false if the user has no active quota scans
  985. func (s *ActiveScans) RemoveUserQuotaScan(username string) bool {
  986. s.Lock()
  987. defer s.Unlock()
  988. for idx, scan := range s.UserScans {
  989. if scan.Username == username {
  990. lastIdx := len(s.UserScans) - 1
  991. s.UserScans[idx] = s.UserScans[lastIdx]
  992. s.UserScans = s.UserScans[:lastIdx]
  993. return true
  994. }
  995. }
  996. return false
  997. }
  998. // GetVFoldersQuotaScans returns the active quota scans for virtual folders
  999. func (s *ActiveScans) GetVFoldersQuotaScans() []ActiveVirtualFolderQuotaScan {
  1000. s.RLock()
  1001. defer s.RUnlock()
  1002. scans := make([]ActiveVirtualFolderQuotaScan, len(s.FolderScans))
  1003. copy(scans, s.FolderScans)
  1004. return scans
  1005. }
  1006. // AddVFolderQuotaScan adds a virtual folder to the ones with active quota scans.
  1007. // Returns false if the folder has a quota scan already running
  1008. func (s *ActiveScans) AddVFolderQuotaScan(folderName string) bool {
  1009. s.Lock()
  1010. defer s.Unlock()
  1011. for _, scan := range s.FolderScans {
  1012. if scan.Name == folderName {
  1013. return false
  1014. }
  1015. }
  1016. s.FolderScans = append(s.FolderScans, ActiveVirtualFolderQuotaScan{
  1017. Name: folderName,
  1018. StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
  1019. })
  1020. return true
  1021. }
  1022. // RemoveVFolderQuotaScan removes a folder from the ones with active quota scans.
  1023. // Returns false if the folder has no active quota scans
  1024. func (s *ActiveScans) RemoveVFolderQuotaScan(folderName string) bool {
  1025. s.Lock()
  1026. defer s.Unlock()
  1027. for idx, scan := range s.FolderScans {
  1028. if scan.Name == folderName {
  1029. lastIdx := len(s.FolderScans) - 1
  1030. s.FolderScans[idx] = s.FolderScans[lastIdx]
  1031. s.FolderScans = s.FolderScans[:lastIdx]
  1032. return true
  1033. }
  1034. }
  1035. return false
  1036. }