Release v2.29.0 #312

Merged
TapTap merged 123 commits from dev into main 2026-09-23 02:05:14 +02:00
Showing only changes of commit ed7527cc2c - Show all commits
+169 -86
View File
@@ -705,69 +705,85 @@ static const char* server_module_gate(const Config* config, void* context) {
return module_gate_install_root(config, module);
}
void handler(int file_descriptor) {
SSL* ssl = io_get_ssl();
/* Per-connection state threaded through the handler phase helpers below. The
* fields are a faithful split of the former handler() locals: the protocol
* session, the config-frame gate context, the accepted config, the optional
* multithreaded pipeline context and the teardown bookkeeping all live here so
* the single `done` epilogue in handler() can release them exactly as before. */
typedef struct ServerSession {
int fd;
SSL* ssl;
ProtocolSession session;
protocol_session_init(&session, file_descriptor, file_descriptor);
protocol_session_set_ssl(&session, ssl);
protocol_session_bind(&session);
ModuleGateContext gate_ctx;
gate_ctx.ssl = ssl;
gate_ctx.fd = file_descriptor;
gate_ctx.super_mode_override = -1;
gate_ctx.has_peer_ip = false;
gate_ctx.peer_ip[0] = '\0';
gate_ctx.is_local = false;
/* All teardown state starts empty so the single `done` epilogue is safe to
* reach from any error path (including before the config frame arrives). */
Config* config = NULL;
PipelineContextReceiver* context = NULL;
char* joined_destination = NULL;
bool charset_ready = false;
config = config_receive_with_validate(file_descriptor, server_module_gate, &gate_ctx);
if (config == NULL) {
Config* config;
PipelineContextReceiver* context;
char* joined_destination;
bool charset_ready;
} ServerSession;
/* Phase 1 -- config receipt + validation. Receives the client config frame
* through the module gate, applies the super-mode override the gate recorded
* exactly once, and installs the per-connection protocol/compression state.
* Returns false when the config frame was refused (the gate has already
* answered the client); the caller jumps to the shared `done` epilogue. */
static bool server_accept_config(ServerSession* state) {
state->config = config_receive_with_validate(state->fd, server_module_gate, &state->gate_ctx);
if (state->config == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to receive config");
goto done;
return false;
}
/* Apply the super-mode veto the gate decided on (operator --no-super, or a
* daemon module without the `client owner = yes` opt-in) exactly once, so
* every downstream gate (identity_apply_ownership via privilege_super_permitted,
* device-node creation) sees SUPER_MODE_OFF. The gate never mutated the
* received config. */
if (gate_ctx.super_mode_override != -1)
config->super_mode = (SuperMode)gate_ctx.super_mode_override;
if (state->gate_ctx.super_mode_override != -1)
state->config->super_mode = (SuperMode)state->gate_ctx.super_mode_override;
/* Install the codec this connection negotiated before the receiver/writer
* threads start (the server forks per connection, so the process-global
* codec is private to this session). */
compression_set_algo((CompressionAlgo)config->compression_algo);
compression_set_algo((CompressionAlgo)state->config->compression_algo);
/* If the client requested ownership but the effective super mode forbids it
* (operator --no-super, a privileged standalone receiver's secure default, or
* a daemon module without `client owner = yes`), say so ONCE per connection so
* a successful -a/-o/-g transfer is not mistaken for preserved ownership. */
if (config->super_mode == SUPER_MODE_OFF && identity_ownership_requested(config))
if (state->config->super_mode == SUPER_MODE_OFF && identity_ownership_requested(state->config))
log_message(LOG_LEVEL_WARNING,
"requested ownership will NOT be applied: super-user activities are disabled "
"for this connection (operator veto, or module without `client owner = yes`)");
protocol_set_8_bit_output(config->eight_bit_output);
protocol_set_8_bit_output(state->config->eight_bit_output);
/* Server-side per-message protocol deadline for every frame from here on.
* `timeout` is not serialized, so this is the server's own config (the server
* has no --timeout CLI and defaults it to 0). A client's --timeout tightens
* only that client's own protocol I/O; the server floors its own deadline at
* SERVER_IO_TIMEOUT_SEC so a silent peer can never hold a session slot
* forever (the socket layer gets the same floor at startup). */
protocol_session_set_io_timeout(&session, protocol_server_io_timeout_sec(config->timeout));
protocol_session_set_io_timeout(&state->session,
protocol_server_io_timeout_sec(state->config->timeout));
return true;
}
/* Phase 2 -- security gates. The ORDER here is load-bearing and must not be
* merged or reordered: transport/authentication (plaintext refusal, TLS
* client-CN verification), then daemon-root confinement (absolute-destination
* rejection, traversal + within-authorized-root), then delete/force
* authorization -- exactly the sequence the former handler() used. Returns
* false after logging the matching rejection; the caller jumps to the shared
* `done` epilogue. */
static bool server_apply_security_gates(ServerSession* state) {
Config* config = state->config;
const char* authorized_root = utils_get_authorized_root_path();
if (!authorized_root) {
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
goto done;
return false;
}
if (!allow_unauthenticated && ssl == NULL) {
if (!allow_unauthenticated && state->ssl == NULL) {
log_message(LOG_LEVEL_ERROR, "Rejected unauthenticated plaintext connection");
goto done;
return false;
}
if (ssl && required_client_cn && !tls_client_identity_allowed(ssl)) {
if (state->ssl && required_client_cn && !tls_client_identity_allowed(state->ssl)) {
log_message(LOG_LEVEL_ERROR, "Rejected TLS client with unauthorized identity");
goto done;
return false;
}
/* Daemon mode: the module's root is the authorized root (installed by
server_module_gate), and the client's destination is a MODULE-RELATIVE
@@ -777,27 +793,27 @@ void handler(int file_descriptor) {
if (g_daemon_conf && config->receive_root_directory && config->receive_root_directory[0] == '/') {
log_message(LOG_LEVEL_ERROR, "Rejected absolute daemon destination (must be relative to the "
"selected module root)");
goto done;
return false;
}
char* destination = config->receive_root_directory;
if (destination && destination[0] != '/')
joined_destination = path_cat(authorized_root, destination);
if (joined_destination)
destination = joined_destination;
state->joined_destination = path_cat(authorized_root, destination);
if (state->joined_destination)
destination = state->joined_destination;
if (!destination || has_path_traversal(destination) ||
!path_is_within_root(authorized_root, destination)) {
log_message(LOG_LEVEL_ERROR, "Rejected destination outside authorized root");
free(joined_destination);
joined_destination = NULL;
goto done;
free(state->joined_destination);
state->joined_destination = NULL;
return false;
}
if (joined_destination) {
if (state->joined_destination) {
free(config->receive_root_directory);
config->receive_root_directory = joined_destination;
joined_destination = NULL;
config->receive_root_directory = state->joined_destination;
state->joined_destination = NULL;
}
if (!config->receive_root_directory) {
goto done;
return false;
}
config->use_delete = config->use_delete && allow_delete;
/* --force (receiver-side) is deletion authority too: it lets an incoming
@@ -807,6 +823,18 @@ void handler(int file_descriptor) {
* --delete-missing-args, so a client cannot use --force to bypass the delete
* policy. */
config->force_delete = config->force_delete && allow_delete;
return true;
}
/* Phase 3 -- session preparation. Installs the negotiated conversion, applies
* the remaining deletion policy, materializes the destination root (--mkpath),
* creates the --delay-updates staging tree, snapshots the identity policy, and
* publishes the --keep-dirlinks/--trust-sender globals and the daemon MOTD.
* All of it must happen before any receiver/writer thread is spawned. Returns
* false after logging the matching failure; the caller jumps to the shared
* `done` epilogue. */
static bool server_prepare_session(ServerSession* state) {
Config* config = state->config;
/* --iconv (protocol 2.16.0): install the receiver-side wire->local conversion
now that the client's full CONVERT_SPEC has been received and validated,
before any received file name is decoded. The server's own --iconv (if
@@ -817,14 +845,14 @@ void handler(int file_descriptor) {
if (!charset_wire_init_receiver(config->iconv_spec, server_iconv_spec)) {
log_message(LOG_LEVEL_ERROR,
"--iconv: unsupported charset conversion requested (LOCAL[,REMOTE])");
goto done;
return false;
}
charset_ready = true;
state->charset_ready = true;
}
/* --delete-missing-args deletes destination mirrors receiver-side, so it is
deletion and stays gated by the same --allow-delete server policy. When
the server policy is off the flag is inert (the missing entries are still
skipped via its implied --ignore-missing-args, but nothing is deleted). */
* deletion and stays gated by the same --allow-delete server policy. When
* the server policy is off the flag is inert (the missing entries are still
* skipped via its implied --ignore-missing-args, but nothing is deleted). */
config->delete_missing_args = config->delete_missing_args && allow_delete;
/* --mkpath: create the destination root (and its missing leading components)
* before anything else; without it the root must pre-exist. The precondition
@@ -839,7 +867,7 @@ void handler(int file_descriptor) {
log_message(LOG_LEVEL_ERROR, "destination root is not available: %s",
escaped_root ? escaped_root : "<allocation failed>");
free(escaped_root);
goto done;
return false;
}
/* A --delay-updates transfer stages under a private 0700 directory inside
the receive root. Create it up front (wiping leftovers of any previously
@@ -849,7 +877,7 @@ void handler(int file_descriptor) {
config->delay_context = delay_updates_context_create(config->receive_root_directory);
if (!config->delay_context || !delay_updates_prepare(config->delay_context)) {
log_message(LOG_LEVEL_ERROR, "Failed to initialize --delay-updates staging area");
goto done;
return false;
}
}
/* Preserve the negotiated identity policy for the fd-relative ownership
@@ -859,7 +887,7 @@ void handler(int file_descriptor) {
rather than silently applying the wrong ownership policy. */
if (!identity_set_active(config)) {
log_message(LOG_LEVEL_ERROR, "Failed to activate identity policy");
goto done;
return false;
}
/* Persist the negotiated --keep-dirlinks policy once, here at config-accept,
before any multithreaded receiver/writer threads are spawned, so the
@@ -887,61 +915,72 @@ void handler(int file_descriptor) {
Wave C note in config.h). */
if (g_daemon_conf) {
char* motd = motd_read_file(g_daemon_conf->global.motd_file);
if (!motd_send(file_descriptor, motd ? motd : "")) {
if (!motd_send(state->fd, motd ? motd : "")) {
free(motd);
log_message(LOG_LEVEL_ERROR, "Failed to send daemon MOTD");
goto done;
return false;
}
free(motd);
}
if (config->use_multithreading) {
return true;
}
/* Phase 4a -- transfer via the multithreaded receiver. Spawns the receive/write
* thread pair, joins them, then commits the late deletion, --delay-updates
* publication and directory times before emitting the terminal stats/success
* frame. On any failure the helper just returns; the caller's `done` epilogue
* releases the pipeline context (which owns the config and queue) exactly as the
* former inline code did. */
static void server_run_mt_receiver(ServerSession* state) {
Config* config = state->config;
Queue* q = queue_create(100, file_destroy);
if (q == NULL)
goto done;
context = pipeline_context_receiver_create(config, q, file_descriptor, ssl);
if (context == NULL) {
return;
state->context = pipeline_context_receiver_create(config, q, state->fd, state->ssl);
if (state->context == NULL) {
queue_destroy(q);
goto done;
return;
}
protocol_session_set_max_alloc(&context->session, config->max_alloc);
protocol_session_set_io_timeout(&context->session,
protocol_session_set_max_alloc(&state->context->session, config->max_alloc);
protocol_session_set_io_timeout(&state->context->session,
protocol_server_io_timeout_sec(config->timeout));
atomic_store(&context->session.total_allocated_bytes,
atomic_load(&session.total_allocated_bytes));
pipeline_context_receiver_set_queue_byte_limit(context, RECEIVER_QUEUE_MAX_BYTES);
atomic_store(&state->context->session.total_allocated_bytes,
atomic_load(&state->session.total_allocated_bytes));
pipeline_context_receiver_set_queue_byte_limit(state->context, RECEIVER_QUEUE_MAX_BYTES);
thrd_t receiver = {0};
thrd_t writer = {0};
bool receiver_created = thrd_create(&receiver, receive_thread, context) == thrd_success;
bool receiver_created = thrd_create(&receiver, receive_thread, state->context) == thrd_success;
bool writer_created = false;
if (receiver_created)
writer_created = thrd_create(&writer, write_thread, context) == thrd_success;
writer_created = thrd_create(&writer, write_thread, state->context) == thrd_success;
if (!receiver_created || !writer_created) {
log_perror("Error creating Threads");
if (receiver_created) {
mtx_lock(&context->mutex);
atomic_store(&context->cancelled, true);
cnd_broadcast(&context->condition_not_full);
cnd_broadcast(&context->condition_not_empty);
mtx_unlock(&context->mutex);
mtx_lock(&state->context->mutex);
atomic_store(&state->context->cancelled, true);
cnd_broadcast(&state->context->condition_not_full);
cnd_broadcast(&state->context->condition_not_empty);
mtx_unlock(&state->context->mutex);
/* Unblock a worker parked in socket I/O without closing the fd (the
* child owns the single close). shutdown() only affects sockets; for
* the --stdio pipe the receiver's per-message poll timeout still
* bounds the join, so do nothing there rather than close a descriptor
* another thread may still be using. */
struct stat fd_stat;
if (fstat(file_descriptor, &fd_stat) == 0 && S_ISSOCK(fd_stat.st_mode))
shutdown(file_descriptor, SHUT_RDWR);
if (fstat(state->fd, &fd_stat) == 0 && S_ISSOCK(fd_stat.st_mode))
shutdown(state->fd, SHUT_RDWR);
thrd_join(receiver, NULL);
}
if (writer_created)
thrd_join(writer, NULL);
goto done;
return;
}
int receiver_result;
int writer_result;
thrd_join(receiver, &receiver_result);
thrd_join(writer, &writer_result);
bool transfer_ok = receiver_result == thrd_success && writer_result == thrd_success;
PipelineContextReceiver* context = state->context;
if (transfer_ok && !config->dry_run) {
/* Commit-style (late) deletion: receive_thread handed the keep-set
manifest here instead of deleting while write_thread might still be
@@ -1008,20 +1047,64 @@ void handler(int file_descriptor) {
Status final_status = context->delete_limit_reached ? STATUS_DELETE_LIMIT : STATUS_OK;
/* Emit the optional wire-stats record first (protocol 2.25.0), then the
success/outcome frame, exactly like the single-threaded receiver. */
if (!receiver_send_stats_frame(file_descriptor, config, &context->stats,
context->would_delete, context->deleted_paths) ||
!receiver_send_final_success(file_descriptor, config, &context->outcomes, final_status))
if (!receiver_send_stats_frame(state->fd, config, &context->stats, context->would_delete,
context->deleted_paths) ||
!receiver_send_final_success(state->fd, config, &context->outcomes, final_status))
transfer_ok = false;
} else {
send_error_detail(file_descriptor, "transfer failed on receiver");
send_error_detail(state->fd, "transfer failed on receiver");
}
if (!transfer_ok)
log_message(LOG_LEVEL_ERROR, "Transfer failed");
} else {
if (receiver_receive_files(config, file_descriptor) != 0)
}
/* Phase 4b -- transfer via the single-threaded receiver. Failure is logged
* exactly as before; the caller's `done` epilogue then releases the config. */
static void server_run_st_receiver(ServerSession* state) {
if (receiver_receive_files(state->config, state->fd) != 0)
log_message(LOG_LEVEL_ERROR, "Transfer failed");
}
/* Phase 4 dispatch -- choose the receiver implementation the config asks for.
* Both helpers own their success/failure logging; the caller falls through to
* the shared `done` epilogue either way. */
static void server_run_transfer(ServerSession* state) {
if (state->config->use_multithreading)
server_run_mt_receiver(state);
else
server_run_st_receiver(state);
}
void handler(int file_descriptor) {
/* Single per-connection state; every phase helper below advances it and
* returns false on a logged failure. All teardown state starts empty so the
* single `done` epilogue is safe to reach from any error path (including
* before the config frame arrives). */
ServerSession state;
state.fd = file_descriptor;
state.ssl = io_get_ssl();
protocol_session_init(&state.session, file_descriptor, file_descriptor);
protocol_session_set_ssl(&state.session, state.ssl);
protocol_session_bind(&state.session);
state.gate_ctx.ssl = state.ssl;
state.gate_ctx.fd = file_descriptor;
state.gate_ctx.super_mode_override = -1;
state.gate_ctx.has_peer_ip = false;
state.gate_ctx.peer_ip[0] = '\0';
state.gate_ctx.is_local = false;
state.config = NULL;
state.context = NULL;
state.joined_destination = NULL;
state.charset_ready = false;
if (!server_accept_config(&state))
goto done;
if (!server_apply_security_gates(&state))
goto done;
if (!server_prepare_session(&state))
goto done;
server_run_transfer(&state);
done:
/* Single cleanup epilogue: every error path jumps here, so the iconv
* receiver conversion is released, the identity snapshot cleared, the
@@ -1029,22 +1112,22 @@ done:
* connection fd is deliberately NOT closed here -- the child functions own
* its single close (plain_child_fn / tls_child_fn), and the --stdio call
* site must leave stdin/stdout open. */
if (charset_ready)
if (state.charset_ready)
charset_wire_free();
/* The delay-updates staging tree is released by config_delete (which the
branch below always reaches), so it is cleaned exactly once. */
identity_clear_active();
protocol_session_unbind();
if (context != NULL) {
if (state.context != NULL) {
/* context owns both the config and the queue it was created with. */
pipeline_context_receiver_destroy(context);
context = NULL;
config = NULL;
pipeline_context_receiver_destroy(state.context);
state.context = NULL;
state.config = NULL;
} else {
config_delete(config);
config = NULL;
config_delete(state.config);
state.config = NULL;
}
free(joined_destination);
free(state.joined_destination);
}
#ifndef FASTSYNC_SERVER_AS_LIB