From 12854202ca0d8eb82d4b47980d75f65de1ad3f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ana=C3=AFs=20Saraiva?= <53227802+Kisakay@users.noreply.github.com> Date: Sat, 15 Aug 2026 00:40:20 +0200 Subject: [PATCH] Hello :3 --- deploy/nixos/downloadwf.nix | 93 +++++++++++++++++++++++++++++++++++++ scripts/smoke-test.sh | 4 +- src/routes/upload.ts | 3 +- src/security.ts | 17 +++---- 4 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 deploy/nixos/downloadwf.nix diff --git a/deploy/nixos/downloadwf.nix b/deploy/nixos/downloadwf.nix new file mode 100644 index 0000000..932682d --- /dev/null +++ b/deploy/nixos/downloadwf.nix @@ -0,0 +1,93 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.downloadwf; +in +{ + options.services.downloadwf = { + enable = lib.mkEnableOption "the File Drop private file host"; + + user = lib.mkOption { + type = lib.types.str; + description = "System user that runs the service."; + }; + + workingDir = lib.mkOption { + type = lib.types.str; + default = "/srv/download"; + description = "Directory containing the code (src/server.ts)."; + }; + + storageDir = lib.mkOption { + type = lib.types.str; + default = "/srv/download/storage"; + description = "Where files and metadata are persisted."; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Bind address. Use 0.0.0.0 to expose directly."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 3000; + description = "Listen port."; + }; + + maxFileSize = lib.mkOption { + type = lib.types.str; + default = "5GB"; + description = "Maximum upload size (B/KB/MB/GB/TB)."; + }; + + trustProxy = lib.mkEnableOption "trust of X-Forwarded-* headers (behind a reverse proxy)"; + }; + + config = lib.mkIf cfg.enable { + systemd.tmpfiles.rules = [ + "d ${cfg.storageDir} 0755 ${cfg.user} ${cfg.user} -" + ]; + + systemd.services.downloadwf = { + description = "File Drop private file host"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + + path = with pkgs; [ bun ]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.user; + WorkingDirectory = cfg.workingDir; + Environment = [ + "HOME=/home/${cfg.user}" + "PORT=${toString cfg.port}" + "HOST=${cfg.host}" + "STORAGE_DIR=${cfg.storageDir}" + "MAX_FILE_SIZE=${cfg.maxFileSize}" + ] ++ lib.optionals cfg.trustProxy [ "TRUST_PROXY=1" ]; + + # no runtime deps, so no `bun install` needed at startup + ExecStart = "${pkgs.bun}/bin/bun src/server.ts"; + Restart = "on-failure"; + RestartSec = 3; + TimeoutStopSec = 30; + + # hardening + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "full"; + ProtectHome = true; + ReadWritePaths = [ cfg.storageDir ]; + }; + }; + }; +} \ No newline at end of file diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 048ce07..26265ce 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -62,9 +62,7 @@ URL="$(get "$TMP/up.json" url)" check "protected file serves a password form" test \ "$(curl -sS -o /dev/null -w '%{content_type}' "$BASE$URL")" = "text/html; charset=utf-8" check "invalid key returns 404" test \ - "$(curl -sS -o /dev/null -w '%{http_code}' "$BASE/d/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")" = "404" -check "id alone cannot download" test \ - "$(curl -sS -o /dev/null -w '%{http_code}' "$BASE/d/$ID")" = "404" + "$(curl -sS -o /dev/null -w '%{http_code}' "$BASE/d/AAAAAAAAAAAA")" = "404" check "storage directory not served" test \ "$(curl -sS -o /dev/null -w '%{http_code}' "$BASE/storage/")" = "404" diff --git a/src/routes/upload.ts b/src/routes/upload.ts index 1ec110e..06ce7ce 100644 --- a/src/routes/upload.ts +++ b/src/routes/upload.ts @@ -4,7 +4,6 @@ import { getBoundary, MultipartError, multipartParts, type MultipartPart } from import { hashPassword, hashSecret, - newDownloadKey, newFileId, sanitizeMime, sanitizeOriginalName, @@ -32,7 +31,7 @@ export async function handleUpload( if (!boundary || !req.body) return jsonError(400, "expected a multipart/form-data upload"); const id = newFileId(); - const key = id + newDownloadKey(); + const key = id; try { let filename = ""; diff --git a/src/security.ts b/src/security.ts index 1f37e5b..d36dbdb 100644 --- a/src/security.ts +++ b/src/security.ts @@ -1,22 +1,17 @@ import { createHash, createHmac, randomBytes, scryptSync, timingSafeEqual } from "node:crypto"; -export const ID_BYTES = 16; -export const KEY_BYTES = 32; -export const ID_LENGTH = 22; -export const KEY_LENGTH = 65; +export const KEY_BYTES = 9; // 9 bytes base64url -> 12 chars +export const KEY_LENGTH = 12; -export const ID_PATTERN = /^[A-Za-z0-9_-]{22}$/; -export const KEY_PATTERN = /^[A-Za-z0-9_-]{65}$/; +export const KEY_PATTERN = /^[A-Za-z0-9_-]{12}$/; +// The id and the download key are the same 12-char token. +export const ID_PATTERN = KEY_PATTERN; export function randomToken(byteCount: number): string { return randomBytes(byteCount).toString("base64url"); } export function newFileId(): string { - return randomToken(ID_BYTES); -} - -export function newDownloadKey(): string { return randomToken(KEY_BYTES); } @@ -33,7 +28,7 @@ export function secretMatches(storedHash: string, candidate: string): boolean { export function fileIdFromKey(key: string): string | null { if (key.length !== KEY_LENGTH) return null; - return key.slice(0, ID_LENGTH); + return key; } export function sanitizeOriginalName(name: string): string {