From 1a94748067020fad69ae5a13b82883d1bf783224 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Fri, 5 Jun 2026 12:00:23 +0200 Subject: [PATCH] Refactored stream protocol helper functions Lifted file stream protocol helpers out of file-private static so that they can be reused for streaming leech2 patches. Ticket: ENT-14104 Signed-off-by: Lars Erik Wik --- libcfnet/Makefile.am | 1 + libcfnet/file_stream.c | 312 +------------------------------------ libcfnet/stream_protocol.c | 248 +++++++++++++++++++++++++++++ libcfnet/stream_protocol.h | 130 ++++++++++++++++ 4 files changed, 380 insertions(+), 311 deletions(-) create mode 100644 libcfnet/stream_protocol.c create mode 100644 libcfnet/stream_protocol.h diff --git a/libcfnet/Makefile.am b/libcfnet/Makefile.am index dcacf70ece..38b5b566fa 100644 --- a/libcfnet/Makefile.am +++ b/libcfnet/Makefile.am @@ -47,5 +47,6 @@ libcfnet_la_SOURCES = \ protocol_version.c protocol_version.h \ server_code.c server_code.h \ stat_cache.c stat_cache.h \ + stream_protocol.c stream_protocol.h \ tls_client.c tls_client.h \ tls_generic.c tls_generic.h diff --git a/libcfnet/file_stream.c b/libcfnet/file_stream.c index 2878bb7cdf..433c35a957 100644 --- a/libcfnet/file_stream.c +++ b/libcfnet/file_stream.c @@ -30,314 +30,10 @@ #include #include #include +#include #include #include -/*********************************************************/ -/* Network protocol */ -/*********************************************************/ - -/** - * @brief Simple network protocol on top of SSL/TCP. Used for client-server - * communication during file stream. - * - * @details Header format: - * +----------+----------+----------+----------+ - * | SDU Len. | Reserved | EOF Flag | ERR Flag | - * +----------+----------+----------+----------+ - * | 12 bits | 2 bits | 1 bit | 1 bit | - * +----------+----------+----------+----------+ - * - * The header consists of 16 bits and the fields are defined as follows: - * SDU Length Length of the SDU (i.e. payload) encapsulated within - * this datagram. - * Reserved 2 bits reserved for future use. - * End-of-File flag Signals whether or not the receiver should expect to - * receive more datagrams. - * Error flag Signals that the transmission must be canceled due to - * unexpected error. - * - * @note If the End-of-File flag is set, there may still be data to process in - * in the payload. If the Error flag is set, there may be an error - * message in the payload. - */ -#define PROTOCOL_HEADER_SIZE 2 - -/** - * @note The TLS Generic API requires that the message length is less than - * CF_BUFSIZE. Furthermore, the protocol can only handle up to 4095 - * Bytes, because it's the largest unsigned integer you can represent - * with 12 bits (2^12 - 1 = 4095). - */ -#define PROTOCOL_MESSAGE_SIZE MIN(CF_BUFSIZE - 1, 4095) - -/** - * @brief Send a message using the file stream protocol - * @warning You probably want to use ProtocolSendMessage() or - * ProtocolSendError() instead - * - * @param conn The SSL connection object - * @param msg The message to send - * @param len The length of the message to send (must be less or equal to - * PROTOCOL_MESSAGE_SIZE Bytes) - * @param eof Set to true if this is the last message in a transaction, - * otherwise false - * @param err Set to true if transaction must be canceled (e.g., due to an - * unexpected error), otherwise false - * @note If the err parameter is set to true, the expected return value is - * still true. - * @return true on success, otherwise false - */ -static bool __ProtocolSendMessage( - SSL *conn, const char *msg, size_t len, bool eof, bool err) -{ - assert(conn != NULL); - assert(msg != NULL || len == 0); - assert(len <= PROTOCOL_MESSAGE_SIZE); - - /* Set message length */ - assert(sizeof(len) >= 3); /* It's probably guaranteed, but let's make sure - * to avoid potentially nasty surprises */ - uint16_t header = len << 4; - - /* Set Error flag */ - if (err) - { - header |= (1 << 0); - } - - /* Set End-of-File flag */ - if (eof) - { - header |= (1 << 1); - } - - /* Send header */ - header = htons(header); - int ret = TLSSend(conn, (char *) &header, PROTOCOL_HEADER_SIZE); - if (ret != PROTOCOL_HEADER_SIZE) - { - Log(LOG_LEVEL_ERR, - "Failed to send message header during file stream: " - "Expected to send %d bytes, but sent %d bytes", - PROTOCOL_HEADER_SIZE, - ret); - return false; - } - - if (len > 0) - { - /* Send payload */ - ret = TLSSend(conn, msg, len); - if (ret != (int) len) - { - Log(LOG_LEVEL_ERR, - "Failed to send message payload during file stream: " - "Expected to send %zu bytes, but sent %d bytes", - len, - ret); - return false; - } - } - - return true; -} - -/** - * @brief Send a message using the file stream protocol - * - * @param conn The SSL connection object - * @param msg The message to send - * @param len The length of the message to send (must be less or equal to - * PROTOCOL_MESSAGE_SIZE Bytes) - * @param eof Set to true if this is the last message in a transaction, - * otherwise false - * @return true on success, otherwise false - */ -static inline bool ProtocolSendMessage( - SSL *conn, const char *msg, size_t len, bool eof) -{ - assert(conn != NULL); - assert(msg != NULL || len == 0); - - return __ProtocolSendMessage(conn, msg, len, eof, false); -} - -/** - * @brief Receive a message using the file stream protocol - * - * @param conn The SSL connection object - * @param msg The message receive buffer (must be PROTOCOL_MESSAGE_SIZE bytes - * large) - * @param len The length of the received message - * @param eof Is set to true if this was the last message in the transaction - * @return true on success, otherwise false - * - * @note ProtocolRecvMessage fails if the communication is broken or if we - * received an error from the remote host. In both cases, we should not - * try to flush the stream. - */ -static bool ProtocolRecvMessage(SSL *conn, char *msg, size_t *len, bool *eof) -{ - assert(conn != NULL); - assert(msg != NULL); - assert(len != NULL); - assert(eof != NULL); - - /* TLSRecv() expects a buffer this size */ - char recv_buffer[CF_BUFSIZE]; - - /* Receive header */ - int ret = TLSRecv(conn, recv_buffer, PROTOCOL_HEADER_SIZE); - if (ret != PROTOCOL_HEADER_SIZE) - { - Log(LOG_LEVEL_ERR, - "Failed to receive message header during file stream: " - "Expected to receive %d bytes, but received %d bytes", - PROTOCOL_HEADER_SIZE, - ret); - return false; - } - - /* Why not receive the bytes directly into header in the TLSRecv()? - * Because it actually writes a NUL-Byte after the requested bytes which - * would cause memory violations. */ - uint16_t header; - memcpy(&header, recv_buffer, PROTOCOL_HEADER_SIZE); - header = ntohs(header); - - /* Extract Error flag */ - bool err = header & (1 << 0); - - /* Extract End-of-File flag */ - *eof = header & (1 << 1); - - /* Extract message length */ - assert(sizeof(*len) >= 2); /* It's probably guaranteed, but let's make - * sure to avoid potentially nasty surprises */ - *len = header >> 4; - - /* Read payload */ - if (*len > 0) - { - /* The TLSRecv() function's doc string says that the returned value - * may be less than the requested length if the other side completed a - * send with less bytes. I take it that this means that there is no - * short reads/recvs. Furthermore, TLSSend() says that its return - * value is always equal to the requested length as long as TLS is - * setup correctly. I take it that the same is true for TLSRecv(). - * Hence, we will interpret a shorter read than what we expect as an - * error. */ - ret = TLSRecv(conn, recv_buffer, *len); - if (ret != *len) - { - Log(LOG_LEVEL_ERR, - "Failed to receive message payload during file stream: " - "Expected to receive %zu bytes, but received %d bytes", - *len, - ret); - return false; - } - memcpy(msg, recv_buffer, *len); - - if (err) - { - /* If the error flag is set, then the payload contains an error - * message of 'len' bytes. */ - assert(*len < sizeof(recv_buffer)); - recv_buffer[*len] = '\0'; /* Set terminating null-byte */ - Log(LOG_LEVEL_ERR, "Remote file stream error: %s", recv_buffer); - } - } - - return !err; -} - -/** - * @brief Flush the file stream - * - * It's used to prevent the remote host from blocking while sending the - * remaining data after we have experienced an unexpected error and need to - * abort the file stream. Once the stream has been successfully flushed, the - * remote host will be ready to receive our error message. - * - * @param conn The SSL connection object - * @return true on success, otherwise false - */ -static bool ProtocolFlushStream(SSL *conn) -{ - assert(conn != NULL); - - char msg[PROTOCOL_MESSAGE_SIZE]; - size_t len; - bool eof; - while (ProtocolRecvMessage(conn, msg, &len, &eof)) - { - if (eof) - { - return true; - } - } - - /* Error is already logged in ProtocolRecvMessage() */ - return false; -} - -/** - * @brief Send an error message using the file stream protocol - * - * @param conn The SSL connection object - * @param flush Whether or not to flush the stream (see ProtocolFlushStream()) - * @param fmt The format string - * @param ... The format string arguments - * @return true on success, otherwise false - */ -static bool ProtocolSendError(SSL *conn, bool flush, const char *fmt, ...) - FUNC_ATTR_PRINTF(3, 4); - -static bool ProtocolSendError(SSL *conn, bool flush, const char *fmt, ...) -{ - assert(conn != NULL); - assert(fmt != NULL); - - va_list ap; - char msg[PROTOCOL_MESSAGE_SIZE]; - - va_start(ap, fmt); - int len = vsnprintf(msg, PROTOCOL_MESSAGE_SIZE, fmt, ap); - va_end(ap); - - assert(len >= 0); /* Let's make sure we detect this in debug builds */ - if (len < 0) - { - Log(LOG_LEVEL_ERR, - "Failed to format error message during file stream"); - len = 0; /* We still want to send the header */ - } - else if (len >= PROTOCOL_MESSAGE_SIZE) - { - Log(LOG_LEVEL_WARNING, - "Error message truncated during file stream: " - "Message is %d bytes, but maximum message size is %d bytes", - len, - PROTOCOL_MESSAGE_SIZE); - /* Add dots to indicate message truncation. We don't need the - * terminating NULL-byte in the buffer. Furthermore, TLSRecv() will - * append one, upon receiving the message */ - msg[PROTOCOL_MESSAGE_SIZE - 1] = '.'; - msg[PROTOCOL_MESSAGE_SIZE - 2] = '.'; - msg[PROTOCOL_MESSAGE_SIZE - 3] = '.'; - len = PROTOCOL_MESSAGE_SIZE; - } - - if (flush) - { - ProtocolFlushStream(conn); - } - - return __ProtocolSendMessage(conn, msg, (size_t) len, false, true); -} - /*********************************************************/ /* Common */ /*********************************************************/ @@ -530,9 +226,6 @@ static bool DrainOutputBufferToFile( /* Server specific */ /*********************************************************/ -#define ERROR_MSG_UNSPECIFIED_SERVER_REFUSAL "Unspecified server refusal" -#define ERROR_MSG_INTERNAL_SERVER_ERROR "Internal server error" - bool FileStreamRefuse(SSL *conn) { return ProtocolSendError( @@ -734,9 +427,6 @@ bool FileStreamServe(SSL *conn, const char *filename) /* Client specific */ /*********************************************************/ -#define ERROR_MSG_INTERNAL_CLIENT_ERROR "Internal client error" - - /** * @brief Get the size of a file * diff --git a/libcfnet/stream_protocol.c b/libcfnet/stream_protocol.c new file mode 100644 index 0000000000..7750dbf9d0 --- /dev/null +++ b/libcfnet/stream_protocol.c @@ -0,0 +1,248 @@ +/* + Copyright 2026 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#include + +#include + +#include +#include +#include + +/** + * @brief Send a message using the stream protocol + * @warning You probably want to use ProtocolSendMessage() or + * ProtocolSendError() instead + * + * @param conn The SSL connection object + * @param msg The message to send + * @param len The length of the message to send (must be less or equal to + * PROTOCOL_MESSAGE_SIZE Bytes) + * @param eof Set to true if this is the last message in a transaction, + * otherwise false + * @param err Set to true if transaction must be canceled (e.g., due to an + * unexpected error), otherwise false + * @note If the err parameter is set to true, the expected return value is + * still true. + * @return true on success, otherwise false + */ +static bool __ProtocolSendMessage( + SSL *conn, const char *msg, size_t len, bool eof, bool err) +{ + assert(conn != NULL); + assert(msg != NULL || len == 0); + assert(len <= PROTOCOL_MESSAGE_SIZE); + + /* Set message length */ + assert(sizeof(len) >= 3); /* It's probably guaranteed, but let's make sure + * to avoid potentially nasty surprises */ + uint16_t header = len << 4; + + /* Set Error flag */ + if (err) + { + header |= (1 << 0); + } + + /* Set End-of-File flag */ + if (eof) + { + header |= (1 << 1); + } + + /* Send header */ + header = htons(header); + int ret = TLSSend(conn, (char *) &header, PROTOCOL_HEADER_SIZE); + if (ret != PROTOCOL_HEADER_SIZE) + { + Log(LOG_LEVEL_ERR, + "Failed to send message header during stream: " + "Expected to send %d bytes, but sent %d bytes", + PROTOCOL_HEADER_SIZE, + ret); + return false; + } + + if (len > 0) + { + /* Send payload */ + ret = TLSSend(conn, msg, len); + if (ret != (int) len) + { + Log(LOG_LEVEL_ERR, + "Failed to send message payload during stream: " + "Expected to send %zu bytes, but sent %d bytes", + len, + ret); + return false; + } + } + + return true; +} + +bool ProtocolSendMessage(SSL *conn, const char *msg, size_t len, bool eof) +{ + assert(conn != NULL); + assert(msg != NULL || len == 0); + + return __ProtocolSendMessage(conn, msg, len, eof, false); +} + +bool ProtocolRecvMessage(SSL *conn, char *msg, size_t *len, bool *eof) +{ + assert(conn != NULL); + assert(msg != NULL); + assert(len != NULL); + assert(eof != NULL); + + /* TLSRecv() expects a buffer this size */ + char recv_buffer[CF_BUFSIZE]; + + /* Receive header */ + int ret = TLSRecv(conn, recv_buffer, PROTOCOL_HEADER_SIZE); + if (ret != PROTOCOL_HEADER_SIZE) + { + Log(LOG_LEVEL_ERR, + "Failed to receive message header during stream: " + "Expected to receive %d bytes, but received %d bytes", + PROTOCOL_HEADER_SIZE, + ret); + return false; + } + + /* Why not receive the bytes directly into header in the TLSRecv()? + * Because it actually writes a NUL-Byte after the requested bytes which + * would cause memory violations. */ + uint16_t header; + memcpy(&header, recv_buffer, PROTOCOL_HEADER_SIZE); + header = ntohs(header); + + /* Extract Error flag */ + bool err = header & (1 << 0); + + /* Extract End-of-File flag */ + *eof = header & (1 << 1); + + /* Extract message length */ + assert(sizeof(*len) >= 2); /* It's probably guaranteed, but let's make + * sure to avoid potentially nasty surprises */ + *len = header >> 4; + + /* Read payload */ + if (*len > 0) + { + /* The TLSRecv() function's doc string says that the returned value + * may be less than the requested length if the other side completed a + * send with less bytes. I take it that this means that there is no + * short reads/recvs. Furthermore, TLSSend() says that its return + * value is always equal to the requested length as long as TLS is + * setup correctly. I take it that the same is true for TLSRecv(). + * Hence, we will interpret a shorter read than what we expect as an + * error. */ + ret = TLSRecv(conn, recv_buffer, *len); + if (ret != *len) + { + Log(LOG_LEVEL_ERR, + "Failed to receive message payload during stream: " + "Expected to receive %zu bytes, but received %d bytes", + *len, + ret); + return false; + } + memcpy(msg, recv_buffer, *len); + + if (err) + { + /* If the error flag is set, then the payload contains an error + * message of 'len' bytes. */ + assert(*len < sizeof(recv_buffer)); + recv_buffer[*len] = '\0'; /* Set terminating null-byte */ + Log(LOG_LEVEL_ERR, "Remote stream error: %s", recv_buffer); + } + } + + return !err; +} + +bool ProtocolFlushStream(SSL *conn) +{ + assert(conn != NULL); + + char msg[PROTOCOL_MESSAGE_SIZE]; + size_t len; + bool eof; + while (ProtocolRecvMessage(conn, msg, &len, &eof)) + { + if (eof) + { + return true; + } + } + + /* Error is already logged in ProtocolRecvMessage() */ + return false; +} + +bool ProtocolSendError(SSL *conn, bool flush, const char *fmt, ...) +{ + assert(conn != NULL); + assert(fmt != NULL); + + va_list ap; + char msg[PROTOCOL_MESSAGE_SIZE]; + + va_start(ap, fmt); + int len = vsnprintf(msg, PROTOCOL_MESSAGE_SIZE, fmt, ap); + va_end(ap); + + assert(len >= 0); /* Let's make sure we detect this in debug builds */ + if (len < 0) + { + Log(LOG_LEVEL_ERR, "Failed to format error message during stream"); + len = 0; /* We still want to send the header */ + } + else if (len >= PROTOCOL_MESSAGE_SIZE) + { + Log(LOG_LEVEL_WARNING, + "Error message truncated during stream: " + "Message is %d bytes, but maximum message size is %d bytes", + len, + PROTOCOL_MESSAGE_SIZE); + /* Add dots to indicate message truncation. We don't need the + * terminating NULL-byte in the buffer. Furthermore, TLSRecv() will + * append one, upon receiving the message */ + msg[PROTOCOL_MESSAGE_SIZE - 1] = '.'; + msg[PROTOCOL_MESSAGE_SIZE - 2] = '.'; + msg[PROTOCOL_MESSAGE_SIZE - 3] = '.'; + len = PROTOCOL_MESSAGE_SIZE; + } + + if (flush) + { + ProtocolFlushStream(conn); + } + + return __ProtocolSendMessage(conn, msg, (size_t) len, false, true); +} diff --git a/libcfnet/stream_protocol.h b/libcfnet/stream_protocol.h new file mode 100644 index 0000000000..f921911735 --- /dev/null +++ b/libcfnet/stream_protocol.h @@ -0,0 +1,130 @@ +/* + Copyright 2026 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#ifndef STREAM_PROTOCOL_H +#define STREAM_PROTOCOL_H + +#include +#include +#include +#include + +/*********************************************************/ +/* Network protocol */ +/*********************************************************/ + +/** + * @brief Simple network protocol on top of SSL/TCP. Used for client-server + * communication during streaming (e.g., file stream and patch stream). + * + * @details Header format: + * +----------+----------+----------+----------+ + * | SDU Len. | Reserved | EOF Flag | ERR Flag | + * +----------+----------+----------+----------+ + * | 12 bits | 2 bits | 1 bit | 1 bit | + * +----------+----------+----------+----------+ + * + * The header consists of 16 bits and the fields are defined as follows: + * SDU Length Length of the SDU (i.e. payload) encapsulated within + * this datagram. + * Reserved 2 bits reserved for future use. + * End-of-File flag Signals whether or not the receiver should expect to + * receive more datagrams. + * Error flag Signals that the transmission must be canceled due to + * unexpected error. + * + * @note If the End-of-File flag is set, there may still be data to process in + * in the payload. If the Error flag is set, there may be an error + * message in the payload. + */ +#define PROTOCOL_HEADER_SIZE 2 + +/** + * @note The TLS Generic API requires that the message length is less than + * CF_BUFSIZE. Furthermore, the protocol can only handle up to 4095 + * Bytes, because it's the largest unsigned integer you can represent + * with 12 bits (2^12 - 1 = 4095). + */ +#define PROTOCOL_MESSAGE_SIZE MIN(CF_BUFSIZE - 1, 4095) + +/* Common error messages */ +#define ERROR_MSG_UNSPECIFIED_SERVER_REFUSAL "Unspecified server refusal" +#define ERROR_MSG_INTERNAL_SERVER_ERROR "Internal server error" +#define ERROR_MSG_INTERNAL_CLIENT_ERROR "Internal client error" + +/** + * @brief Send a message using the stream protocol + * + * @param conn The SSL connection object + * @param msg The message to send + * @param len The length of the message to send (must be less or equal to + * PROTOCOL_MESSAGE_SIZE Bytes) + * @param eof Set to true if this is the last message in a transaction, + * otherwise false + * @return true on success, otherwise false + */ +bool ProtocolSendMessage(SSL *conn, const char *msg, size_t len, bool eof); + +/** + * @brief Receive a message using the stream protocol + * + * @param conn The SSL connection object + * @param msg The message receive buffer (must be PROTOCOL_MESSAGE_SIZE bytes + * large) + * @param len The length of the received message + * @param eof Is set to true if this was the last message in the transaction + * @return true on success, otherwise false + * + * @note ProtocolRecvMessage fails if the communication is broken or if we + * received an error from the remote host. In both cases, we should not + * try to flush the stream. + */ +bool ProtocolRecvMessage(SSL *conn, char *msg, size_t *len, bool *eof); + +/** + * @brief Flush the stream + * + * It's used to prevent the remote host from blocking while sending the + * remaining data after we have experienced an unexpected error and need to + * abort the stream. Once the stream has been successfully flushed, the + * remote host will be ready to receive our error message. + * + * @param conn The SSL connection object + * @return true on success, otherwise false + */ +bool ProtocolFlushStream(SSL *conn); + +/** + * @brief Send an error message using the stream protocol + * + * @param conn The SSL connection object + * @param flush Whether or not to flush the stream (see ProtocolFlushStream()) + * @param fmt The format string + * @param ... The format string arguments + * @return true on success, otherwise false + */ +bool ProtocolSendError(SSL *conn, bool flush, const char *fmt, ...) + FUNC_ATTR_PRINTF(3, 4); + +#endif // STREAM_PROTOCOL_H