new version
This commit is contained in:
@@ -25,6 +25,10 @@ MAX_STORAGE_USAGE=0.5
|
||||
# In milliseconds. Default and hard maximum is 48 hours (172800000).
|
||||
FILE_TTL_MS=172800000
|
||||
|
||||
# Length in characters of the upload/download key token (a-z0-9).
|
||||
# Default 8. Longer keys are harder to brute-force.
|
||||
KEY_LENGTH=8
|
||||
|
||||
# Brute-force protection: rate limit per client IP on download and
|
||||
# deletion endpoints. Set RATE_LIMIT_ENABLED=0 to disable.
|
||||
RATE_LIMIT_ENABLED=1
|
||||
|
||||
@@ -65,6 +65,7 @@ STORAGE_DIR=/opt/filehost/storage
|
||||
MAX_FILE_SIZE=10GB # must be <= proxy upload limit
|
||||
FILE_TTL_MS=172800000 # expire files after 48 hours (hard maximum)
|
||||
MAX_STORAGE_USAGE=0.5 # block uploads when >50% of the disk is used
|
||||
KEY_LENGTH=8 # length of the upload/download key token
|
||||
RATE_LIMIT_ENABLED=1
|
||||
TRUST_PROXY=1 # proxy sets X-Forwarded-For
|
||||
```
|
||||
|
||||
@@ -51,6 +51,7 @@ cp .env.example .env
|
||||
| `MAX_FILE_SIZE` | `10GB` | Max upload size. Units: `B`, `KB`, `MB`, `GB`, `TB` (or plain int). |
|
||||
| `MAX_STORAGE_USAGE` | `0.5` | Max fraction (0.0–1.0) of the storage disk that may be used; uploads are rejected with `503 try again later` above this. |
|
||||
| `FILE_TTL_MS` | `172800000` | Milliseconds before an upload expires and is deleted (48 hours, hard maximum). |
|
||||
| `KEY_LENGTH` | `8` | Length in characters of the upload/download key token (a-z0-9). |
|
||||
| `RATE_LIMIT_ENABLED` | `1` | Enable per-IP rate limiting on `/(key)` and `/api/files/`. |
|
||||
| `RATE_LIMIT_MAX` | `300` | Max requests per IP per window. |
|
||||
| `RATE_LIMIT_WINDOW_MS` | `60000` | Rate-limit window length in ms. |
|
||||
|
||||
@@ -58,6 +58,12 @@ in
|
||||
description = "Maximum fraction (0.0-1.0) of the storage filesystem that may be used; uploads are rejected above this.";
|
||||
};
|
||||
|
||||
keyLength = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 8;
|
||||
description = "Length in characters of the upload/download key token.";
|
||||
};
|
||||
|
||||
publicBaseUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
@@ -134,6 +140,7 @@ in
|
||||
"MAX_FILE_SIZE=${cfg.maxFileSize}"
|
||||
"FILE_TTL_MS=${toString cfg.fileTtlMs}"
|
||||
"MAX_STORAGE_USAGE=${toString cfg.maxStorageUsage}"
|
||||
"KEY_LENGTH=${toString cfg.keyLength}"
|
||||
]
|
||||
++ lib.optionals (cfg.publicBaseUrl != null) [ "PUBLIC_BASE_URL=${cfg.publicBaseUrl}" ]
|
||||
++ lib.optionals cfg.trustProxy [ "TRUST_PROXY=1" ];
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface Config {
|
||||
maxFileSize: number;
|
||||
fileTtlMs: number;
|
||||
maxStorageUsage: number;
|
||||
keyLength: number;
|
||||
publicBaseUrl?: string;
|
||||
rateLimitEnabled: boolean;
|
||||
rateLimitMax: number;
|
||||
@@ -80,6 +81,11 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
|
||||
|
||||
const trustProxy = boolFromEnv(env.TRUST_PROXY, false);
|
||||
|
||||
const keyLength = Number(env.KEY_LENGTH ?? "8");
|
||||
if (!Number.isInteger(keyLength) || keyLength < 4 || keyLength > 64) {
|
||||
throw new Error(`Invalid KEY_LENGTH: ${env.KEY_LENGTH}`);
|
||||
}
|
||||
|
||||
let publicBaseUrl: string | undefined;
|
||||
const rawBase = env.PUBLIC_BASE_URL?.trim();
|
||||
if (rawBase) {
|
||||
@@ -97,6 +103,7 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
|
||||
maxFileSize,
|
||||
fileTtlMs,
|
||||
maxStorageUsage,
|
||||
keyLength,
|
||||
publicBaseUrl,
|
||||
rateLimitEnabled: boolFromEnv(env.RATE_LIMIT_ENABLED, true),
|
||||
rateLimitMax,
|
||||
|
||||
+11
-5
@@ -1,11 +1,17 @@
|
||||
import { createHash, createHmac, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
||||
|
||||
export const KEY_LENGTH = 8;
|
||||
export let KEY_LENGTH = 8;
|
||||
|
||||
// Keys are lowercase alphanumeric only (a-z0-9), 8 characters.
|
||||
export const KEY_PATTERN = /^[a-z0-9]{8}$/;
|
||||
// The id and the download key are the same 12-char token.
|
||||
export const ID_PATTERN = KEY_PATTERN;
|
||||
// Keys are lowercase alphanumeric only (a-z0-9).
|
||||
export let KEY_PATTERN: RegExp = /^[a-z0-9]{8}$/;
|
||||
// The id and the download key are the same token.
|
||||
export let ID_PATTERN = KEY_PATTERN;
|
||||
|
||||
export function setKeyLength(length: number): void {
|
||||
KEY_LENGTH = length;
|
||||
KEY_PATTERN = new RegExp(`^[a-z0-9]{${length}}$`);
|
||||
ID_PATTERN = KEY_PATTERN;
|
||||
}
|
||||
|
||||
const KEY_CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const CHARSET_SIZE = KEY_CHARSET.length;
|
||||
|
||||
+2
-1
@@ -7,10 +7,11 @@ import { handleAuth } from "./routes/auth";
|
||||
import { handleDelete } from "./routes/delete";
|
||||
import { handleDownload } from "./routes/download";
|
||||
import { handleUpload } from "./routes/upload";
|
||||
import { KEY_PATTERN } from "./security";
|
||||
import { KEY_PATTERN, setKeyLength } from "./security";
|
||||
import { Storage } from "./storage";
|
||||
|
||||
const cfg = loadConfig();
|
||||
setKeyLength(cfg.keyLength);
|
||||
|
||||
const storage = new Storage(cfg.storageDir);
|
||||
await storage.init();
|
||||
|
||||
Reference in New Issue
Block a user