Skip to content
Snippets Groups Projects
banfile.go 4.9 KiB
Newer Older
Dean's avatar
Dean committed
package routes

import (
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"regexp"

	"owo.codes/whats-this/api/lib/apierrors"
	"owo.codes/whats-this/api/lib/db"
	"owo.codes/whats-this/api/lib/middleware"

	"github.com/go-chi/render"
	"github.com/pkg/errors"
	"github.com/rs/zerolog/log"
	"github.com/spf13/viper"
)

// banFileBody is the request body for a create user request.
type banFileBody struct {
	ID               string `json:"id"`                // "sha256:abcdef..." or "object:bucket/key"
	QuarantineSample bool   `json:"quarantine_sample"` // move file to quarantine instead of deleting it
Dean's avatar
Dean committed
	Reason           int    `json:"reason"`            // 0=other, 1=malware, 2=takedown, 3=tos_violation
Dean's avatar
Dean committed

Dean's avatar
Dean committed
	Description *string `json:"other_description"` // optional
	MalwareName *string `json:"malware_name"`      // required, only when Reason == 1
Dean's avatar
Dean committed
}

// createUserResponse is the response body for a successful create user request.
type banFileResponse struct {
	Success    bool   `json:"success"`
	StatusCode *int   `json:"errorcode"`
	SHA256     string `json:"sha256"`
}

// File ID regexes
var sha256IDRegex = regexp.MustCompile(`^sha256:([a-fA-F0-9]{64})$`)
var objectIDRegex = regexp.MustCompile(`^object:(.+)/(.*)$`)

// BanFile handles file banning requests. A banned file (identified by SHA256 hash) cannot be uploaded by any user.
// Optionally, existing objects representing this file can be turned into tombstones, and the file deleted from the
// server. If this happens, optionally a sample of the file can be retained in the quarantine bucket.
func BanFile(w http.ResponseWriter, r *http.Request) {
	// Only authorized admin users can use this route
	user := middleware.GetAuthorizedUser(r)
	if user.ID == "" || user.IsBlocked || !user.IsAdmin {
		panic(apierrors.Unauthorized)
	}

	// Parse request body
	decoder := json.NewDecoder(r.Body)
	var body banFileBody
	err := decoder.Decode(&body)
	if err != nil {
		panic(apierrors.InvalidJSONPayload)
	}

	// Try to get the SHA256 hash of the file
	var sha256 []byte
	var object db.Object
	if match := sha256IDRegex.FindStringSubmatch(body.ID); len(match) == 2 {
		sha256, err = hex.DecodeString(match[1])
		if err != nil {
			panic(apierrors.BadFileID)
		}
	} else if match := objectIDRegex.FindStringSubmatch(body.ID); len(match) == 3 {
		object, err = db.GetObject(match[1], match[2])
		switch {
		case errors.Cause(err) == sql.ErrNoRows:
			panic(apierrors.NoObjectFound)
		case err != nil:
			log.Error().Err(err).Msg("failed to get object")
			panic(apierrors.InternalServerError)
		}
		if object.Type != 0 { // file
			panic(apierrors.BadFileID)
		}
		sha256 = object.SHA256HashBytes
	} else {
		panic(apierrors.BadFileID)
	}

	// Validate request body
	if body.Reason < 0 || body.Reason > 3 {
		panic(apierrors.InvalidBanFileReason)
	}
	if body.Reason == 1 && (body.MalwareName == nil || *body.MalwareName == "") {
		panic(apierrors.InvalidBanFileMalwareName)
	}

	// Check if file is already banned
	exists, err := db.CheckIfFileBanExists(sha256)
	if err != nil {
		log.Error().Err(err).Msg("failed to check if file ban exists")
		panic(apierrors.InternalServerError)
	}
	if exists {
		panic(apierrors.FileIsAlreadyBanned)
	}

	// Quarantine file if specified
	if body.QuarantineSample {
		// Move object to quarantine
		sha256String := hex.EncodeToString(sha256)
		origPath := filepath.Join(viper.GetString("files.storageLocation"), sha256String)
		destPath := filepath.Join(viper.GetString("files.quarantineLocation"), sha256String)
		err = os.Rename(origPath, destPath)
		if os.IsNotExist(err) {
			panic(apierrors.CannotQuarantineDueToNoMatchingObjects)
		}
		if err != nil {
			log.Error().Err(err).Msg("failed to move file to quarantine")
			panic(apierrors.InternalServerError)
		}
	}

	// Delete objects referencing the object
	reason := "file banned by administrator by SHA256 hash"
	switch body.Reason {
	case 1: // malware
		reason = fmt.Sprintf("file banned for containing malware (%s)", *body.MalwareName)
	case 2: // takedown
		reason += " due to a takedown request"
	case 3: // tos_violation
		reason += " due to a TOS violation"
	}
	if body.Description != nil && *body.Description != "" {
		reason += ": " + *body.Description
	}
	if len(reason) > 256 {
		reason = reason[:253] + "..."
	}
	err = db.UpdateObjectToTombstoneBySHA256Hash(sha256, &reason, true, body.QuarantineSample)
	if err != nil {
		log.Error().Err(err).Msg("failed to tombstone existing objects")
		panic(apierrors.InternalServerError)
	}

	// Insert file ban
	err = db.InsertFileBan(sha256, body.QuarantineSample, body.Reason, body.Description, body.MalwareName)
	if err != nil {
		log.Error().Err(err).Msg("failed to insert user into database")
		panic(apierrors.InternalServerError)
	}

	// Return response
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	render.JSON(w, r, banFileResponse{
		Success:    true,
		StatusCode: nil,
		SHA256:     hex.EncodeToString(sha256),
	})
}