Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feature/allow_insecur…
Browse files Browse the repository at this point in the history
…e_links
  • Loading branch information
benbusby committed Feb 4, 2025
2 parents 9d6485f + d5ce571 commit e0e7422
Show file tree
Hide file tree
Showing 36 changed files with 889 additions and 92 deletions.
25 changes: 25 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: Bug report
about: Report a bug in YeetFile
title: "[BUG] (describe bug)"
labels: bug
assignees: ''

---

**Please check the box indicating where you encountered the bug:**

- [ ] Self-hosted instance
- [ ] Official instance (yeetfile.com)

**Describe the bug**



**If self-hosted, please include non-private environment variables below**

```sh
# Paste environment variables below these lines
# Do not include secret vars, like YEETFILE_DB_XXXX, YEETFILE_SECRET_KEY, etc

```
10 changes: 10 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: Feature request
about: Suggest a new feature for YeetFile
title: "[FEATURE] (describe feature here)"
labels: enhancement
assignees: ''

---

**Describe your requested feature below:**
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Contents
1. [Self-Hosting](#self-hosting)
- [Access](#access)
- [Email Registration](#email-registration)
- [Administration](#administration)
- [Logging](#logging)
1. [CLI Configuration](#cli-configuration)
1. [Development](#development)
1. [Requirements](#requirements)
Expand Down Expand Up @@ -152,6 +154,48 @@ YEETFILE_EMAIL_USER=...
YEETFILE_EMAIL_PASSWORD=...
```

#### Administration

You can declare yourself as the admin of your instance by setting the
`YEETFILE_INSTANCE_ADMIN` environment variable to your YeetFile account ID or
email address.

This will allow you to manage users and their files on the instance. Note that
file names are encrypted, but you will be able to see the following metadata
for each file:

- File ID
- Last Modified
- Size
- Owner ID

#### Logging

Endpoints beginning with `/api/...` should be monitored for error codes to prevent bruteforcing.

For example:

- `/login` is the endpoint for the login web page, this only loads static content
- This will always return a `200` response, since there is nothing sensitive about loading
the login page.
- `/api/login` is the endpoint for submitting credentials
- This can return an error code depending on the failure (i.e. `403` for invalid credentials,
`404` for a non-existent user, etc)

You can limit requests to all `/api` endpoints in a Nginx config, for example, with something like
this:

```nginx
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/m;
// ...
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}
```

## CLI Configuration

The YeetFile CLI tool can be configured using a `config.yml` file in the following path:
Expand Down Expand Up @@ -236,6 +280,10 @@ All environment variables can be defined in a file named `.env` at the root leve
| YEETFILE_TLS_KEY | The SSL key to use for connections | | The string key contents (not a file path) |
| YEETFILE_TLS_CERT | The SSL cert to use for connections | | The string cert contents (not a file path) |
| YEETFILE_ALLOW_INSECURE_LINKS | Allows YeetFile Send links to include the key in a URL param | 0 | `0` (disabled) or `1` (enabled) |
| YEETFILE_INSTANCE_ADMIN | The user ID or email of the user to set as admin | | A valid YeetFile email or account ID |
| YEETFILE_LIMITER_SECONDS | The number of seconds to use in rate limiting repeated requests | 30 | Any number of seconds |
| YEETFILE_LIMITER_ATTEMPTS | The number of attempts to allow before rate limiting | 6 | Any number of requests |
| YEETFILE_LOCKDOWN | Disables anonymous (not logged in) interactions | 0 | `1` to enable lockdown, `0` to allow anonymous usage |

#### Backblaze Environment Variables

Expand Down
18 changes: 14 additions & 4 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ var (
defaultUserSend = utils.GetEnvVarInt64("YEETFILE_DEFAULT_USER_SEND", -1)
maxNumUsers = utils.GetEnvVarInt("YEETFILE_MAX_NUM_USERS", -1)
password = []byte(utils.GetEnvVar("YEETFILE_SERVER_PASSWORD", ""))
allowInsecureLink = utils.GetEnvVarBool("YEETFILE_ALLOW_INSECURE_LINK", false)
allowInsecureLinks = utils.GetEnvVarBool("YEETFILE_ALLOW_INSECURE_LINKS", false)

// Limiter config
limiterSeconds = utils.GetEnvVarInt("YEETFILE_LIMITER_SECONDS", 30)
limiterAttempts = utils.GetEnvVarInt("YEETFILE_LIMITER_ATTEMPTS", 6)

defaultSecret = []byte("yeetfile-debug-secret-key-123456")
secret = utils.GetEnvVarBytesB64("YEETFILE_SERVER_SECRET", defaultSecret)
Expand All @@ -41,7 +45,9 @@ var (
TLSCert = utils.GetEnvVar("YEETFILE_TLS_CERT", "")
TLSKey = utils.GetEnvVar("YEETFILE_TLS_KEY", "")

IsDebugMode = utils.GetEnvVarBool("YEETFILE_DEBUG", false)
IsDebugMode = utils.GetEnvVarBool("YEETFILE_DEBUG", false)
IsLockedDown = utils.GetEnvVarBool("YEETFILE_LOCKDOWN", false)
InstanceAdmin = utils.GetEnvVar("YEETFILE_INSTANCE_ADMIN", "")
)

// =============================================================================
Expand Down Expand Up @@ -116,7 +122,9 @@ type ServerConfig struct {
PasswordHash []byte
ServerSecret []byte
FallbackWebSecret []byte
AllowInsecureLink bool
AllowInsecureLinks bool
LimiterSeconds int
LimiterAttempts int
}

type TemplateConfig struct {
Expand Down Expand Up @@ -171,7 +179,9 @@ func init() {
PasswordHash: passwordHash,
ServerSecret: secret,
FallbackWebSecret: fallbackWebSecret,
AllowInsecureLink: allowInsecureLink,
AllowInsecureLinks: allowInsecureLinks,
LimiterSeconds: limiterSeconds,
LimiterAttempts: limiterAttempts,
}

// Subset of main server config to use in HTML templating
Expand Down
2 changes: 1 addition & 1 deletion backend/db/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var tasks = []CronTask{
{
Name: LimiterTask,
Interval: time.Second,
IntervalAmount: constants.LimiterSeconds,
IntervalAmount: config.YeetFileConfig.LimiterSeconds,
Enabled: true,
TaskFn: func() {}, // Set in InitCronTasks
},
Expand Down
6 changes: 2 additions & 4 deletions backend/db/expiry.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ type FileExpiry struct {
Date time.Time
}

func SetFileExpiry(id string, downloads int, date time.Time) {
func SetFileExpiry(id string, downloads int, date time.Time) error {
s := `INSERT INTO expiry
(id, downloads, date)
VALUES ($1, $2, $3)`
_, err := db.Exec(s, id, downloads, date)
if err != nil {
panic(err)
}
return err
}

func DecrementDownloads(id string) int {
Expand Down
74 changes: 68 additions & 6 deletions backend/db/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ type FileMetadata struct {

// InsertMetadata creates a new metadata entry in the db and returns a unique ID for
// that entry.
func InsertMetadata(chunks int, name string, plaintext bool) (string, error) {
func InsertMetadata(chunks int, ownerID, name string, textOnly bool) (string, error) {
prefix := constants.FileIDPrefix
if plaintext {
if textOnly {
prefix = constants.PlaintextIDPrefix
}

Expand All @@ -43,9 +43,9 @@ func InsertMetadata(chunks int, name string, plaintext bool) (string, error) {
}

s := `INSERT INTO metadata
(id, chunks, filename, b2_id, length)
VALUES ($1, $2, $3, $4, $5)`
_, err := db.Exec(s, id, chunks, name, "", -1)
(id, chunks, filename, b2_id, length, owner_id, modified)
VALUES ($1, $2, $3, $4, $5, $6, $7)`
_, err := db.Exec(s, id, chunks, name, "", -1, ownerID, time.Now().UTC())
if err != nil {
panic(err)
}
Expand All @@ -70,7 +70,7 @@ func MetadataIDExists(id string) bool {
}

func RetrieveMetadata(id string) (FileMetadata, error) {
s := `SELECT m.*, e.downloads, e.date
s := `SELECT m.id, m.chunks, m.filename, m.b2_id, m.length, e.downloads, e.date
FROM metadata m
JOIN expiry e on m.id = e.id
WHERE m.id = $1`
Expand Down Expand Up @@ -146,3 +146,65 @@ func DeleteMetadata(id string) bool {

return true
}

func AdminRetrieveSendMetadata(fileID string) (shared.AdminFileInfoResponse, error) {
var (
id string
name string
length int64
ownerID string
modified time.Time
)

s := `SELECT id, filename, length, owner_id, modified FROM metadata WHERE id=$1`
err := db.QueryRow(s, fileID).Scan(&id, &name, &length, &ownerID, &modified)
return shared.AdminFileInfoResponse{
ID: id,
BucketName: name,
Size: shared.ReadableFileSize(length),
OwnerID: ownerID,
Modified: modified,

RawSize: length,
}, err
}

func AdminFetchSentFiles(userID string) ([]shared.AdminFileInfoResponse, error) {
result := []shared.AdminFileInfoResponse{}

s := `SELECT id, filename, length, owner_id, modified
FROM metadata
WHERE owner_id=$1`

rows, err := db.Query(s, userID)
if err != nil {
return result, err
}

defer rows.Close()
for rows.Next() {
var (
id string
filename string
length int64
ownerID string
modified time.Time
)

err = rows.Scan(&id, &filename, &length, &ownerID, &modified)
if err != nil {
return result, err
}

result = append(result, shared.AdminFileInfoResponse{
ID: id,
BucketName: filename,
Size: shared.ReadableFileSize(length),
OwnerID: ownerID,
Modified: modified,
RawSize: length,
})
}

return result, nil
}
5 changes: 5 additions & 0 deletions backend/db/scripts/migrations/4_send_metadata.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE metadata ADD COLUMN owner_id text DEFAULT '';
UPDATE metadata SET owner_id = '';

ALTER TABLE metadata ADD COLUMN modified TIMESTAMP DEFAULT now();
UPDATE metadata SET modified = now();
12 changes: 12 additions & 0 deletions backend/db/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,18 @@ func CheckUpgradeExpiration() {
}
}

func IsUserAdmin(id string) (bool, error) {
var isAdmin bool
s := `SELECT admin FROM users WHERE id=$1`
err := db.QueryRow(s, id).Scan(&isAdmin)

if err != nil {
return false, err
}

return isAdmin, nil
}

// ExpDateRollover checks to see if the user's upgrade expiration date takes
// place on a day that doesn't exist in other months. If so, the user's transfer
// limit should be upgraded "early". For example:
Expand Down
69 changes: 69 additions & 0 deletions backend/db/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,53 @@ func DeleteVaultFile(id, ownerID string) error {
return err
}

// AdminDeleteFile deletes an entry in the file vault regardless of owner
func AdminDeleteFile(id string) error {
s := `DELETE FROM vault WHERE id=$1 OR ref_id=$1`
_, err := db.Exec(s, id)
return err
}

// AdminFetchVaultFiles fetches all files for a specific user
func AdminFetchVaultFiles(userID string) ([]shared.AdminFileInfoResponse, error) {
response := []shared.AdminFileInfoResponse{}

s := `SELECT id, name, length, owner_id, modified FROM vault WHERE owner_id=$1`
rows, err := db.Query(s, userID)
if err != nil {
log.Printf("Error retrieving files: %v\n", err)
return response, err
}

defer rows.Close()
for rows.Next() {
var (
id string
name string
length int64
ownerID string
modified time.Time
)

err = rows.Scan(&id, &name, &length, &ownerID, &modified)
if err != nil {
return response, err
}

response = append(response, shared.AdminFileInfoResponse{
ID: id,
BucketName: name,
Size: shared.ReadableFileSize(length),
OwnerID: ownerID,
Modified: modified,

RawSize: length,
})
}

return response, err
}

// DeleteSharedFile deletes a shared file from the recipient's vault
func DeleteSharedFile(id, ownerID string) error {
s := `DELETE FROM vault WHERE id=$1 AND owner_id=$2 RETURNING ref_id`
Expand Down Expand Up @@ -439,6 +486,28 @@ func RetrieveFullItemInfo(id, ownerID string) (shared.VaultItemInfo, error) {
}, nil
}

func AdminRetrieveMetadata(fileID string) (shared.AdminFileInfoResponse, error) {
var (
id string
name string
length int64
ownerID string
modified time.Time
)

s := `SELECT id, name, length, owner_id, modified FROM vault WHERE id=$1`
err := db.QueryRow(s, fileID).Scan(&id, &name, &length, &ownerID, &modified)
return shared.AdminFileInfoResponse{
ID: id,
BucketName: name,
Size: shared.ReadableFileSize(length),
OwnerID: ownerID,
Modified: modified,

RawSize: length,
}, err
}

// RetrieveVaultMetadata returns a FileMetadata struct containing a specific
// file's metadata
func RetrieveVaultMetadata(id, ownerID string) (FileMetadata, error) {
Expand Down
Loading

0 comments on commit e0e7422

Please sign in to comment.