feat(protocol): add optional STATUS_ERROR_DETAIL rejection reason (2.21.0)
Today a server rejection sends a bare STATUS_ERROR and the reason only
reaches the server log, so the client cannot say why a transfer was
refused. Add an optional, bounded server->client error-detail frame:
- Status gains STATUS_ERROR_DETAIL appended LAST so existing wire
values are unchanged.
- send_error_detail(fd, msg) sends STATUS_ERROR_DETAIL followed by the
existing length-prefixed string primitive, slicing over-long messages
to MAX_ERROR_DETAIL_BYTES (4096).
- receive_status() (and the timed/keepalive status readers) always
consume the detail body and map the status back to STATUS_ERROR,
capturing the text into a thread-local buffer exposed by
protocol_last_error(); a bare STATUS_ERROR leaves it cleared. Every
existing call site keeps working and the stream cannot desync.
- Upgrade the daemon module gate / config validation (config.c), the
final transfer failure (server.c) and receiver-side path/node
validation (file_receive.c) to send a concrete reason; surface it on
the client in client_send.c/config.c.
- Bump PROTOCOL_VERSION to 2.21.0 (CMake VERSION, CHANGELOG, docs) and
update the pinned config wire golden hash / CLI-version tests.
- Add tests/test_protocol_error.c covering mapping+capture, the
over-long bound, bare-error clearing, and thread-locality.
This commit is contained in:
@@ -48,6 +48,17 @@
|
||||
Always cast to double when dividing so the output stays fractional. */
|
||||
#define BYTES_PER_MIB (1024ULL * 1024ULL)
|
||||
|
||||
/* Surface a server rejection to the user. When the last status exchange
|
||||
carried a STATUS_ERROR_DETAIL reason (protocol 2.21.0) it is appended to the
|
||||
client-side context; a bare STATUS_ERROR still logs the context alone. */
|
||||
static void log_server_rejection(const char* context) {
|
||||
const char* detail = protocol_last_error();
|
||||
if (detail && detail[0] != '\0')
|
||||
log_message(LOG_LEVEL_ERROR, "%s: %s", context, detail);
|
||||
else
|
||||
log_message(LOG_LEVEL_ERROR, "%s", context);
|
||||
}
|
||||
|
||||
/* Forward declaration for progress-reporting thread used in multithreaded send. */
|
||||
static int progress_thread_fn(void* arg);
|
||||
|
||||
@@ -608,8 +619,10 @@ static bool finalize_transfer(Client* client, const Config* config, ArrayList* r
|
||||
Status per_file;
|
||||
if (!receive_status(client->file_descriptor, &per_file))
|
||||
return false;
|
||||
if (per_file == STATUS_ERROR)
|
||||
if (per_file == STATUS_ERROR) {
|
||||
log_server_rejection("Receiver reported a per-file error");
|
||||
return false;
|
||||
}
|
||||
if (per_file == STATUS_OK) {
|
||||
((SourceFile*)remove_sources->items[i])->skipped = true;
|
||||
} else if (per_file != STATUS_NEXT) {
|
||||
@@ -619,7 +632,13 @@ static bool finalize_transfer(Client* client, const Config* config, ArrayList* r
|
||||
}
|
||||
}
|
||||
Status status;
|
||||
return receive_status(client->file_descriptor, &status) && status == STATUS_OK;
|
||||
if (!receive_status(client->file_descriptor, &status))
|
||||
return false;
|
||||
if (status != STATUS_OK) {
|
||||
log_server_rejection("Receiver reported transfer failure");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void pipeline_cancel(PipelineContextSender* context) {
|
||||
@@ -914,7 +933,7 @@ static bool send_delete_manifest_early(Client* client, ArrayList* manifest,
|
||||
return false;
|
||||
}
|
||||
if (ack != STATUS_OK) {
|
||||
log_message(LOG_LEVEL_ERROR, "Server failed to delete files before the transfer");
|
||||
log_server_rejection("Server failed to delete files before the transfer");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -991,7 +1010,7 @@ static int incremental_check(Client* client, File* file, const Config* config,
|
||||
if (!receive_status(client->file_descriptor, &s))
|
||||
return -1;
|
||||
if (s == STATUS_ERROR) {
|
||||
log_message(LOG_LEVEL_ERROR, "Server reported error for file");
|
||||
log_server_rejection("Server reported error for file");
|
||||
return -1;
|
||||
}
|
||||
if (s == STATUS_OK)
|
||||
@@ -1025,7 +1044,7 @@ static int incremental_check(Client* client, File* file, const Config* config,
|
||||
return 3;
|
||||
}
|
||||
if (s != STATUS_NEXT) {
|
||||
log_message(LOG_LEVEL_ERROR, "Unexpected server status");
|
||||
log_server_rejection("Unexpected server status");
|
||||
send_status(client->file_descriptor, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
@@ -1119,6 +1138,7 @@ static int send_append(const Client* client, File* file, Config* config,
|
||||
return rc;
|
||||
}
|
||||
if (resp != STATUS_APPEND_OK) {
|
||||
log_server_rejection("Unexpected append-verify response");
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
|
||||
+1
-1
@@ -899,7 +899,7 @@ void handler(int file_descriptor) {
|
||||
if (!receiver_send_final_success(file_descriptor, config, &context->outcomes))
|
||||
transfer_ok = false;
|
||||
} else {
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, "transfer failed on receiver");
|
||||
}
|
||||
if (!transfer_ok)
|
||||
log_message(LOG_LEVEL_ERROR, "Transfer failed");
|
||||
|
||||
+16
-6
@@ -773,8 +773,8 @@ static bool config_receive_module(int fd, Config* c, ConfigStringBudget* budget)
|
||||
return false;
|
||||
if (*module != '\0' && !daemon_module_name_valid(module)) {
|
||||
log_message(LOG_LEVEL_WARNING, "Daemon client sent an invalid or over-long module name");
|
||||
send_error_detail(fd, "invalid or over-long daemon module name");
|
||||
free(module);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return false;
|
||||
}
|
||||
if (*module != '\0') {
|
||||
@@ -1238,7 +1238,11 @@ bool config_send(int file_descriptor, const Config* config) {
|
||||
return false;
|
||||
}
|
||||
if (status != STATUS_OK) {
|
||||
log_message(LOG_LEVEL_ERROR, "Error transmitting config");
|
||||
const char* detail = protocol_last_error();
|
||||
if (detail && detail[0] != '\0')
|
||||
log_message(LOG_LEVEL_ERROR, "Error transmitting config: %s", detail);
|
||||
else
|
||||
log_message(LOG_LEVEL_ERROR, "Error transmitting config");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1258,8 +1262,11 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
||||
char* escaped_version = output_escape(config->version, false);
|
||||
log_message(LOG_LEVEL_ERROR, "Protocol version mismatch: client=%s, server=%s",
|
||||
escaped_version ? escaped_version : "<allocation failed>", PROTOCOL_VERSION);
|
||||
char detail[160];
|
||||
snprintf(detail, sizeof(detail), "protocol version mismatch (client=%s, server=%s)",
|
||||
escaped_version ? escaped_version : "<allocation failed>", PROTOCOL_VERSION);
|
||||
send_error_detail(file_descriptor, detail);
|
||||
free(escaped_version);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
goto error;
|
||||
}
|
||||
if (!receive_core_fields(file_descriptor, config, &budget) ||
|
||||
@@ -1285,13 +1292,16 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
||||
char* escaped_choice = output_escape(config->compress_choice, config->eight_bit_output);
|
||||
log_message(LOG_LEVEL_ERROR, "Unsupported compression choice: %s",
|
||||
escaped_choice ? escaped_choice : "<allocation failed>");
|
||||
char detail[128];
|
||||
snprintf(detail, sizeof(detail), "unsupported compression choice: %s",
|
||||
escaped_choice ? escaped_choice : "<allocation failed>");
|
||||
send_error_detail(file_descriptor, detail);
|
||||
free(escaped_choice);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
goto error;
|
||||
}
|
||||
if (!validate_received_config(config)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid configuration received from client");
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, "invalid configuration received from client");
|
||||
goto error;
|
||||
}
|
||||
if (validate) {
|
||||
@@ -1305,7 +1315,7 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
||||
* written. */
|
||||
if (rejection != CONFIG_VALIDATE_ALREADY_TERMINATED) {
|
||||
log_message(LOG_LEVEL_ERROR, "%s", rejection);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, rejection);
|
||||
}
|
||||
goto error;
|
||||
}
|
||||
|
||||
+17
-3
@@ -76,7 +76,7 @@ typedef struct {
|
||||
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode;
|
||||
|
||||
/* ===========================================================================
|
||||
* Config wire-field table (single source of truth for protocol 2.20.0).
|
||||
* Config wire-field table (single source of truth for protocol 2.21.0).
|
||||
*
|
||||
* Every field below crosses the wire. The table is the ONLY place a
|
||||
* serialized field is named: config.h expands CONFIG_WIRE_FIELDS() to declare
|
||||
@@ -756,8 +756,22 @@ typedef struct Config {
|
||||
* The bump is therefore a deliberate lockstep-release marker, not a
|
||||
* desynchronization fix — the strict same-version handshake still rejects a
|
||||
* mixed 2.19/2.20 deployment. The chunk codec, which already used the packed
|
||||
* metadata_to_buf()/metadata_from_buf() form, is unchanged. */
|
||||
#define PROTOCOL_VERSION "2.20.0"
|
||||
* metadata_to_buf()/metadata_from_buf() form, is unchanged.
|
||||
*
|
||||
* Error-Detail Wave: 2.20.0 -> 2.21.0.
|
||||
*
|
||||
* WHY the bump, grounded in the wire: a server may now answer a rejected
|
||||
* operation with STATUS_ERROR_DETAIL followed by a bounded (<=
|
||||
* MAX_ERROR_DETAIL_BYTES) length-prefixed string instead of a bare
|
||||
* STATUS_ERROR (see protocol.h). The config-frame LAYOUT is unchanged, but the
|
||||
* FRAME STREAM gains a new framed body after a status, so a 2.20 peer that does
|
||||
* not consume it would desynchronize on the following exchange. The strict
|
||||
* same-version handshake (config_receive rejects a mismatched version before
|
||||
* parsing anything else) is what keeps a 2.21 client and a 2.20 server from ever
|
||||
* reaching that state. receive_status() transparently maps STATUS_ERROR_DETAIL
|
||||
* back to STATUS_ERROR for every existing call site and captures the reason into
|
||||
* a thread-local buffer consulted via protocol_last_error(). */
|
||||
#define PROTOCOL_VERSION "2.21.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||
#define MAX_BASIS_DIRS 64
|
||||
|
||||
+12
-12
@@ -1721,7 +1721,7 @@ static IncrementalCheckOutcome incremental_check_receive_request(IncrementalChec
|
||||
return INCREMENTAL_ERROR;
|
||||
if (!receive_n_data(fd, &state->check_mtime_nsec, sizeof(state->check_mtime_nsec)) ||
|
||||
state->check_mtime_nsec < 0 || state->check_mtime_nsec >= 1000000000LL) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
send_error_detail(fd, "invalid check mtime nanoseconds");
|
||||
return INCREMENTAL_ERROR;
|
||||
}
|
||||
if ((config->checksum || config_has_basis(config))) {
|
||||
@@ -1729,7 +1729,7 @@ static IncrementalCheckOutcome incremental_check_receive_request(IncrementalChec
|
||||
if (!receive_n_data(fd, &wire_len, sizeof(wire_len)) || wire_len == 0 ||
|
||||
wire_len > CHECKSUM_MAX_DIGEST_LEN ||
|
||||
wire_len != checksum_digest_len((ChecksumAlgo)config->checksum_algo)) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
send_error_detail(fd, "invalid check digest length");
|
||||
return INCREMENTAL_ERROR;
|
||||
}
|
||||
state->check_digest_len = wire_len;
|
||||
@@ -1738,7 +1738,7 @@ static IncrementalCheckOutcome incremental_check_receive_request(IncrementalChec
|
||||
}
|
||||
|
||||
if (state->check_size > MAX_RECEIVE_WHOLE_FILE_SIZE) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
send_error_detail(fd, "check size exceeds receiver limit");
|
||||
return INCREMENTAL_ERROR;
|
||||
}
|
||||
|
||||
@@ -1757,7 +1757,7 @@ static IncrementalCheckOutcome incremental_check_receive_request(IncrementalChec
|
||||
static IncrementalCheckOutcome incremental_check_open_destination(IncrementalCheckState* state) {
|
||||
char* full_path = path_cat(state->config->receive_root_directory, state->check_path);
|
||||
if (!full_path) {
|
||||
send_status(state->fd, STATUS_ERROR);
|
||||
send_error_detail(state->fd, "could not build destination path");
|
||||
return INCREMENTAL_ERROR;
|
||||
}
|
||||
state->full_path = full_path;
|
||||
@@ -1911,7 +1911,7 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
|
||||
File** out_file) {
|
||||
int fd = state->fd;
|
||||
const Config* config = state->config;
|
||||
char* check_path = state->check_path;
|
||||
const char* check_path = state->check_path;
|
||||
unsigned long long old_size = state->old_size;
|
||||
unsigned long long check_size = state->check_size;
|
||||
|
||||
@@ -2464,7 +2464,7 @@ File* file_receive_hardlink(int file_descriptor) {
|
||||
escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
free(path);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, "invalid hard-link path");
|
||||
return NULL;
|
||||
}
|
||||
int gid;
|
||||
@@ -2484,7 +2484,7 @@ File* file_receive_hardlink(int file_descriptor) {
|
||||
free(escaped);
|
||||
free(target);
|
||||
free(path);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, "invalid hard-link target path");
|
||||
return NULL;
|
||||
}
|
||||
File* file = file_create(path);
|
||||
@@ -2514,7 +2514,7 @@ File* file_receive_symlink(int file_descriptor, const Config* config) {
|
||||
escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
free(path);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, "invalid symlink path");
|
||||
return NULL;
|
||||
}
|
||||
char* target = receive_wire_str(file_descriptor);
|
||||
@@ -2529,7 +2529,7 @@ File* file_receive_symlink(int file_descriptor, const Config* config) {
|
||||
free(escaped);
|
||||
free(target);
|
||||
free(path);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, "invalid symlink target");
|
||||
return NULL;
|
||||
}
|
||||
File* file = file_create(path);
|
||||
@@ -2569,7 +2569,7 @@ File* file_receive_special(int file_descriptor) {
|
||||
escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
free(path);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, "invalid special path");
|
||||
return NULL;
|
||||
}
|
||||
int meta_ok = 1;
|
||||
@@ -2593,14 +2593,14 @@ File* file_receive_special(int file_descriptor) {
|
||||
if (!metadata) {
|
||||
log_message(LOG_LEVEL_ERROR, "Special node sent without metadata (mode)");
|
||||
free(path);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, "special node sent without metadata");
|
||||
return NULL;
|
||||
}
|
||||
if (!file_special_rdev_valid(major, minor, metadata->mode)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid special rdev received (%d:%d)", (int)major, (int)minor);
|
||||
free(path);
|
||||
file_metadata_destroy(metadata);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
send_error_detail(file_descriptor, "invalid special device rdev");
|
||||
return NULL;
|
||||
}
|
||||
File* file = file_create(path);
|
||||
|
||||
@@ -22,6 +22,10 @@ static __thread ProtocolSession* bound_session;
|
||||
static __thread ProtocolSession legacy_io_session = {
|
||||
.read_fd = -1, .write_fd = -1, .max_alloc = DEFAULT_MAX_ALLOC};
|
||||
|
||||
/* Last STATUS_ERROR_DETAIL reason received on this thread (protocol 2.21.0).
|
||||
* Empty when the last status read carried no detail. */
|
||||
static __thread char io_error_detail[MAX_ERROR_DETAIL_BYTES + 1];
|
||||
|
||||
static unsigned long long io_bwlimit = 0;
|
||||
static mtx_t bw_mutex;
|
||||
static once_flag bw_mutex_once = ONCE_FLAG_INIT;
|
||||
@@ -446,6 +450,8 @@ static const char* status_to_string(Status status) {
|
||||
return "AUTH_OK";
|
||||
case STATUS_AUTH_FAILED:
|
||||
return "AUTH_FAILED";
|
||||
case STATUS_ERROR_DETAIL:
|
||||
return "ERROR_DETAIL";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -600,9 +606,40 @@ bool protocol_send_status(ProtocolSession* session, Status status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Consume the optional detail body of a STATUS_ERROR_DETAIL frame and map the
|
||||
* status back to STATUS_ERROR for existing callers. Invoked for EVERY status
|
||||
* read (bare STATUS_OK/STATUS_ERROR too) so a stale detail from an earlier
|
||||
* exchange is never reported for a later one. The body is always read, even
|
||||
* when the caller ignores protocol_last_error(), so the stream never
|
||||
* desynchronizes. */
|
||||
static void protocol_capture_error_detail(ProtocolSession* session, Status* status) {
|
||||
io_error_detail[0] = '\0';
|
||||
if (*status != STATUS_ERROR_DETAIL)
|
||||
return;
|
||||
*status = STATUS_ERROR;
|
||||
/* The body MUST be drained even when the session's allocation ceiling is
|
||||
* smaller than the message (e.g. a tiny --max-alloc), otherwise the string
|
||||
* body would be left on the stream and desynchronize the next exchange.
|
||||
* Lift the ceiling for this one bounded string read and restore it. */
|
||||
unsigned long long saved_max_alloc = session->max_alloc;
|
||||
if (saved_max_alloc < MAX_STRING_SIZE + 1)
|
||||
session->max_alloc = MAX_STRING_SIZE + 1;
|
||||
char* detail = protocol_receive_str(session);
|
||||
session->max_alloc = saved_max_alloc;
|
||||
if (!detail)
|
||||
return;
|
||||
size_t len = strlen(detail);
|
||||
if (len > MAX_ERROR_DETAIL_BYTES)
|
||||
len = MAX_ERROR_DETAIL_BYTES;
|
||||
memcpy(io_error_detail, detail, len);
|
||||
io_error_detail[len] = '\0';
|
||||
free(detail);
|
||||
}
|
||||
|
||||
bool protocol_receive_status(ProtocolSession* session, Status* status) {
|
||||
if (!protocol_receive_n_data(session, status, sizeof(Status)))
|
||||
return false;
|
||||
protocol_capture_error_detail(session, status);
|
||||
log_debug_message(LOG_DEBUG_PROTO, "Received Status: %s", status_to_string(*status));
|
||||
return true;
|
||||
}
|
||||
@@ -614,6 +651,7 @@ bool protocol_receive_status(ProtocolSession* session, Status* status) {
|
||||
bool protocol_receive_status_timed(ProtocolSession* session, Status* status, int timeout_sec) {
|
||||
if (!protocol_receive_n_data_timed(session, status, sizeof(Status), timeout_sec))
|
||||
return false;
|
||||
protocol_capture_error_detail(session, status);
|
||||
log_debug_message(LOG_DEBUG_PROTO, "Received Status: %s", status_to_string(*status));
|
||||
return true;
|
||||
}
|
||||
@@ -725,6 +763,7 @@ bool protocol_receive_status_keepalive(ProtocolSession* session, Status* status,
|
||||
Status received;
|
||||
if (!protocol_read_status_until(session, &received, &deadline))
|
||||
return false;
|
||||
protocol_capture_error_detail(session, &received);
|
||||
if (received == STATUS_KEEPALIVE) {
|
||||
/* The receiver's answer to one of our keepalives. */
|
||||
replies_seen++;
|
||||
@@ -751,6 +790,7 @@ bool protocol_receive_status_keepalive(ProtocolSession* session, Status* status,
|
||||
keepalives_sent - replies_seen);
|
||||
break;
|
||||
}
|
||||
protocol_capture_error_detail(session, &drained);
|
||||
if (drained != STATUS_KEEPALIVE) {
|
||||
log_message(LOG_LEVEL_ERROR, "Unexpected status while draining keepalive replies");
|
||||
return false;
|
||||
@@ -807,3 +847,24 @@ bool receive_status_keepalive(int fd, Status* status, int timeout_sec, int keepa
|
||||
return protocol_receive_status_keepalive(legacy_session(fd, -1), status, timeout_sec,
|
||||
keepalive_interval_sec, abort_check);
|
||||
}
|
||||
|
||||
bool send_error_detail(int fd, const char* message) {
|
||||
if (!message)
|
||||
message = "";
|
||||
char bounded[MAX_ERROR_DETAIL_BYTES + 1];
|
||||
size_t len = strlen(message);
|
||||
if (len > MAX_ERROR_DETAIL_BYTES) {
|
||||
memcpy(bounded, message, MAX_ERROR_DETAIL_BYTES);
|
||||
bounded[MAX_ERROR_DETAIL_BYTES] = '\0';
|
||||
message = bounded;
|
||||
}
|
||||
return send_status(fd, STATUS_ERROR_DETAIL) && send_str(fd, message);
|
||||
}
|
||||
|
||||
const char* protocol_last_error(void) {
|
||||
return io_error_detail;
|
||||
}
|
||||
|
||||
void protocol_clear_last_error(void) {
|
||||
io_error_detail[0] = '\0';
|
||||
}
|
||||
|
||||
+26
-1
@@ -9,6 +9,12 @@
|
||||
/* Maximum allowed string size for receive_str (64 KB) */
|
||||
#define MAX_STRING_SIZE (64 * 1024)
|
||||
|
||||
/* Hard cap on the optional server->client rejection detail carried by
|
||||
* STATUS_ERROR_DETAIL (protocol 2.21.0). A longer message is sliced to this
|
||||
* many bytes before it is sent, so a peer can never be made to retain more than
|
||||
* this for a rejection and the detail frame stays a small, fixed bound. */
|
||||
#define MAX_ERROR_DETAIL_BYTES 4096
|
||||
|
||||
/* Maximum uncompressed file payload accepted by the receiver's whole-file
|
||||
* paths. A single whole file is charged against the per-connection memory
|
||||
* reservation (MAX_CONNECTION_MEMORY) and against the server allocation
|
||||
@@ -132,7 +138,15 @@ enum NET_STATUS {
|
||||
STATUS_AUTH_CHALLENGE,
|
||||
STATUS_AUTH_RESPONSE,
|
||||
STATUS_AUTH_OK,
|
||||
STATUS_AUTH_FAILED
|
||||
STATUS_AUTH_FAILED,
|
||||
/* Optional server->client rejection detail (protocol 2.21.0). When the
|
||||
* server refuses a transfer for a concrete reason it may send
|
||||
* STATUS_ERROR_DETAIL followed by a length-prefixed, bounded string instead
|
||||
* of a bare STATUS_ERROR. receive_status() consumes the string and maps the
|
||||
* status back to STATUS_ERROR, so every pre-2.21 call site keeps working;
|
||||
* callers that want the human-readable reason consult protocol_last_error().
|
||||
* Appended last so the existing wire values never move. */
|
||||
STATUS_ERROR_DETAIL
|
||||
};
|
||||
|
||||
void io_set_fds(int read_fd, int write_fd);
|
||||
@@ -192,6 +206,17 @@ bool send_int(int file_descriptor, int data);
|
||||
bool receive_int(int file_descriptor, int* data);
|
||||
bool send_status(int file_descriptor, Status status);
|
||||
bool receive_status(int file_descriptor, Status* status);
|
||||
/* Send STATUS_ERROR_DETAIL followed by a bounded (<= MAX_ERROR_DETAIL_BYTES)
|
||||
* length-prefixed string. Over-long messages are sliced and NULL is treated
|
||||
* as "". Returns false if the status or the string could not be sent. */
|
||||
bool send_error_detail(int file_descriptor, const char* message);
|
||||
/* Human-readable reason captured from the most recent STATUS_ERROR_DETAIL
|
||||
* received on this thread, or "" when the last status was a bare STATUS_ERROR
|
||||
* (or no detail was seen). Thread-local, and valid until the next status read
|
||||
* on the same thread. */
|
||||
const char* protocol_last_error(void);
|
||||
/* Clear the thread-local last-error buffer. */
|
||||
void protocol_clear_last_error(void);
|
||||
/* receive_status with an explicit per-message deadline in seconds, instead of
|
||||
the default RECEIVE_TIMEOUT_SEC. A reply that may legitimately take longer
|
||||
(e.g. the early-delete ACK after a large receiver-side deletion) must use
|
||||
|
||||
Reference in New Issue
Block a user