sftpd.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. // Package sftpd implements the SSH File Transfer Protocol as described in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02.
  2. // It uses pkg/sftp library:
  3. // https://github.com/pkg/sftp
  4. package sftpd
  5. import (
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "sync"
  13. "time"
  14. "github.com/drakkan/sftpgo/dataprovider"
  15. "github.com/drakkan/sftpgo/logger"
  16. "github.com/drakkan/sftpgo/utils"
  17. )
  18. const (
  19. logSender = "sftpd"
  20. logSenderSCP = "scp"
  21. uploadLogSender = "Upload"
  22. downloadLogSender = "Download"
  23. renameLogSender = "Rename"
  24. rmdirLogSender = "Rmdir"
  25. mkdirLogSender = "Mkdir"
  26. symlinkLogSender = "Symlink"
  27. removeLogSender = "Remove"
  28. operationDownload = "download"
  29. operationUpload = "upload"
  30. operationDelete = "delete"
  31. operationRename = "rename"
  32. protocolSFTP = "SFTP"
  33. protocolSCP = "SCP"
  34. )
  35. var (
  36. mutex sync.RWMutex
  37. openConnections map[string]Connection
  38. activeTransfers []*Transfer
  39. idleConnectionTicker *time.Ticker
  40. idleTimeout time.Duration
  41. activeQuotaScans []ActiveQuotaScan
  42. dataProvider dataprovider.Provider
  43. actions Actions
  44. uploadMode int
  45. )
  46. type connectionTransfer struct {
  47. OperationType string `json:"operation_type"`
  48. StartTime int64 `json:"start_time"`
  49. Size int64 `json:"size"`
  50. LastActivity int64 `json:"last_activity"`
  51. Path string `json:"path"`
  52. }
  53. // ActiveQuotaScan defines an active quota scan
  54. type ActiveQuotaScan struct {
  55. // Username to which the quota scan refers
  56. Username string `json:"username"`
  57. // quota scan start time as unix timestamp in milliseconds
  58. StartTime int64 `json:"start_time"`
  59. }
  60. // Actions to execute on SFTP create, download, delete and rename.
  61. // An external command can be executed and/or an HTTP notification can be fired
  62. type Actions struct {
  63. // Valid values are download, upload, delete, rename. Empty slice to disable
  64. ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
  65. // Absolute path to the command to execute, empty to disable
  66. Command string `json:"command" mapstructure:"command"`
  67. // The URL to notify using an HTTP GET, empty to disable
  68. HTTPNotificationURL string `json:"http_notification_url" mapstructure:"http_notification_url"`
  69. }
  70. // ConnectionStatus status for an active connection
  71. type ConnectionStatus struct {
  72. // Logged in username
  73. Username string `json:"username"`
  74. // Unique identifier for the connection
  75. ConnectionID string `json:"connection_id"`
  76. // client's version string
  77. ClientVersion string `json:"client_version"`
  78. // Remote address for this connection
  79. RemoteAddress string `json:"remote_address"`
  80. // Connection time as unix timestamp in milliseconds
  81. ConnectionTime int64 `json:"connection_time"`
  82. // Last activity as unix timestamp in milliseconds
  83. LastActivity int64 `json:"last_activity"`
  84. // Protocol for this connection: SFTP or SCP
  85. Protocol string `json:"protocol"`
  86. // active uploads/downloads
  87. Transfers []connectionTransfer `json:"active_transfers"`
  88. }
  89. func init() {
  90. openConnections = make(map[string]Connection)
  91. idleConnectionTicker = time.NewTicker(5 * time.Minute)
  92. }
  93. // SetDataProvider sets the data provider to use to authenticate users and to get/update their disk quota
  94. func SetDataProvider(provider dataprovider.Provider) {
  95. dataProvider = provider
  96. }
  97. func getActiveSessions(username string) int {
  98. mutex.RLock()
  99. defer mutex.RUnlock()
  100. numSessions := 0
  101. for _, c := range openConnections {
  102. if c.User.Username == username {
  103. numSessions++
  104. }
  105. }
  106. return numSessions
  107. }
  108. // GetQuotaScans returns the active quota scans
  109. func GetQuotaScans() []ActiveQuotaScan {
  110. mutex.RLock()
  111. defer mutex.RUnlock()
  112. scans := make([]ActiveQuotaScan, len(activeQuotaScans))
  113. copy(scans, activeQuotaScans)
  114. return scans
  115. }
  116. // AddQuotaScan add an user to the ones with active quota scans.
  117. // Returns false if the user has a quota scan already running
  118. func AddQuotaScan(username string) bool {
  119. mutex.Lock()
  120. defer mutex.Unlock()
  121. for _, s := range activeQuotaScans {
  122. if s.Username == username {
  123. return false
  124. }
  125. }
  126. activeQuotaScans = append(activeQuotaScans, ActiveQuotaScan{
  127. Username: username,
  128. StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()),
  129. })
  130. return true
  131. }
  132. // RemoveQuotaScan removes an user from the ones with active quota scans
  133. func RemoveQuotaScan(username string) error {
  134. mutex.Lock()
  135. defer mutex.Unlock()
  136. var err error
  137. indexToRemove := -1
  138. for i, s := range activeQuotaScans {
  139. if s.Username == username {
  140. indexToRemove = i
  141. break
  142. }
  143. }
  144. if indexToRemove >= 0 {
  145. activeQuotaScans[indexToRemove] = activeQuotaScans[len(activeQuotaScans)-1]
  146. activeQuotaScans = activeQuotaScans[:len(activeQuotaScans)-1]
  147. } else {
  148. logger.Warn(logSender, "quota scan to remove not found for user: %v", username)
  149. err = fmt.Errorf("quota scan to remove not found for user: %v", username)
  150. }
  151. return err
  152. }
  153. // CloseActiveConnection closes an active SFTP connection.
  154. // It returns true on success
  155. func CloseActiveConnection(connectionID string) bool {
  156. result := false
  157. mutex.RLock()
  158. defer mutex.RUnlock()
  159. for _, c := range openConnections {
  160. if c.ID == connectionID {
  161. logger.Debug(logSender, "closing connection with id: %v", connectionID)
  162. c.sshConn.Close()
  163. result = true
  164. break
  165. }
  166. }
  167. return result
  168. }
  169. // GetConnectionsStats returns stats for active connections
  170. func GetConnectionsStats() []ConnectionStatus {
  171. mutex.RLock()
  172. defer mutex.RUnlock()
  173. stats := []ConnectionStatus{}
  174. for _, c := range openConnections {
  175. conn := ConnectionStatus{
  176. Username: c.User.Username,
  177. ConnectionID: c.ID,
  178. ClientVersion: c.ClientVersion,
  179. RemoteAddress: c.RemoteAddr.String(),
  180. ConnectionTime: utils.GetTimeAsMsSinceEpoch(c.StartTime),
  181. LastActivity: utils.GetTimeAsMsSinceEpoch(c.lastActivity),
  182. Protocol: c.protocol,
  183. Transfers: []connectionTransfer{},
  184. }
  185. for _, t := range activeTransfers {
  186. if t.connectionID == c.ID {
  187. if t.lastActivity.UnixNano() > c.lastActivity.UnixNano() {
  188. conn.LastActivity = utils.GetTimeAsMsSinceEpoch(t.lastActivity)
  189. }
  190. var operationType string
  191. var size int64
  192. if t.transferType == transferUpload {
  193. operationType = operationUpload
  194. size = t.bytesReceived
  195. } else {
  196. operationType = operationDownload
  197. size = t.bytesSent
  198. }
  199. connTransfer := connectionTransfer{
  200. OperationType: operationType,
  201. StartTime: utils.GetTimeAsMsSinceEpoch(t.start),
  202. Size: size,
  203. LastActivity: utils.GetTimeAsMsSinceEpoch(t.lastActivity),
  204. Path: c.User.GetRelativePath(t.path),
  205. }
  206. conn.Transfers = append(conn.Transfers, connTransfer)
  207. }
  208. }
  209. stats = append(stats, conn)
  210. }
  211. return stats
  212. }
  213. func startIdleTimer(maxIdleTime time.Duration) {
  214. idleTimeout = maxIdleTime
  215. go func() {
  216. for t := range idleConnectionTicker.C {
  217. logger.Debug(logSender, "idle connections check ticker %v", t)
  218. CheckIdleConnections()
  219. }
  220. }()
  221. }
  222. // CheckIdleConnections disconnects clients idle for too long, based on IdleTimeout setting
  223. func CheckIdleConnections() {
  224. mutex.RLock()
  225. defer mutex.RUnlock()
  226. for _, c := range openConnections {
  227. idleTime := time.Since(c.lastActivity)
  228. for _, t := range activeTransfers {
  229. if t.connectionID == c.ID {
  230. transferIdleTime := time.Since(t.lastActivity)
  231. if transferIdleTime < idleTime {
  232. logger.Debug(logSender, "idle time: %v setted to transfer idle time: %v connection id: %v",
  233. idleTime, transferIdleTime, c.ID)
  234. idleTime = transferIdleTime
  235. }
  236. }
  237. }
  238. if idleTime > idleTimeout {
  239. logger.Debug(logSender, "close idle connection id: %v idle time: %v", c.ID, idleTime)
  240. err := c.sshConn.Close()
  241. logger.Debug(logSender, "idle connection closed id: %v, err: %v", c.ID, err)
  242. }
  243. }
  244. logger.Debug(logSender, "check idle connections ended")
  245. }
  246. func addConnection(id string, conn Connection) {
  247. mutex.Lock()
  248. defer mutex.Unlock()
  249. openConnections[id] = conn
  250. logger.Debug(logSender, "connection added, num open connections: %v", len(openConnections))
  251. }
  252. func removeConnection(id string) {
  253. mutex.Lock()
  254. defer mutex.Unlock()
  255. delete(openConnections, id)
  256. logger.Debug(logSender, "connection removed, num open connections: %v", len(openConnections))
  257. }
  258. func addTransfer(transfer *Transfer) {
  259. mutex.Lock()
  260. defer mutex.Unlock()
  261. activeTransfers = append(activeTransfers, transfer)
  262. }
  263. func removeTransfer(transfer *Transfer) error {
  264. mutex.Lock()
  265. defer mutex.Unlock()
  266. var err error
  267. indexToRemove := -1
  268. for i, v := range activeTransfers {
  269. if v == transfer {
  270. indexToRemove = i
  271. break
  272. }
  273. }
  274. if indexToRemove >= 0 {
  275. activeTransfers[indexToRemove] = activeTransfers[len(activeTransfers)-1]
  276. activeTransfers = activeTransfers[:len(activeTransfers)-1]
  277. } else {
  278. logger.Warn(logSender, "transfer to remove not found!")
  279. err = fmt.Errorf("transfer to remove not found")
  280. }
  281. return err
  282. }
  283. func updateConnectionActivity(id string) {
  284. mutex.Lock()
  285. defer mutex.Unlock()
  286. if c, ok := openConnections[id]; ok {
  287. c.lastActivity = time.Now()
  288. openConnections[id] = c
  289. }
  290. }
  291. func executeAction(operation string, username string, path string, target string) error {
  292. if !utils.IsStringInSlice(operation, actions.ExecuteOn) {
  293. return nil
  294. }
  295. var err error
  296. if len(actions.Command) > 0 && filepath.IsAbs(actions.Command) {
  297. if _, err = os.Stat(actions.Command); err == nil {
  298. command := exec.Command(actions.Command, operation, username, path, target)
  299. err = command.Start()
  300. logger.Debug(logSender, "executed command \"%v\" with arguments: %v, %v, %v, %v, error: %v",
  301. actions.Command, operation, username, path, target, err)
  302. } else {
  303. logger.Warn(logSender, "Invalid action command \"%v\" : %v", actions.Command, err)
  304. }
  305. }
  306. if len(actions.HTTPNotificationURL) > 0 {
  307. var url *url.URL
  308. url, err = url.Parse(actions.HTTPNotificationURL)
  309. if err == nil {
  310. q := url.Query()
  311. q.Add("action", operation)
  312. q.Add("username", username)
  313. q.Add("path", path)
  314. if len(target) > 0 {
  315. q.Add("target_path", target)
  316. }
  317. url.RawQuery = q.Encode()
  318. go func() {
  319. startTime := time.Now()
  320. httpClient := &http.Client{
  321. Timeout: 15 * time.Second,
  322. }
  323. resp, err := httpClient.Get(url.String())
  324. respCode := 0
  325. if err == nil {
  326. respCode = resp.StatusCode
  327. resp.Body.Close()
  328. }
  329. logger.Debug(logSender, "notified action to URL: %v status code: %v, elapsed: %v err: %v",
  330. url.String(), respCode, time.Since(startTime), err)
  331. }()
  332. } else {
  333. logger.Warn(logSender, "Invalid http_notification_url \"%v\" : %v", actions.HTTPNotificationURL, err)
  334. }
  335. }
  336. return err
  337. }