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 }