Newer
Older
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
}
// 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),
})
}