merge: resolve dev (quality refactor) into security-fixes
- file.c split layout retained; security-hardened secure-fs helpers (open_secure_parent/to_disk_secure/rename_secure/stat_secure) now live in file.c with file_ prefix and are shared with file_receive.c - file_receive.c takes the security branch's bounded allocations (receive_data_limited, data_decompress_limited, size checks) and STATUS_ERROR signaling - file_send.c gains the data consistency check on file->data - client_validation.c: stricter --tls requiring --ca, log_message style - utils.c: hardened openat/mkdirat mkdir_r from security branch
This commit is contained in:
+63
-750
@@ -1,31 +1,33 @@
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.h>
|
||||
#include <poll.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/sendfile.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#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 "log.h"
|
||||
#include "metadata.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define MAX_SERVER_DELETE_COUNT 100000U
|
||||
#define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE
|
||||
static bool write_all(int fd, const void* data, unsigned long long size) {
|
||||
const unsigned char* p = data;
|
||||
unsigned long long done = 0;
|
||||
while (done < size) {
|
||||
ssize_t n = write(fd, p + done, (size_t)(size - done));
|
||||
if (n < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (n <= 0)
|
||||
return false;
|
||||
done += (unsigned long long)n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool file_checksum(File* file, uint64_t* checksum) {
|
||||
if (!file || !checksum || !file->data)
|
||||
@@ -45,7 +47,7 @@ File* file_create(const char* path) {
|
||||
return NULL;
|
||||
File* file = (File*)malloc(sizeof(File));
|
||||
if (file == NULL) {
|
||||
perror("ERROR: Could not allocate memory for file struct");
|
||||
log_perror("ERROR: Could not allocate memory for file struct");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -85,7 +87,7 @@ void file_destroy(void* item) {
|
||||
FileMetadata* file_metadata_create(const struct stat* stats) {
|
||||
FileMetadata* m = malloc(sizeof(FileMetadata));
|
||||
if (m == NULL) {
|
||||
perror("ERROR: Could not allocate memory for file metadata");
|
||||
log_perror("ERROR: Could not allocate memory for file metadata");
|
||||
return NULL;
|
||||
}
|
||||
m->mode = stats->st_mode;
|
||||
@@ -112,7 +114,7 @@ bool file_load_data(File* file) {
|
||||
return true;
|
||||
file->data->data = malloc(file->data->size);
|
||||
if (file->data->data == NULL) {
|
||||
perror("Could not allocate memory for file data");
|
||||
log_perror("Could not allocate memory for file data");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -127,62 +129,26 @@ 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 || (file->data->size != 0 && !file->data->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;
|
||||
size_t file_content_to_buffer(File* file) {
|
||||
if (!file || !file->path || !file->data || (!file->data->data && file->data->size != 0))
|
||||
return 0;
|
||||
FILE* file_pointer = fopen(file->path, "rb");
|
||||
if (file_pointer == NULL) {
|
||||
log_perror("Could not open the file!");
|
||||
return 0;
|
||||
}
|
||||
if (send_path && !send_str(file_descriptor, file->path)) {
|
||||
data_destroy(compressed_data);
|
||||
return false;
|
||||
size_t bytes_read = fread(file->data->data, 1, file->data->size, file_pointer);
|
||||
if (bytes_read != (size_t)file->data->size) {
|
||||
fclose(file_pointer);
|
||||
log_perror("Read unexpected number of bytes from File!");
|
||||
return 0;
|
||||
}
|
||||
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;
|
||||
fclose(file_pointer);
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
static bool to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, const FileMetadata* metadata);
|
||||
static int open_secure_parent(const char* path, char** leaf_out, bool create_dirs);
|
||||
static bool ensure_directory_secure(const char* path);
|
||||
/* ---- Secure filesystem primitives ---- */
|
||||
|
||||
bool file_path_exists_secure(const char* path) {
|
||||
struct stat st;
|
||||
return file_stat_secure(path, &st);
|
||||
}
|
||||
|
||||
bool file_stat_secure(const char* path, struct stat* st) {
|
||||
if (!path || !st)
|
||||
return false;
|
||||
char* leaf = NULL;
|
||||
int parent_fd = open_secure_parent(path, &leaf, false);
|
||||
if (parent_fd < 0)
|
||||
return false;
|
||||
int fd = openat(parent_fd, leaf, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW);
|
||||
bool exists = fd >= 0 && fstat(fd, st) == 0 && S_ISREG(st->st_mode);
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
return exists;
|
||||
}
|
||||
static bool rename_secure(const char* old_path, const char* new_path);
|
||||
static int authorized_root_fd = -1;
|
||||
static char* authorized_root_path;
|
||||
|
||||
@@ -205,460 +171,28 @@ bool file_set_authorized_root(int fd, const char* canonical_path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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, *disk_path = NULL;
|
||||
char *backup_path = NULL, *parent_copy = NULL;
|
||||
|
||||
if (!file || !file->path || !file->data || (file->data->size != 0 && !file->data->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;
|
||||
}
|
||||
|
||||
const char* actual_root =
|
||||
(partial_dir && config && config->partial) ? confined_partial : root_directory;
|
||||
disk_path = path_cat(actual_root, file->path);
|
||||
if (disk_path == NULL) {
|
||||
free(confined_backup);
|
||||
free(confined_partial);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* --update is receiver-side policy: never replace a newer destination. */
|
||||
if (config && config->update) {
|
||||
struct stat destination_stat;
|
||||
if (file_stat_secure(disk_path, &destination_stat) && file->metadata &&
|
||||
destination_stat.st_mtime > file->metadata->mtime_sec) {
|
||||
free(confined_backup);
|
||||
free(confined_partial);
|
||||
free(disk_path);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (backup_enabled) {
|
||||
struct stat backup_stat;
|
||||
if (file_stat_secure(disk_path, &backup_stat)) {
|
||||
if (backup_dir) {
|
||||
backup_path = path_cat(confined_backup, file->path);
|
||||
} else {
|
||||
size_t path_len = strlen(disk_path);
|
||||
size_t suffix_len = strlen(backup_suffix);
|
||||
if (path_len > SIZE_MAX - suffix_len - 1)
|
||||
goto fail;
|
||||
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)
|
||||
goto fail;
|
||||
parent_copy = str_dup(backup_path);
|
||||
if (!parent_copy || !ensure_directory_secure(dirname(parent_copy)))
|
||||
goto fail;
|
||||
free(parent_copy);
|
||||
parent_copy = NULL;
|
||||
if (!rename_secure(disk_path, backup_path))
|
||||
goto fail;
|
||||
free(backup_path);
|
||||
backup_path = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = to_disk_secure(disk_path, file->data->data, file->data->size, inplace, sparse,
|
||||
file->metadata);
|
||||
free(parent_copy);
|
||||
free(backup_path);
|
||||
free(confined_backup);
|
||||
free(confined_partial);
|
||||
free(disk_path);
|
||||
return ok;
|
||||
|
||||
fail:
|
||||
free(parent_copy);
|
||||
free(backup_path);
|
||||
free(confined_backup);
|
||||
free(confined_partial);
|
||||
free(disk_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
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_limited(fd, MAX_RECEIVE_FILE_SIZE);
|
||||
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_limited(delta_data, MAX_RECEIVE_FILE_SIZE);
|
||||
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;
|
||||
}
|
||||
|
||||
uint64_t new_size = delta->new_file_size;
|
||||
if (new_size > MAX_RECEIVE_FILE_SIZE || new_size > SIZE_MAX) {
|
||||
delta_destroy(delta);
|
||||
free(old_data);
|
||||
delta_signature_destroy(sig);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
void* new_data = delta_apply(old_data, old_size, delta, config->delta_block_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* replacement = data_create(new_data, (size_t)new_size);
|
||||
if (replacement == NULL) {
|
||||
file_destroy(file);
|
||||
free(old_data);
|
||||
delta_signature_destroy(sig);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
data_destroy(file->data);
|
||||
file->data = replacement;
|
||||
|
||||
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_limited(fd, MAX_RECEIVE_FILE_SIZE);
|
||||
if (file_data == NULL) {
|
||||
file_destroy(file);
|
||||
*failed = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (config->use_compression) {
|
||||
Data* uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_FILE_SIZE);
|
||||
data_destroy(file_data);
|
||||
if (uncompressed == NULL) {
|
||||
file_destroy(file);
|
||||
*failed = true;
|
||||
return NULL;
|
||||
}
|
||||
if (uncompressed->size > MAX_FILE_DATA_SIZE) {
|
||||
data_destroy(uncompressed);
|
||||
file_destroy(file);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
file_data = uncompressed;
|
||||
}
|
||||
|
||||
data_destroy(file->data);
|
||||
file->data = file_data;
|
||||
return file;
|
||||
}
|
||||
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
*failed = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
||||
if (!config || !skipped) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
*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 (check_size > MAX_RECEIVE_FILE_SIZE) {
|
||||
free(check_path);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
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);
|
||||
if (!full_path) {
|
||||
free(check_path);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
bool file_path_exists_secure(const char* path) {
|
||||
struct stat st;
|
||||
bool has_old_file = false;
|
||||
int old_fd = -1;
|
||||
char* leaf = NULL;
|
||||
int parent_fd = open_secure_parent(full_path, &leaf, false);
|
||||
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_size <= MAX_RECEIVE_FILE_SIZE && old_size <= SIZE_MAX) {
|
||||
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 && old_data != NULL &&
|
||||
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_limited(fd, MAX_RECEIVE_FILE_SIZE);
|
||||
if (file_data == NULL) {
|
||||
file_destroy(file);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (config->use_compression) {
|
||||
Data* uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_FILE_SIZE);
|
||||
data_destroy(file_data);
|
||||
if (uncompressed == NULL) {
|
||||
file_destroy(file);
|
||||
return NULL;
|
||||
}
|
||||
if (uncompressed->size > MAX_FILE_DATA_SIZE) {
|
||||
data_destroy(uncompressed);
|
||||
file_destroy(file);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
file_data = uncompressed;
|
||||
}
|
||||
|
||||
data_destroy(file->data);
|
||||
file->data = file_data;
|
||||
return file;
|
||||
return file_stat_secure(path, &st);
|
||||
}
|
||||
|
||||
static int open_secure_parent(const char* path, char** leaf_out, bool create_dirs) {
|
||||
bool file_stat_secure(const char* path, struct stat* st) {
|
||||
if (!path || !st)
|
||||
return false;
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(path, &leaf, false);
|
||||
if (parent_fd < 0)
|
||||
return false;
|
||||
int fd = openat(parent_fd, leaf, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW);
|
||||
bool exists = fd >= 0 && fstat(fd, st) == 0 && S_ISREG(st->st_mode);
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
return exists;
|
||||
}
|
||||
|
||||
int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs) {
|
||||
char* copy = str_dup(path);
|
||||
if (!copy)
|
||||
return -1;
|
||||
@@ -734,9 +268,9 @@ static int open_secure_parent(const char* path, char** leaf_out, bool create_dir
|
||||
return fd;
|
||||
}
|
||||
|
||||
static bool ensure_directory_secure(const char* path) {
|
||||
bool file_ensure_directory_secure(const char* path) {
|
||||
char* leaf = NULL;
|
||||
int parent_fd = open_secure_parent(path, &leaf, true);
|
||||
int parent_fd = file_open_secure_parent(path, &leaf, true);
|
||||
if (parent_fd < 0)
|
||||
return false;
|
||||
|
||||
@@ -753,10 +287,10 @@ static bool ensure_directory_secure(const char* path) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool rename_secure(const char* old_path, const char* new_path) {
|
||||
bool file_rename_secure(const char* old_path, const char* new_path) {
|
||||
char *old_leaf = NULL, *new_leaf = NULL;
|
||||
int old_parent = open_secure_parent(old_path, &old_leaf, false);
|
||||
int new_parent = open_secure_parent(new_path, &new_leaf, true);
|
||||
int old_parent = file_open_secure_parent(old_path, &old_leaf, false);
|
||||
int new_parent = file_open_secure_parent(new_path, &new_leaf, true);
|
||||
bool ok = old_parent >= 0 && new_parent >= 0 &&
|
||||
renameat(old_parent, old_leaf, new_parent, new_leaf) == 0;
|
||||
if (old_parent >= 0)
|
||||
@@ -768,24 +302,10 @@ static bool rename_secure(const char* old_path, const char* new_path) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool write_all(int fd, const void* data, unsigned long long size) {
|
||||
const unsigned char* p = data;
|
||||
unsigned long long done = 0;
|
||||
while (done < size) {
|
||||
ssize_t n = write(fd, p + done, (size_t)(size - done));
|
||||
if (n < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (n <= 0)
|
||||
return false;
|
||||
done += (unsigned long long)n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, const FileMetadata* metadata) {
|
||||
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, const FileMetadata* metadata) {
|
||||
char* leaf = NULL;
|
||||
int dirfd = open_secure_parent(path, &leaf, true);
|
||||
int dirfd = file_open_secure_parent(path, &leaf, true);
|
||||
if (dirfd < 0)
|
||||
return false;
|
||||
int fd = -1;
|
||||
@@ -827,216 +347,9 @@ static bool to_disk_secure(const char* path, const void* data, unsigned long lon
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool 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 to_disk_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_limited(file_descriptor, MAX_RECEIVE_FILE_SIZE);
|
||||
if (file_data == NULL) {
|
||||
file_destroy(file);
|
||||
return NULL;
|
||||
}
|
||||
if (config->use_compression && !compression_should_skip(file->path)) {
|
||||
Data* file_data_uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_FILE_SIZE);
|
||||
data_destroy(file_data);
|
||||
if (file_data_uncompressed == NULL) {
|
||||
file_destroy(file);
|
||||
return NULL;
|
||||
}
|
||||
if (file_data_uncompressed->size > MAX_FILE_DATA_SIZE) {
|
||||
data_destroy(file_data_uncompressed);
|
||||
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) {
|
||||
if (!file || !file->path || !file->data || (!file->data->data && file->data->size != 0))
|
||||
return 0;
|
||||
FILE* file_pointer = fopen(file->path, "rb");
|
||||
if (file_pointer == NULL) {
|
||||
perror("Could not open the file!");
|
||||
return 0;
|
||||
}
|
||||
size_t bytes_read = fread(file->data->data, 1, file->data->size, file_pointer);
|
||||
if (bytes_read != (size_t)file->data->size) {
|
||||
fclose(file_pointer);
|
||||
perror("Read unexpected number of bytes from File!");
|
||||
return 0;
|
||||
}
|
||||
fclose(file_pointer);
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
int receive_manifest(int fd, const Config* config, int* next_status) {
|
||||
if (!config) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
int received_status = STATUS_ERROR;
|
||||
int* status_out = next_status ? next_status : &received_status;
|
||||
int count;
|
||||
if (!receive_int(fd, &count)) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
if (count < 0 || count > MAX_MANIFEST_ENTRIES) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
ArrayList* manifest = array_list_create(free);
|
||||
if (!manifest) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
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);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (!receive_status(fd, status_out)) {
|
||||
array_list_delete(manifest);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
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);
|
||||
if (*status_out != STATUS_FINISHED)
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return *status_out == STATUS_FINISHED ? 0 : -1;
|
||||
}
|
||||
fprintf(stderr, "Deleting files not in manifest...\n");
|
||||
bool deletion_ok =
|
||||
delete_extras_limited(config->receive_root_directory, manifest, MAX_SERVER_DELETE_COUNT);
|
||||
array_list_delete(manifest);
|
||||
if (!deletion_ok)
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return deletion_ok ? 0 : -1;
|
||||
return file_to_disk_secure(path, data, data_size, inplace, sparse, NULL);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user