Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • whats-this/api
  • spotlightishere/api
  • bramhaag/api
  • easrng/api
4 results
Show changes
Commits on Source (29)
Showing with 1789 additions and 163 deletions
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.go]
indent_style = tab
indent_size = 8
tab_width = 8
FROM golang:alpine
FROM golang:1.21-alpine
COPY go.mod /git/owo.codes/whats-this/api/
COPY go.sum /git/owo.codes/whats-this/api/
......@@ -7,9 +7,9 @@ COPY lib /git/owo.codes/whats-this/api/lib
RUN apk add --no-cache --virtual .build-deps git build-base && \
cd /git/owo.codes/whats-this/api && \
go mod download && \
go build main.go && \
apk del .build-deps
WORKDIR /git/owo.codes/whats-this/api
ENTRYPOINT ["./main"]
......@@ -14,6 +14,18 @@ resultURL = "https://awau.moe/"
[pomf]
maxFilesPerUpload = 3
[files]
tempLocation = "/var/data/buckets/_temp"
storageLocation = "/var/data/buckets/public"
storageLocation = "/var/data/buckets/data"
quarantineLocation = "/var/data/buckets/_quarantine"
[fileWebhook]
# URL will receive POST with JSON body containing only a `sha256_hash` key for each NEW file
enable = false
url = "http://localhost:3100/scan"
[ratelimiter]
enable = true
redisURL = "redis://localhost:6379/0"
module owo.codes/whats-this/api
go 1.21
require (
github.com/akamensky/base58 v0.0.0-20210829145138-ce8bf8802e8f
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/render v1.0.3
github.com/go-redis/redis v6.15.9+incompatible
github.com/gofrs/uuid v4.4.0+incompatible
github.com/lib/pq v1.10.9
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.31.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.17.0
)
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/akamensky/base58 v0.0.0-20170920141933-92b0f56f531a
github.com/go-chi/chi v4.0.1+incompatible
github.com/go-chi/render v1.0.1
github.com/go-redis/redis v6.15.1+incompatible
github.com/gofrs/uuid v3.2.0+incompatible
github.com/lib/pq v1.0.0
github.com/onsi/ginkgo v1.7.0 // indirect
github.com/onsi/gomega v1.4.3 // indirect
github.com/pkg/errors v0.8.1
github.com/rs/zerolog v1.11.0
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.1
github.com/ajg/form v1.5.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.18.1 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
This diff is collapsed.
......@@ -43,6 +43,27 @@ var (
// InsufficientTokens is a 429 too many requests error.
InsufficientTokens = APIError{false, 429, "too many requests", false}
// BadFileID is a 400 bad request error.
BadFileID = APIError{false, 400, "bad file ID", false}
// InvalidBanFileReason is a 400 bad request error.
InvalidBanFileReason = APIError{false, 400, "invalid reason, must be an integer from 0 to 3 inclusive", false}
// InvalidBanFileMalwareName is a 400 bad request error.
InvalidBanFileMalwareName = APIError{false, 400, "malware_name is required when reason is 1 (malware)", false}
// FileIsAlreadyBanned is a 409 conflict error.
FileIsAlreadyBanned = APIError{false, 409, "file is already banned", false}
// CannotQuarantineDueToNoMatchingObjects is a 404 not found error.
CannotQuarantineDueToNoMatchingObjects = APIError{false, 404, "cannot quarantine due to no matching objects", false}
// FileIsNotBanned is a 404 not found error.
FileIsNotBanned = APIError{false, 404, "specified file is not banned", false}
// InvalidObjectFilter is a 400 bad request error.
InvalidObjectFilter = APIError{false, 400, `invalid filter, must be "", "files" or "links"`, false}
)
// Pomf errors
......@@ -64,6 +85,9 @@ var (
// InternalServerError is a 500 internal server error.
InternalServerError = APIError{false, 500, "internal server error", false}
// FileIsBanned is a 409 conflict error.
FileIsBanned = APIError{false, 409, "file is banned", false}
)
// Polr errors
......
......@@ -14,18 +14,34 @@ type User struct {
// Object represents an object from the database.
type Object struct {
Bucket string `json:"bucket"`
Key string `json:"key"`
Directory string `json:"dir"`
Type int `json:"type"`
DestURL *string `json:"dest_url"`
ContentType *string `json:"content_type"`
ContentLength *int64 `json:"content_length"`
CreatedAt time.Time `json:"created_at"`
DeletedAt *time.Time `json:"deleted_at"`
DeleteReason *string `json:"delete_reason"`
AssociatedUser *string `json:"-"`
MD5Hash *string `json:"md5_hash"`
Bucket string `json:"bucket"`
Key string `json:"key"`
Directory string `json:"dir"`
Type int `json:"type"`
DestURL *string `json:"dest_url"`
ContentType *string `json:"content_type"`
ContentLength *int64 `json:"content_length"`
CreatedAt time.Time `json:"created_at"`
DeletedAt *time.Time `json:"deleted_at"`
DeleteReason *string `json:"delete_reason"`
AssociatedUser *string `json:"-"`
MD5HashBytes []byte `json:"-"`
SHA256HashBytes []byte `json:"-"`
AssociatedWithCurrentUser *bool `json:"associated_with_current_user,omitempty"`
// Computed fields
MD5Hash *string `json:"md5_hash"`
SHA256Hash *string `json:"sha256_hash"`
AssociatedWithCurrentUser *bool `json:"associated_with_current_user,omitempty"`
}
// FileBan represents a banned file from the database.
type FileBan struct {
SHA256HashBytes []byte `json:"-"`
DidQuarantine bool `json:"did_quarantine"`
Reason int `json:"reason"`
Description *string `json:"description"`
MalwareName *string `json:"malware_name"`
// Computed fields
SHA256Hash *string `json:"sha256_hash"`
}
......@@ -2,6 +2,7 @@ package db
import (
"database/sql"
"encoding/hex"
"fmt"
"strings"
......@@ -61,18 +62,19 @@ func InsertShortURL(bucket, key, destURL string, associatedUser *string) error {
}
// InsertFile inserts a file object into the database.
func InsertFile(bucket, key, ext, contentType string, contentLength int64, md5Hash string, associatedUser *string) error {
func InsertFile(bucket, key, ext, contentType string, contentLength int64, md5Hash, sha256Hash []byte, associatedUser *string) error {
if !strings.HasPrefix(key, "/") {
key = "/" + key
}
result, err := DB.Exec(insertFile,
bucket+key,
bucket+key+ext,
bucket,
key+ext,
key[1:],
contentType,
contentLength,
md5Hash,
sha256Hash,
associatedUser)
if err != nil {
return err
......@@ -147,7 +149,8 @@ func scanObject(scanner scanner) (Object, error) {
var deletedAt pq.NullTime
var deleteReason sql.NullString
var associatedUser sql.NullString
var md5Hash sql.NullString
var md5Hash []byte
var sha256Hash []byte
err := scanner.Scan(&object.Bucket,
&object.Key,
&object.Directory,
......@@ -159,32 +162,52 @@ func scanObject(scanner scanner) (Object, error) {
&deletedAt,
&deleteReason,
&md5Hash,
&sha256Hash,
&associatedUser)
if err != nil {
return Object{}, errors.Wrap(err, "failed to Scan row from query")
}
if object.Type < 0 || object.Type > 2 {
return Object{}, errors.Wrap(err, "invalid Object scanned from query")
}
if object.Type != 1 {
if md5Hash != nil && len(md5Hash) == 16 {
object.MD5HashBytes = md5Hash
md5String := hex.EncodeToString(md5Hash)
object.MD5Hash = &md5String
}
if sha256Hash != nil && len(sha256Hash) == 32 {
object.SHA256HashBytes = sha256Hash
sha256String := hex.EncodeToString(sha256Hash)
object.SHA256Hash = &sha256String
}
}
if object.Type == 0 {
if !contentType.Valid || !contentLength.Valid || !md5Hash.Valid {
if !contentType.Valid || !contentLength.Valid || object.MD5HashBytes == nil || object.SHA256HashBytes == nil {
return Object{}, errors.Wrap(err, "invalid Object scanned from query")
}
object.ContentType = &contentType.String
object.ContentLength = &contentLength.Int64
object.MD5Hash = &md5Hash.String
} else if object.Type == 1 {
}
if object.Type == 1 {
if !destURL.Valid {
return Object{}, errors.Wrap(err, "invalid Object scanned from query")
}
object.DestURL = &destURL.String
}
if deletedAt.Valid {
object.DeletedAt = &deletedAt.Time
if deleteReason.Valid {
object.DeleteReason = &deleteReason.String
}
} else {
if deleteReason.Valid {
return Object{}, errors.Wrap(err, "invalid Object scanned from query")
if object.Type == 2 {
if deletedAt.Valid {
object.DeletedAt = &deletedAt.Time
if deleteReason.Valid {
object.DeleteReason = &deleteReason.String
}
} else {
if deleteReason.Valid {
return Object{}, errors.Wrap(err, "invalid Object scanned from query")
}
}
} else if deletedAt.Valid || deleteReason.Valid {
return Object{}, errors.Wrap(err, "invalid Object scanned from query")
}
if associatedUser.Valid {
object.AssociatedUser = &associatedUser.String
......@@ -194,13 +217,19 @@ func scanObject(scanner scanner) (Object, error) {
// ListObjectsByAssociatedUser returns all objects (paginated) associated with a
// user.
func ListObjectsByAssociatedUser(userID string, asc bool, offset, limit int) ([]Object, error) {
func ListObjectsByAssociatedUser(userID string, typ int, asc bool, offset, limit int) ([]Object, error) {
objects := []Object{}
order := "DESC"
if asc {
order = "ASC"
}
rows, err := DB.Query(fmt.Sprintf(listObjectsByAssociatedUser, order), userID, limit, offset)
typeFilter := "!= 2"
if typ != -1 {
typeFilter = fmt.Sprintf("= %v", typ)
}
rows, err := DB.Query(fmt.Sprintf(listObjectsByAssociatedUser, typeFilter, order), userID, limit, offset)
if err != nil {
return objects, err
}
......@@ -217,15 +246,163 @@ func ListObjectsByAssociatedUser(userID string, asc bool, offset, limit int) ([]
return objects, nil
}
// CountObjectsByAssociatedUser returns the count of all objects associated with a user.
func CountObjectsByAssociatedUser(userID string, typ int) (int, error) {
typeFilter := "!= 2"
if typ != -1 {
typeFilter = fmt.Sprintf("= %v", typ)
}
var count int
query := fmt.Sprintf(countObjectsByAssociatedUser, typeFilter)
err := DB.QueryRow(query, userID).Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
// GetObject returns an object.
func GetObject(bucket, key string) (Object, error) {
row := DB.QueryRow(getObjectByBucketKey, fmt.Sprintf("%s/%s", bucket, key))
return scanObject(row)
}
// UpdateObjectToTombstone sets an object to be a tombstone.
func UpdateObjectToTombstone(bucket, key string, reason *string) error {
result, err := DB.Exec(updateObjectToTombstone, reason, fmt.Sprintf("%s/%s", bucket, key))
// CheckIfObjectExists returns true if an object exists with the specified hash.
func CheckIfObjectExists(sha256 []byte) (bool, error) {
var count int
err := DB.QueryRow(countOfObjectsBySHA256, sha256).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
// UpdateObjectToTombstoneByBucketKey sets an object to be a tombstone by bucket and key.
func UpdateObjectToTombstoneByBucketKey(bucket, key string, reason *string, retainAssociatedUser bool, retainHashes bool) error {
sql := updateObjectToTombstoneByBucketKey
if retainAssociatedUser {
sql = updateObjectToTombstoneKeepAssociatedUserByBucketKey
if retainHashes {
sql = updateObjectToTombstoneKeepHashesAndAssociatedUserByBucketKey
}
}
result, err := DB.Exec(sql, reason, fmt.Sprintf("%s/%s", bucket, key))
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows != 1 {
return errors.Errorf("unexpected amount of rows affected: expected 1, got %v", rows)
}
return nil
}
// UpdateObjectToTombstoneBySHA256Hash sets all objects with a matching SHA256 hash to be a tombstone.
func UpdateObjectToTombstoneBySHA256Hash(sha256 []byte, reason *string, retainAssociatedUser bool, retainHashes bool) error {
sql := updateObjectToTombstoneBySHA256Hash
if retainAssociatedUser {
sql = updateObjectToTombstoneKeepAssociatedUserBySHA256Hash
if retainHashes {
sql = updateObjectToTombstoneKeepHashesAndAssociatedUserBySHA256Hash
}
}
_, err := DB.Exec(sql, reason, sha256)
if err != nil {
return err
}
return nil
}
// CheckIfFileBanExists returns a boolean specifying if the given key is already
// in use by another object.
func CheckIfFileBanExists(sha256 []byte) (bool, error) {
var count int
err := DB.QueryRow(countOfFileBanBySHA256, sha256).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
// InsertFileBan inserts a file ban into the database.
func InsertFileBan(sha256 []byte, didQuarantine bool, reason int, description, malwareName *string) error {
if reason < 0 || reason > 3 {
return errors.New("invalid file ban reason")
}
if malwareName != nil && *malwareName == "" {
malwareName = nil
}
result, err := DB.Exec(insertFileBan, sha256, didQuarantine, reason, description, malwareName)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows != 1 {
return errors.Errorf("unexpected amount of rows affected: expected 1, got %v", rows)
}
return nil
}
// scanFileBan scans an file_bans row into an FileBan.
func scanFileBan(scanner scanner) (FileBan, error) {
fileBan := FileBan{}
err := scanner.Scan(&fileBan.SHA256HashBytes,
&fileBan.DidQuarantine,
&fileBan.Reason,
&fileBan.Description,
&fileBan.MalwareName)
if err != nil {
return FileBan{}, errors.Wrap(err, "failed to Scan row from query")
}
if fileBan.Reason < 0 || fileBan.Reason > 3 {
return FileBan{}, errors.Wrap(err, "invalid FileBan scanned from query")
}
if fileBan.Reason != 1 && fileBan.MalwareName != nil {
return FileBan{}, errors.Wrap(err, "invalid FileBan scanned from query")
}
if fileBan.SHA256HashBytes != nil && len(fileBan.SHA256HashBytes) == 32 {
sha256String := hex.EncodeToString(fileBan.SHA256HashBytes)
fileBan.SHA256Hash = &sha256String
}
return fileBan, nil
}
// ListBannedFiles returns all banned files.
func ListBannedFiles() ([]FileBan, error) {
bannedFiles := []FileBan{}
rows, err := DB.Query(listBannedFiles)
if err != nil {
return bannedFiles, err
}
defer rows.Close()
// Scan into bannedFiles
for rows.Next() {
bannedFile, err := scanFileBan(rows)
if err != nil {
return []FileBan{}, err
}
bannedFiles = append(bannedFiles, bannedFile)
}
return bannedFiles, nil
}
// GetBannedFile returns an FileBan.
func GetBannedFile(sha256 []byte) (FileBan, error) {
row := DB.QueryRow(getBannedFileBySHA256Hash, sha256)
return scanFileBan(row)
}
// DeleteBannedFile deletes a banned file from the database, effectively unbanning it.
func DeleteBannedFile(sha256 []byte) error {
result, err := DB.Exec(deleteFileBan, sha256)
if err != nil {
return err
}
......
......@@ -36,9 +36,9 @@ VALUES
var insertFile = `
INSERT INTO
objects (bucket_key, bucket, key, random_key, dir, content_type, content_length, md5_hash, associated_user)
objects (bucket_key, bucket, key, random_key, dir, content_type, content_length, md5_hash, sha256_hash, associated_user)
VALUES
($1, $2, $3, $4, '/', $5, $6, $7, $8)
($1, $2, $3, $4, '/', $5, $6, $7, $8, $9)
`
var selectUserByUsernameOrEmail = `
......@@ -65,6 +65,16 @@ VALUES
($1, $2)
`
var countObjectsByAssociatedUser = `
SELECT
COUNT(*) as count
FROM
objects
WHERE
associated_user = $1 AND
"type" %v
`
var listObjectsByAssociatedUser = `
SELECT
bucket,
......@@ -78,11 +88,13 @@ SELECT
deleted_at,
delete_reason,
md5_hash,
sha256_hash,
associated_user
FROM
objects
WHERE
associated_user = $1
associated_user = $1 AND
"type" %v
ORDER BY
created_at %s
LIMIT $2
......@@ -102,6 +114,7 @@ SELECT
deleted_at,
delete_reason,
md5_hash,
sha256_hash,
associated_user
FROM
objects
......@@ -110,7 +123,17 @@ WHERE
LIMIT 1
`
var updateObjectToTombstone = `
var countOfObjectsBySHA256 = `
SELECT
COUNT(*)
FROM
objects
WHERE
"type" = 0 AND
sha256_hash = $1
`
var updateObjectToTombstoneByBucketKey = `
UPDATE
objects
SET
......@@ -121,7 +144,133 @@ SET
content_length = NULL,
delete_reason = $1,
md5_hash = NULL,
sha256_hash = NULL,
associated_user = NULL
WHERE
bucket_key = $2
`
var updateObjectToTombstoneKeepAssociatedUserByBucketKey = `
UPDATE
objects
SET
type = 2,
deleted_at = CURRENT_TIMESTAMP,
dest_url = NULL,
content_type = NULL,
content_length = NULL,
delete_reason = $1,
md5_hash = NULL,
sha256_hash = NULL
WHERE
bucket_key = $2
`
var updateObjectToTombstoneKeepHashesAndAssociatedUserByBucketKey = `
UPDATE
objects
SET
type = 2,
deleted_at = CURRENT_TIMESTAMP,
dest_url = NULL,
content_type = NULL,
content_length = NULL,
delete_reason = $1
WHERE
bucket_key = $2
`
var updateObjectToTombstoneBySHA256Hash = `
UPDATE
objects
SET
type = 2,
deleted_at = CURRENT_TIMESTAMP,
dest_url = NULL,
content_type = NULL,
content_length = NULL,
delete_reason = $1,
md5_hash = NULL,
sha256_hash = NULL,
associated_user = NULL
WHERE
sha256_hash = $2
`
var updateObjectToTombstoneKeepAssociatedUserBySHA256Hash = `
UPDATE
objects
SET
type = 2,
deleted_at = CURRENT_TIMESTAMP,
dest_url = NULL,
content_type = NULL,
content_length = NULL,
delete_reason = $1,
md5_hash = NULL,
sha256_hash = NULL
WHERE
sha256_hash = $2
`
var updateObjectToTombstoneKeepHashesAndAssociatedUserBySHA256Hash = `
UPDATE
objects
SET
type = 2,
deleted_at = CURRENT_TIMESTAMP,
dest_url = NULL,
content_type = NULL,
content_length = NULL,
delete_reason = $1
WHERE
sha256_hash = $2
`
var countOfFileBanBySHA256 = `
SELECT
COUNT(*) as count
FROM
file_bans
WHERE
sha256_hash = $1
`
var insertFileBan = `
INSERT INTO
file_bans (sha256_hash, did_quarantine, reason, description, malware_name)
VALUES
($1, $2, $3, $4, $5)
`
var listBannedFiles = `
SELECT
sha256_hash,
did_quarantine,
reason,
description,
malware_name
FROM
file_bans
`
var getBannedFileBySHA256Hash = `
SELECT
sha256_hash,
did_quarantine,
reason,
description,
malware_name
FROM
file_bans
WHERE
sha256_hash = $1
LIMIT 1
`
var deleteFileBan = `
DELETE FROM
file_bans
WHERE
sha256_hash = $1
`
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),
})
}
package routes
import (
"database/sql"
"encoding/hex"
"net/http"
"strings"
"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"
)
// bannedFileResponse is the response format for Object.
type bannedFileResponse struct {
Success bool `json:"success"`
Data db.FileBan `json:"data"`
}
// BannedFile returns metadata about a banned file to an administrator.
func BannedFile(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)
}
// Get the SHA256 hash
sha256String := r.URL.Path
if strings.HasPrefix(sha256String, "/bannedfiles/") {
sha256String = sha256String[13:]
}
sha256, err := hex.DecodeString(sha256String)
if err != nil {
panic(apierrors.BadFileID)
}
// Get the file ban
bannedFile, err := db.GetBannedFile(sha256)
switch {
case errors.Cause(err) == sql.ErrNoRows:
panic(apierrors.FileIsNotBanned)
case err != nil:
log.Error().Err(err).Msg("failed to get FileBan")
panic(apierrors.InternalServerError)
}
// Return response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
render.JSON(w, r, bannedFileResponse{true, bannedFile})
}
package routes
import (
"encoding/hex"
"net/http"
"strings"
"owo.codes/whats-this/api/lib/apierrors"
"owo.codes/whats-this/api/lib/db"
"owo.codes/whats-this/api/lib/middleware"
"github.com/rs/zerolog/log"
)
// DeleteBannedFile deletes a FileBan.
func DeleteBannedFile(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)
}
// Get the SHA256 hash
sha256String := r.URL.Path
if strings.HasPrefix(sha256String, "/bannedfiles/") {
sha256String = sha256String[13:]
}
sha256, err := hex.DecodeString(sha256String)
if err != nil {
panic(apierrors.BadFileID)
}
// Get the file ban
banned, err := db.CheckIfFileBanExists(sha256)
if err != nil {
log.Error().Err(err).Msg("failed to check if file is banned in DeleteBannedFile")
panic(apierrors.InternalServerError)
}
if !banned {
panic(apierrors.FileIsNotBanned)
}
// Unban the file
err = db.DeleteBannedFile(sha256)
if err != nil {
log.Error().Err(err).Msg("failed to delete file ban")
panic(apierrors.InternalServerError)
}
// Return success response
w.WriteHeader(http.StatusNoContent)
}
......@@ -70,9 +70,11 @@ func DeleteObject(w http.ResponseWriter, r *http.Request) {
// Determine delete reason
deleteReason := defaultDeleteReason
deletedByAdmin := false
if user.IsAdmin {
if object.AssociatedUser != nil && *object.AssociatedUser != user.ID {
deleteReason = adminDeleteReason
deletedByAdmin = true
}
if delReason := r.URL.Query().Get("reason"); delReason != "" {
deleteReason = delReason
......@@ -80,7 +82,7 @@ func DeleteObject(w http.ResponseWriter, r *http.Request) {
}
// Mark as tombstone
err = db.UpdateObjectToTombstone(viper.GetString("database.objectBucket"), key, &deleteReason)
err = db.UpdateObjectToTombstoneByBucketKey(viper.GetString("database.objectBucket"), key, &deleteReason, deletedByAdmin, false)
if err != nil {
log.Error().Err(err).Msg("failed to set object to tombstone")
panic(apierrors.InternalServerError)
......@@ -88,12 +90,20 @@ func DeleteObject(w http.ResponseWriter, r *http.Request) {
// Delete file from disk if type was 0
if object.Type == 0 {
destPath := filepath.Join(viper.GetString("pomf.storageLocation"), object.Key)
err = os.Remove(destPath)
exists, err := db.CheckIfObjectExists(object.SHA256HashBytes)
if err != nil {
log.Error().Err(err).Msg("failed to delete object file from disk")
log.Error().Err(err).Str("hash", *object.SHA256Hash).Msg("failed to check if file exists")
panic(apierrors.InternalServerError)
}
if !exists {
destPath := filepath.Join(viper.GetString("files.storageLocation"), *object.SHA256Hash)
err = os.Remove(destPath)
if err != nil {
log.Error().Err(err).Msg("failed to delete object file from disk")
panic(apierrors.InternalServerError)
}
}
}
// Return tombstone response
......
package routes
import (
"fmt"
"net/http"
"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/rs/zerolog/log"
)
// listBannedFilesResponse is the response format for ListBannedFiles.
type listBannedFilesResponse struct {
Success bool `json:"success"`
Data []db.FileBan `json:"data"`
}
// ListBannedFiles returns a list of all banned files to an administrator.
func ListBannedFiles(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)
}
// Determine output format
format := r.URL.Query().Get("format")
if format != "json" && format != "text" {
format = "json"
}
// Get the data
bannedFiles, err := db.ListBannedFiles()
if err != nil {
log.Error().Err(err).Msg("failed to list banned files")
panic(apierrors.InternalServerError)
}
// Return response
switch format {
case "json":
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
render.JSON(w, r, listBannedFilesResponse{true, bannedFiles})
case "text":
w.Header().Set("Content-Type", "text/plain; charset=utf8")
w.WriteHeader(http.StatusOK)
for _, bannedFile := range bannedFiles {
fmt.Fprint(w, bannedFile.SHA256Hash)
}
}
}
......@@ -14,13 +14,20 @@ import (
"github.com/spf13/viper"
)
// Maximum objects per page
const maxLimit = 100
const (
// Maximum objects per page
maxLimit = 100
// filter keys for file vs short link.
filterFiles = "file"
filterLinks = "link"
)
// listObjectsResponse is the response format for ListObjects.
type listObjectsResponse struct {
Success bool `json:"success"`
Data []db.Object `json:"data"`
Success bool `json:"success"`
TotalObjects int `json:"total_objects"`
Data []db.Object `json:"data"`
}
// ListObjects returns a paginated list of all objects owned by a user.
......@@ -41,12 +48,17 @@ func ListObjects(w http.ResponseWriter, r *http.Request) {
panic(apierrors.InternalServerError)
}
// Determine offset and limit information
query := r.URL.Query()
l := query.Get("limit")
limit, err := strconv.Atoi(l)
if err != nil || limit < 0 {
panic(apierrors.InvalidOffsetOrLimit)
// Determine offset, limit and filter params
var (
query = r.URL.Query()
limit = maxLimit
l = query.Get("limit")
)
if l != "" {
limit, err = strconv.Atoi(l)
if err != nil || limit < 0 {
panic(apierrors.InvalidOffsetOrLimit)
}
}
if limit > maxLimit {
panic(apierrors.LimitTooLarge)
......@@ -60,9 +72,26 @@ func ListObjects(w http.ResponseWriter, r *http.Request) {
if query.Get("order") == "asc" {
asc = true
}
filter := query.Get("type")
if filter != "" && filter != filterFiles && filter != filterLinks {
panic(apierrors.InvalidObjectFilter)
}
f := -1
if filter == filterFiles {
f = 0
} else if filter == filterLinks {
f = 1
}
// Get the data
objects, err := db.ListObjectsByAssociatedUser(user.ID, asc, offset, limit)
count, err := db.CountObjectsByAssociatedUser(user.ID, f)
if err != nil {
log.Error().Err(err).Msg("failed to count objects for user")
panic(apierrors.InternalServerError)
}
objects, err := db.ListObjectsByAssociatedUser(user.ID, f, asc, offset, limit)
if err != nil {
log.Error().Err(err).Msg("failed to list objects for user")
panic(apierrors.InternalServerError)
......@@ -75,5 +104,5 @@ func ListObjects(w http.ResponseWriter, r *http.Request) {
// Return response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
render.JSON(w, r, listObjectsResponse{true, objects})
render.JSON(w, r, listObjectsResponse{true, count, objects})
}
......@@ -16,7 +16,7 @@ import (
"github.com/spf13/viper"
)
// objectsResponse is the response format for Object.
// objectResponse is the response format for Object.
type objectResponse struct {
Success bool `json:"success"`
Data db.Object `json:"data"`
......@@ -56,6 +56,13 @@ func Object(w http.ResponseWriter, r *http.Request) {
log.Error().Err(err).Msg("failed to get object")
panic(apierrors.InternalServerError)
}
if object.Type == 2 { // tombstone
object.MD5HashBytes = nil
object.SHA256HashBytes = nil
object.MD5Hash = nil
object.SHA256Hash = nil
object.AssociatedUser = nil
}
associatedWithCurrentUser := false
if object.AssociatedUser != nil && *object.AssociatedUser == user.ID {
associatedWithCurrentUser = true
......
package routes
import (
"bytes"
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
......@@ -22,7 +26,7 @@ import (
"github.com/spf13/viper"
)
// Maximum memory per upload.
// Maximum memory per upload before using temporary files.
const maxMemory = 1000 * 1000 * 50 // 50 MB
// File field name for multipart/form-data.
......@@ -55,7 +59,7 @@ type fileResponse struct {
Success bool `json:"success"`
StatusCode int `json:"errorcode,omitempty"`
Description string `json:"description,omitempty"`
Hash string `json:"hash,omitempty"`
Hash string `json:"hash,omitempty"` // MD5 hash, not SHA256 hash
Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"`
Size int64 `json:"size,omitempty"`
......@@ -67,8 +71,13 @@ type fullResponse struct {
Files []fileResponse `json:"files"`
}
// fileWebhookRequest represents the data submitted in a file webhook request.
type fileWebhookRequest struct {
SHA256Hash string `json:"sha256_hash"`
}
// UploadPomf handles Pomf multipart/form-data upload requests.
func UploadPomf(associateObjectsWithUser bool) func(http.ResponseWriter, *http.Request) {
func UploadPomf(associateObjectsWithUser bool, simpleResponse bool) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// Only authorized users can use this route
user := middleware.GetAuthorizedUser(r)
......@@ -168,12 +177,13 @@ func UploadPomf(associateObjectsWithUser bool) func(http.ResponseWriter, *http.R
continue
}
// Write file to MD5 and to temp file
hash := md5.New()
tempPath := filepath.Join(viper.GetString("pomf.tempLocation"), key+ext)
// Write file to MD5 and SHA256 hashers and to temp file
md5Hash := md5.New()
sha256Hash := sha256.New()
tempPath := filepath.Join(viper.GetString("files.tempLocation"), key+ext)
tempFile, err := os.Create(tempPath)
if err != nil {
log.Error().Err(err).Msg("failed to create destination file")
log.Error().Err(err).Msg("failed to create temporary destination file")
if len(files) == 1 {
panic(apierrors.InternalServerError)
}
......@@ -185,11 +195,11 @@ func UploadPomf(associateObjectsWithUser bool) func(http.ResponseWriter, *http.R
})
continue
}
writer := io.MultiWriter(hash, tempFile)
writer := io.MultiWriter(md5Hash, sha256Hash, tempFile)
_, err = io.Copy(writer, f)
tempFile.Close()
if err != nil {
log.Error().Err(err).Msg("failed to write to MD5 hasher and temporary path")
log.Error().Err(err).Msg("failed to write to hashers and temporary path")
err = os.Remove(tempPath)
if err != nil {
log.Error().Err(err).Msg("failed to delete temporary file after error")
......@@ -206,33 +216,105 @@ func UploadPomf(associateObjectsWithUser bool) func(http.ResponseWriter, *http.R
continue
}
// Move file to the destination
destPath := filepath.Join(viper.GetString("pomf.storageLocation"), key+ext)
err = os.Rename(tempPath, destPath)
// Get checksums
md5Bytes := md5Hash.Sum(nil)
sha256Bytes := sha256Hash.Sum(nil)
// Check if file is banned
banned, err := db.CheckIfFileBanExists(sha256Bytes)
if err != nil {
log.Error().Err(err).Msg("failed to check if file is banned in UploadPomf")
panic(apierrors.InternalServerError)
}
if banned {
if len(files) == 1 {
panic(apierrors.FileIsBanned)
}
fileResponses = append(fileResponses, fileResponse{
Success: false,
StatusCode: 409,
Description: "file is banned",
Name: file.Filename,
})
continue
}
// Check if destination file exists, if not move temporary file to destination
destPath := filepath.Join(viper.GetString("files.storageLocation"), hex.EncodeToString(sha256Bytes))
_, err = os.Stat(destPath)
if err != nil {
log.Error().Err(err).Msg("failed to move file to the destination")
if !os.IsNotExist(err) {
log.Error().Err(err).Msg("failed to check if destination file exists")
err = os.Remove(tempPath)
if err != nil {
log.Error().Err(err).Msg("failed to delete temporary file after error")
}
if len(files) == 1 {
panic(apierrors.InternalServerError)
}
fileResponses = append(fileResponses, fileResponse{
Success: false,
StatusCode: 500,
Description: "internal server error",
Name: file.Filename,
})
}
// Move file to the destination
err = os.Rename(tempPath, destPath)
if err != nil {
log.Error().Err(err).Msg("failed to move file to the destination")
err = os.Remove(tempPath)
if err != nil {
log.Error().Err(err).Msg("failed to delete temporary file after error")
}
if len(files) == 1 {
panic(apierrors.InternalServerError)
}
fileResponses = append(fileResponses, fileResponse{
Success: false,
StatusCode: 500,
Description: "internal server error",
Name: file.Filename,
})
}
// Fire fileWebhook in goroutine
if viper.GetBool("fileWebhook.enable") {
go func() {
reqData := fileWebhookRequest{hex.EncodeToString(sha256Bytes)}
m, err := json.Marshal(reqData)
if err != nil {
log.Warn().Err(err).Msg("failed to marshal reqData in fileWebhook goroutine")
return
}
buf := bytes.NewBuffer(m)
resp, err := http.Post(viper.GetString("fileWebhook.url"), "application/json", buf)
if err != nil {
log.Warn().Err(err).Msg("failed to send request in fileWebhook goroutine")
return
}
if resp.StatusCode < 200 || resp.StatusCode > 399 {
log.Warn().Msgf("got unexpected status code from fileWebhook url: %v", resp.StatusCode)
}
}()
}
} else {
// Delete temporary file
err = os.Remove(tempPath)
if err != nil {
log.Error().Err(err).Msg("failed to delete temporary file after error")
log.Warn().Err(err).Msg("failed to delete temporary file")
}
panic(apierrors.InternalServerError)
}
// Get MD5 hash digest
md5Hash := hex.EncodeToString(hash.Sum(nil))
// Insert object into database
var associatedUser *string
if associateObjectsWithUser {
associatedUser = &user.ID
}
err = db.InsertFile(bucket, key, ext, contentType, file.Size, md5Hash, associatedUser)
err = db.InsertFile(bucket, key, ext, contentType, file.Size, md5Bytes, sha256Bytes, associatedUser)
if err != nil {
log.Error().Err(err).Msg("failed to create DB object for file upload")
err = os.Remove(destPath)
if err != nil {
log.Error().Err(err).Msg("failed to delete destination file after error")
}
if len(files) == 1 {
panic(apierrors.InternalServerError)
}
......@@ -247,7 +329,7 @@ func UploadPomf(associateObjectsWithUser bool) func(http.ResponseWriter, *http.R
fileResponses = append(fileResponses, fileResponse{
Success: true,
Hash: md5Hash,
Hash: hex.EncodeToString(md5Bytes),
Name: file.Filename,
URL: key + ext,
Size: file.Size,
......@@ -268,8 +350,24 @@ func UploadPomf(associateObjectsWithUser bool) func(http.ResponseWriter, *http.R
}
// Return response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
render.JSON(w, r, fullResponse{Success: true, Files: fileResponses})
if simpleResponse {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(statusCode)
for _, fResponse := range fileResponses {
if fResponse.Success {
w.Write([]byte(fmt.Sprintf("%s,200\n", fResponse.URL)))
} else {
status := fResponse.StatusCode
if status == 0 {
status = 500
}
w.Write([]byte(fmt.Sprintf(",%v\n", status)))
}
}
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
render.JSON(w, r, fullResponse{Success: true, Files: fileResponses})
}
}
}
......@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
......@@ -15,7 +16,7 @@ import (
"owo.codes/whats-this/api/lib/ratelimiter"
"owo.codes/whats-this/api/lib/routes"
"github.com/go-chi/chi"
chi "github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/pflag"
......@@ -26,7 +27,7 @@ import (
const (
configLocationUnix = "/etc/whats-this/api/config.toml"
shutdownTimeout = 10 * time.Second
version = "1.6.5"
version = "1.7.1"
)
// printConfiguration iterates through a configuration map[string]interface{}
......@@ -63,6 +64,7 @@ func init() {
viper.SetDefault("database.objectBucket", "public")
viper.SetDefault("http.listenAddress", ":49544")
viper.BindPFlag("log.level", flags.Lookup("log-level")) // default is 1 (info)
viper.SetDefault("pomf.maxFilesPerUpload", 3)
viper.SetDefault("ratelimiter.enable", false)
viper.SetDefault("ratelimiter.defaultBucketCapacity", 50)
viper.SetDefault("ratelimiter.bucketExpiryDuration", time.Second*30)
......@@ -72,6 +74,7 @@ func init() {
viper.SetDefault("ratelimiter.listObjectsCost", ratelimiter.ListObjectsCost)
viper.SetDefault("ratelimiter.objectCost", ratelimiter.ObjectCost)
viper.SetDefault("ratelimiter.deleteObjectCost", ratelimiter.DeleteObjectCost)
viper.SetDefault("fileWebhook.enable", false)
// Load configuration file
viper.SetConfigType("toml")
......@@ -125,15 +128,21 @@ func init() {
if viper.GetString("polr.resultURL") == "" {
log.Fatal().Msg("Configuration: polr.resultURL is required")
}
if viper.GetString("pomf.storageLocation") == "" {
log.Fatal().Msg("Configuration: pomf.storageLocation is required")
if viper.GetString("files.quarantineLocation") == "" {
log.Fatal().Msg("Configuration: files.quarantineLocation is required")
}
if viper.GetString("pomf.tempLocation") == "" {
log.Fatal().Msg("Configuration: pomf.tempLocation is required")
if viper.GetString("files.storageLocation") == "" {
log.Fatal().Msg("Configuration: files.storageLocation is required")
}
if viper.GetString("files.tempLocation") == "" {
log.Fatal().Msg("Configuration: files.tempLocation is required")
}
if viper.GetBool("ratelimiter.enable") && viper.GetString("ratelimiter.redisURL") == "" {
log.Fatal().Msg("Configuration: ratelimiter.redisURL is required when ratelimiter is enabled")
}
if viper.GetBool("fileWebhook.enable") && viper.GetString("fileWebhook.url") == "" {
log.Fatal().Msg("Configuration: fileWebhook.url is required when fileWebhook is enabled")
}
}
func main() {
......@@ -166,13 +175,19 @@ func main() {
// Route handlers
r.Get("/shorten/polr", routes.ShortenPolr(false))
r.Get("/shorten/polr/associated", routes.ShortenPolr(true))
r.Post("/upload/pomf", routes.UploadPomf(false))
r.Post("/upload/pomf/associated", routes.UploadPomf(true))
r.Post("/upload/pomf", routes.UploadPomf(false, false))
r.Post("/upload/pomf/associated", routes.UploadPomf(true, false))
r.Post("/upload/simple", routes.UploadPomf(false, true))
r.Post("/upload/simple/associated", routes.UploadPomf(true, true))
r.Post("/users", routes.CreateUser)
r.Get("/users/me", routes.Me)
r.Get("/objects", routes.ListObjects)
r.Get("/objects/*", routes.Object)
r.Delete("/objects/*", routes.DeleteObject)
r.Post("/bannedfiles", routes.BanFile)
r.Get("/bannedfiles", routes.ListBannedFiles)
r.Get("/bannedfiles/*", routes.BannedFile)
r.Delete("/bannedfiles/*", routes.DeleteBannedFile)
// MethodNotAllowed handler
r.MethodNotAllowed(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
......@@ -184,8 +199,9 @@ func main() {
// Create HTTP server on specified listening address
listenAddress := viper.GetString("http.listenAddress")
server := http.Server{
Addr: listenAddress,
Handler: chi.ServerBaseContext(baseCtx, r),
Addr: listenAddress,
Handler: r,
BaseContext: func(_ net.Listener) context.Context { return baseCtx },
}
// Listen for interrupts (^C) and exit gracefully
......
This directory contains the PostgreSQL scripts required to prepare the database
for use with the API server and cdn-origin.
Reference:
- v1.sql: the raw, unprocessed output of pg_dump --no-owner --no-privileges
--schema-only over the main owo database, version 1.
--
-- PostgreSQL database dump
--
-- Dumped from database version 14.5
-- Dumped by pg_dump version 14.5
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: file_bans; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.file_bans (
sha256_hash bytea NOT NULL,
did_quarantine boolean DEFAULT false NOT NULL,
reason integer DEFAULT 0 NOT NULL,
description character varying(1024) DEFAULT NULL::character varying,
malware_name character varying(256) DEFAULT NULL::character varying
);
--
-- Name: objects; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.objects (
bucket_key character varying(1088) NOT NULL,
bucket character varying(20) NOT NULL,
key character varying(1024) NOT NULL,
dir character varying(1024) NOT NULL,
type integer DEFAULT 0 NOT NULL,
backend_file_id character varying(33) DEFAULT NULL::character varying,
dest_url character varying(4096) DEFAULT NULL::character varying,
content_type character varying(255) DEFAULT 'application/octet-stream'::character varying,
content_length integer,
created_at timestamp without time zone DEFAULT now() NOT NULL,
random_key character varying(1024) DEFAULT NULL::character varying,
associated_user character varying(36) DEFAULT NULL::character varying,
deleted_at timestamp without time zone,
delete_reason character varying(256) DEFAULT NULL::character varying,
sha256_hash bytea,
md5_hash bytea
);
--
-- Name: tokens; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.tokens (
id integer NOT NULL,
user_id character varying(255) NOT NULL,
token character varying(255) NOT NULL
);
--
-- Name: tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.tokens_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.tokens_id_seq OWNED BY public.tokens.id;
--
-- Name: users; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.users (
id character varying(255) NOT NULL,
username character varying(255) NOT NULL,
email character varying(255) NOT NULL,
is_admin boolean DEFAULT false NOT NULL,
is_blocked boolean DEFAULT false NOT NULL,
username_lower character varying(255) NOT NULL,
bucket_capacity integer
);
--
-- Name: tokens id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.tokens ALTER COLUMN id SET DEFAULT nextval('public.tokens_id_seq'::regclass);
--
-- Name: file_bans file_bans_sha256_hash_key; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.file_bans
ADD CONSTRAINT file_bans_sha256_hash_key UNIQUE (sha256_hash);
--
-- Name: objects objects_bucket_key_key; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.objects
ADD CONSTRAINT objects_bucket_key_key UNIQUE (bucket_key);
--
-- Name: tokens tokens_pk; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.tokens
ADD CONSTRAINT tokens_pk PRIMARY KEY (id);
--
-- Name: tokens tokens_token_key; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.tokens
ADD CONSTRAINT tokens_token_key UNIQUE (token);
--
-- Name: users username_lower_unique; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT username_lower_unique UNIQUE (username_lower);
--
-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_email_key UNIQUE (email);
--
-- Name: users users_pk; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_pk PRIMARY KEY (id);
--
-- Name: users users_username_key; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_username_key UNIQUE (username);
--
-- Name: objects_associated_user; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX objects_associated_user ON public.objects USING btree (associated_user);
--
-- Name: objects_bucket_random_key; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX objects_bucket_random_key ON public.objects USING btree (bucket, random_key);
--
-- Name: objects_sha256_hash_idx; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX objects_sha256_hash_idx ON public.objects USING btree (sha256_hash);
--
-- PostgreSQL database dump complete
--