api_events.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. // Copyright (C) 2019-2022 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/drakkan/sftpgo/v2/internal/dataprovider"
  25. "github.com/drakkan/sftpgo/v2/internal/plugin"
  26. "github.com/drakkan/sftpgo/v2/internal/util"
  27. )
  28. func getCommonSearchParamsFromRequest(r *http.Request) (eventsearcher.CommonSearchParams, error) {
  29. c := eventsearcher.CommonSearchParams{}
  30. c.Limit = 100
  31. if _, ok := r.URL.Query()["limit"]; ok {
  32. limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
  33. if err != nil {
  34. return c, util.NewValidationError(fmt.Sprintf("invalid limit: %v", err))
  35. }
  36. if limit < 1 || limit > 1000 {
  37. return c, util.NewValidationError(fmt.Sprintf("limit is out of the 1-1000 range: %v", limit))
  38. }
  39. c.Limit = limit
  40. }
  41. if _, ok := r.URL.Query()["order"]; ok {
  42. order := r.URL.Query().Get("order")
  43. if order != dataprovider.OrderASC && order != dataprovider.OrderDESC {
  44. return c, util.NewValidationError(fmt.Sprintf("invalid order %#v", order))
  45. }
  46. if order == dataprovider.OrderASC {
  47. c.Order = 1
  48. }
  49. }
  50. if _, ok := r.URL.Query()["start_timestamp"]; ok {
  51. ts, err := strconv.ParseInt(r.URL.Query().Get("start_timestamp"), 10, 64)
  52. if err != nil {
  53. return c, util.NewValidationError(fmt.Sprintf("invalid start_timestamp: %v", err))
  54. }
  55. c.StartTimestamp = ts
  56. }
  57. if _, ok := r.URL.Query()["end_timestamp"]; ok {
  58. ts, err := strconv.ParseInt(r.URL.Query().Get("end_timestamp"), 10, 64)
  59. if err != nil {
  60. return c, util.NewValidationError(fmt.Sprintf("invalid end_timestamp: %v", err))
  61. }
  62. c.EndTimestamp = ts
  63. }
  64. c.Actions = getCommaSeparatedQueryParam(r, "actions")
  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.ExcludeIDs = getCommaSeparatedQueryParam(r, "exclude_ids")
  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.SSHCmd = r.URL.Query().Get("ssh_cmd")
  88. s.Bucket = r.URL.Query().Get("bucket")
  89. s.Endpoint = r.URL.Query().Get("endpoint")
  90. s.Protocols = getCommaSeparatedQueryParam(r, "protocols")
  91. statuses := getCommaSeparatedQueryParam(r, "statuses")
  92. for _, status := range statuses {
  93. val, err := strconv.Atoi(status)
  94. if err != nil {
  95. return s, util.NewValidationError(fmt.Sprintf("invalid status: %v", status))
  96. }
  97. s.Statuses = append(s.Statuses, int32(val))
  98. }
  99. return s, nil
  100. }
  101. func getProviderSearchParamsFromRequest(r *http.Request) (eventsearcher.ProviderEventSearch, error) {
  102. var err error
  103. s := eventsearcher.ProviderEventSearch{}
  104. s.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)
  105. if err != nil {
  106. return s, err
  107. }
  108. s.ObjectName = r.URL.Query().Get("object_name")
  109. s.ObjectTypes = getCommaSeparatedQueryParam(r, "object_types")
  110. return s, nil
  111. }
  112. func searchFsEvents(w http.ResponseWriter, r *http.Request) {
  113. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  114. claims, err := getTokenClaims(r)
  115. if err != nil || claims.Username == "" {
  116. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  117. return
  118. }
  119. filters, err := getFsSearchParamsFromRequest(r)
  120. if err != nil {
  121. sendAPIResponse(w, r, err, "", getRespStatus(err))
  122. return
  123. }
  124. filters.Role = getRoleFilterForEventSearch(r, claims.Role)
  125. if getBoolQueryParam(r, "csv_export") {
  126. filters.Limit = 100
  127. if err := exportFsEvents(w, &filters); err != nil {
  128. panic(http.ErrAbortHandler)
  129. }
  130. return
  131. }
  132. data, _, _, err := plugin.Handler.SearchFsEvents(&filters)
  133. if err != nil {
  134. sendAPIResponse(w, r, err, "", getRespStatus(err))
  135. return
  136. }
  137. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  138. w.Write(data) //nolint:errcheck
  139. }
  140. func searchProviderEvents(w http.ResponseWriter, r *http.Request) {
  141. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  142. claims, err := getTokenClaims(r)
  143. if err != nil || claims.Username == "" {
  144. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  145. return
  146. }
  147. var filters eventsearcher.ProviderEventSearch
  148. if filters, err = getProviderSearchParamsFromRequest(r); err != nil {
  149. sendAPIResponse(w, r, err, "", getRespStatus(err))
  150. return
  151. }
  152. filters.Role = getRoleFilterForEventSearch(r, claims.Role)
  153. filters.OmitObjectData = getBoolQueryParam(r, "omit_object_data")
  154. if getBoolQueryParam(r, "csv_export") {
  155. filters.Limit = 100
  156. filters.OmitObjectData = true
  157. if err := exportProviderEvents(w, &filters); err != nil {
  158. panic(http.ErrAbortHandler)
  159. }
  160. return
  161. }
  162. data, _, _, err := plugin.Handler.SearchProviderEvents(&filters)
  163. if err != nil {
  164. sendAPIResponse(w, r, err, "", getRespStatus(err))
  165. return
  166. }
  167. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  168. w.Write(data) //nolint:errcheck
  169. }
  170. func exportFsEvents(w http.ResponseWriter, filters *eventsearcher.FsEventSearch) error {
  171. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=fslogs-%s.csv", time.Now().Format("2006-01-02T15-04-05")))
  172. w.Header().Set("Content-Type", "text/csv")
  173. w.Header().Set("Accept-Ranges", "none")
  174. w.WriteHeader(http.StatusOK)
  175. csvWriter := csv.NewWriter(w)
  176. ev := fsEvent{}
  177. err := csvWriter.Write(ev.getCSVHeader())
  178. if err != nil {
  179. return err
  180. }
  181. results := make([]fsEvent, 0, filters.Limit)
  182. for {
  183. data, _, _, err := plugin.Handler.SearchFsEvents(filters)
  184. if err != nil {
  185. return err
  186. }
  187. if err := json.Unmarshal(data, &results); err != nil {
  188. return err
  189. }
  190. for _, event := range results {
  191. if err := csvWriter.Write(event.getCSVData()); err != nil {
  192. return err
  193. }
  194. }
  195. if len(results) == 0 || len(results) < filters.Limit {
  196. break
  197. }
  198. filters.StartTimestamp = results[len(results)-1].Timestamp
  199. filters.ExcludeIDs = []string{results[len(results)-1].ID}
  200. results = nil
  201. }
  202. csvWriter.Flush()
  203. return csvWriter.Error()
  204. }
  205. func exportProviderEvents(w http.ResponseWriter, filters *eventsearcher.ProviderEventSearch) error {
  206. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=providerlogs-%s.csv", time.Now().Format("2006-01-02T15-04-05")))
  207. w.Header().Set("Content-Type", "text/csv")
  208. w.Header().Set("Accept-Ranges", "none")
  209. w.WriteHeader(http.StatusOK)
  210. ev := providerEvent{}
  211. csvWriter := csv.NewWriter(w)
  212. err := csvWriter.Write(ev.getCSVHeader())
  213. if err != nil {
  214. return err
  215. }
  216. results := make([]providerEvent, 0, filters.Limit)
  217. for {
  218. data, _, _, err := plugin.Handler.SearchProviderEvents(filters)
  219. if err != nil {
  220. return err
  221. }
  222. if err := json.Unmarshal(data, &results); err != nil {
  223. return err
  224. }
  225. for _, event := range results {
  226. if err := csvWriter.Write(event.getCSVData()); err != nil {
  227. return err
  228. }
  229. }
  230. if len(results) == 0 || len(results) < filters.Limit {
  231. break
  232. }
  233. filters.StartTimestamp = results[len(results)-1].Timestamp
  234. filters.ExcludeIDs = []string{results[len(results)-1].ID}
  235. results = nil
  236. }
  237. csvWriter.Flush()
  238. return csvWriter.Error()
  239. }
  240. func getRoleFilterForEventSearch(r *http.Request, defaultValue string) string {
  241. if defaultValue != "" {
  242. return defaultValue
  243. }
  244. return r.URL.Query().Get("role")
  245. }
  246. type fsEvent struct {
  247. ID string `json:"id"`
  248. Timestamp int64 `json:"timestamp"`
  249. Action string `json:"action"`
  250. Username string `json:"username"`
  251. FsPath string `json:"fs_path"`
  252. FsTargetPath string `json:"fs_target_path,omitempty"`
  253. VirtualPath string `json:"virtual_path"`
  254. VirtualTargetPath string `json:"virtual_target_path,omitempty"`
  255. SSHCmd string `json:"ssh_cmd,omitempty"`
  256. FileSize int64 `json:"file_size,omitempty"`
  257. Status int `json:"status"`
  258. Protocol string `json:"protocol"`
  259. IP string `json:"ip,omitempty"`
  260. SessionID string `json:"session_id"`
  261. FsProvider int `json:"fs_provider"`
  262. Bucket string `json:"bucket,omitempty"`
  263. Endpoint string `json:"endpoint,omitempty"`
  264. OpenFlags int `json:"open_flags,omitempty"`
  265. Role string `json:"role,omitempty"`
  266. InstanceID string `json:"instance_id,omitempty"`
  267. }
  268. func (e *fsEvent) getCSVHeader() []string {
  269. return []string{"Time", "Action", "Path", "Size", "Status", "User", "Protocol",
  270. "IP", "SSH command"}
  271. }
  272. func (e *fsEvent) getCSVData() []string {
  273. timestamp := time.Unix(0, e.Timestamp).UTC()
  274. var pathInfo strings.Builder
  275. pathInfo.Write([]byte(e.VirtualPath))
  276. if e.VirtualTargetPath != "" {
  277. pathInfo.WriteString(" => ")
  278. pathInfo.WriteString(e.VirtualTargetPath)
  279. }
  280. var status string
  281. switch e.Status {
  282. case 1:
  283. status = "OK"
  284. case 2:
  285. status = "KO"
  286. case 3:
  287. status = "Quota exceeded"
  288. }
  289. var fileSize string
  290. if e.FileSize > 0 {
  291. fileSize = util.ByteCountIEC(e.FileSize)
  292. }
  293. return []string{timestamp.Format(time.RFC3339Nano), e.Action, pathInfo.String(),
  294. fileSize, status, e.Username, e.Protocol, e.IP, e.SSHCmd}
  295. }
  296. type providerEvent struct {
  297. ID string `json:"id"`
  298. Timestamp int64 `json:"timestamp"`
  299. Action string `json:"action"`
  300. Username string `json:"username"`
  301. IP string `json:"ip,omitempty"`
  302. ObjectType string `json:"object_type"`
  303. ObjectName string `json:"object_name"`
  304. ObjectData []byte `json:"object_data"`
  305. Role string `json:"role,omitempty"`
  306. InstanceID string `json:"instance_id,omitempty"`
  307. }
  308. func (e *providerEvent) getCSVHeader() []string {
  309. return []string{"Time", "Action", "Object Type", "Object Name", "User", "IP"}
  310. }
  311. func (e *providerEvent) getCSVData() []string {
  312. timestamp := time.Unix(0, e.Timestamp).UTC()
  313. return []string{timestamp.Format(time.RFC3339Nano), e.Action, e.ObjectType, e.ObjectName,
  314. e.Username, e.IP}
  315. }