api_events.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. // Copyright (C) 2019-2023 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package httpd
  15. import (
  16. "encoding/csv"
  17. "encoding/json"
  18. "fmt"
  19. "net/http"
  20. "strconv"
  21. "strings"
  22. "time"
  23. "github.com/sftpgo/sdk/plugin/eventsearcher"
  24. "github.com/sftpgo/sdk/plugin/notifier"
  25. "github.com/drakkan/sftpgo/v2/internal/dataprovider"
  26. "github.com/drakkan/sftpgo/v2/internal/plugin"
  27. "github.com/drakkan/sftpgo/v2/internal/util"
  28. )
  29. func getCommonSearchParamsFromRequest(r *http.Request) (eventsearcher.CommonSearchParams, error) {
  30. c := eventsearcher.CommonSearchParams{}
  31. c.Limit = 100
  32. if _, ok := r.URL.Query()["limit"]; ok {
  33. limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
  34. if err != nil {
  35. return c, util.NewValidationError(fmt.Sprintf("invalid limit: %v", err))
  36. }
  37. if limit < 1 || limit > 1000 {
  38. return c, util.NewValidationError(fmt.Sprintf("limit is out of the 1-1000 range: %v", limit))
  39. }
  40. c.Limit = limit
  41. }
  42. if _, ok := r.URL.Query()["order"]; ok {
  43. order := r.URL.Query().Get("order")
  44. if order != dataprovider.OrderASC && order != dataprovider.OrderDESC {
  45. return c, util.NewValidationError(fmt.Sprintf("invalid order %q", order))
  46. }
  47. if order == dataprovider.OrderASC {
  48. c.Order = 1
  49. }
  50. }
  51. if _, ok := r.URL.Query()["start_timestamp"]; ok {
  52. ts, err := strconv.ParseInt(r.URL.Query().Get("start_timestamp"), 10, 64)
  53. if err != nil {
  54. return c, util.NewValidationError(fmt.Sprintf("invalid start_timestamp: %v", err))
  55. }
  56. c.StartTimestamp = ts
  57. }
  58. if _, ok := r.URL.Query()["end_timestamp"]; ok {
  59. ts, err := strconv.ParseInt(r.URL.Query().Get("end_timestamp"), 10, 64)
  60. if err != nil {
  61. return c, util.NewValidationError(fmt.Sprintf("invalid end_timestamp: %v", err))
  62. }
  63. c.EndTimestamp = ts
  64. }
  65. c.Username = r.URL.Query().Get("username")
  66. c.IP = r.URL.Query().Get("ip")
  67. c.InstanceIDs = getCommaSeparatedQueryParam(r, "instance_ids")
  68. c.FromID = r.URL.Query().Get("from_id")
  69. return c, nil
  70. }
  71. func getFsSearchParamsFromRequest(r *http.Request) (eventsearcher.FsEventSearch, error) {
  72. var err error
  73. s := eventsearcher.FsEventSearch{}
  74. s.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)
  75. if err != nil {
  76. return s, err
  77. }
  78. s.FsProvider = -1
  79. if _, ok := r.URL.Query()["fs_provider"]; ok {
  80. provider := r.URL.Query().Get("fs_provider")
  81. val, err := strconv.Atoi(provider)
  82. if err != nil {
  83. return s, util.NewValidationError(fmt.Sprintf("invalid fs_provider: %v", provider))
  84. }
  85. s.FsProvider = val
  86. }
  87. s.Actions = getCommaSeparatedQueryParam(r, "actions")
  88. s.SSHCmd = r.URL.Query().Get("ssh_cmd")
  89. s.Bucket = r.URL.Query().Get("bucket")
  90. s.Endpoint = r.URL.Query().Get("endpoint")
  91. s.Protocols = getCommaSeparatedQueryParam(r, "protocols")
  92. statuses := getCommaSeparatedQueryParam(r, "statuses")
  93. for _, status := range statuses {
  94. val, err := strconv.ParseInt(status, 10, 32)
  95. if err != nil {
  96. return s, util.NewValidationError(fmt.Sprintf("invalid status: %v", status))
  97. }
  98. s.Statuses = append(s.Statuses, int32(val))
  99. }
  100. return s, nil
  101. }
  102. func getProviderSearchParamsFromRequest(r *http.Request) (eventsearcher.ProviderEventSearch, error) {
  103. var err error
  104. s := eventsearcher.ProviderEventSearch{}
  105. s.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)
  106. if err != nil {
  107. return s, err
  108. }
  109. s.Actions = getCommaSeparatedQueryParam(r, "actions")
  110. s.ObjectName = r.URL.Query().Get("object_name")
  111. s.ObjectTypes = getCommaSeparatedQueryParam(r, "object_types")
  112. return s, nil
  113. }
  114. func getLogSearchParamsFromRequest(r *http.Request) (eventsearcher.LogEventSearch, error) {
  115. var err error
  116. s := eventsearcher.LogEventSearch{}
  117. s.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)
  118. if err != nil {
  119. return s, err
  120. }
  121. s.Protocols = getCommaSeparatedQueryParam(r, "protocols")
  122. events := getCommaSeparatedQueryParam(r, "events")
  123. for _, ev := range events {
  124. evType, err := strconv.ParseUint(ev, 10, 32)
  125. if err == nil {
  126. s.Events = append(s.Events, int32(evType))
  127. }
  128. }
  129. return s, nil
  130. }
  131. func searchFsEvents(w http.ResponseWriter, r *http.Request) {
  132. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  133. claims, err := getTokenClaims(r)
  134. if err != nil || claims.Username == "" {
  135. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  136. return
  137. }
  138. filters, err := getFsSearchParamsFromRequest(r)
  139. if err != nil {
  140. sendAPIResponse(w, r, err, "", getRespStatus(err))
  141. return
  142. }
  143. filters.Role = getRoleFilterForEventSearch(r, claims.Role)
  144. if getBoolQueryParam(r, "csv_export") {
  145. filters.Limit = 100
  146. if err := exportFsEvents(w, &filters); err != nil {
  147. panic(http.ErrAbortHandler)
  148. }
  149. return
  150. }
  151. data, err := plugin.Handler.SearchFsEvents(&filters)
  152. if err != nil {
  153. sendAPIResponse(w, r, err, "", getRespStatus(err))
  154. return
  155. }
  156. w.Header().Set("Content-Type", "application/json")
  157. w.Write(data) //nolint:errcheck
  158. }
  159. func searchProviderEvents(w http.ResponseWriter, r *http.Request) {
  160. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  161. claims, err := getTokenClaims(r)
  162. if err != nil || claims.Username == "" {
  163. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  164. return
  165. }
  166. var filters eventsearcher.ProviderEventSearch
  167. if filters, err = getProviderSearchParamsFromRequest(r); err != nil {
  168. sendAPIResponse(w, r, err, "", getRespStatus(err))
  169. return
  170. }
  171. filters.Role = getRoleFilterForEventSearch(r, claims.Role)
  172. filters.OmitObjectData = getBoolQueryParam(r, "omit_object_data")
  173. if getBoolQueryParam(r, "csv_export") {
  174. filters.Limit = 100
  175. filters.OmitObjectData = true
  176. if err := exportProviderEvents(w, &filters); err != nil {
  177. panic(http.ErrAbortHandler)
  178. }
  179. return
  180. }
  181. data, err := plugin.Handler.SearchProviderEvents(&filters)
  182. if err != nil {
  183. sendAPIResponse(w, r, err, "", getRespStatus(err))
  184. return
  185. }
  186. w.Header().Set("Content-Type", "application/json")
  187. w.Write(data) //nolint:errcheck
  188. }
  189. func searchLogEvents(w http.ResponseWriter, r *http.Request) {
  190. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  191. claims, err := getTokenClaims(r)
  192. if err != nil || claims.Username == "" {
  193. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  194. return
  195. }
  196. var filters eventsearcher.LogEventSearch
  197. if filters, err = getLogSearchParamsFromRequest(r); err != nil {
  198. sendAPIResponse(w, r, err, "", getRespStatus(err))
  199. return
  200. }
  201. filters.Role = getRoleFilterForEventSearch(r, claims.Role)
  202. if getBoolQueryParam(r, "csv_export") {
  203. filters.Limit = 100
  204. if err := exportLogEvents(w, &filters); err != nil {
  205. panic(http.ErrAbortHandler)
  206. }
  207. return
  208. }
  209. data, err := plugin.Handler.SearchLogEvents(&filters)
  210. if err != nil {
  211. sendAPIResponse(w, r, err, "", getRespStatus(err))
  212. return
  213. }
  214. w.Header().Set("Content-Type", "application/json")
  215. w.Write(data) //nolint:errcheck
  216. }
  217. func exportFsEvents(w http.ResponseWriter, filters *eventsearcher.FsEventSearch) error {
  218. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=fslogs-%s.csv", time.Now().Format("2006-01-02T15-04-05")))
  219. w.Header().Set("Content-Type", "text/csv")
  220. w.Header().Set("Accept-Ranges", "none")
  221. w.WriteHeader(http.StatusOK)
  222. csvWriter := csv.NewWriter(w)
  223. ev := fsEvent{}
  224. err := csvWriter.Write(ev.getCSVHeader())
  225. if err != nil {
  226. return err
  227. }
  228. results := make([]fsEvent, 0, filters.Limit)
  229. for {
  230. data, err := plugin.Handler.SearchFsEvents(filters)
  231. if err != nil {
  232. return err
  233. }
  234. if err := json.Unmarshal(data, &results); err != nil {
  235. return err
  236. }
  237. for _, event := range results {
  238. if err := csvWriter.Write(event.getCSVData()); err != nil {
  239. return err
  240. }
  241. }
  242. if len(results) == 0 || len(results) < filters.Limit {
  243. break
  244. }
  245. filters.StartTimestamp = results[len(results)-1].Timestamp
  246. filters.FromID = results[len(results)-1].ID
  247. results = nil
  248. }
  249. csvWriter.Flush()
  250. return csvWriter.Error()
  251. }
  252. func exportProviderEvents(w http.ResponseWriter, filters *eventsearcher.ProviderEventSearch) error {
  253. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=providerlogs-%s.csv", time.Now().Format("2006-01-02T15-04-05")))
  254. w.Header().Set("Content-Type", "text/csv")
  255. w.Header().Set("Accept-Ranges", "none")
  256. w.WriteHeader(http.StatusOK)
  257. ev := providerEvent{}
  258. csvWriter := csv.NewWriter(w)
  259. err := csvWriter.Write(ev.getCSVHeader())
  260. if err != nil {
  261. return err
  262. }
  263. results := make([]providerEvent, 0, filters.Limit)
  264. for {
  265. data, err := plugin.Handler.SearchProviderEvents(filters)
  266. if err != nil {
  267. return err
  268. }
  269. if err := json.Unmarshal(data, &results); err != nil {
  270. return err
  271. }
  272. for _, event := range results {
  273. if err := csvWriter.Write(event.getCSVData()); err != nil {
  274. return err
  275. }
  276. }
  277. if len(results) < filters.Limit || len(results) == 0 {
  278. break
  279. }
  280. filters.FromID = results[len(results)-1].ID
  281. filters.StartTimestamp = results[len(results)-1].Timestamp
  282. results = nil
  283. }
  284. csvWriter.Flush()
  285. return csvWriter.Error()
  286. }
  287. func exportLogEvents(w http.ResponseWriter, filters *eventsearcher.LogEventSearch) error {
  288. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=logs-%s.csv", time.Now().Format("2006-01-02T15-04-05")))
  289. w.Header().Set("Content-Type", "text/csv")
  290. w.Header().Set("Accept-Ranges", "none")
  291. w.WriteHeader(http.StatusOK)
  292. ev := logEvent{}
  293. csvWriter := csv.NewWriter(w)
  294. err := csvWriter.Write(ev.getCSVHeader())
  295. if err != nil {
  296. return err
  297. }
  298. results := make([]logEvent, 0, filters.Limit)
  299. for {
  300. data, err := plugin.Handler.SearchLogEvents(filters)
  301. if err != nil {
  302. return err
  303. }
  304. if err := json.Unmarshal(data, &results); err != nil {
  305. return err
  306. }
  307. for _, event := range results {
  308. if err := csvWriter.Write(event.getCSVData()); err != nil {
  309. return err
  310. }
  311. }
  312. if len(results) == 0 || len(results) < filters.Limit {
  313. break
  314. }
  315. filters.StartTimestamp = results[len(results)-1].Timestamp
  316. filters.FromID = results[len(results)-1].ID
  317. results = nil
  318. }
  319. csvWriter.Flush()
  320. return csvWriter.Error()
  321. }
  322. func getRoleFilterForEventSearch(r *http.Request, defaultValue string) string {
  323. if defaultValue != "" {
  324. return defaultValue
  325. }
  326. return r.URL.Query().Get("role")
  327. }
  328. type fsEvent struct {
  329. ID string `json:"id"`
  330. Timestamp int64 `json:"timestamp"`
  331. Action string `json:"action"`
  332. Username string `json:"username"`
  333. FsPath string `json:"fs_path"`
  334. FsTargetPath string `json:"fs_target_path,omitempty"`
  335. VirtualPath string `json:"virtual_path"`
  336. VirtualTargetPath string `json:"virtual_target_path,omitempty"`
  337. SSHCmd string `json:"ssh_cmd,omitempty"`
  338. FileSize int64 `json:"file_size,omitempty"`
  339. Elapsed int64 `json:"elapsed,omitempty"`
  340. Status int `json:"status"`
  341. Protocol string `json:"protocol"`
  342. IP string `json:"ip,omitempty"`
  343. SessionID string `json:"session_id"`
  344. FsProvider int `json:"fs_provider"`
  345. Bucket string `json:"bucket,omitempty"`
  346. Endpoint string `json:"endpoint,omitempty"`
  347. OpenFlags int `json:"open_flags,omitempty"`
  348. Role string `json:"role,omitempty"`
  349. InstanceID string `json:"instance_id,omitempty"`
  350. }
  351. func (e *fsEvent) getCSVHeader() []string {
  352. return []string{"Time", "Action", "Path", "Size", "Elapsed", "Status", "User", "Protocol",
  353. "IP", "SSH command"}
  354. }
  355. func (e *fsEvent) getCSVData() []string {
  356. timestamp := time.Unix(0, e.Timestamp).UTC()
  357. var pathInfo strings.Builder
  358. pathInfo.Write([]byte(e.VirtualPath))
  359. if e.VirtualTargetPath != "" {
  360. pathInfo.WriteString(" => ")
  361. pathInfo.WriteString(e.VirtualTargetPath)
  362. }
  363. var status string
  364. switch e.Status {
  365. case 1:
  366. status = "OK"
  367. case 2:
  368. status = "KO"
  369. case 3:
  370. status = "Quota exceeded"
  371. }
  372. var fileSize string
  373. if e.FileSize > 0 {
  374. fileSize = util.ByteCountIEC(e.FileSize)
  375. }
  376. var elapsed string
  377. if e.Elapsed > 0 {
  378. elapsed = (time.Duration(e.Elapsed) * time.Millisecond).String()
  379. }
  380. return []string{timestamp.Format(time.RFC3339Nano), e.Action, pathInfo.String(),
  381. fileSize, elapsed, status, e.Username, e.Protocol, e.IP, e.SSHCmd}
  382. }
  383. type providerEvent struct {
  384. ID string `json:"id"`
  385. Timestamp int64 `json:"timestamp"`
  386. Action string `json:"action"`
  387. Username string `json:"username"`
  388. IP string `json:"ip,omitempty"`
  389. ObjectType string `json:"object_type"`
  390. ObjectName string `json:"object_name"`
  391. ObjectData []byte `json:"object_data"`
  392. Role string `json:"role,omitempty"`
  393. InstanceID string `json:"instance_id,omitempty"`
  394. }
  395. func (e *providerEvent) getCSVHeader() []string {
  396. return []string{"Time", "Action", "Object Type", "Object Name", "User", "IP"}
  397. }
  398. func (e *providerEvent) getCSVData() []string {
  399. timestamp := time.Unix(0, e.Timestamp).UTC()
  400. return []string{timestamp.Format(time.RFC3339Nano), e.Action, e.ObjectType, e.ObjectName,
  401. e.Username, e.IP}
  402. }
  403. type logEvent struct {
  404. ID string `json:"id"`
  405. Timestamp int64 `json:"timestamp"`
  406. Event int `json:"event"`
  407. Protocol string `json:"protocol"`
  408. Username string `json:"username,omitempty"`
  409. IP string `json:"ip,omitempty"`
  410. Message string `json:"message,omitempty"`
  411. Role string `json:"role,omitempty"`
  412. }
  413. func (e *logEvent) getCSVHeader() []string {
  414. return []string{"Time", "Event", "Protocol", "User", "IP", "Message"}
  415. }
  416. func (e *logEvent) getCSVData() []string {
  417. timestamp := time.Unix(0, e.Timestamp).UTC()
  418. return []string{timestamp.Format(time.RFC3339Nano), getLogEventString(notifier.LogEventType(e.Event)),
  419. e.Protocol, e.Username, e.IP, e.Message}
  420. }
  421. func getLogEventString(event notifier.LogEventType) string {
  422. switch event {
  423. case notifier.LogEventTypeLoginFailed:
  424. return "Login failed"
  425. case notifier.LogEventTypeLoginNoUser:
  426. return "Login with non-existent user"
  427. case notifier.LogEventTypeNoLoginTried:
  428. return "No login tried"
  429. case notifier.LogEventTypeNotNegotiated:
  430. return "Algorithm negotiation failed"
  431. default:
  432. return ""
  433. }
  434. }