simplefiles

A lightweight C CGI web application for managing files.
git clone https://scm.kuandu.systems/git-raw/simplefiles.git
Log | Files | Refs | README | LICENSE

upload.c (14596B)


      1 #include <ctype.h>
      2 #include <dirent.h>
      3 #include <errno.h>
      4 #include <fcntl.h>
      5 #include <limits.h>
      6 #include <stdio.h>
      7 #include <stdlib.h>
      8 #include <string.h>
      9 #include <sys/stat.h>
     10 #include <sys/statvfs.h>
     11 #include <sys/types.h>
     12 #include <unistd.h>
     13 
     14 #include "common.h"
     15 #include "config.h"
     16 #include "http.h"
     17 #include "html.h"
     18 #include "multipart.h"
     19 
     20 #define MAX_DUPLICATES 	1000
     21 #define DEFAULT_UPLOAD_TMP_DIR "/run/simplefiles/uploads"
     22 
     23 /* Helper */
     24 static int do_get(void);
     25 static int do_post(void);
     26 static const char *upload_tmp_dir(void);
     27 
     28 /* Utility */
     29 static off_t repo_quota_bytes(void);
     30 static off_t min_free_bytes(void);
     31 
     32 /* Multipart parsing */
     33 static int open_spool(const char *, char *, size_t);
     34 
     35 /* File handling */
     36 static int open_upload_temp(const char *, char *, size_t);
     37 static int finalize_upload(const char *, const char *, const char *, char *, size_t);
     38 static int check_free_space(const char *dir, off_t required);
     39 static int check_repo_quota(const char *dir, off_t incoming_size);
     40 
     41 /* Utility */
     42 static off_t
     43 repo_quota_bytes(void)
     44 {
     45 	const char *val;
     46 	char *end;
     47 	long long parsed;
     48 
     49 	val = getenv("REPO_QUOTA_BYTES");
     50 
     51 	if (val == NULL || *val == '\0')
     52 		return config_repo_quota();
     53 
     54 	errno = 0;
     55 	parsed = strtoll(val, &end, 10);
     56 
     57 	if (errno != 0 ||
     58 	    end == val ||
     59 	    *end != '\0' ||
     60 	    parsed <= 0)
     61 		return config_repo_quota();
     62 
     63 	return (off_t) parsed;
     64 }
     65 
     66 static off_t
     67 min_free_bytes(void)
     68 {
     69 	const char *val;
     70 	char *ep;
     71 	long long parsed;
     72 
     73 	val = getenv("MIN_FREE_BYTES");
     74 
     75 	if (val == NULL || *val == '\0')
     76 		return DEFAULT_MIN_FREE;
     77 
     78 	errno = 0;
     79 	parsed = strtoll(val, &ep, 10);
     80 
     81 	if (errno != 0 ||
     82 	    ep == val ||
     83 	    *ep != '\0' ||
     84 	    parsed <= 0)
     85 		return DEFAULT_MIN_FREE;
     86 
     87 	return (off_t) parsed;
     88 }
     89 /* File handling. */
     90 static int
     91 open_upload_temp(const char *dir, char *path, size_t len)
     92 {
     93 	int fd;
     94 	struct stat st;
     95 
     96 	if (mkdir(dir, 0700) == -1 && errno != EEXIST) {
     97 		reply("500 Internal Server Error",
     98 		    "Could not create upload directory");
     99 		return -1;
    100 	}
    101 	if (stat(dir, &st) == -1 || !S_ISDIR(st.st_mode)) {
    102 		reply("500 Internal Server Error",
    103 		    "Upload directory is not a directory");
    104 		return -1;
    105 	}
    106 	if (st.st_mode & (S_IWGRP | S_IWOTH)) {
    107 		reply("500 Internal Server Error",
    108 		    "Unsafe upload directory permissions");
    109 		return -1;
    110 	}
    111 	if (snprintf(path, len, "%s/.upload-XXXXXX", dir) >= (int) len) {
    112 		reply("500 Internal Server Error", "Path too long");
    113 		return -1;
    114 	}
    115 	fd = mkstemp(path);
    116 	if (fd == -1) {
    117 		reply("500 Internal Server Error",
    118 		    "Could not create temporary upload");
    119 		return -1;
    120 	}
    121 	return fd;
    122 }
    123 
    124 static int
    125 open_spool(const char *dir, char *path, size_t len)
    126 {
    127 	int fd;
    128 	struct stat st;
    129 
    130 	if (mkdir(dir, 0700) == -1 && errno != EEXIST) {
    131 		reply("500 Internal Server Error",
    132 		    "Could not create upload directory");
    133 		return -1;
    134 	}
    135 	if (stat(dir, &st) == -1 || !S_ISDIR(st.st_mode)) {
    136 		reply("500 Internal Server Error",
    137 		    "Upload directory is not a directory");
    138 		return -1;
    139 	}
    140 	if (st.st_mode & (S_IWGRP | S_IWOTH)) {
    141 		reply("500 Internal Server Error",
    142 		    "Unsafe upload directory permissions");
    143 		return -1;
    144 	}
    145 	if (snprintf(path, len, "%s/.spool-XXXXXX", dir) >= (int) len) {
    146 		reply("500 Internal Server Error",
    147 		    "Path too long");
    148 		return -1;
    149 	}
    150 	fd = mkstemp(path);
    151 	if (fd == -1) {
    152 		reply("500 Internal Server Error",
    153 		    "Could not create request spool");
    154 		return -1;
    155 	}
    156 	if (fchmod(fd, 0600) == -1) {
    157 		close(fd);
    158 		unlink(path);
    159 		reply("500 Internal Server Error",
    160 		    "Could not set spool permissions");
    161 		return -1;
    162 	}
    163 	return fd;
    164 }
    165 
    166 static int
    167 finalize_upload(const char *dir,
    168     const char *filename,
    169     const char *temp_path,
    170     char *path,
    171     size_t len)
    172 {
    173 	const char *ext;
    174 	char base[NAME_MAX + 1];
    175 	size_t baselen;
    176 	int i;
    177 
    178 	ext = strrchr(filename, '.');
    179 	if (ext == filename)
    180 		ext = NULL;
    181 
    182 	if (ext == NULL) {
    183 		if (strlcpy(base, filename, sizeof(base)) >= sizeof(base)) {
    184 			reply("500 Internal Server Error", "Filename too long");
    185 			return -1;
    186 		}
    187 		ext = "";
    188 	} else {
    189 		baselen = (size_t) (ext - filename);
    190 
    191 		if (baselen >= sizeof(base)) {
    192 			reply("500 Internal Server Error", "Filename too long");
    193 			return -1;
    194 		}
    195 		memcpy(base, filename, baselen);
    196 		base[baselen] = '\0';
    197 	}
    198 
    199 	/* Try the original filename first. */
    200 	if (snprintf(path, len, "%s/%s", dir, filename) >= (int) len) {
    201 		reply("500 Internal Server Error", "Path too long");
    202 		return -1;
    203 	}
    204 	if (link(temp_path, path) == 0)
    205 		return 0;
    206 
    207 	if (errno != EEXIST) {
    208 		reply("500 Internal Server Error",
    209 		    "Could not finalize upload");
    210 		return -1;
    211 	}
    212 	/* Original filename exists; try .1, .2, ... */
    213 	for (i = 1; i < MAX_DUPLICATES; i++) {
    214 		if (snprintf(path, len, "%s/%s.%d%s",
    215 			dir, base, i, ext) >= (int) len) {
    216 			reply("500 Internal Server Error", "Path too long");
    217 			return -1;
    218 		}
    219 		if (link(temp_path, path) == 0)
    220 			return 0;
    221 
    222 		if (errno != EEXIST) {
    223 			reply("500 Internal Server Error",
    224 			    "Could not finalize upload");
    225 			return -1;
    226 		}
    227 	}
    228 
    229 	reply("500 Internal Server Error", "Too many duplicate filenames");
    230 	return -1;
    231 }
    232 
    233 static int
    234 check_free_space(const char *path, off_t required)
    235 {
    236 	struct statvfs vfs;
    237 	off_t min_free = min_free_bytes();
    238 	off_t avail;
    239 	char buf[PATH_MAX];
    240 	char *p;
    241 	char *slash;
    242 
    243 	if (required < 0)
    244 		return -1;
    245 
    246 	if (statvfs(path, &vfs) == 0) {
    247 		avail = (off_t) vfs.f_bavail * (off_t) vfs.f_frsize;
    248 
    249 		if (avail < min_free ||
    250 		    required > avail - min_free)
    251 			return -1;
    252 
    253 		return 0;
    254 	}
    255 	if (errno != ENOENT)
    256 		return -1;
    257 
    258 	if (strlcpy(buf, path, sizeof(buf)) >= sizeof(buf))
    259 		return -1;
    260 
    261 	p = buf;
    262 	for (;;) {
    263 		if (statvfs(p, &vfs) == 0) {
    264 			avail = (off_t) vfs.f_bavail * (off_t) vfs.f_frsize;
    265 
    266 			if (avail < min_free ||
    267 			    required > avail - min_free)
    268 				return -1;
    269 
    270 			return 0;
    271 		}
    272 		if (errno != ENOENT)
    273 			return -1;
    274 
    275 		slash = strrchr(p, '/');
    276 		if (slash == NULL || slash == p) {
    277 			if (statvfs("/", &vfs) == 0) {
    278 				avail =
    279 				    (off_t) vfs.f_bavail *
    280 				    (off_t) vfs.f_frsize;
    281 
    282 				if (avail < min_free ||
    283 				    required > avail - min_free)
    284 					return -1;
    285 
    286 				return 0;
    287 			}
    288 			return -1;
    289 		}
    290 		*slash = '\0';
    291 	}
    292 }
    293 static int
    294 check_repo_quota(const char *dir, off_t incoming_size)
    295 {
    296 	DIR *d;
    297 	struct dirent *de;
    298 	char path[PATH_MAX];
    299 	struct stat st;
    300 	off_t used = 0;
    301 	off_t repo_quota = repo_quota_bytes();
    302 
    303 	/* Calculate current repository usage */
    304 	d = opendir(dir);
    305 	if (d == NULL) {
    306 		if (errno == ENOENT) {
    307 			/*
    308                          * Directory doesn't exist yet → no files,
    309                          * so usage is 0.
    310                          */
    311 			used = 0;
    312 		} else {
    313 			/* Real error (permissions, etc.) */
    314 			return -1;
    315 		}
    316 	} else {
    317 		while ((de = readdir(d)) != NULL) {
    318 			if (strcmp(de->d_name, ".") == 0 ||
    319 			    strcmp(de->d_name, "..") == 0)
    320 				continue;
    321 
    322 			if (snprintf(path, sizeof(path), "%s/%s",
    323 				dir, de->d_name) >= (int) sizeof(path))
    324 				continue;
    325 
    326 			if (stat(path, &st) == 0 && S_ISREG(st.st_mode)) {
    327 				if (st.st_size > repo_quota || used > repo_quota - st.st_size)
    328 					return -1;
    329 				used += st.st_size;
    330 			}
    331 		}
    332 		closedir(d);
    333 	}
    334 
    335 	/* Check if incoming file fits within quota */
    336 	if (incoming_size > repo_quota || used > repo_quota - incoming_size)
    337 		return -1;
    338 
    339 	return 0;
    340 }
    341 
    342 static int
    343 do_get(void)
    344 {
    345 	char max_upload[32];
    346 	char max_upload_bytes[32];
    347 	char quota[32];
    348 	char template[PATH_MAX];
    349 	off_t max_upload_value;
    350 	off_t quota_value;
    351 
    352 	if (!config_write_allowed()) {
    353 		reply("403 Forbidden", "Repository is read-only");
    354 		return 0;
    355 	}
    356 	max_upload_value = config_max_upload();
    357 	quota_value = config_repo_quota();
    358 
    359 	struct html_var vars[] = {
    360 		{"MAX_UPLOAD", max_upload},
    361 		{"MAX_UPLOAD_BYTES", max_upload_bytes},
    362 		{"REPO_QUOTA", quota},
    363 	};
    364 
    365 	snprintf(max_upload, sizeof(max_upload),
    366 	    "%lld MB",
    367 	    (long long) (max_upload_value / (1024LL * 1024)));
    368 
    369 	snprintf(max_upload_bytes, sizeof(max_upload_bytes),
    370 	    "%lld",
    371 	    (long long) max_upload_value);
    372 
    373 	snprintf(quota, sizeof(quota),
    374 	    "%lld GB",
    375 	    (long long) (quota_value / (1024LL * 1024 * 1024)));
    376 
    377 	if (snprintf(template, sizeof(template),
    378 		"%s/%s",
    379 		simplefiles_root(),
    380 		UPLOAD_TEMPLATE_NAME) >= (int) sizeof(template)) {
    381 		reply("500 Internal Server Error", "Template path too long");
    382 		return 0;
    383 	}
    384 	if (render_template_response(
    385 		template,
    386 		vars,
    387 		sizeof(vars) / sizeof(vars[0]),
    388 		NULL,
    389 		0) == -1) {
    390 		reply("500 Internal Server Error", "Template error");
    391 		return 0;
    392 	}
    393 	return 0;
    394 }
    395 
    396 static int
    397 do_post(void)
    398 {
    399 	struct multipart_request mreq = {0};
    400 	struct multipart_part part;
    401 
    402 	char path[PATH_MAX];
    403 	char repodir[PATH_MAX];
    404 	char temppath[PATH_MAX];
    405 	char spoolpath[PATH_MAX];
    406 
    407 	int spool_fd = -1;
    408 	int spool_created = 0;
    409 	int fd = -1;
    410 	int temp_created = 0;
    411 	int lock_fd = -1;
    412 	int rc;
    413 	int ret;
    414 
    415 	if (!config_write_allowed()) {
    416 		reply("403 Forbidden", "Repository is read-only");
    417 		return 0;
    418 	}
    419 	rc = check_csrf_origin();
    420 
    421 	if (rc == -2) {
    422 		reply("500 Internal Server Error",
    423 		    "CSRF origin is not configured");
    424 		return 0;
    425 	}
    426 	if (rc == -1) {
    427 		reply("403 Forbidden", "Forbidden");
    428 		return 0;
    429 	}
    430 	/*
    431 	 * Parse the multipart request metadata:
    432 	 *
    433 	 * - Content-Type
    434 	 * - boundary
    435 	 * - Content-Length
    436 	 * - maximum upload size
    437 	 */
    438 	ret = multipart_parse_request(&mreq);
    439 
    440 	switch (ret) {
    441 	case MULTIPART_OK:
    442 		break;
    443 
    444 	case MULTIPART_MISSING_CONTENT_TYPE:
    445 		reply("400 Bad Request", "Missing CONTENT_TYPE");
    446 		goto cleanup;
    447 
    448 	case MULTIPART_INVALID_CONTENT_TYPE:
    449 		reply("400 Bad Request",
    450 		    "Expected multipart/form-data");
    451 		goto cleanup;
    452 
    453 	case MULTIPART_MISSING_BOUNDARY:
    454 		reply("400 Bad Request", "Missing boundary");
    455 		goto cleanup;
    456 
    457 	case MULTIPART_INVALID_BOUNDARY:
    458 		reply("400 Bad Request", "Invalid boundary");
    459 		goto cleanup;
    460 
    461 	case MULTIPART_MISSING_LENGTH:
    462 		reply("411 Length Required", "Missing CONTENT_LENGTH");
    463 		goto cleanup;
    464 
    465 	case MULTIPART_INVALID_LENGTH:
    466 		reply("400 Bad Request", "Invalid CONTENT_LENGTH");
    467 		goto cleanup;
    468 
    469 	case MULTIPART_TOO_LARGE:
    470 		reply("413 Payload Too Large", "Upload too large");
    471 		goto cleanup;
    472 
    473 	default:
    474 		reply("400 Bad Request", "Invalid multipart request");
    475 		goto cleanup;
    476 	}
    477 
    478 	if (build_repository_path(repodir, sizeof(repodir),
    479 		storage_root(), repository()) == -1) {
    480 		reply("500 Internal Server Error",
    481 		    "Path too long");
    482 		return 0;
    483 	}
    484 	if (check_free_space(repodir, mreq.length) == -1) {
    485 		reply("507 Insufficient Storage",
    486 		    "Server running low on disk space");
    487 		return 0;
    488 	}
    489 	/*
    490 	 * Spool the complete multipart request to disk.
    491 	 *
    492 	 * multipart.c owns the request-body reading logic.
    493 	 */
    494 	spool_fd = open_spool(upload_tmp_dir(),
    495 	    spoolpath, sizeof(spoolpath));
    496 	if (spool_fd == -1)
    497 		return 0;
    498 
    499 	spool_created = 1;
    500 
    501 	if (multipart_spool_request(spool_fd, mreq.length) != MULTIPART_OK) {
    502 		reply("400 Bad Request",
    503 		    "Could not read request body");
    504 		goto cleanup;
    505 	}
    506 	/*
    507 	 * Extract the uploaded part from the spooled multipart body.
    508 	 *
    509 	 * multipart.c now owns:
    510 	 *   - filename extraction
    511 	 *   - multipart header parsing
    512 	 *   - boundary detection
    513 	 *   - payload offset calculation
    514 	 *   - payload length calculation
    515 	 */
    516 	if (multipart_extract_part(spool_fd, &mreq, &part) != MULTIPART_OK) {
    517 		reply("400 Bad Request",
    518 		    "Malformed multipart body");
    519 		goto cleanup;
    520 	}
    521 	if (!valid_filename(part.filename)) {
    522 		reply("400 Bad Request",
    523 		    "Invalid filename");
    524 		goto cleanup;
    525 	}
    526 	/*
    527 	 * Check that creating the temporary upload will still leave
    528 	 * MIN_FREE_BYTES available. The request spool already exists,
    529 	 * so its disk usage is already reflected in statvfs().
    530 	 */
    531 	if (check_free_space(repodir, part.length) == -1) {
    532 		reply("507 Insufficient Storage",
    533 		    "Server running low on disk space");
    534 		goto cleanup;
    535 	}
    536 	/*
    537 	 * Write the uploaded file to a temporary file first.
    538 	 *
    539 	 * This is deliberately done without holding the repository
    540 	 * lock because this operation may take a long time.
    541 	 */
    542 	fd = open_upload_temp(upload_tmp_dir(),
    543 	    temppath, sizeof(temppath));
    544 	if (fd == -1)
    545 		goto cleanup;
    546 
    547 	temp_created = 1;
    548 
    549 	if (multipart_copy_part(spool_fd, &part, fd) != MULTIPART_OK) {
    550 		reply("500 Internal Server Error", "Write failed");
    551 		goto cleanup;
    552 	}
    553 	if (close(fd) == -1) {
    554 		reply("500 Internal Server Error",
    555 		    "Close failed");
    556 		fd = -1;
    557 		goto cleanup;
    558 	}
    559 	fd = -1;
    560 
    561 	/*
    562 	 * Serialize repository quota checking and finalization.
    563 	 *
    564 	 * All uploads to the same repository use the same lock.
    565 	 */
    566 	lock_fd = repository_lock(repository());
    567 	if (lock_fd == -1) {
    568 		reply("500 Internal Server Error",
    569 		    "Could not lock repository");
    570 		goto cleanup;
    571 	}
    572 	/*
    573 	 * The quota check must happen while holding the repository
    574 	 * lock. Otherwise two concurrent uploads could both observe
    575 	 * the same available quota and both proceed.
    576 	 */
    577 	if (check_repo_quota(repodir, part.length) == -1) {
    578 		reply("507 Insufficient Storage",
    579 		    "Repository quota exceeded");
    580 		goto cleanup;
    581 	}
    582 	/*
    583 	 * Finalization must also happen while holding the lock,
    584 	 * because it modifies the repository contents.
    585 	 */
    586 	if (finalize_upload(repodir,
    587 		part.filename,
    588 		temppath,
    589 		path,
    590 		sizeof(path)) == -1)
    591 		goto cleanup;
    592 
    593 	/*
    594 	 * The repository has now been modified successfully.
    595 	 * No more repository operation needs to be protected.
    596 	 */
    597 	repository_unlock(lock_fd);
    598 	lock_fd = -1;
    599 
    600 	/*
    601 	 * The temporary file is now no longer needed.
    602 	 */
    603 	if (unlink(temppath) == -1) {
    604 		fprintf(stderr, "warning: could not remove temporary upload %s: %s\n", temppath, strerror(errno));
    605 	} else {
    606 		temp_created = 0;
    607 	}
    608 
    609 	if (close(spool_fd) == -1) {
    610 		spool_fd = -1;
    611 		reply("500 Internal Server Error",
    612 		    "Could not close request spool");
    613 		unlink(spoolpath);
    614 		return 0;
    615 	}
    616 	spool_fd = -1;
    617 
    618 	if (unlink(spoolpath) == -1) {
    619 		reply("500 Internal Server Error",
    620 		    "Could not remove request spool");
    621 		return 0;
    622 	}
    623 	spool_created = 0;
    624 
    625 	redirect(DEFAULT_REDIRECT_URI);
    626 
    627 	return 0;
    628 
    629 cleanup:
    630 	if (lock_fd != -1)
    631 		repository_unlock(lock_fd);
    632 
    633 	if (fd != -1)
    634 		close(fd);
    635 
    636 	if (temp_created)
    637 		unlink(temppath);
    638 
    639 	if (spool_fd != -1)
    640 		close(spool_fd);
    641 
    642 	if (spool_created)
    643 		unlink(spoolpath);
    644 
    645 	return 0;
    646 }
    647 
    648 
    649 static const char *
    650 upload_tmp_dir(void)
    651 {
    652 	const char *dir;
    653 
    654 	dir = getenv("UPLOAD_TMP_DIR");
    655 	if (dir != NULL && *dir != '\0')
    656 		return dir;
    657 
    658 	return DEFAULT_UPLOAD_TMP_DIR;
    659 }
    660 
    661 int
    662 main(void)
    663 {
    664 	const char *method;
    665 
    666 	method = getenv("REQUEST_METHOD");
    667 
    668 	if (method == NULL) {
    669 		reply("400 Bad Request",
    670 		    "Missing request method");
    671 		return 0;
    672 	}
    673 	if (strcmp(method, "GET") == 0)
    674 		return do_get();
    675 
    676 	if (strcmp(method, "POST") == 0)
    677 		return do_post();
    678 
    679 	reply("405 Method Not Allowed",
    680 	    "GET or POST required");
    681 
    682 	return 0;
    683 }