diff --git a/src/shared/file.c b/src/shared/file.c index c8afb0e..17c65c0 100644 --- a/src/shared/file.c +++ b/src/shared/file.c @@ -1,27 +1,14 @@ -#include -#include -#include -#include -#include -#include -#include #include #include #include -#include #include #include -#include -#include "compression.h" -#include "delta.h" -#include "log.h" -#include "config.h" #include "data.h" +#include "delta.h" #include "file.h" #include "file_store.h" -#include "metadata.h" -#include "protocol.h" +#include "log.h" #include "utils.h" bool file_checksum(File* file, uint64_t* checksum) { @@ -124,664 +111,17 @@ bool file_load_data(File* file) { return true; } -bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata, - int compression_level, bool send_path) { - if (!file || !file->path || !file->data) - return false; - const Data* data_to_send = file->data; - Data* compressed_data = NULL; - if (compression_level > 0 && !compression_should_skip(file->path)) { - compressed_data = data_compress(file->data, compression_level); - if (compressed_data == NULL) { - log_message(LOG_LEVEL_ERROR, "Failed to compress file data"); - return false; - } - data_to_send = compressed_data; - } - if (send_path && !send_str(file_descriptor, file->path)) { - data_destroy(compressed_data); - return false; - } - if (use_metadata && !metadata_send(file_descriptor, file->metadata)) { - data_destroy(compressed_data); - return false; - } - if (!send_data(file_descriptor, data_to_send)) { - data_destroy(compressed_data); - return false; - } - data_destroy(compressed_data); - return true; -} - bool file_set_authorized_root(int fd, const char* canonical_path) { return file_store_set_authorized_root(fd, canonical_path); } -static bool path_is_within_root(const char* root, const char* path) { - size_t n = strlen(root); - return strncmp(root, path, n) == 0 && (path[n] == '\0' || path[n] == '/'); -} - -bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) { - bool backup_enabled = config && config->backup; - bool inplace = config && config->inplace; - bool sparse = config && config->preserve_sparse; - const char* backup_suffix = (config && config->suffix) ? config->suffix : "~"; - const char* backup_dir = (config && config->backup_dir) ? config->backup_dir : NULL; - const char* partial_dir = (config && config->partial_dir) ? config->partial_dir : NULL; - char *confined_backup = NULL, *confined_partial = NULL; - - if (!file || !file->path || !file->data || has_path_traversal(file->path) || - (backup_enabled && - (!backup_suffix || backup_suffix[0] == '\0' || strchr(backup_suffix, '/') != NULL || - strcmp(backup_suffix, ".") == 0 || strcmp(backup_suffix, "..") == 0))) { - log_message(LOG_LEVEL_ERROR, "Invalid file or path received"); - return false; - } - - /* These options arrive from the client. They are names below the server - root, never independent filesystem roots. */ - if ((backup_dir && (backup_dir[0] == '/' || has_path_traversal(backup_dir))) || - (partial_dir && (partial_dir[0] == '/' || has_path_traversal(partial_dir)))) - return false; - if (backup_dir && !(confined_backup = path_cat(root_directory, backup_dir))) - return false; - if (partial_dir && !(confined_partial = path_cat(root_directory, partial_dir))) { - free(confined_backup); - return false; - } - - char* resolved_root = NULL; - const char* actual_root = - (partial_dir && config && config->partial) ? confined_partial : root_directory; - resolved_root = realpath(actual_root, NULL); - if (resolved_root == NULL) { - if (mkdir_r(actual_root)) { - resolved_root = realpath(actual_root, NULL); - } - } - if (resolved_root == NULL) { - log_message(LOG_LEVEL_ERROR, "Failed to resolve destination root: %s", actual_root); - free(confined_backup); - free(confined_partial); - return false; - } - char* resolved_base = realpath(root_directory, NULL); - if (resolved_base == NULL || !path_is_within_root(resolved_base, resolved_root)) { - free(resolved_base); - free(confined_backup); - free(confined_partial); - free(resolved_root); - return false; - } - free(resolved_base); - - char* disk_path = path_cat(resolved_root, file->path); - if (disk_path == NULL) { - free(confined_backup); - free(confined_partial); - free(resolved_root); - return false; - } - - /* --update is receiver-side policy: never replace a newer destination. */ - if (config && config->update) { - struct stat destination_stat; - if (stat(disk_path, &destination_stat) == 0 && file->metadata && - destination_stat.st_mtime > file->metadata->mtime_sec) { - free(resolved_root); - free(confined_backup); - free(confined_partial); - free(disk_path); - return true; - } - } - - if (backup_enabled) { - struct stat backup_stat; - if (stat(disk_path, &backup_stat) == 0) { - char* backup_path = NULL; - if (backup_dir) { - char* resolved_backup_dir = realpath(confined_backup, NULL); - if (!resolved_backup_dir) { - mkdir_r(confined_backup); - resolved_backup_dir = realpath(confined_backup, NULL); - } - if (resolved_backup_dir) { - char* backup_base = realpath(root_directory, NULL); - if (backup_base && path_is_within_root(backup_base, resolved_backup_dir)) - backup_path = path_cat(resolved_backup_dir, file->path); - free(backup_base); - free(resolved_backup_dir); - } - } - if (!backup_path) { - size_t path_len = strlen(disk_path); - size_t suffix_len = strlen(backup_suffix); - backup_path = malloc(path_len + suffix_len + 1); - if (backup_path) { - memcpy(backup_path, disk_path, path_len); - memcpy(backup_path + path_len, backup_suffix, suffix_len + 1); - } - } - if (backup_path) { - char* backup_dir_path = str_dup(backup_path); - if (backup_dir_path) { - const char* bdir = dirname(backup_dir_path); - mkdir_r(bdir); - free(backup_dir_path); - } - if (!file_store_rename_secure(disk_path, backup_path)) { - free(backup_path); - free(resolved_root); - free(confined_backup); - free(confined_partial); - free(disk_path); - return false; - } - free(backup_path); - } - } - } - - char* dir_dup = str_dup(disk_path); - if (!dir_dup) { - free(confined_backup); - free(confined_partial); - free(resolved_root); - free(disk_path); - return false; - } - char* dir_str = dirname(dir_dup); - if (!mkdir_r(dir_str)) { - free(dir_dup); - free(confined_backup); - free(confined_partial); - free(resolved_root); - free(disk_path); - return false; - } - char* resolved_dir = realpath(dir_str, NULL); - free(dir_dup); - if (resolved_dir == NULL) { - log_message(LOG_LEVEL_ERROR, "Failed to resolve directory for: %s", disk_path); - free(confined_backup); - free(confined_partial); - free(resolved_root); - free(disk_path); - return false; - } - - size_t root_len = strlen(resolved_root); - if (strncmp(resolved_dir, resolved_root, root_len) != 0 || - (resolved_dir[root_len] != '\0' && resolved_dir[root_len] != '/')) { - log_message(LOG_LEVEL_ERROR, "Path escape detected: %s is outside %s", disk_path, actual_root); - free(resolved_dir); - free(confined_backup); - free(confined_partial); - free(resolved_root); - free(disk_path); - return false; - } - free(resolved_dir); - free(resolved_root); - - bool ok = file_store_write_secure(disk_path, file->data->data, file->data->size, inplace, sparse, - file->metadata); - free(confined_backup); - free(confined_partial); - free(disk_path); - return ok; -} - -static File* receive_delta_file(int fd, const Config* config, const char* check_path, - void* old_data, unsigned long long old_size, bool* failed) { - if (!old_data) - return NULL; - - DeltaSignature* sig = delta_signature_create(old_data, old_size, config->delta_block_size); - if (!sig) { - free(old_data); - *failed = true; - return NULL; - } - - Data* sig_data = delta_signature_serialize(sig); - if (!sig_data) { - delta_signature_destroy(sig); - free(old_data); - *failed = true; - return NULL; - } - - bool sig_sent = send_status(fd, STATUS_DELTA_SIGNATURE) && send_data(fd, sig_data); - data_destroy(sig_data); - - if (!sig_sent) { - delta_signature_destroy(sig); - free(old_data); - *failed = true; - return NULL; - } - - Status resp; - if (!receive_status(fd, &resp)) { - delta_signature_destroy(sig); - free(old_data); - *failed = true; - return NULL; - } - - if (resp == STATUS_DELTA_DATA) { - Data* delta_data = receive_data(fd); - if (!delta_data) { - delta_signature_destroy(sig); - free(old_data); - *failed = true; - return NULL; - } - - Data* raw_delta = delta_data; - if (config->use_compression) { - raw_delta = data_decompress(delta_data); - data_destroy(delta_data); - if (!raw_delta) { - free(old_data); - delta_signature_destroy(sig); - *failed = true; - return NULL; - } - } - - Delta* delta = delta_deserialize(raw_delta); - data_destroy(raw_delta); - if (!delta) { - free(old_data); - delta_signature_destroy(sig); - *failed = true; - return NULL; - } - - void* new_data = delta_apply(old_data, old_size, delta, config->delta_block_size); - uint64_t new_size = delta->new_file_size; - delta_destroy(delta); - - if (!new_data) { - free(old_data); - delta_signature_destroy(sig); - *failed = true; - return NULL; - } - - File* file = file_create(check_path); - if (!file) { - free(new_data); - free(old_data); - delta_signature_destroy(sig); - *failed = true; - return NULL; - } - - if (config->use_metadata) { - int meta_ok = 1; - file->metadata = metadata_receive(fd, &meta_ok); - if (!meta_ok) { - file_destroy(file); - free(new_data); - free(old_data); - delta_signature_destroy(sig); - *failed = true; - return NULL; - } - } - - data_destroy(file->data); - file->data = data_create(new_data, (size_t)new_size); - - free(old_data); - delta_signature_destroy(sig); - return file; - } - - if (resp == STATUS_NEXT) { - delta_signature_destroy(sig); - free(old_data); - - File* file = file_create(check_path); - if (!file) { - *failed = true; - return NULL; - } - - if (config->use_metadata) { - int meta_ok = 1; - file->metadata = metadata_receive(fd, &meta_ok); - if (!meta_ok) { - file_destroy(file); - *failed = true; - return NULL; - } - } - - Data* file_data = receive_data(fd); - if (file_data == NULL) { - file_destroy(file); - *failed = true; - return NULL; - } - - if (config->use_compression) { - Data* uncompressed = data_decompress(file_data); - data_destroy(file_data); - if (uncompressed == NULL) { - file_destroy(file); - *failed = true; - return NULL; - } - file_data = uncompressed; - } - - data_destroy(file->data); - file->data = file_data; - return file; - } - - delta_signature_destroy(sig); - free(old_data); - *failed = true; - return NULL; -} - -File* receive_incremental_check(int fd, const Config* config, bool* skipped) { - *skipped = false; - char* check_path = receive_str(fd); - if (check_path == NULL) { - return NULL; - } - - unsigned long long check_size; - long long check_mtime; - uint64_t check_checksum = 0; - if (!receive_n_data(fd, &check_size, sizeof(check_size)) || - !receive_n_data(fd, &check_mtime, sizeof(check_mtime))) { - free(check_path); - return NULL; - } - if (config->checksum && !receive_n_data(fd, &check_checksum, sizeof(check_checksum))) { - free(check_path); - return NULL; - } - - if (has_path_traversal(check_path)) { - log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s", check_path); - free(check_path); - return NULL; - } - - char* full_path = path_cat(config->receive_root_directory, check_path); - struct stat st; - bool has_old_file = false; - int old_fd = -1; - if (full_path) { - char* leaf = NULL; - int parent_fd = file_store_open_secure_parent(full_path, &leaf); - if (parent_fd >= 0) { - old_fd = openat(parent_fd, leaf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); - free(leaf); - close(parent_fd); - has_old_file = old_fd >= 0 && fstat(old_fd, &st) == 0 && S_ISREG(st.st_mode); - } - } - unsigned long long old_size = has_old_file ? (unsigned long long)st.st_size : 0; - void* old_data = NULL; - if (has_old_file && old_size > 0) { - old_data = malloc((size_t)old_size); - if (old_data) { - size_t got = 0; - while (got < (size_t)old_size) { - ssize_t n = read(old_fd, (char*)old_data + got, (size_t)old_size - got); - if (n <= 0) { - free(old_data); - old_data = NULL; - break; - } - got += (size_t)n; - } - } - } - if (old_fd >= 0) { - close(old_fd); - } - - bool match = has_old_file && (unsigned long long)st.st_size == check_size; - if (match && config->checksum) { - uint64_t old_checksum = old_size == 0 ? delta_xxhash64("", 0) : 0; - if (old_data) - old_checksum = delta_xxhash64(old_data, (size_t)old_size); - match = (old_size == 0 || old_data) && old_checksum == check_checksum; - free(old_data); - old_data = NULL; - } else if (match) { - match = (long long)st.st_mtime == check_mtime; - } - - if (match) { - free(old_data); - if (!send_status(fd, STATUS_OK)) { - free(full_path); - free(check_path); - return NULL; - } - free(full_path); - free(check_path); - *skipped = true; - return NULL; - } - - bool try_delta = config->use_delta && has_old_file && - delta_should_attempt(old_size, check_size, config->delta_max_file_size); - - if (try_delta) { - bool delta_failed = false; - File* delta_file = - receive_delta_file(fd, config, check_path, old_data, old_size, &delta_failed); - old_data = NULL; /* receive_delta_file consumes the snapshot on every path */ - if (delta_file) { - free(full_path); - free(check_path); - return delta_file; - } - if (delta_failed) { - free(full_path); - free(check_path); - return NULL; - } - free(old_data); - old_data = NULL; - try_delta = false; - } - - if (!try_delta) { - if (!send_status(fd, STATUS_NEXT)) { - free(full_path); - free(check_path); - return NULL; - } - } - - File* file = file_create(check_path); - free(check_path); - free(full_path); - if (file == NULL) { - return NULL; - } - - if (config->use_metadata) { - int meta_ok = 1; - file->metadata = metadata_receive(fd, &meta_ok); - if (!meta_ok) { - file_destroy(file); - return NULL; - } - } - - Data* file_data = receive_data(fd); - if (file_data == NULL) { - file_destroy(file); - return NULL; - } - - if (config->use_compression) { - Data* uncompressed = data_decompress(file_data); - data_destroy(file_data); - if (uncompressed == NULL) { - file_destroy(file); - return NULL; - } - file_data = uncompressed; - } - - data_destroy(file->data); - file->data = file_data; - return file; -} - -bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace, - bool sparse) { +bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, + bool inplace, bool sparse) { if (!path || (!data && data_size != 0) || has_path_traversal(path)) return false; return file_store_write_secure(path, data, data_size, inplace, sparse, NULL); } -bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level, - bool send_path) { - if (!file || !file->path || !file->data) - return false; - if (compression_level > 0) - return file_send_single_calls(file, file_descriptor, use_metadata, compression_level, - send_path); - - if (send_path && !send_str(file_descriptor, file->path)) - return false; - if (use_metadata && !metadata_send(file_descriptor, file->metadata)) - return false; - - int fd = open(file->path, O_RDONLY); - if (fd == -1) { - perror("Could not open file for sendfile"); - return false; - } - - unsigned long long file_size = file->data->size; - struct stat source_stat; - if (fstat(fd, &source_stat) != 0 || !S_ISREG(source_stat.st_mode) || - (unsigned long long)source_stat.st_size < file_size) { - close(fd); - return false; - } - if (!send_n_data(file_descriptor, &file_size, sizeof(unsigned long long))) { - close(fd); - return false; - } - - /* sendfile cannot encrypt TLS records. Keep the framing identical but - route encrypted transfers through the deadline-aware IO layer. */ - if (io_get_ssl() != NULL) { - unsigned char buffer[64 * 1024]; - unsigned long long remaining = file_size; - bool ok = true; - while (remaining > 0) { - size_t want = remaining > sizeof(buffer) ? sizeof(buffer) : (size_t)remaining; - ssize_t got = read(fd, buffer, want); - if (got <= 0 || !send_n_data(file_descriptor, buffer, (size_t)got)) { - ok = false; - break; - } - remaining -= (unsigned long long)got; - } - close(fd); - return ok; - } - - off_t offset = 0; - struct timespec deadline; - clock_gettime(CLOCK_MONOTONIC, &deadline); - deadline.tv_sec += 60; - while ((unsigned long long)offset < file_size) { - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - long long remaining = (long long)(deadline.tv_sec - now.tv_sec) * 1000LL + - (deadline.tv_nsec - now.tv_nsec) / 1000000LL; - if (remaining <= 0) { - close(fd); - return false; - } - struct pollfd pfd = {.fd = file_descriptor, .events = POLLOUT}; - int timeout = remaining > INT_MAX ? INT_MAX : (int)remaining; - int polled = poll(&pfd, 1, timeout); - if (polled <= 0 || (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) { - close(fd); - return false; - } - ssize_t sent = sendfile(file_descriptor, fd, &offset, file_size - offset); - if (sent == -1) { - if (errno == EAGAIN || errno == EINTR) - continue; - perror("sendfile failed"); - close(fd); - return false; - } - if (sent == 0) { - close(fd); - return false; - } - } - - close(fd); - return true; -} - -File* file_receive(const Config* config, int file_descriptor) { - char* path = receive_str(file_descriptor); - if (path == NULL) - return NULL; - if (path[0] == '\0' || has_path_traversal(path)) { - log_message(LOG_LEVEL_ERROR, "Invalid received file path: %s", path); - free(path); - return NULL; - } - File* file = file_create(path); - free(path); - if (file == NULL) - return NULL; - if (config->use_metadata) { - int meta_ok = 1; - file->metadata = metadata_receive(file_descriptor, &meta_ok); - if (!meta_ok) { - file_destroy(file); - return NULL; - } - } - Data* file_data = receive_data(file_descriptor); - if (file_data == NULL) { - file_destroy(file); - return NULL; - } - if (config->use_compression) { - Data* file_data_uncompressed = data_decompress(file_data); - data_destroy(file_data); - if (file_data_uncompressed == NULL) { - file_destroy(file); - return NULL; - } - file_data = file_data_uncompressed; - } - data_destroy(file->data); - file->data = file_data; - return file; -} - size_t file_content_to_buffer(File* file) { FILE* file_pointer = fopen(file->path, "rb"); if (file_pointer == NULL) { @@ -797,42 +137,3 @@ size_t file_content_to_buffer(File* file) { fclose(file_pointer); return bytes_read; } - -int receive_manifest(int fd, const Config* config, int* next_status) { - int received_status = STATUS_ERROR; - int* status_out = next_status ? next_status : &received_status; - int count; - if (!receive_int(fd, &count)) - return -1; - if (count < 0 || count > MAX_MANIFEST_ENTRIES) - return -1; - ArrayList* manifest = array_list_create(free); - if (!manifest) - return -1; - size_t manifest_bytes = 0; - for (int i = 0; i < count; i++) { - char* s = receive_str(fd); - size_t entry_size = s ? strlen(s) : 0; - if (!s || s[0] == '\0' || s[0] == '/' || has_path_traversal(s) || - entry_size > MAX_MANIFEST_BYTES - manifest_bytes || - (manifest_bytes += entry_size) > MAX_MANIFEST_BYTES || !array_list_add(manifest, s)) { - free(s); - array_list_delete(manifest); - return -1; - } - } - if (!receive_status(fd, status_out)) { - array_list_delete(manifest); - return -1; - } - /* Deletion is a commit operation: never perform it until the sender has - completed the manifest frame successfully. */ - if (*status_out != STATUS_FINISHED || !config->use_delete) { - array_list_delete(manifest); - return *status_out == STATUS_FINISHED ? 0 : -1; - } - fprintf(stderr, "Deleting files not in manifest...\n"); - bool deletion_ok = delete_extras(config->receive_root_directory, manifest); - array_list_delete(manifest); - return deletion_ok ? 0 : -1; -} diff --git a/src/shared/file.h b/src/shared/file.h index 3c388c2..7f72116 100644 --- a/src/shared/file.h +++ b/src/shared/file.h @@ -1,45 +1,23 @@ #ifndef FILE_H #define FILE_H -#include "config.h" -#include "data.h" +#include "file_send.h" +#include "file_receive.h" +#include "file_types.h" #include -#include +#include -typedef enum { FILE_TYPE_REGULAR, FILE_TYPE_SYMLINK, FILE_TYPE_DIR } FileType; - -typedef struct { - mode_t mode; - uid_t uid; - gid_t gid; - time_t mtime_sec; - long mtime_nsec; -} FileMetadata; - -typedef struct { - char* path; - Data* data; - FileMetadata* metadata; - bool skip; -} File; +/* File/FileMetadata lifecycle and local disk helpers. */ File* file_create(const char* path); void file_destroy(void* item); bool file_load_data(File* file); bool file_checksum(File* file, uint64_t* checksum); -File* file_receive(const Config* config, int file_descriptor); -bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata, - int compression_level, bool send_path); -bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level, - bool send_path); size_t file_content_to_buffer(File* file); FileMetadata* file_metadata_create(const struct stat* stats); void file_metadata_destroy(void* metadata); -bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace, - bool sparse); -bool file_save_to_disk(const char* root_directory, const File* file, const Config* config); +bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, + bool inplace, bool sparse); bool file_set_authorized_root(int fd, const char* canonical_path); -File* receive_incremental_check(int fd, const Config* config, bool* skipped); -int receive_manifest(int fd, const Config* config, int* next_status); #endif diff --git a/src/shared/file_receive.c b/src/shared/file_receive.c new file mode 100644 index 0000000..f809b7c --- /dev/null +++ b/src/shared/file_receive.c @@ -0,0 +1,588 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "array_list.h" +#include "compression.h" +#include "config.h" +#include "data.h" +#include "delta.h" +#include "file.h" +#include "file_store.h" +#include "log.h" +#include "metadata.h" +#include "protocol.h" +#include "utils.h" + +static bool path_is_within_root(const char* root, const char* path) { + size_t n = strlen(root); + return strncmp(root, path, n) == 0 && (path[n] == '\0' || path[n] == '/'); +} + +bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) { + bool backup_enabled = config && config->backup; + bool inplace = config && config->inplace; + bool sparse = config && config->preserve_sparse; + const char* backup_suffix = (config && config->suffix) ? config->suffix : "~"; + const char* backup_dir = (config && config->backup_dir) ? config->backup_dir : NULL; + const char* partial_dir = (config && config->partial_dir) ? config->partial_dir : NULL; + char *confined_backup = NULL, *confined_partial = NULL; + + if (!file || !file->path || !file->data || has_path_traversal(file->path) || + (backup_enabled && + (!backup_suffix || backup_suffix[0] == '\0' || strchr(backup_suffix, '/') != NULL || + strcmp(backup_suffix, ".") == 0 || strcmp(backup_suffix, "..") == 0))) { + log_message(LOG_LEVEL_ERROR, "Invalid file or path received"); + return false; + } + + /* These options arrive from the client. They are names below the server + root, never independent filesystem roots. */ + if ((backup_dir && (backup_dir[0] == '/' || has_path_traversal(backup_dir))) || + (partial_dir && (partial_dir[0] == '/' || has_path_traversal(partial_dir)))) + return false; + if (backup_dir && !(confined_backup = path_cat(root_directory, backup_dir))) + return false; + if (partial_dir && !(confined_partial = path_cat(root_directory, partial_dir))) { + free(confined_backup); + return false; + } + + char* resolved_root = NULL; + const char* actual_root = + (partial_dir && config && config->partial) ? confined_partial : root_directory; + resolved_root = realpath(actual_root, NULL); + if (resolved_root == NULL) { + if (mkdir_r(actual_root)) { + resolved_root = realpath(actual_root, NULL); + } + } + if (resolved_root == NULL) { + log_message(LOG_LEVEL_ERROR, "Failed to resolve destination root: %s", actual_root); + free(confined_backup); + free(confined_partial); + return false; + } + char* resolved_base = realpath(root_directory, NULL); + if (resolved_base == NULL || !path_is_within_root(resolved_base, resolved_root)) { + free(resolved_base); + free(confined_backup); + free(confined_partial); + free(resolved_root); + return false; + } + free(resolved_base); + + char* disk_path = path_cat(resolved_root, file->path); + if (disk_path == NULL) { + free(confined_backup); + free(confined_partial); + free(resolved_root); + return false; + } + + /* --update is receiver-side policy: never replace a newer destination. */ + if (config && config->update) { + struct stat destination_stat; + if (stat(disk_path, &destination_stat) == 0 && file->metadata && + destination_stat.st_mtime > file->metadata->mtime_sec) { + free(resolved_root); + free(confined_backup); + free(confined_partial); + free(disk_path); + return true; + } + } + + if (backup_enabled) { + struct stat backup_stat; + if (stat(disk_path, &backup_stat) == 0) { + char* backup_path = NULL; + if (backup_dir) { + char* resolved_backup_dir = realpath(confined_backup, NULL); + if (!resolved_backup_dir) { + mkdir_r(confined_backup); + resolved_backup_dir = realpath(confined_backup, NULL); + } + if (resolved_backup_dir) { + char* backup_base = realpath(root_directory, NULL); + if (backup_base && path_is_within_root(backup_base, resolved_backup_dir)) + backup_path = path_cat(resolved_backup_dir, file->path); + free(backup_base); + free(resolved_backup_dir); + } + } + if (!backup_path) { + size_t path_len = strlen(disk_path); + size_t suffix_len = strlen(backup_suffix); + backup_path = malloc(path_len + suffix_len + 1); + if (backup_path) { + memcpy(backup_path, disk_path, path_len); + memcpy(backup_path + path_len, backup_suffix, suffix_len + 1); + } + } + if (backup_path) { + char* backup_dir_path = str_dup(backup_path); + if (backup_dir_path) { + const char* bdir = dirname(backup_dir_path); + mkdir_r(bdir); + free(backup_dir_path); + } + if (!file_store_rename_secure(disk_path, backup_path)) { + free(backup_path); + free(resolved_root); + free(confined_backup); + free(confined_partial); + free(disk_path); + return false; + } + free(backup_path); + } + } + } + + char* dir_dup = str_dup(disk_path); + if (!dir_dup) { + free(confined_backup); + free(confined_partial); + free(resolved_root); + free(disk_path); + return false; + } + char* dir_str = dirname(dir_dup); + if (!mkdir_r(dir_str)) { + free(dir_dup); + free(confined_backup); + free(confined_partial); + free(resolved_root); + free(disk_path); + return false; + } + char* resolved_dir = realpath(dir_str, NULL); + free(dir_dup); + if (resolved_dir == NULL) { + log_message(LOG_LEVEL_ERROR, "Failed to resolve directory for: %s", disk_path); + free(confined_backup); + free(confined_partial); + free(resolved_root); + free(disk_path); + return false; + } + + size_t root_len = strlen(resolved_root); + if (strncmp(resolved_dir, resolved_root, root_len) != 0 || + (resolved_dir[root_len] != '\0' && resolved_dir[root_len] != '/')) { + log_message(LOG_LEVEL_ERROR, "Path escape detected: %s is outside %s", disk_path, actual_root); + free(resolved_dir); + free(confined_backup); + free(confined_partial); + free(resolved_root); + free(disk_path); + return false; + } + free(resolved_dir); + free(resolved_root); + + bool ok = file_store_write_secure(disk_path, file->data->data, file->data->size, inplace, sparse, + file->metadata); + free(confined_backup); + free(confined_partial); + free(disk_path); + return ok; +} + +static File* receive_delta_file(int fd, const Config* config, const char* check_path, + void* old_data, unsigned long long old_size, bool* failed) { + if (!old_data) + return NULL; + + DeltaSignature* sig = delta_signature_create(old_data, old_size, config->delta_block_size); + if (!sig) { + free(old_data); + *failed = true; + return NULL; + } + + Data* sig_data = delta_signature_serialize(sig); + if (!sig_data) { + delta_signature_destroy(sig); + free(old_data); + *failed = true; + return NULL; + } + + bool sig_sent = send_status(fd, STATUS_DELTA_SIGNATURE) && send_data(fd, sig_data); + data_destroy(sig_data); + + if (!sig_sent) { + delta_signature_destroy(sig); + free(old_data); + *failed = true; + return NULL; + } + + Status resp; + if (!receive_status(fd, &resp)) { + delta_signature_destroy(sig); + free(old_data); + *failed = true; + return NULL; + } + + if (resp == STATUS_DELTA_DATA) { + Data* delta_data = receive_data(fd); + if (!delta_data) { + delta_signature_destroy(sig); + free(old_data); + *failed = true; + return NULL; + } + + Data* raw_delta = delta_data; + if (config->use_compression) { + raw_delta = data_decompress(delta_data); + data_destroy(delta_data); + if (!raw_delta) { + free(old_data); + delta_signature_destroy(sig); + *failed = true; + return NULL; + } + } + + Delta* delta = delta_deserialize(raw_delta); + data_destroy(raw_delta); + if (!delta) { + free(old_data); + delta_signature_destroy(sig); + *failed = true; + return NULL; + } + + void* new_data = delta_apply(old_data, old_size, delta, config->delta_block_size); + uint64_t new_size = delta->new_file_size; + delta_destroy(delta); + + if (!new_data) { + free(old_data); + delta_signature_destroy(sig); + *failed = true; + return NULL; + } + + File* file = file_create(check_path); + if (!file) { + free(new_data); + free(old_data); + delta_signature_destroy(sig); + *failed = true; + return NULL; + } + + if (config->use_metadata) { + int meta_ok = 1; + file->metadata = metadata_receive(fd, &meta_ok); + if (!meta_ok) { + file_destroy(file); + free(new_data); + free(old_data); + delta_signature_destroy(sig); + *failed = true; + return NULL; + } + } + + data_destroy(file->data); + file->data = data_create(new_data, (size_t)new_size); + + free(old_data); + delta_signature_destroy(sig); + return file; + } + + if (resp == STATUS_NEXT) { + delta_signature_destroy(sig); + free(old_data); + + File* file = file_create(check_path); + if (!file) { + *failed = true; + return NULL; + } + + if (config->use_metadata) { + int meta_ok = 1; + file->metadata = metadata_receive(fd, &meta_ok); + if (!meta_ok) { + file_destroy(file); + *failed = true; + return NULL; + } + } + + Data* file_data = receive_data(fd); + if (file_data == NULL) { + file_destroy(file); + *failed = true; + return NULL; + } + + if (config->use_compression) { + Data* uncompressed = data_decompress(file_data); + data_destroy(file_data); + if (uncompressed == NULL) { + file_destroy(file); + *failed = true; + return NULL; + } + file_data = uncompressed; + } + + data_destroy(file->data); + file->data = file_data; + return file; + } + + delta_signature_destroy(sig); + free(old_data); + *failed = true; + return NULL; +} + +File* receive_incremental_check(int fd, const Config* config, bool* skipped) { + *skipped = false; + char* check_path = receive_str(fd); + if (check_path == NULL) { + return NULL; + } + + unsigned long long check_size; + long long check_mtime; + uint64_t check_checksum = 0; + if (!receive_n_data(fd, &check_size, sizeof(check_size)) || + !receive_n_data(fd, &check_mtime, sizeof(check_mtime))) { + free(check_path); + return NULL; + } + if (config->checksum && !receive_n_data(fd, &check_checksum, sizeof(check_checksum))) { + free(check_path); + return NULL; + } + + if (has_path_traversal(check_path)) { + log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s", check_path); + free(check_path); + return NULL; + } + + char* full_path = path_cat(config->receive_root_directory, check_path); + struct stat st; + bool has_old_file = false; + int old_fd = -1; + if (full_path) { + char* leaf = NULL; + int parent_fd = file_store_open_secure_parent(full_path, &leaf); + if (parent_fd >= 0) { + old_fd = openat(parent_fd, leaf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + free(leaf); + close(parent_fd); + has_old_file = old_fd >= 0 && fstat(old_fd, &st) == 0 && S_ISREG(st.st_mode); + } + } + unsigned long long old_size = has_old_file ? (unsigned long long)st.st_size : 0; + void* old_data = NULL; + if (has_old_file && old_size > 0) { + old_data = malloc((size_t)old_size); + if (old_data) { + size_t got = 0; + while (got < (size_t)old_size) { + ssize_t n = read(old_fd, (char*)old_data + got, (size_t)old_size - got); + if (n <= 0) { + free(old_data); + old_data = NULL; + break; + } + got += (size_t)n; + } + } + } + if (old_fd >= 0) { + close(old_fd); + } + + bool match = has_old_file && (unsigned long long)st.st_size == check_size; + if (match && config->checksum) { + uint64_t old_checksum = old_size == 0 ? delta_xxhash64("", 0) : 0; + if (old_data) + old_checksum = delta_xxhash64(old_data, (size_t)old_size); + match = (old_size == 0 || old_data) && old_checksum == check_checksum; + free(old_data); + old_data = NULL; + } else if (match) { + match = (long long)st.st_mtime == check_mtime; + } + + if (match) { + free(old_data); + if (!send_status(fd, STATUS_OK)) { + free(full_path); + free(check_path); + return NULL; + } + free(full_path); + free(check_path); + *skipped = true; + return NULL; + } + + bool try_delta = config->use_delta && has_old_file && + delta_should_attempt(old_size, check_size, config->delta_max_file_size); + + if (try_delta) { + bool delta_failed = false; + File* delta_file = + receive_delta_file(fd, config, check_path, old_data, old_size, &delta_failed); + old_data = NULL; /* receive_delta_file consumes the snapshot on every path */ + if (delta_file) { + free(full_path); + free(check_path); + return delta_file; + } + if (delta_failed) { + free(full_path); + free(check_path); + return NULL; + } + free(old_data); + old_data = NULL; + try_delta = false; + } + + if (!try_delta) { + if (!send_status(fd, STATUS_NEXT)) { + free(full_path); + free(check_path); + return NULL; + } + } + + File* file = file_create(check_path); + free(check_path); + free(full_path); + if (file == NULL) { + return NULL; + } + + if (config->use_metadata) { + int meta_ok = 1; + file->metadata = metadata_receive(fd, &meta_ok); + if (!meta_ok) { + file_destroy(file); + return NULL; + } + } + + Data* file_data = receive_data(fd); + if (file_data == NULL) { + file_destroy(file); + return NULL; + } + + if (config->use_compression) { + Data* uncompressed = data_decompress(file_data); + data_destroy(file_data); + if (uncompressed == NULL) { + file_destroy(file); + return NULL; + } + file_data = uncompressed; + } + + data_destroy(file->data); + file->data = file_data; + return file; +} + +File* file_receive(const Config* config, int file_descriptor) { + char* path = receive_str(file_descriptor); + if (path == NULL) + return NULL; + if (path[0] == '\0' || has_path_traversal(path)) { + log_message(LOG_LEVEL_ERROR, "Invalid received file path: %s", path); + free(path); + return NULL; + } + File* file = file_create(path); + free(path); + if (file == NULL) + return NULL; + if (config->use_metadata) { + int meta_ok = 1; + file->metadata = metadata_receive(file_descriptor, &meta_ok); + if (!meta_ok) { + file_destroy(file); + return NULL; + } + } + Data* file_data = receive_data(file_descriptor); + if (file_data == NULL) { + file_destroy(file); + return NULL; + } + if (config->use_compression) { + Data* file_data_uncompressed = data_decompress(file_data); + data_destroy(file_data); + if (file_data_uncompressed == NULL) { + file_destroy(file); + return NULL; + } + file_data = file_data_uncompressed; + } + data_destroy(file->data); + file->data = file_data; + return file; +} + +int receive_manifest(int fd, const Config* config, int* next_status) { + int received_status = STATUS_ERROR; + int* status_out = next_status ? next_status : &received_status; + int count; + if (!receive_int(fd, &count)) + return -1; + if (count < 0 || count > MAX_MANIFEST_ENTRIES) + return -1; + ArrayList* manifest = array_list_create(free); + if (!manifest) + return -1; + size_t manifest_bytes = 0; + for (int i = 0; i < count; i++) { + char* s = receive_str(fd); + size_t entry_size = s ? strlen(s) : 0; + if (!s || s[0] == '\0' || s[0] == '/' || has_path_traversal(s) || + entry_size > MAX_MANIFEST_BYTES - manifest_bytes || + (manifest_bytes += entry_size) > MAX_MANIFEST_BYTES || !array_list_add(manifest, s)) { + free(s); + array_list_delete(manifest); + return -1; + } + } + if (!receive_status(fd, status_out)) { + array_list_delete(manifest); + return -1; + } + /* Deletion is a commit operation: never perform it until the sender has + completed the manifest frame successfully. */ + if (*status_out != STATUS_FINISHED || !config->use_delete) { + array_list_delete(manifest); + return *status_out == STATUS_FINISHED ? 0 : -1; + } + fprintf(stderr, "Deleting files not in manifest...\n"); + bool deletion_ok = delete_extras(config->receive_root_directory, manifest); + array_list_delete(manifest); + return deletion_ok ? 0 : -1; +} diff --git a/src/shared/file_receive.h b/src/shared/file_receive.h new file mode 100644 index 0000000..5b2d2a4 --- /dev/null +++ b/src/shared/file_receive.h @@ -0,0 +1,15 @@ +#ifndef FILE_RECEIVE_H +#define FILE_RECEIVE_H + +#include "config.h" +#include "file_types.h" +#include + +/* Server-side file receive/save path. */ + +File* file_receive(const Config* config, int file_descriptor); +File* receive_incremental_check(int fd, const Config* config, bool* skipped); +int receive_manifest(int fd, const Config* config, int* next_status); +bool file_save_to_disk(const char* root_directory, const File* file, const Config* config); + +#endif diff --git a/src/shared/file_send.c b/src/shared/file_send.c new file mode 100644 index 0000000..61e1762 --- /dev/null +++ b/src/shared/file_send.c @@ -0,0 +1,136 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "compression.h" +#include "data.h" +#include "file.h" +#include "log.h" +#include "metadata.h" +#include "protocol.h" + +bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata, + int compression_level, bool send_path) { + if (!file || !file->path || !file->data) + return false; + const Data* data_to_send = file->data; + Data* compressed_data = NULL; + if (compression_level > 0 && !compression_should_skip(file->path)) { + compressed_data = data_compress(file->data, compression_level); + if (compressed_data == NULL) { + log_message(LOG_LEVEL_ERROR, "Failed to compress file data"); + return false; + } + data_to_send = compressed_data; + } + if (send_path && !send_str(file_descriptor, file->path)) { + data_destroy(compressed_data); + return false; + } + if (use_metadata && !metadata_send(file_descriptor, file->metadata)) { + data_destroy(compressed_data); + return false; + } + if (!send_data(file_descriptor, data_to_send)) { + data_destroy(compressed_data); + return false; + } + data_destroy(compressed_data); + return true; +} + +bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level, + bool send_path) { + if (!file || !file->path || !file->data) + return false; + if (compression_level > 0) + return file_send_single_calls(file, file_descriptor, use_metadata, compression_level, + send_path); + + if (send_path && !send_str(file_descriptor, file->path)) + return false; + if (use_metadata && !metadata_send(file_descriptor, file->metadata)) + return false; + + int fd = open(file->path, O_RDONLY); + if (fd == -1) { + perror("Could not open file for sendfile"); + return false; + } + + unsigned long long file_size = file->data->size; + struct stat source_stat; + if (fstat(fd, &source_stat) != 0 || !S_ISREG(source_stat.st_mode) || + (unsigned long long)source_stat.st_size < file_size) { + close(fd); + return false; + } + if (!send_n_data(file_descriptor, &file_size, sizeof(unsigned long long))) { + close(fd); + return false; + } + + /* sendfile cannot encrypt TLS records. Keep the framing identical but + route encrypted transfers through the deadline-aware IO layer. */ + if (io_get_ssl() != NULL) { + unsigned char buffer[64 * 1024]; + unsigned long long remaining = file_size; + bool ok = true; + while (remaining > 0) { + size_t want = remaining > sizeof(buffer) ? sizeof(buffer) : (size_t)remaining; + ssize_t got = read(fd, buffer, want); + if (got <= 0 || !send_n_data(file_descriptor, buffer, (size_t)got)) { + ok = false; + break; + } + remaining -= (unsigned long long)got; + } + close(fd); + return ok; + } + + off_t offset = 0; + struct timespec deadline; + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 60; + while ((unsigned long long)offset < file_size) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + long long remaining = (long long)(deadline.tv_sec - now.tv_sec) * 1000LL + + (deadline.tv_nsec - now.tv_nsec) / 1000000LL; + if (remaining <= 0) { + close(fd); + return false; + } + struct pollfd pfd = {.fd = file_descriptor, .events = POLLOUT}; + int timeout = remaining > INT_MAX ? INT_MAX : (int)remaining; + int polled = poll(&pfd, 1, timeout); + if (polled <= 0 || (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) { + close(fd); + return false; + } + ssize_t sent = sendfile(file_descriptor, fd, &offset, file_size - offset); + if (sent == -1) { + if (errno == EAGAIN || errno == EINTR) + continue; + perror("sendfile failed"); + close(fd); + return false; + } + if (sent == 0) { + close(fd); + return false; + } + } + + close(fd); + return true; +} diff --git a/src/shared/file_send.h b/src/shared/file_send.h new file mode 100644 index 0000000..86c17ad --- /dev/null +++ b/src/shared/file_send.h @@ -0,0 +1,14 @@ +#ifndef FILE_SEND_H +#define FILE_SEND_H + +#include "file_types.h" +#include + +/* Client-side file send path. */ + +bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata, + int compression_level, bool send_path); +bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level, + bool send_path); + +#endif diff --git a/src/shared/file_types.h b/src/shared/file_types.h new file mode 100644 index 0000000..a791fe9 --- /dev/null +++ b/src/shared/file_types.h @@ -0,0 +1,25 @@ +#ifndef FILE_TYPES_H +#define FILE_TYPES_H + +#include "data.h" +#include +#include + +typedef enum { FILE_TYPE_REGULAR, FILE_TYPE_SYMLINK, FILE_TYPE_DIR } FileType; + +typedef struct { + mode_t mode; + uid_t uid; + gid_t gid; + time_t mtime_sec; + long mtime_nsec; +} FileMetadata; + +typedef struct { + char* path; + Data* data; + FileMetadata* metadata; + bool skip; +} File; + +#endif