feat(d5-daemon-motd): daemon MOTD display + --no-motd
This commit is contained in:
@@ -149,6 +149,7 @@ static void config_set_defaults(Config* config) {
|
||||
config->daemon = false;
|
||||
config->daemon_config = NULL;
|
||||
config->server_mode = false;
|
||||
config->no_motd = false;
|
||||
config->checksum = false;
|
||||
config->checksum_algo = CHECKSUM_ALGO_XXH64;
|
||||
config->checksum_seed = 0;
|
||||
|
||||
+15
-1
@@ -340,6 +340,12 @@ typedef struct Config {
|
||||
bool daemon;
|
||||
char* daemon_config;
|
||||
bool server_mode;
|
||||
/* --no-motd (Wave C): CLIENT-ONLY, never crosses the wire. Suppresses
|
||||
* DISPLAY of the daemon's MOTD; the daemon still sends the MOTD frame, so
|
||||
* the client reads and discards it to keep the stream in sync. rsync's
|
||||
* --no-motd is likewise a client-side display switch. Default false (the
|
||||
* MOTD is shown when a daemon offers one). */
|
||||
bool no_motd;
|
||||
|
||||
// PR #183: Checksum comparison
|
||||
bool checksum;
|
||||
@@ -479,7 +485,15 @@ typedef struct Config {
|
||||
* build always read and write the same full layout (the strict same-version
|
||||
* handshake rejects any other version before a byte of the frame is parsed),
|
||||
* so a peer can never desynchronize on the added tail. The 2.15.0 release
|
||||
* ships Wave A + Wave B together; the bump stays owned by Wave A. */
|
||||
* ships Wave A + Wave B together; the bump stays owned by Wave A.
|
||||
*
|
||||
* Wave C (MOTD) adds NO config-frame field and no version bump either. On the
|
||||
* daemon listener path only, the server sends one MOTD string frame AFTER the
|
||||
* config-frame STATUS_OK (server.c handler), and every 2.15.0 daemon client
|
||||
* reads that frame right after the ack (client_send.c) -- symmetric
|
||||
* server->client in every build, so the strict same-version handshake keeps the
|
||||
* two peers in lockstep and nothing can desynchronize. The --stdio SSH path
|
||||
* sends/reads no MOTD at all. */
|
||||
#define PROTOCOL_VERSION "2.15.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "motd.h"
|
||||
#include "protocol.h"
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
char* motd_read_file(const char* path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return NULL;
|
||||
FILE* fp = fopen(path, "rb");
|
||||
if (!fp)
|
||||
return NULL;
|
||||
char* buffer = malloc(MOTD_MAX_BYTES + 1);
|
||||
if (!buffer) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
/* fread stops at the bound; a larger file is truncated rather than read
|
||||
* unbounded. ferror distinguishes a truncated read from an I/O failure. */
|
||||
size_t total = fread(buffer, 1, MOTD_MAX_BYTES, fp);
|
||||
if (ferror(fp)) {
|
||||
free(buffer);
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
fclose(fp);
|
||||
buffer[total] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
char* motd_render(const char* motd, bool eight_bit_output) {
|
||||
if (!motd)
|
||||
return NULL;
|
||||
size_t length = strlen(motd);
|
||||
if (length > (SIZE_MAX - 1) / 5)
|
||||
return NULL;
|
||||
char* rendered = malloc(length * 5 + 1);
|
||||
if (!rendered)
|
||||
return NULL;
|
||||
size_t out = 0;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
unsigned char byte = (unsigned char)motd[i];
|
||||
if (byte == '\n' || byte == '\t') {
|
||||
rendered[out++] = (char)byte;
|
||||
} else if ((byte >= 32 && byte <= 126) || (eight_bit_output && byte >= 128)) {
|
||||
rendered[out++] = (char)byte;
|
||||
} else {
|
||||
rendered[out++] = '\\';
|
||||
rendered[out++] = '#';
|
||||
rendered[out++] = (char)('0' + ((byte >> 6) & 7));
|
||||
rendered[out++] = (char)('0' + ((byte >> 3) & 7));
|
||||
rendered[out++] = (char)('0' + (byte & 7));
|
||||
}
|
||||
}
|
||||
rendered[out] = '\0';
|
||||
return rendered;
|
||||
}
|
||||
|
||||
bool motd_send(int file_descriptor, const char* motd) {
|
||||
return send_str(file_descriptor, motd ? motd : "");
|
||||
}
|
||||
|
||||
char* motd_receive(int file_descriptor) {
|
||||
char* motd = receive_str(file_descriptor);
|
||||
if (!motd)
|
||||
return NULL;
|
||||
/* Guard against a hostile/oversized peer: receive_str already bounded the
|
||||
* frame at MAX_STRING_SIZE and consumed it, so discarding an over-bound
|
||||
* body here keeps the stream framed while refusing to display it. */
|
||||
if (strlen(motd) > MOTD_MAX_BYTES) {
|
||||
free(motd);
|
||||
return NULL;
|
||||
}
|
||||
return motd;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef MOTD_H
|
||||
#define MOTD_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Daemon Message-Of-The-Day (Wave C).
|
||||
*
|
||||
* The daemon listener (fastsync-server --daemon) may advertise a `motd file`
|
||||
* configured in its globals. When a client connects with a host::module/path
|
||||
* destination and the module gate accepts the connection, the server sends the
|
||||
* MOTD as a single string frame BEFORE any transfer data (rsync sends its MOTD
|
||||
* as the first thing from the server at the start of a daemon connection).
|
||||
* The client reads that frame right after the config/status handshake and
|
||||
* displays it on stdout unless --no-motd was given.
|
||||
*
|
||||
* The MOTD is ordinary display text, never a secret, so it uses the normal
|
||||
* (non-redacted) string primitive. The exchange is strictly server->client
|
||||
* and happens on the daemon listener path only; the --stdio SSH path has no
|
||||
* MOTD.
|
||||
*
|
||||
* No PROTOCOL_VERSION bump is involved: the frame is sent and read
|
||||
* symmetrically by every 2.15.0 daemon build (the strict same-version
|
||||
* handshake rejects any other version before the frame), so it cannot
|
||||
* desynchronize a peer. */
|
||||
|
||||
/* Upper bound on the MOTD bytes the server will read from disk and put on the
|
||||
* wire. Kept far below MAX_STRING_SIZE (64 KB) so a huge/hostile motd file
|
||||
* can never produce an unbounded frame or allocation. */
|
||||
#define MOTD_MAX_BYTES 4096
|
||||
|
||||
/* Read a daemon MOTD file, bounded to MOTD_MAX_BYTES. Returns a malloc'd
|
||||
* NUL-terminated copy of the file content (bytes beyond the bound are
|
||||
* truncated) or NULL when path is NULL/empty, the file cannot be opened or
|
||||
* read, or allocation fails. An absent or unreadable motd file is NOT an
|
||||
* error: the caller simply sends an empty MOTD frame and continues. */
|
||||
char* motd_read_file(const char* path);
|
||||
|
||||
/* Render MOTD text for terminal display. Newlines and tabs are preserved so
|
||||
* a multi-line motd still reads naturally, while every other non-printable /
|
||||
* control byte (ESC included) is escaped with FastSync's `\NNN` octal
|
||||
* convention, so a hostile server cannot inject terminal escape sequences
|
||||
* through the MOTD. eight_bit_output keeps bytes >= 0x80 verbatim (matching
|
||||
* --8-bit-output). Returns a malloc'd string or NULL on allocation failure. */
|
||||
char* motd_render(const char* motd, bool eight_bit_output);
|
||||
|
||||
/* Send/receive the MOTD string frame. These wrap the normal string
|
||||
* primitive: the MOTD is not a credential, so no redaction is used. The
|
||||
* receiver additionally rejects an over-bound frame (> MOTD_MAX_BYTES) as a
|
||||
* hostile input guard; the frame itself is always fully consumed first, so the
|
||||
* stream stays framed. */
|
||||
bool motd_send(int file_descriptor, const char* motd);
|
||||
char* motd_receive(int file_descriptor);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user