123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- // Copyright (C) 2019-2022 Nicola Murino
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published
- // by the Free Software Foundation, version 3.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- package httpd
- import (
- "encoding/csv"
- "encoding/json"
- "fmt"
- "net/http"
- "strconv"
- "strings"
- "time"
- "github.com/sftpgo/sdk/plugin/eventsearcher"
- "github.com/drakkan/sftpgo/v2/internal/dataprovider"
- "github.com/drakkan/sftpgo/v2/internal/plugin"
- "github.com/drakkan/sftpgo/v2/internal/util"
- )
- func getCommonSearchParamsFromRequest(r *http.Request) (eventsearcher.CommonSearchParams, error) {
- c := eventsearcher.CommonSearchParams{}
- c.Limit = 100
- if _, ok := r.URL.Query()["limit"]; ok {
- limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
- if err != nil {
- return c, util.NewValidationError(fmt.Sprintf("invalid limit: %v", err))
- }
- if limit < 1 || limit > 1000 {
- return c, util.NewValidationError(fmt.Sprintf("limit is out of the 1-1000 range: %v", limit))
- }
- c.Limit = limit
- }
- if _, ok := r.URL.Query()["order"]; ok {
- order := r.URL.Query().Get("order")
- if order != dataprovider.OrderASC && order != dataprovider.OrderDESC {
- return c, util.NewValidationError(fmt.Sprintf("invalid order %#v", order))
- }
- if order == dataprovider.OrderASC {
- c.Order = 1
- }
- }
- if _, ok := r.URL.Query()["start_timestamp"]; ok {
- ts, err := strconv.ParseInt(r.URL.Query().Get("start_timestamp"), 10, 64)
- if err != nil {
- return c, util.NewValidationError(fmt.Sprintf("invalid start_timestamp: %v", err))
- }
- c.StartTimestamp = ts
- }
- if _, ok := r.URL.Query()["end_timestamp"]; ok {
- ts, err := strconv.ParseInt(r.URL.Query().Get("end_timestamp"), 10, 64)
- if err != nil {
- return c, util.NewValidationError(fmt.Sprintf("invalid end_timestamp: %v", err))
- }
- c.EndTimestamp = ts
- }
- c.Actions = getCommaSeparatedQueryParam(r, "actions")
- c.Username = r.URL.Query().Get("username")
- c.IP = r.URL.Query().Get("ip")
- c.InstanceIDs = getCommaSeparatedQueryParam(r, "instance_ids")
- c.ExcludeIDs = getCommaSeparatedQueryParam(r, "exclude_ids")
- return c, nil
- }
- func getFsSearchParamsFromRequest(r *http.Request) (eventsearcher.FsEventSearch, error) {
- var err error
- s := eventsearcher.FsEventSearch{}
- s.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)
- if err != nil {
- return s, err
- }
- s.FsProvider = -1
- if _, ok := r.URL.Query()["fs_provider"]; ok {
- provider := r.URL.Query().Get("fs_provider")
- val, err := strconv.Atoi(provider)
- if err != nil {
- return s, util.NewValidationError(fmt.Sprintf("invalid fs_provider: %v", provider))
- }
- s.FsProvider = val
- }
- s.SSHCmd = r.URL.Query().Get("ssh_cmd")
- s.Bucket = r.URL.Query().Get("bucket")
- s.Endpoint = r.URL.Query().Get("endpoint")
- s.Protocols = getCommaSeparatedQueryParam(r, "protocols")
- statuses := getCommaSeparatedQueryParam(r, "statuses")
- for _, status := range statuses {
- val, err := strconv.Atoi(status)
- if err != nil {
- return s, util.NewValidationError(fmt.Sprintf("invalid status: %v", status))
- }
- s.Statuses = append(s.Statuses, int32(val))
- }
- return s, nil
- }
- func getProviderSearchParamsFromRequest(r *http.Request) (eventsearcher.ProviderEventSearch, error) {
- var err error
- s := eventsearcher.ProviderEventSearch{}
- s.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)
- if err != nil {
- return s, err
- }
- s.ObjectName = r.URL.Query().Get("object_name")
- s.ObjectTypes = getCommaSeparatedQueryParam(r, "object_types")
- return s, nil
- }
- func searchFsEvents(w http.ResponseWriter, r *http.Request) {
- r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
- claims, err := getTokenClaims(r)
- if err != nil || claims.Username == "" {
- sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
- return
- }
- filters, err := getFsSearchParamsFromRequest(r)
- if err != nil {
- sendAPIResponse(w, r, err, "", getRespStatus(err))
- return
- }
- filters.Role = getRoleFilterForEventSearch(r, claims.Role)
- if getBoolQueryParam(r, "csv_export") {
- filters.Limit = 100
- if err := exportFsEvents(w, &filters); err != nil {
- panic(http.ErrAbortHandler)
- }
- return
- }
- data, _, _, err := plugin.Handler.SearchFsEvents(&filters)
- if err != nil {
- sendAPIResponse(w, r, err, "", getRespStatus(err))
- return
- }
- w.Header().Set("Content-Type", "application/json; charset=utf-8")
- w.Write(data) //nolint:errcheck
- }
- func searchProviderEvents(w http.ResponseWriter, r *http.Request) {
- r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
- claims, err := getTokenClaims(r)
- if err != nil || claims.Username == "" {
- sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
- return
- }
- var filters eventsearcher.ProviderEventSearch
- if filters, err = getProviderSearchParamsFromRequest(r); err != nil {
- sendAPIResponse(w, r, err, "", getRespStatus(err))
- return
- }
- filters.Role = getRoleFilterForEventSearch(r, claims.Role)
- filters.OmitObjectData = getBoolQueryParam(r, "omit_object_data")
- if getBoolQueryParam(r, "csv_export") {
- filters.Limit = 100
- filters.OmitObjectData = true
- if err := exportProviderEvents(w, &filters); err != nil {
- panic(http.ErrAbortHandler)
- }
- return
- }
- data, _, _, err := plugin.Handler.SearchProviderEvents(&filters)
- if err != nil {
- sendAPIResponse(w, r, err, "", getRespStatus(err))
- return
- }
- w.Header().Set("Content-Type", "application/json; charset=utf-8")
- w.Write(data) //nolint:errcheck
- }
- func exportFsEvents(w http.ResponseWriter, filters *eventsearcher.FsEventSearch) error {
- w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=fslogs-%s.csv", time.Now().Format("2006-01-02T15-04-05")))
- w.Header().Set("Content-Type", "text/csv")
- w.Header().Set("Accept-Ranges", "none")
- w.WriteHeader(http.StatusOK)
- csvWriter := csv.NewWriter(w)
- ev := fsEvent{}
- err := csvWriter.Write(ev.getCSVHeader())
- if err != nil {
- return err
- }
- results := make([]fsEvent, 0, filters.Limit)
- for {
- data, _, _, err := plugin.Handler.SearchFsEvents(filters)
- if err != nil {
- return err
- }
- if err := json.Unmarshal(data, &results); err != nil {
- return err
- }
- for _, event := range results {
- if err := csvWriter.Write(event.getCSVData()); err != nil {
- return err
- }
- }
- if len(results) == 0 || len(results) < filters.Limit {
- break
- }
- filters.StartTimestamp = results[len(results)-1].Timestamp
- filters.ExcludeIDs = []string{results[len(results)-1].ID}
- results = nil
- }
- csvWriter.Flush()
- return csvWriter.Error()
- }
- func exportProviderEvents(w http.ResponseWriter, filters *eventsearcher.ProviderEventSearch) error {
- w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=providerlogs-%s.csv", time.Now().Format("2006-01-02T15-04-05")))
- w.Header().Set("Content-Type", "text/csv")
- w.Header().Set("Accept-Ranges", "none")
- w.WriteHeader(http.StatusOK)
- ev := providerEvent{}
- csvWriter := csv.NewWriter(w)
- err := csvWriter.Write(ev.getCSVHeader())
- if err != nil {
- return err
- }
- results := make([]providerEvent, 0, filters.Limit)
- for {
- data, _, _, err := plugin.Handler.SearchProviderEvents(filters)
- if err != nil {
- return err
- }
- if err := json.Unmarshal(data, &results); err != nil {
- return err
- }
- for _, event := range results {
- if err := csvWriter.Write(event.getCSVData()); err != nil {
- return err
- }
- }
- if len(results) == 0 || len(results) < filters.Limit {
- break
- }
- filters.StartTimestamp = results[len(results)-1].Timestamp
- filters.ExcludeIDs = []string{results[len(results)-1].ID}
- results = nil
- }
- csvWriter.Flush()
- return csvWriter.Error()
- }
- func getRoleFilterForEventSearch(r *http.Request, defaultValue string) string {
- if defaultValue != "" {
- return defaultValue
- }
- return r.URL.Query().Get("role")
- }
- type fsEvent struct {
- ID string `json:"id"`
- Timestamp int64 `json:"timestamp"`
- Action string `json:"action"`
- Username string `json:"username"`
- FsPath string `json:"fs_path"`
- FsTargetPath string `json:"fs_target_path,omitempty"`
- VirtualPath string `json:"virtual_path"`
- VirtualTargetPath string `json:"virtual_target_path,omitempty"`
- SSHCmd string `json:"ssh_cmd,omitempty"`
- FileSize int64 `json:"file_size,omitempty"`
- Status int `json:"status"`
- Protocol string `json:"protocol"`
- IP string `json:"ip,omitempty"`
- SessionID string `json:"session_id"`
- FsProvider int `json:"fs_provider"`
- Bucket string `json:"bucket,omitempty"`
- Endpoint string `json:"endpoint,omitempty"`
- OpenFlags int `json:"open_flags,omitempty"`
- Role string `json:"role,omitempty"`
- InstanceID string `json:"instance_id,omitempty"`
- }
- func (e *fsEvent) getCSVHeader() []string {
- return []string{"Time", "Action", "Path", "Size", "Status", "User", "Protocol",
- "IP", "SSH command"}
- }
- func (e *fsEvent) getCSVData() []string {
- timestamp := time.Unix(0, e.Timestamp).UTC()
- var pathInfo strings.Builder
- pathInfo.Write([]byte(e.VirtualPath))
- if e.VirtualTargetPath != "" {
- pathInfo.WriteString(" => ")
- pathInfo.WriteString(e.VirtualTargetPath)
- }
- var status string
- switch e.Status {
- case 1:
- status = "OK"
- case 2:
- status = "KO"
- case 3:
- status = "Quota exceeded"
- }
- var fileSize string
- if e.FileSize > 0 {
- fileSize = util.ByteCountIEC(e.FileSize)
- }
- return []string{timestamp.Format(time.RFC3339Nano), e.Action, pathInfo.String(),
- fileSize, status, e.Username, e.Protocol, e.IP, e.SSHCmd}
- }
- type providerEvent struct {
- ID string `json:"id"`
- Timestamp int64 `json:"timestamp"`
- Action string `json:"action"`
- Username string `json:"username"`
- IP string `json:"ip,omitempty"`
- ObjectType string `json:"object_type"`
- ObjectName string `json:"object_name"`
- ObjectData []byte `json:"object_data"`
- Role string `json:"role,omitempty"`
- InstanceID string `json:"instance_id,omitempty"`
- }
- func (e *providerEvent) getCSVHeader() []string {
- return []string{"Time", "Action", "Object Type", "Object Name", "User", "IP"}
- }
- func (e *providerEvent) getCSVData() []string {
- timestamp := time.Unix(0, e.Timestamp).UTC()
- return []string{timestamp.Format(time.RFC3339Nano), e.Action, e.ObjectType, e.ObjectName,
- e.Username, e.IP}
- }
|