Skip to content
Snippets Groups Projects
banfile.go 4.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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
    	Reason           int   `json:"reason"`            // 0=other, 1=malware, 2=takedown, 3=tos_violation
    
    	Description     *string `json:"other_description"` // optional
    	MalwareName     *string `json:"malware_name"`      // required, only when Reason == 1
    }
    
    // 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),
    	})
    }