2022-07-17 18:16:00 +00:00
|
|
|
// 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/>.
|
|
|
|
|
2021-10-23 13:47:21 +00:00
|
|
|
package httpd
|
|
|
|
|
|
|
|
import (
|
2022-12-07 17:47:38 +00:00
|
|
|
"encoding/csv"
|
|
|
|
"encoding/json"
|
2021-10-23 13:47:21 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
2022-12-07 17:47:38 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
2021-10-23 13:47:21 +00:00
|
|
|
|
2022-01-06 10:54:43 +00:00
|
|
|
"github.com/sftpgo/sdk/plugin/eventsearcher"
|
|
|
|
|
2022-07-24 14:18:54 +00:00
|
|
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/util"
|
2021-10-23 13:47:21 +00:00
|
|
|
)
|
|
|
|
|
2022-01-03 16:02:52 +00:00
|
|
|
func getCommonSearchParamsFromRequest(r *http.Request) (eventsearcher.CommonSearchParams, error) {
|
|
|
|
c := eventsearcher.CommonSearchParams{}
|
2021-10-23 13:47:21 +00:00
|
|
|
c.Limit = 100
|
|
|
|
|
|
|
|
if _, ok := r.URL.Query()["limit"]; ok {
|
|
|
|
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
|
|
if err != nil {
|
2022-01-03 16:02:52 +00:00
|
|
|
return c, util.NewValidationError(fmt.Sprintf("invalid limit: %v", err))
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
if limit < 1 || limit > 1000 {
|
2022-01-03 16:02:52 +00:00
|
|
|
return c, util.NewValidationError(fmt.Sprintf("limit is out of the 1-1000 range: %v", limit))
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
c.Limit = limit
|
|
|
|
}
|
|
|
|
if _, ok := r.URL.Query()["order"]; ok {
|
|
|
|
order := r.URL.Query().Get("order")
|
|
|
|
if order != dataprovider.OrderASC && order != dataprovider.OrderDESC {
|
2022-01-03 16:02:52 +00:00
|
|
|
return c, util.NewValidationError(fmt.Sprintf("invalid order %#v", order))
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
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 {
|
2022-01-03 16:02:52 +00:00
|
|
|
return c, util.NewValidationError(fmt.Sprintf("invalid start_timestamp: %v", err))
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
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 {
|
2022-01-03 16:02:52 +00:00
|
|
|
return c, util.NewValidationError(fmt.Sprintf("invalid end_timestamp: %v", err))
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
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")
|
|
|
|
|
2022-01-03 16:02:52 +00:00
|
|
|
return c, nil
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
|
2022-01-03 16:02:52 +00:00
|
|
|
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
|
|
|
|
}
|
2022-01-06 17:09:49 +00:00
|
|
|
s.FsProvider = -1
|
2022-01-03 16:02:52 +00:00
|
|
|
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
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
s.SSHCmd = r.URL.Query().Get("ssh_cmd")
|
2022-01-03 16:02:52 +00:00
|
|
|
s.Bucket = r.URL.Query().Get("bucket")
|
|
|
|
s.Endpoint = r.URL.Query().Get("endpoint")
|
2021-10-23 13:47:21 +00:00
|
|
|
s.Protocols = getCommaSeparatedQueryParam(r, "protocols")
|
|
|
|
statuses := getCommaSeparatedQueryParam(r, "statuses")
|
|
|
|
for _, status := range statuses {
|
|
|
|
val, err := strconv.Atoi(status)
|
|
|
|
if err != nil {
|
2022-01-03 16:02:52 +00:00
|
|
|
return s, util.NewValidationError(fmt.Sprintf("invalid status: %v", status))
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
s.Statuses = append(s.Statuses, int32(val))
|
|
|
|
}
|
|
|
|
|
2022-01-03 16:02:52 +00:00
|
|
|
return s, nil
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
|
2022-01-03 16:02:52 +00:00
|
|
|
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
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
s.ObjectName = r.URL.Query().Get("object_name")
|
|
|
|
s.ObjectTypes = getCommaSeparatedQueryParam(r, "object_types")
|
2022-01-03 16:02:52 +00:00
|
|
|
return s, nil
|
2021-10-23 13:47:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func searchFsEvents(w http.ResponseWriter, r *http.Request) {
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
2022-12-03 10:45:27 +00:00
|
|
|
claims, err := getTokenClaims(r)
|
|
|
|
if err != nil || claims.Username == "" {
|
|
|
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
2021-10-23 13:47:21 +00:00
|
|
|
|
2022-01-03 16:02:52 +00:00
|
|
|
filters, err := getFsSearchParamsFromRequest(r)
|
2021-10-23 13:47:21 +00:00
|
|
|
if err != nil {
|
|
|
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
|
|
|
return
|
|
|
|
}
|
2022-12-03 10:45:27 +00:00
|
|
|
filters.Role = getRoleFilterForEventSearch(r, claims.Role)
|
2021-10-23 13:47:21 +00:00
|
|
|
|
2022-12-07 17:47:38 +00:00
|
|
|
if getBoolQueryParam(r, "csv_export") {
|
|
|
|
filters.Limit = 100
|
|
|
|
if err := exportFsEvents(w, &filters); err != nil {
|
|
|
|
panic(http.ErrAbortHandler)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-03 16:02:52 +00:00
|
|
|
data, _, _, err := plugin.Handler.SearchFsEvents(&filters)
|
2021-10-23 13:47:21 +00:00
|
|
|
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)
|
2022-12-03 10:45:27 +00:00
|
|
|
claims, err := getTokenClaims(r)
|
|
|
|
if err != nil || claims.Username == "" {
|
|
|
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
2021-10-23 13:47:21 +00:00
|
|
|
|
2022-12-07 17:47:38 +00:00
|
|
|
var filters eventsearcher.ProviderEventSearch
|
|
|
|
if filters, err = getProviderSearchParamsFromRequest(r); err != nil {
|
2021-10-23 13:47:21 +00:00
|
|
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
|
|
|
return
|
|
|
|
}
|
2022-12-03 10:45:27 +00:00
|
|
|
filters.Role = getRoleFilterForEventSearch(r, claims.Role)
|
2022-12-08 09:02:12 +00:00
|
|
|
filters.OmitObjectData = getBoolQueryParam(r, "omit_object_data")
|
2021-10-23 13:47:21 +00:00
|
|
|
|
2022-12-07 17:47:38 +00:00
|
|
|
if getBoolQueryParam(r, "csv_export") {
|
|
|
|
filters.Limit = 100
|
2022-12-08 09:02:12 +00:00
|
|
|
filters.OmitObjectData = true
|
2022-12-07 17:47:38 +00:00
|
|
|
if err := exportProviderEvents(w, &filters); err != nil {
|
|
|
|
panic(http.ErrAbortHandler)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-03 16:02:52 +00:00
|
|
|
data, _, _, err := plugin.Handler.SearchProviderEvents(&filters)
|
2021-10-23 13:47:21 +00:00
|
|
|
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
|
|
|
|
}
|
2022-12-03 10:45:27 +00:00
|
|
|
|
2022-12-07 17:47:38 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2022-12-03 10:45:27 +00:00
|
|
|
func getRoleFilterForEventSearch(r *http.Request, defaultValue string) string {
|
|
|
|
if defaultValue != "" {
|
|
|
|
return defaultValue
|
|
|
|
}
|
|
|
|
return r.URL.Query().Get("role")
|
|
|
|
}
|
2022-12-07 17:47:38 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2022-12-08 09:02:12 +00:00
|
|
|
return []string{timestamp.Format(time.RFC3339Nano), e.Action, pathInfo.String(),
|
2022-12-07 17:47:38 +00:00
|
|
|
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()
|
2022-12-08 09:02:12 +00:00
|
|
|
return []string{timestamp.Format(time.RFC3339Nano), e.Action, e.ObjectType, e.ObjectName,
|
2022-12-07 17:47:38 +00:00
|
|
|
e.Username, e.IP}
|
|
|
|
}
|