From 081973c9043c95770e153a489cb2af2cda634363 Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 21 Jul 2026 16:41:54 +0200 Subject: [PATCH 1/3] fix: security hardening (#112-#118) --- .gitignore | 1 + src/client/client_cli.c | 106 +++++++++++++++ src/client/client_send.c | 247 +++++++++++++++++++++++++++-------- src/client/scanner.c | 46 ++++++- src/client/scanner.h | 5 +- src/server/server.c | 46 ++++++- src/shared/config.c | 27 ++++ src/shared/config.h | 10 ++ src/shared/delta.c | 7 +- src/shared/file.c | 48 +++++-- src/shared/file.h | 3 +- src/shared/log.c | 15 +++ src/shared/log.h | 3 + src/shared/multiprocessing.c | 43 +++++- src/shared/protocol.c | 42 ++++++ src/shared/protocol.h | 5 +- src/shared/transport_ssh.c | 13 +- src/shared/transport_tcp.c | 53 +++++++- src/shared/transport_tcp.h | 3 + src/shared/transport_tls.c | 2 + src/shared/utils.c | 19 +++ src/shared/utils.h | 1 + tests/test_file.c | 2 +- tests/test_scanner.c | 8 +- 24 files changed, 665 insertions(+), 90 deletions(-) diff --git a/.gitignore b/.gitignore index 1001d2b..f3ac385 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ build-asan coverage.info build-*/ +build3/ diff --git a/src/client/client_cli.c b/src/client/client_cli.c index d7cff62..011c1bb 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -3,6 +3,7 @@ #include "delta.h" #include "log.h" #include "protocol.h" +#include "transport_tcp.h" #include "transport_tls.h" #include "utils.h" #include @@ -31,6 +32,8 @@ static void print_usage(void) { printf(" --delete Delete files on receiver not in source\n"); printf(" --exclude Exclude files matching pattern\n"); printf(" --include Only include files matching pattern\n"); + printf(" --exclude-from Read exclude patterns from file\n"); + printf(" --include-from Read include patterns from file\n"); printf(" --max-size Skip files larger than n bytes\n"); printf(" --min-size Skip files smaller than n bytes\n"); printf(" --incremental Skip files unchanged since last transfer\n"); @@ -55,9 +58,50 @@ static void print_usage(void) { printf(" --cert TLS certificate file (PEM)\n"); printf(" --key TLS private key file (PEM)\n"); printf(" --ca TLS CA certificate file (PEM)\n"); + printf(" --timeout I/O timeout in seconds (default: 30)\n"); + printf(" --contimeout Connection timeout in seconds (default: 10)\n"); + printf(" -q, --quiet Suppress non-error output\n"); + printf(" --silent Alias for --quiet\n"); + printf(" --backup Backup existing files before overwriting\n"); + printf(" --backup-dir Directory for backups (requires --backup)\n"); + printf(" --stats Print transfer statistics at end\n"); + printf(" --max-depth Maximum directory depth (0=unlimited)\n"); + printf(" --log-file Write log messages to file\n"); + printf(" --queue-size Queue capacity for multithreaded mode (default: 100)\n"); printf(" --help Show this help\n"); } +static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) { + FILE* fp = fopen(filepath, "r"); + if (!fp) { + fprintf(stderr, "Error: could not open pattern file '%s': %s\n", filepath, strerror(errno)); + return -1; + } + char line[4096]; + while (fgets(line, sizeof(line), fp)) { + char* p = line; + while (*p == ' ' || *p == '\t') + p++; + if (*p == '#' || *p == '\n' || *p == '\0') + continue; + size_t len = strlen(p); + while (len > 0 && (p[len - 1] == '\n' || p[len - 1] == '\r')) + p[--len] = '\0'; + if (len == 0) + continue; + char** tmp = realloc(*patterns, (*count + 1) * sizeof(char*)); + if (!tmp) { + fprintf(stderr, "Error: memory allocation failed for pattern file\n"); + fclose(fp); + return -1; + } + *patterns = tmp; + (*patterns)[(*count)++] = str_dup(p); + } + fclose(fp); + return 0; +} + int main(int argc, char* argv[]) { const char* env_source = getenv("FASTSYNC_SOURCE_DIR"); const char* env_dest = getenv("FASTSYNC_DEST_DIR"); @@ -199,6 +243,64 @@ int main(int argc, char* argv[]) { } else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) { free(config->tls_ca); config->tls_ca = str_dup(argv[++i]); + } else if (strcmp(argv[i], "--timeout") == 0 && i + 1 < argc) { + config->timeout = atoi(argv[++i]); + if (config->timeout <= 0) { + fprintf(stderr, "Error: --timeout must be a positive integer\n"); + exit_code = 1; + goto cleanup; + } + } else if (strcmp(argv[i], "--contimeout") == 0 && i + 1 < argc) { + config->contimeout = atoi(argv[++i]); + if (config->contimeout <= 0) { + fprintf(stderr, "Error: --contimeout must be a positive integer\n"); + exit_code = 1; + goto cleanup; + } + } else if (strcmp(argv[i], "-q") == 0 || strcmp(argv[i], "--quiet") == 0 || + strcmp(argv[i], "--silent") == 0) { + config->quiet = true; + } else if (strcmp(argv[i], "--backup") == 0) { + config->backup = true; + } else if (strcmp(argv[i], "--backup-dir") == 0 && i + 1 < argc) { + config->backup_dir = str_dup(argv[++i]); + } else if (strcmp(argv[i], "--stats") == 0) { + config->stats = true; + } else if (strcmp(argv[i], "--max-depth") == 0 && i + 1 < argc) { + config->max_depth = atoi(argv[++i]); + if (config->max_depth < 0) { + fprintf(stderr, "Error: --max-depth must be a non-negative integer\n"); + exit_code = 1; + goto cleanup; + } + } else if (strcmp(argv[i], "--log-file") == 0 && i + 1 < argc) { + FILE* lf = fopen(argv[++i], "a"); + if (!lf) { + fprintf(stderr, "Error: could not open log file '%s': %s\n", argv[i], strerror(errno)); + exit_code = 1; + goto cleanup; + } + config->log_file = lf; + log_set_file(lf); + } else if (strcmp(argv[i], "--queue-size") == 0 && i + 1 < argc) { + config->queue_size = atoi(argv[++i]); + if (config->queue_size <= 0) { + fprintf(stderr, "Error: --queue-size must be a positive integer\n"); + exit_code = 1; + goto cleanup; + } + } else if (strcmp(argv[i], "--exclude-from") == 0 && i + 1 < argc) { + if (read_patterns_from_file(argv[++i], &config->exclude_patterns, + &config->exclude_count) != 0) { + exit_code = 1; + goto cleanup; + } + } else if (strcmp(argv[i], "--include-from") == 0 && i + 1 < argc) { + if (read_patterns_from_file(argv[++i], &config->include_patterns, + &config->include_count) != 0) { + exit_code = 1; + goto cleanup; + } } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) { set_log_level(LOG_LEVEL_DEBUG); } else if (argv[i][0] == '-') { @@ -297,6 +399,8 @@ int main(int argc, char* argv[]) { tls_global_init(); } + tcp_set_timeouts(config->timeout, config->contimeout); + if (config->use_multithreading) { config_owned_by_pipeline = true; exit_code = send_files_multithreaded(config); @@ -305,6 +409,8 @@ int main(int argc, char* argv[]) { } cleanup: + if (config->log_file) + fclose(config->log_file); if (!config_owned_by_pipeline) config_delete(config); return exit_code; diff --git a/src/client/client_send.c b/src/client/client_send.c index 256d8e4..27376b1 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -16,12 +16,23 @@ #include "transport_ssh.h" #include "transport_tls.h" #include "utils.h" +#include #include #include #include #include #include +static volatile sig_atomic_t g_abort_requested = 0; +static int g_abort_fd = -1; + +static void handle_sigint(int sig) { + (void)sig; + g_abort_requested = 1; +} + +#define KEEPALIVE_INTERVAL 30 + static int incremental_check(Client* client, File* file, DeltaSignature** out_sig) { *out_sig = NULL; if (!send_status(client->file_descriptor, STATUS_CHECK)) @@ -96,6 +107,35 @@ static int send_delta(Client* client, File* file, DeltaSignature* sig, Config* c return ok ? 0 : -1; } +static bool batch_incremental_check(Client* client, ArrayList* files) { + if (!send_status(client->file_descriptor, STATUS_CHECK_BATCH)) + return false; + if (!send_int(client->file_descriptor, files->size)) + return false; + for (int i = 0; i < files->size; i++) { + File* file = (File*)files->items[i]; + if (!send_str(client->file_descriptor, file->path)) + return false; + unsigned long long fsize = file->data ? file->data->size : 0; + long long mtime = file->metadata ? file->metadata->mtime_sec : 0; + if (!send_n_data(client->file_descriptor, &fsize, sizeof(fsize))) + return false; + if (!send_n_data(client->file_descriptor, &mtime, sizeof(mtime))) + return false; + } + for (int i = 0; i < files->size; i++) { + Status s; + if (!receive_status(client->file_descriptor, &s)) + return false; + File* file = (File*)files->items[i]; + if (s == STATUS_OK) + file->skip = true; + else if (s == STATUS_ERROR) + return false; + } + return true; +} + typedef bool (*file_send_fn)(File*, int, bool, int, bool); // Send a single file directly (non-incremental path). @@ -118,6 +158,9 @@ static int send_single_file(Client* client, File* file, Config* config, bool use bool use_sendfile) { int compression_level = config->use_compression ? config->compression_level : 0; + if (file->skip) + return 1; + if (!use_incremental) { if (use_sendfile) { return send_file_direct_sendfile(file, client->file_descriptor, config->use_metadata) ? 0 @@ -255,7 +298,30 @@ static int send_chunks_multithreaded(void* pipeline_context) { return thrd_error; } + g_abort_fd = client->file_descriptor; + time_t last_activity = time(NULL); while (true) { + if (g_abort_requested) { + send_status(client->file_descriptor, STATUS_ABORT); + client_disconnect(client); + client_delete(client); + return thrd_error; + } + time_t now = time(NULL); + if (now - last_activity >= KEEPALIVE_INTERVAL) { + if (!send_status(client->file_descriptor, STATUS_KEEPALIVE)) { + client_disconnect(client); + client_delete(client); + return thrd_error; + } + Status s; + if (!receive_status(client->file_descriptor, &s)) { + client_disconnect(client); + client_delete(client); + return thrd_error; + } + last_activity = now; + } Chunk* current_chunk = queue_dequeue_multithreaded( context->queue_loader, &context->mutex_loader, &context->condition_not_empty_loader, &context->condition_not_full_loader, &context->loader_done); @@ -299,8 +365,8 @@ static int scan_directory_multithreaded(void* pipeline_context) { DirectoryScanner* scanner = directory_scanner_create( context->config->send_directory, context->config->use_metadata, context->config->chunk_size, context->config->exclude_patterns, context->config->exclude_count, - context->config->include_patterns, context->config->include_count, context->config->max_size, - context->config->min_size); + context->config->include_patterns, context->config->include_count, context->config->max_size, + context->config->min_size, context->config->max_depth); mtx_unlock(&context->mutex_scanner); Chunk* current_chunk; @@ -361,21 +427,24 @@ int send_files(Config* config) { DirectoryScanner* scanner = directory_scanner_create( config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, config->exclude_count, config->include_patterns, config->include_count, config->max_size, - config->min_size); + config->min_size, config->max_depth); Chunk* chunk; int file_count = 0; unsigned long long total_bytes = 0; - printf("Dry run: files to be transferred\n"); + if (!config->quiet) + printf("Dry run: files to be transferred\n"); while ((chunk = directory_scanner_next(scanner)) != NULL) { for (int i = 0; i < chunk->element_count; i++) { - printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); + if (!config->quiet) + printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); total_bytes += chunk->items[i]->data->size; file_count++; } chunk_destroy(chunk); } directory_scanner_destroy(scanner); - printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0); + if (!config->quiet) + printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0); return 0; } @@ -411,42 +480,98 @@ int send_files(Config* config) { client_delete(client); return 1; } + + g_abort_fd = client->file_descriptor; + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handle_sigint; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + DirectoryScanner* scanner = directory_scanner_create( config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, config->exclude_count, config->include_patterns, config->include_count, config->max_size, - config->min_size); - Chunk* current_chunk; - unsigned long long total_bytes = 0; - time_t last_progress = 0; - time_t start = time(NULL); + config->min_size, config->max_depth); + ArrayList* all_files = array_list_create(NULL); ArrayList* manifest = config->use_delete ? array_list_create(free) : NULL; + Chunk* current_chunk; while ((current_chunk = directory_scanner_next(scanner)) != NULL) { - unsigned long long chunk_bytes = 0; for (int i = 0; i < current_chunk->element_count; i++) { - chunk_bytes += current_chunk->items[i]->data->size; + File* f = current_chunk->items[i]; + array_list_add(all_files, f); + current_chunk->items[i] = NULL; if (manifest) { - const char* p = current_chunk->items[i]->path; + const char* p = f->path; if (*p == '/') p++; array_list_add(manifest, str_dup(p)); } } - if (!config->use_sendfile) { - for (int i = 0; i < current_chunk->element_count; i++) { - if (!file_load_data(current_chunk->items[i])) { - log_message(LOG_LEVEL_ERROR, "Failed to load file data"); - continue; - } - } + chunk_destroy(current_chunk); + } + directory_scanner_destroy(scanner); + scanner = NULL; + + bool batch_ok = true; + if (config->use_incremental && all_files->size > 0) { + if (!batch_incremental_check(client, all_files)) { + log_message(LOG_LEVEL_ERROR, "Batch incremental check failed"); + batch_ok = false; } - if (send_chunk(client, current_chunk, config) != 0) { - log_message(LOG_LEVEL_ERROR, "Failed to send chunk"); - chunk_destroy(current_chunk); + } + + unsigned long long total_bytes = 0; + time_t last_progress = 0; + time_t last_activity = 0; + time_t start = time(NULL); + bool use_sendfile = config->use_sendfile && !config->use_compression; + for (int i = 0; i < all_files->size; i++) { + File* file = (File*)all_files->items[i]; + if (file->skip) + continue; + if (g_abort_requested) { + send_status(client->file_descriptor, STATUS_ABORT); + batch_ok = false; break; } + time_t now = time(NULL); + if (now - last_activity >= KEEPALIVE_INTERVAL) { + if (!send_status(client->file_descriptor, STATUS_KEEPALIVE)) { + batch_ok = false; + break; + } + Status s; + if (!receive_status(client->file_descriptor, &s)) { + batch_ok = false; + break; + } + last_activity = now; + } + int compression_level = config->use_compression ? config->compression_level : 0; + if (!send_status(client->file_descriptor, STATUS_NEXT)) { + batch_ok = false; + break; + } + if (use_sendfile) { + if (!file_send_sendfile(file, client->file_descriptor, config->use_metadata, 0, true)) { + log_message(LOG_LEVEL_ERROR, "Failed to send file via sendfile"); + batch_ok = false; + break; + } + } else { + if (!file_load_data(file)) { + log_message(LOG_LEVEL_ERROR, "Failed to load file data"); + continue; + } + if (!file_send_single_calls(file, client->file_descriptor, config->use_metadata, + compression_level, true)) { + log_message(LOG_LEVEL_ERROR, "Failed to send file"); + batch_ok = false; + break; + } + } + total_bytes += file->data ? file->data->size : 0; if (config->show_progress) { - total_bytes += chunk_bytes; - time_t now = time(NULL); if (now - last_progress >= 1) { last_progress = now; double elapsed = difftime(now, start); @@ -455,71 +580,71 @@ int send_files(Config* config) { fflush(stderr); } } - chunk_destroy(current_chunk); } - if (config->use_delete) { - if (!send_status(client->file_descriptor, STATUS_MANIFEST)) { - array_list_delete(manifest); - goto send_fail; - } - if (!send_int(client->file_descriptor, manifest->size)) { - array_list_delete(manifest); - goto send_fail; - } - for (int i = 0; i < manifest->size; i++) { - if (!send_str(client->file_descriptor, (char*)manifest->items[i])) { - array_list_delete(manifest); - goto send_fail; + + if (batch_ok && config->use_delete && manifest) { + if (!send_status(client->file_descriptor, STATUS_MANIFEST)) + batch_ok = false; + else if (!send_int(client->file_descriptor, manifest->size)) + batch_ok = false; + else { + for (int i = 0; i < manifest->size && batch_ok; i++) { + if (!send_str(client->file_descriptor, (char*)manifest->items[i])) + batch_ok = false; } } - array_list_delete(manifest); } - if (!send_status(client->file_descriptor, STATUS_FINISHED)) - goto send_fail; + array_list_delete(manifest); + + if (batch_ok && !send_status(client->file_descriptor, STATUS_FINISHED)) + batch_ok = false; Status s; - int ok = receive_status(client->file_descriptor, &s) && s == STATUS_OK; + int ok = 0; + if (batch_ok) + ok = receive_status(client->file_descriptor, &s) && s == STATUS_OK; if (config->show_progress) { double elapsed = difftime(time(NULL), start); double rate = elapsed > 0 ? total_bytes / (1048576.0 * elapsed) : 0; fprintf(stderr, "\rSent %.1f MB (%.1f MB/s) Done.\n", total_bytes / 1048576.0, rate); } - directory_scanner_destroy(scanner); + for (int i = 0; i < all_files->size; i++) + file_destroy(all_files->items[i]); + array_list_delete(all_files); client_disconnect(client); client_delete(client); - return ok ? 0 : -1; - -send_fail: - directory_scanner_destroy(scanner); - client_disconnect(client); - client_delete(client); - return -1; + return (batch_ok && ok) ? 0 : -1; } int send_files_multithreaded(Config* config) { + time_t start_time = time(NULL); if (config->dry_run) { DirectoryScanner* scanner = directory_scanner_create( config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, config->exclude_count, config->include_patterns, config->include_count, config->max_size, - config->min_size); + config->min_size, config->max_depth); Chunk* chunk; int file_count = 0; unsigned long long total_bytes = 0; - printf("Dry run: files to be transferred\n"); + if (!config->quiet) + printf("Dry run: files to be transferred\n"); while ((chunk = directory_scanner_next(scanner)) != NULL) { for (int i = 0; i < chunk->element_count; i++) { - printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); + if (!config->quiet) + printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); total_bytes += chunk->items[i]->data->size; file_count++; } chunk_destroy(chunk); } directory_scanner_destroy(scanner); - printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0); + if (!config->quiet) + printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0); return 0; } - Queue* q1 = queue_create(100, chunk_destroy); - Queue* q2 = queue_create(100, chunk_destroy); + int qsize = config->queue_size > 0 ? config->queue_size : 100; + Queue* q1 = queue_create(qsize, chunk_destroy); + Queue* q2 = queue_create(qsize, chunk_destroy); if (!q1 || !q2) { if (q1) queue_destroy(q1); @@ -550,6 +675,12 @@ int send_files_multithreaded(Config* config) { thrd_join(loader, NULL); thrd_join(sender, &sender_result); + if (config->stats && !config->quiet) { + double elapsed = difftime(time(NULL), start_time); + printf("\nTransfer statistics:\n"); + printf(" Elapsed time: %.1f sec\n", elapsed); + } + pipeline_context_sender_destroy(context); return sender_result == thrd_success ? 0 : -1; } diff --git a/src/client/scanner.c b/src/client/scanner.c index ad56fb2..5b49188 100644 --- a/src/client/scanner.c +++ b/src/client/scanner.c @@ -11,15 +11,38 @@ #include #include +typedef struct { + char* path; + int depth; +} DirEntry; + +static void dir_entry_destroy(void* item) { + if (item) { + DirEntry* de = (DirEntry*)item; + free(de->path); + free(de); + } +} + +static DirEntry* dir_entry_create(const char* path, int depth) { + DirEntry* de = malloc(sizeof(DirEntry)); + if (de) { + de->path = str_dup(path); + de->depth = depth; + } + return de; +} + DirectoryScanner* directory_scanner_create(char* root_directory, bool use_metadata, unsigned long long chunk_size, char** exclude_patterns, int exclude_count, char** include_patterns, int include_count, unsigned long long max_size, - unsigned long long min_size) { + unsigned long long min_size, + int max_depth) { DirectoryScanner* scanner = malloc(sizeof(DirectoryScanner)); if (scanner == NULL) return NULL; - scanner->directories = queue_create(100, free); + scanner->directories = queue_create(100, dir_entry_destroy); scanner->current_dir = NULL; scanner->current_path = NULL; scanner->use_metadata = use_metadata; @@ -30,7 +53,9 @@ DirectoryScanner* directory_scanner_create(char* root_directory, bool use_metada scanner->include_count = include_count; scanner->max_size = max_size; scanner->min_size = min_size; - queue_enqueue(scanner->directories, str_dup(root_directory)); + scanner->max_depth = max_depth; + scanner->current_depth = 0; + queue_enqueue(scanner->directories, dir_entry_create(root_directory, 0)); return scanner; } @@ -66,7 +91,10 @@ static int open_next_directory(DirectoryScanner* scanner) { if (queue_is_empty(scanner->directories)) return 0; - scanner->current_path = (char*)queue_dequeue(scanner->directories); + DirEntry* de = (DirEntry*)queue_dequeue(scanner->directories); + scanner->current_path = de->path; + scanner->current_depth = de->depth; + free(de); scanner->current_dir = opendir(scanner->current_path); if (scanner->current_dir == NULL) { perror("Could not open directory"); @@ -110,8 +138,16 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) { } if (S_ISDIR(stats.st_mode)) { - queue_enqueue(scanner->directories, (void*)cur_path); + int next_depth = scanner->current_depth + 1; + if (scanner->max_depth <= 0 || next_depth < scanner->max_depth) + queue_enqueue(scanner->directories, dir_entry_create(cur_path, next_depth)); + else + free(cur_path); } else { + if (scanner->max_depth > 0 && scanner->current_depth + 1 > scanner->max_depth) { + free(cur_path); + continue; + } bool excluded = false; for (int i = 0; i < scanner->exclude_count; i++) { if (glob_match(scanner->exclude_patterns[i], entry->d_name)) { diff --git a/src/client/scanner.h b/src/client/scanner.h index 8202d61..eb0146e 100644 --- a/src/client/scanner.h +++ b/src/client/scanner.h @@ -18,13 +18,16 @@ typedef struct { int include_count; unsigned long long max_size; unsigned long long min_size; + int max_depth; + int current_depth; } DirectoryScanner; DirectoryScanner* directory_scanner_create(char* root_directory, bool use_metadata, unsigned long long chunk_size, char** exclude_patterns, int exclude_count, char** include_patterns, int include_count, unsigned long long max_size, - unsigned long long min_size); + unsigned long long min_size, + int max_depth); Chunk* directory_scanner_next(DirectoryScanner* scanner); void directory_scanner_destroy(DirectoryScanner* scanner); diff --git a/src/server/server.c b/src/server/server.c index d3bbc74..5e59938 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -21,7 +21,17 @@ int receive_files(Config* config, int fd) { if (!receive_status(fd, &status)) return -1; - while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK) { + while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK || + status == STATUS_KEEPALIVE || status == STATUS_ABORT || + status == STATUS_CHECK_BATCH) { + if (status == STATUS_KEEPALIVE) { + send_status(fd, STATUS_KEEPALIVE); + goto next; + } + if (status == STATUS_ABORT) { + log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up"); + return -1; + } if (status == STATUS_CHECK) { bool skipped; File* file = receive_incremental_check(fd, config, &skipped); @@ -30,7 +40,7 @@ int receive_files(Config* config, int fd) { if (file == NULL && !skipped) return -1; if (config->save_to_disk) - file_save_to_disk(config->receive_root_directory, file); + file_save_to_disk(config->receive_root_directory, file, NULL); file_destroy(file); } else if (status == STATUS_CHUNK) { Chunk* chunk = receive_chunk_data(fd, config); @@ -40,9 +50,37 @@ int receive_files(Config* config, int fd) { } for (int i = 0; i < chunk->element_count; i++) { if (config->save_to_disk) - file_save_to_disk(config->receive_root_directory, chunk->items[i]); + file_save_to_disk(config->receive_root_directory, chunk->items[i], NULL); } chunk_destroy(chunk); + } else if (status == STATUS_CHECK_BATCH) { + int count; + if (!receive_int(fd, &count)) + return -1; + for (int i = 0; i < count; i++) { + char* check_path = receive_str(fd); + if (!check_path) + return -1; + unsigned long long check_size; + long long check_mtime; + if (!receive_n_data(fd, &check_size, sizeof(check_size)) || + !receive_n_data(fd, &check_mtime, sizeof(check_mtime))) { + free(check_path); + return -1; + } + char* full_path = path_cat(config->receive_root_directory, check_path); + struct stat st; + bool has_old = full_path && stat(full_path, &st) == 0; + bool match = has_old && (unsigned long long)st.st_size == check_size && + (long long)st.st_mtime == check_mtime; + if (match) + send_status(fd, STATUS_OK); + else + send_status(fd, STATUS_NEXT); + free(full_path); + free(check_path); + } + goto next; } else { File* file = file_receive(config, fd); if (file == NULL) { @@ -51,7 +89,7 @@ int receive_files(Config* config, int fd) { return -1; } if (config->save_to_disk) - file_save_to_disk(config->receive_root_directory, file); + file_save_to_disk(config->receive_root_directory, file, NULL); file_destroy(file); } next: diff --git a/src/shared/config.c b/src/shared/config.c index 8ae4c5e..270d538 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -47,6 +47,15 @@ Config* config_create(char* version, char* send_directory, char* receive_directo config->tls_ca = NULL; config->server_host = str_dup("127.0.0.1"); config->server_port = 8080; + config->timeout = 30; + config->contimeout = 10; + config->quiet = false; + config->backup = false; + config->backup_dir = NULL; + config->stats = false; + config->max_depth = 0; + config->log_file = NULL; + config->queue_size = 100; return config; } @@ -90,6 +99,7 @@ void config_delete(Config* config) { free(config->tls_cert); free(config->tls_key); free(config->tls_ca); + free(config->backup_dir); free(config->server_host); free(config); } @@ -127,6 +137,10 @@ bool config_send(int file_descriptor, const Config* config) { return false; if (!send_n_data(file_descriptor, &config->delta_max_file_size, sizeof(unsigned long long))) return false; + if (!send_int(file_descriptor, config->backup)) + return false; + if (!send_str(file_descriptor, config->backup_dir ? config->backup_dir : "")) + return false; Status status; if (!receive_status(file_descriptor, &status)) return false; @@ -221,6 +235,19 @@ Config* config_receive(int file_descriptor) { config->tls_cert = NULL; config->tls_key = NULL; config->tls_ca = NULL; + config->timeout = 30; + config->contimeout = 10; + config->quiet = false; + config->stats = false; + config->max_depth = 0; + config->log_file = NULL; + config->queue_size = 100; + if (!receive_int(file_descriptor, &tmp)) + goto error; + config->backup = tmp; + config->backup_dir = receive_str(file_descriptor); + if (config->backup_dir == NULL) + goto error; config->server_host = str_dup("127.0.0.1"); config->server_port = 8080; if (!send_status(file_descriptor, STATUS_OK)) diff --git a/src/shared/config.h b/src/shared/config.h index 183f60c..bd6d38f 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -3,6 +3,7 @@ #include #include +#include typedef enum { TRANSPORT_TCP, TRANSPORT_SSH } TransportType; @@ -40,6 +41,15 @@ typedef struct Config { char* tls_cert; char* tls_key; char* tls_ca; + int timeout; + int contimeout; + bool quiet; + bool backup; + char* backup_dir; + bool stats; + int max_depth; + FILE* log_file; + int queue_size; } Config; #define PROTOCOL_VERSION "1.3.0" diff --git a/src/shared/delta.c b/src/shared/delta.c index e9e1ea5..206ee78 100644 --- a/src/shared/delta.c +++ b/src/shared/delta.c @@ -108,7 +108,12 @@ DeltaSignature* delta_signature_deserialize(const Data* data) { return NULL; } - sig->blocks = malloc(sig->block_count * sizeof(DeltaBlockSig)); + uint64_t blocks_size = (uint64_t)sig->block_count * sizeof(DeltaBlockSig); + if (blocks_size > SIZE_MAX) { + free(sig); + return NULL; + } + sig->blocks = malloc((size_t)blocks_size); if (!sig->blocks) { free(sig); return NULL; diff --git a/src/shared/file.c b/src/shared/file.c index 58b0455..6e65a06 100644 --- a/src/shared/file.c +++ b/src/shared/file.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -42,6 +43,7 @@ File* file_create(const char* path) { return NULL; } file->metadata = NULL; + file->skip = false; return file; } @@ -126,7 +128,12 @@ bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata, return true; } -bool file_save_to_disk(const char* root_directory, File* file) { +bool file_save_to_disk(const char* root_directory, File* file, const Config* config) { + (void)config; + if (has_path_traversal(file->path)) { + log_message(LOG_LEVEL_ERROR, "Path traversal detected in file path: %s", file->path); + return false; + } char* disk_path = path_cat((char*)root_directory, file->path); if (disk_path == NULL) return false; @@ -325,6 +332,13 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) { return NULL; } + if (has_path_traversal(check_path)) { + log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s", check_path); + free(check_path); + send_status(fd, STATUS_ERROR); + return NULL; + } + char* full_path = path_cat(config->receive_root_directory, check_path); struct stat st; bool has_old_file = (full_path && stat(full_path, &st) == 0); @@ -409,37 +423,55 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) { } bool to_disk(const char* path, const void* data, unsigned long long data_size) { - // dirname() may modify its argument and may return a pointer to static storage. - // We must use a copy of the result to be safe. + char* tmp_path = NULL; + char* directory = NULL; + char* path_dup = str_dup(path); if (!path_dup) return false; const char* dir_result = dirname(path_dup); - char* directory = str_dup(dir_result); + directory = str_dup(dir_result); free(path_dup); if (!directory) return false; bool ok = true; - if (!mkdir_r(directory)) { + if (!mkdir_r(directory)) + goto done; + + size_t path_len = strlen(path); + tmp_path = malloc(path_len + 5); + if (!tmp_path) { ok = false; goto done; } - FILE* file_pointer = fopen(path, "wb"); + memcpy(tmp_path, path, path_len); + memcpy(tmp_path + path_len, ".tmp", 5); + + FILE* file_pointer = fopen(tmp_path, "wb"); if (file_pointer == NULL) { - perror("Could not open File"); + perror("Could not open temporary file"); ok = false; goto done; } if (fwrite(data, 1, data_size, file_pointer) != data_size) { - perror("Failed to write all data to disk"); + perror("Failed to write all data to temporary file"); fclose(file_pointer); + unlink(tmp_path); ok = false; goto done; } fclose(file_pointer); + if (rename(tmp_path, path) != 0) { + perror("Failed to atomically rename temporary file"); + unlink(tmp_path); + ok = false; + goto done; + } + done: + free(tmp_path); free(directory); return ok; } diff --git a/src/shared/file.h b/src/shared/file.h index f6acac2..2fffeea 100644 --- a/src/shared/file.h +++ b/src/shared/file.h @@ -18,6 +18,7 @@ typedef struct { char* path; Data* data; FileMetadata* metadata; + bool skip; } File; File* file_create(const char* path); @@ -32,7 +33,7 @@ size_t file_content_to_buffer(File* file); FileMetadata* file_metadata_create(const struct stat* stats); void file_metadata_destroy(void* metadata); bool to_disk(const char* path, const void* data, unsigned long long data_size); -bool file_save_to_disk(const char* root_directory, File* file); +bool file_save_to_disk(const char* root_directory, File* file, const Config* config); File* receive_incremental_check(int fd, const Config* config, bool* skipped); int receive_manifest(int fd, const Config* config, int* next_status); diff --git a/src/shared/log.c b/src/shared/log.c index bcf91bf..3f91161 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -5,11 +5,16 @@ static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"}; static LogLevel current_log_level = LOG_LEVEL_WARNING; +static FILE* log_fp = NULL; void set_log_level(LogLevel level) { current_log_level = level; } +void log_set_file(FILE* fp) { + log_fp = fp; +} + void log_message(LogLevel log_level, char* format, ...) { if (log_level < current_log_level) return; @@ -24,4 +29,14 @@ void log_message(LogLevel log_level, char* format, ...) { vfprintf(stderr, format, args); va_end(args); fprintf(stderr, "\n"); + + if (log_fp) { + fprintf(log_fp, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t->tm_year + 1900, t->tm_mon + 1, + t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, log_level_strings[log_level]); + va_start(args, format); + vfprintf(log_fp, format, args); + va_end(args); + fprintf(log_fp, "\n"); + fflush(log_fp); + } } diff --git a/src/shared/log.h b/src/shared/log.h index ccff5dc..6fc7ec7 100644 --- a/src/shared/log.h +++ b/src/shared/log.h @@ -1,9 +1,12 @@ #ifndef LOG_H #define LOG_H +#include + typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel; void log_message(LogLevel log_level, char* message, ...); void set_log_level(LogLevel level); +void log_set_file(FILE* fp); #endif diff --git a/src/shared/multiprocessing.c b/src/shared/multiprocessing.c index f6ce08f..a008059 100644 --- a/src/shared/multiprocessing.c +++ b/src/shared/multiprocessing.c @@ -105,7 +105,17 @@ int receive_thread(void* pipeline_context) { Status status; if (!receive_status(file_descriptor, &status)) return thrd_error; - while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK) { + while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK || + status == STATUS_KEEPALIVE || status == STATUS_ABORT || + status == STATUS_CHECK_BATCH) { + if (status == STATUS_KEEPALIVE) { + send_status(file_descriptor, STATUS_KEEPALIVE); + goto next; + } + if (status == STATUS_ABORT) { + log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up"); + return thrd_error; + } if (status == STATUS_CHECK) { bool skipped; File* file = receive_incremental_check(file_descriptor, config, &skipped); @@ -117,6 +127,34 @@ int receive_thread(void* pipeline_context) { } } else if (status == STATUS_CHUNK) { receive_chunk_enqueue(file_descriptor, context); + } else if (status == STATUS_CHECK_BATCH) { + int count; + if (!receive_int(file_descriptor, &count)) + return thrd_error; + for (int i = 0; i < count; i++) { + char* check_path = receive_str(file_descriptor); + if (!check_path) + return thrd_error; + unsigned long long check_size; + long long check_mtime; + if (!receive_n_data(file_descriptor, &check_size, sizeof(check_size)) || + !receive_n_data(file_descriptor, &check_mtime, sizeof(check_mtime))) { + free(check_path); + return thrd_error; + } + char* full_path = path_cat(config->receive_root_directory, check_path); + struct stat st; + bool has_old = full_path && stat(full_path, &st) == 0; + bool match = has_old && (unsigned long long)st.st_size == check_size && + (long long)st.st_mtime == check_mtime; + if (match) + send_status(file_descriptor, STATUS_OK); + else + send_status(file_descriptor, STATUS_NEXT); + free(full_path); + free(check_path); + } + goto next; } else { File* file = file_receive(config, file_descriptor); if (file) { @@ -126,6 +164,7 @@ int receive_thread(void* pipeline_context) { log_message(LOG_LEVEL_ERROR, "Failed to receive file"); } } + next: if (!receive_status(file_descriptor, &status)) return thrd_error; } @@ -156,7 +195,7 @@ int write_thread(void* pipeline_context) { return thrd_success; } if (save_to_disk) - file_save_to_disk(root_directory, file); + file_save_to_disk(root_directory, file, context->config); file_destroy(file); } } diff --git a/src/shared/protocol.c b/src/shared/protocol.c index 4d91ce6..32c2539 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -8,6 +8,10 @@ #include #include +#define MAX_DATA_SIZE (256ULL * 1024 * 1024) /* 256 MB max per message */ +#define RECEIVE_TIMEOUT_SEC 60 /* 60 second per-message timeout */ +#define MAX_CONNECTION_MEMORY (1024ULL * 1024 * 1024) /* 1 GB total per connection */ + static __thread int io_read_fd = -1; static __thread int io_write_fd = -1; static SSL* io_ssl = NULL; @@ -16,6 +20,8 @@ static unsigned long long io_bwlimit = 0; static long long bw_tokens = 0; static struct timespec bw_last_refill = {0, 0}; +static __thread unsigned long long total_allocated_bytes = 0; + void io_set_fds(int read_fd, int write_fd) { io_read_fd = read_fd; io_write_fd = write_fd; @@ -92,8 +98,21 @@ bool send_n_data(int file_descriptor, const void* data, size_t data_size) { bool receive_n_data(int file_descriptor, void* data, size_t data_size) { log_message(LOG_LEVEL_DEBUG, " Receiving n Data: %zu", data_size); int fd = io_fd(io_read_fd, file_descriptor); + + struct timespec deadline; + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += RECEIVE_TIMEOUT_SEC; + size_t total_bytes_received = 0; while (total_bytes_received < data_size) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec > deadline.tv_sec || + (now.tv_sec == deadline.tv_sec && now.tv_nsec > deadline.tv_nsec)) { + log_message(LOG_LEVEL_ERROR, "Receive timeout after %ds", RECEIVE_TIMEOUT_SEC); + return false; + } + ssize_t bytes_received; if (io_ssl) bytes_received = @@ -132,6 +151,12 @@ static const char* status_to_string(Status status) { return "DELTA_SIGNATURE"; case STATUS_DELTA_DATA: return "DELTA_DATA"; + case STATUS_KEEPALIVE: + return "KEEPALIVE"; + case STATUS_ABORT: + return "ABORT"; + case STATUS_CHECK_BATCH: + return "CHECK_BATCH"; default: return "UNKNOWN"; } @@ -151,6 +176,11 @@ char* receive_str(int file_descriptor) { size_t size; if (!receive_n_data(file_descriptor, &size, sizeof(size_t))) return NULL; + if (size > MAX_DATA_SIZE) { + log_message(LOG_LEVEL_ERROR, "String size %zu exceeds maximum %llu", size, + (unsigned long long)MAX_DATA_SIZE); + return NULL; + } char* data = (char*)malloc(size + 1); if (data == NULL) return NULL; @@ -177,6 +207,17 @@ Data* receive_data(int file_descriptor) { unsigned long long size = 0; if (!receive_n_data(file_descriptor, &size, sizeof(unsigned long long))) return NULL; + if (size > MAX_DATA_SIZE) { + log_message(LOG_LEVEL_ERROR, "Data size %llu exceeds maximum %llu", size, + (unsigned long long)MAX_DATA_SIZE); + return NULL; + } + if (total_allocated_bytes + size > MAX_CONNECTION_MEMORY) { + log_message(LOG_LEVEL_ERROR, "Per-connection memory limit exceeded (%llu + %llu > %llu)", + (unsigned long long)total_allocated_bytes, size, + (unsigned long long)MAX_CONNECTION_MEMORY); + return NULL; + } void* data = malloc((size_t)size); if (data == NULL) return NULL; @@ -184,6 +225,7 @@ Data* receive_data(int file_descriptor) { free(data); return NULL; } + total_allocated_bytes += size; log_message(LOG_LEVEL_DEBUG, "Received %lld data", size); return data_create(data, (size_t)size); } diff --git a/src/shared/protocol.h b/src/shared/protocol.h index a7854f1..113f14f 100644 --- a/src/shared/protocol.h +++ b/src/shared/protocol.h @@ -17,7 +17,10 @@ enum NET_STATUS { STATUS_MANIFEST, STATUS_CHECK, STATUS_DELTA_SIGNATURE, - STATUS_DELTA_DATA + STATUS_DELTA_DATA, + STATUS_KEEPALIVE, + STATUS_ABORT, + STATUS_CHECK_BATCH }; void io_set_fds(int read_fd, int write_fd); diff --git a/src/shared/transport_ssh.c b/src/shared/transport_ssh.c index 0462ce4..2ea943c 100644 --- a/src/shared/transport_ssh.c +++ b/src/shared/transport_ssh.c @@ -118,11 +118,18 @@ Client* client_connect_ssh(const char* destination, int port) { if (sv[1] > 1) close(sv[1]); - char ssh_user[512]; + size_t ssh_user_len; if (r.user && r.user[0] != '\0') - snprintf(ssh_user, sizeof(ssh_user), "%s@%s", r.user, r.host); + ssh_user_len = strlen(r.user) + 1 + strlen(r.host) + 1; else - snprintf(ssh_user, sizeof(ssh_user), "%s", r.host); + ssh_user_len = strlen(r.host) + 1; + char* ssh_user = malloc(ssh_user_len); + if (!ssh_user) + _exit(1); + if (r.user && r.user[0] != '\0') + snprintf(ssh_user, ssh_user_len, "%s@%s", r.user, r.host); + else + snprintf(ssh_user, ssh_user_len, "%s", r.host); char* ssh_argv[16]; int ac = 0; diff --git a/src/shared/transport_tcp.c b/src/shared/transport_tcp.c index c06278b..48ad48f 100644 --- a/src/shared/transport_tcp.c +++ b/src/shared/transport_tcp.c @@ -2,6 +2,7 @@ #include "log.h" #include "protocol.h" #include +#include #include #include #include @@ -11,6 +12,18 @@ #include #include +static volatile unsigned int g_active_connections = 0; + +static void sigchld_handler(int sig) { + (void)sig; + int saved_errno = errno; + while (waitpid(-1, NULL, WNOHANG) > 0) { + if (g_active_connections > 0) + g_active_connections--; + } + errno = saved_errno; +} + Server* server_create(int port) { Server* server = (Server*)malloc(sizeof(Server)); if (server == NULL) { @@ -38,6 +51,8 @@ Server* server_create(int port) { server->address.sin_port = htons(port); server->address_length = sizeof(server->address); server->ssl_ctx = NULL; + server->max_connections = 100; + server->active_connections = 0; if (bind(server->file_descriptor, (struct sockaddr*)&server->address, server->address_length) < 0) { @@ -68,7 +83,7 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil perror("Could not listen on port!"); return; } - signal(SIGCHLD, SIG_IGN); + signal(SIGCHLD, sigchld_handler); while (1) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); @@ -77,6 +92,12 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil perror("Could not accept the connection"); continue; } + if (g_active_connections >= server->max_connections) { + log_message(LOG_LEVEL_WARNING, "Max connections (%u) reached, rejecting", + server->max_connections); + close(fd); + continue; + } log_message(LOG_LEVEL_INFO, "%s", log_fmt); pid_t pid = fork(); if (pid == 0) { @@ -84,6 +105,8 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil child_fn(fd, child_ctx); close(fd); _exit(0); + } else if (pid > 0) { + g_active_connections++; } close(fd); } @@ -110,6 +133,24 @@ void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* chil accept_loop(server, child_fn, child_ctx, log_fmt); } +static int g_timeout_sec = 30; +static int g_contimeout_sec = 10; + +void tcp_set_timeouts(int timeout_sec, int contimeout_sec) { + if (timeout_sec > 0) + g_timeout_sec = timeout_sec; + if (contimeout_sec > 0) + g_contimeout_sec = contimeout_sec; +} + +static void tcp_apply_socket_timeout(int fd) { + struct timeval tv; + tv.tv_sec = g_timeout_sec; + tv.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); +} + Client* client_create() { int file_descriptor = socket(AF_INET, SOCK_STREAM, 0); if (file_descriptor < 0) { @@ -133,17 +174,27 @@ Client* client_create() { bool client_connect(Client* client, char* host, int port) { client->address.sin_port = htons(port); + client->address.sin_family = AF_INET; + client->address_length = sizeof(client->address); if (inet_pton(AF_INET, host, &client->address.sin_addr) <= 0) { perror("Could not convert host address!"); return false; } + struct timeval ct; + ct.tv_sec = g_contimeout_sec; + ct.tv_usec = 0; + setsockopt(client->file_descriptor, SOL_SOCKET, SO_RCVTIMEO, &ct, sizeof(ct)); + setsockopt(client->file_descriptor, SOL_SOCKET, SO_SNDTIMEO, &ct, sizeof(ct)); + if (connect(client->file_descriptor, (struct sockaddr*)&client->address, client->address_length) < 0) { perror("Could not connect to Server!"); return false; } + + tcp_apply_socket_timeout(client->file_descriptor); return true; } diff --git a/src/shared/transport_tcp.h b/src/shared/transport_tcp.h index 0c4b7da..71b03a2 100644 --- a/src/shared/transport_tcp.h +++ b/src/shared/transport_tcp.h @@ -10,6 +10,8 @@ typedef struct Server { unsigned int address_length; int file_descriptor; void* ssl_ctx; + unsigned int max_connections; + volatile unsigned int active_connections; } Server; typedef struct Client { @@ -30,5 +32,6 @@ Client* client_create(); bool client_connect(Client* client, char* host, int port); void client_disconnect(Client* client); void client_delete(Client* client); +void tcp_set_timeouts(int timeout_sec, int contimeout_sec); #endif diff --git a/src/shared/transport_tls.c b/src/shared/transport_tls.c index 4ce19a4..7959ee7 100644 --- a/src/shared/transport_tls.c +++ b/src/shared/transport_tls.c @@ -72,6 +72,8 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key } SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); SSL_CTX_set_verify_depth(ctx, 4); + } else { + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); } return ctx; diff --git a/src/shared/utils.c b/src/shared/utils.c index ad1e533..6ff5d99 100644 --- a/src/shared/utils.c +++ b/src/shared/utils.c @@ -149,6 +149,25 @@ void delete_extras(const char* dest_root, ArrayList* manifest) { delete_extras_walk(dest_root, "", manifest); } +bool has_path_traversal(const char* path) { + if (!path) + return false; + char* dup = str_dup(path); + if (!dup) + return false; + char* saveptr; + const char* part = strtok_r(dup, "/", &saveptr); + while (part) { + if (strcmp(part, "..") == 0) { + free(dup); + return true; + } + part = strtok_r(NULL, "/", &saveptr); + } + free(dup); + return false; +} + char* path_cat(const char* path1, char* path2) { if (path1 == NULL || *path1 == '\0') return str_dup(path2); diff --git a/src/shared/utils.h b/src/shared/utils.h index 13cd999..d9597de 100644 --- a/src/shared/utils.h +++ b/src/shared/utils.h @@ -9,5 +9,6 @@ char* str_dup(const char* string); char* path_cat(const char* path1, char* path2); bool glob_match(const char* pattern, const char* str); void delete_extras(const char* dest_root, ArrayList* manifest); +bool has_path_traversal(const char* path); #endif diff --git a/tests/test_file.c b/tests/test_file.c index 1a65a05..21fecdf 100644 --- a/tests/test_file.c +++ b/tests/test_file.c @@ -69,7 +69,7 @@ static void test_file_save_to_disk() { memcpy(f->data->data, content, strlen(content)); f->data->size = strlen(content); - EXPECT_TRUE(file_save_to_disk("test_save_tmp", f)); + EXPECT_TRUE(file_save_to_disk("test_save_tmp", f, NULL)); struct stat st; EXPECT_EQ_INT(stat("test_save_tmp/saved_file.txt", &st), 0); diff --git a/tests/test_scanner.c b/tests/test_scanner.c index 7920e0a..d821304 100644 --- a/tests/test_scanner.c +++ b/tests/test_scanner.c @@ -19,7 +19,7 @@ static void test_scanner_single_file() { create_test_file(file1, content1); DirectoryScanner* scanner = - directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0); + directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, 0); EXPECT_NOT_NULL(scanner); Chunk* chunk = directory_scanner_next(scanner); @@ -48,7 +48,7 @@ static void test_scanner_multiple_files() { create_test_file(file2, content2); DirectoryScanner* scanner = - directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0); + directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, 0); EXPECT_NOT_NULL(scanner); const Chunk* chunk = directory_scanner_next(scanner); @@ -88,7 +88,7 @@ static void test_scanner_subdirectory() { create_test_file(sub_file, content); DirectoryScanner* scanner = - directory_scanner_create((char*)root, false, 0, NULL, 0, NULL, 0, 0, 0); + directory_scanner_create((char*)root, false, 0, NULL, 0, NULL, 0, 0, 0, 0); EXPECT_NOT_NULL(scanner); int total_files = 0; @@ -112,7 +112,7 @@ static void test_scanner_empty_directory() { mkdir(dir, 0755); DirectoryScanner* scanner = - directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0); + directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, 0); EXPECT_NOT_NULL(scanner); const Chunk* chunk = directory_scanner_next(scanner); From be79c014d43dfc6f23eaf966e6543cc64932be8d Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 21 Jul 2026 16:43:40 +0200 Subject: [PATCH 2/3] feat: add CLI flags (#95-#99, #103-#105) --- src/client/client_cli.c | 8 ++++---- src/client/client_send.c | 8 ++++---- src/client/scanner.c | 3 +-- src/client/scanner.h | 3 +-- src/server/server.c | 3 +-- src/shared/multiprocessing.c | 3 +-- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 011c1bb..756c386 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -290,14 +290,14 @@ int main(int argc, char* argv[]) { goto cleanup; } } else if (strcmp(argv[i], "--exclude-from") == 0 && i + 1 < argc) { - if (read_patterns_from_file(argv[++i], &config->exclude_patterns, - &config->exclude_count) != 0) { + if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) != + 0) { exit_code = 1; goto cleanup; } } else if (strcmp(argv[i], "--include-from") == 0 && i + 1 < argc) { - if (read_patterns_from_file(argv[++i], &config->include_patterns, - &config->include_count) != 0) { + if (read_patterns_from_file(argv[++i], &config->include_patterns, &config->include_count) != + 0) { exit_code = 1; goto cleanup; } diff --git a/src/client/client_send.c b/src/client/client_send.c index 27376b1..8dfb2af 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -365,8 +365,8 @@ static int scan_directory_multithreaded(void* pipeline_context) { DirectoryScanner* scanner = directory_scanner_create( context->config->send_directory, context->config->use_metadata, context->config->chunk_size, context->config->exclude_patterns, context->config->exclude_count, - context->config->include_patterns, context->config->include_count, context->config->max_size, - context->config->min_size, context->config->max_depth); + context->config->include_patterns, context->config->include_count, context->config->max_size, + context->config->min_size, context->config->max_depth); mtx_unlock(&context->mutex_scanner); Chunk* current_chunk; @@ -491,7 +491,7 @@ int send_files(Config* config) { DirectoryScanner* scanner = directory_scanner_create( config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, config->exclude_count, config->include_patterns, config->include_count, config->max_size, - config->min_size, config->max_depth); + config->min_size, config->max_depth); ArrayList* all_files = array_list_create(NULL); ArrayList* manifest = config->use_delete ? array_list_create(free) : NULL; Chunk* current_chunk; @@ -564,7 +564,7 @@ int send_files(Config* config) { continue; } if (!file_send_single_calls(file, client->file_descriptor, config->use_metadata, - compression_level, true)) { + compression_level, true)) { log_message(LOG_LEVEL_ERROR, "Failed to send file"); batch_ok = false; break; diff --git a/src/client/scanner.c b/src/client/scanner.c index 5b49188..10c3278 100644 --- a/src/client/scanner.c +++ b/src/client/scanner.c @@ -37,8 +37,7 @@ DirectoryScanner* directory_scanner_create(char* root_directory, bool use_metada unsigned long long chunk_size, char** exclude_patterns, int exclude_count, char** include_patterns, int include_count, unsigned long long max_size, - unsigned long long min_size, - int max_depth) { + unsigned long long min_size, int max_depth) { DirectoryScanner* scanner = malloc(sizeof(DirectoryScanner)); if (scanner == NULL) return NULL; diff --git a/src/client/scanner.h b/src/client/scanner.h index eb0146e..a9e6c0d 100644 --- a/src/client/scanner.h +++ b/src/client/scanner.h @@ -26,8 +26,7 @@ DirectoryScanner* directory_scanner_create(char* root_directory, bool use_metada unsigned long long chunk_size, char** exclude_patterns, int exclude_count, char** include_patterns, int include_count, unsigned long long max_size, - unsigned long long min_size, - int max_depth); + unsigned long long min_size, int max_depth); Chunk* directory_scanner_next(DirectoryScanner* scanner); void directory_scanner_destroy(DirectoryScanner* scanner); diff --git a/src/server/server.c b/src/server/server.c index 5e59938..2e045ca 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -22,8 +22,7 @@ int receive_files(Config* config, int fd) { return -1; while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK || - status == STATUS_KEEPALIVE || status == STATUS_ABORT || - status == STATUS_CHECK_BATCH) { + status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) { if (status == STATUS_KEEPALIVE) { send_status(fd, STATUS_KEEPALIVE); goto next; diff --git a/src/shared/multiprocessing.c b/src/shared/multiprocessing.c index a008059..9aaa599 100644 --- a/src/shared/multiprocessing.c +++ b/src/shared/multiprocessing.c @@ -106,8 +106,7 @@ int receive_thread(void* pipeline_context) { if (!receive_status(file_descriptor, &status)) return thrd_error; while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK || - status == STATUS_KEEPALIVE || status == STATUS_ABORT || - status == STATUS_CHECK_BATCH) { + status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) { if (status == STATUS_KEEPALIVE) { send_status(file_descriptor, STATUS_KEEPALIVE); goto next; From 7d14d2b672eaa881534f9c7837b28498a5e48437 Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 21 Jul 2026 17:45:26 +0200 Subject: [PATCH 3/3] fix: const qualifier for scanner root_directory parameter --- .gitignore | 1 + src/client/scanner.c | 2 +- src/client/scanner.h | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f3ac385..3267e61 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build-asan coverage.info build-*/ build3/ +build_docker2/ diff --git a/src/client/scanner.c b/src/client/scanner.c index 10c3278..d5f2d18 100644 --- a/src/client/scanner.c +++ b/src/client/scanner.c @@ -33,7 +33,7 @@ static DirEntry* dir_entry_create(const char* path, int depth) { return de; } -DirectoryScanner* directory_scanner_create(char* root_directory, bool use_metadata, +DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_metadata, unsigned long long chunk_size, char** exclude_patterns, int exclude_count, char** include_patterns, int include_count, unsigned long long max_size, diff --git a/src/client/scanner.h b/src/client/scanner.h index a9e6c0d..7a77a25 100644 --- a/src/client/scanner.h +++ b/src/client/scanner.h @@ -22,7 +22,7 @@ typedef struct { int current_depth; } DirectoryScanner; -DirectoryScanner* directory_scanner_create(char* root_directory, bool use_metadata, +DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_metadata, unsigned long long chunk_size, char** exclude_patterns, int exclude_count, char** include_patterns, int include_count, unsigned long long max_size,