From: Vsevolod Stakhov Date: Sat, 7 Feb 2026 15:55:53 +0000 (+0000) Subject: [Feature] rspamc: Add --msgpack flag for v3 protocol X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5ee32775d4342c900fa31d8c594265a858dcaf0f;p=thirdparty%2Frspamd.git [Feature] rspamc: Add --msgpack flag for v3 protocol Add --msgpack option to rspamc that sends metadata as msgpack instead of JSON and requests msgpack responses when using --protocol-v3. The client serializes metadata via UCL_EMIT_MSGPACK, sets the metadata part Content-Type to application/msgpack, and sends Accept: application/msgpack so the server returns results in msgpack format. Add functional tests for rspamc v3 with zstd compression, httpcrypt encryption, msgpack metadata, and encrypted+msgpack combinations. --- diff --git a/src/client/rspamc.cxx b/src/client/rspamc.cxx index b367e183a9..3fce08d560 100644 --- a/src/client/rspamc.cxx +++ b/src/client/rspamc.cxx @@ -90,6 +90,7 @@ static gboolean profile = FALSE; static gboolean skip_images = FALSE; static gboolean skip_attachments = FALSE; static gboolean protocol_v3 = FALSE; +static gboolean msgpack_mode = FALSE; static const char *pubkey = nullptr; static const char *user_agent = "rspamc"; static const char *files_list = nullptr; @@ -196,6 +197,8 @@ static GOptionEntry entries[] = "Skip attachments when learning/unlearning fuzzy", nullptr}, {"protocol-v3", '\0', 0, G_OPTION_ARG_NONE, &protocol_v3, "Use v3 multipart protocol (structured metadata, multipart response)", nullptr}, + {"msgpack", '\0', 0, G_OPTION_ARG_NONE, &msgpack_mode, + "Use msgpack for v3 metadata and response (requires --protocol-v3)", nullptr}, {"user-agent", 'U', 0, G_OPTION_ARG_STRING, &user_agent, "Use specific User-Agent instead of \"rspamc\"", nullptr}, {"files-list", '\0', 0, G_OPTION_ARG_FILENAME, &files_list, @@ -2362,6 +2365,7 @@ rspamc_process_input(struct ev_loop *ev_base, const struct rspamc_command &cmd, rspamd_client_command_v3(conn, "checkv3", metadata, in, rspamc_client_cb, cbdata, compressed, + msgpack_mode, cbdata->filename.c_str(), &err); ucl_object_unref(metadata); } diff --git a/src/client/rspamdclient.c b/src/client/rspamdclient.c index 0c2a87c969..e304de36e2 100644 --- a/src/client/rspamdclient.c +++ b/src/client/rspamdclient.c @@ -705,6 +705,7 @@ rspamd_client_command_v3(struct rspamd_client_connection *conn, rspamd_client_callback cb, gpointer ud, gboolean compressed, + gboolean msgpack, const char *filename, GError **err) { @@ -761,17 +762,36 @@ rspamd_client_command_v3(struct rspamd_client_connection *conn, req->input = input; } - /* Serialize metadata to JSON */ - char *metadata_json = NULL; + /* Serialize metadata to JSON or msgpack */ + char *metadata_buf = NULL; gsize metadata_len = 0; + const char *metadata_ctype = "application/json"; if (metadata) { - metadata_json = (char *) ucl_object_emit(metadata, UCL_EMIT_JSON_COMPACT); - metadata_len = strlen(metadata_json); + if (msgpack) { + size_t emit_len; + metadata_buf = (char *) ucl_object_emit_len(metadata, + UCL_EMIT_MSGPACK, &emit_len); + metadata_len = emit_len; + metadata_ctype = "application/msgpack"; + } + else { + metadata_buf = (char *) ucl_object_emit(metadata, UCL_EMIT_JSON_COMPACT); + metadata_len = strlen(metadata_buf); + } } else { - metadata_json = g_strdup("{}"); - metadata_len = 2; + if (msgpack) { + /* Empty msgpack map: 0x80 */ + metadata_buf = g_malloc(1); + metadata_buf[0] = '\x80'; + metadata_len = 1; + metadata_ctype = "application/msgpack"; + } + else { + metadata_buf = g_strdup("{}"); + metadata_len = 2; + } } /* Build multipart/form-data body with random boundary */ @@ -786,10 +806,10 @@ rspamd_client_command_v3(struct rspamd_client_connection *conn, rspamd_printf_gstring(mp_body, "--%s\r\n" "Content-Disposition: form-data; name=\"metadata\"\r\n" - "Content-Type: application/json\r\n" + "Content-Type: %s\r\n" "\r\n", - boundary); - g_string_append_len(mp_body, metadata_json, metadata_len); + boundary, metadata_ctype); + g_string_append_len(mp_body, metadata_buf, metadata_len); g_string_append(mp_body, "\r\n"); /* Message part */ @@ -804,7 +824,7 @@ rspamd_client_command_v3(struct rspamd_client_connection *conn, if (ZSTD_isError(comp_len)) { g_set_error(err, RCLIENT_ERROR, 500, "compression error"); g_free(comp_buf); - g_free(metadata_json); + g_free(metadata_buf); g_string_free(mp_body, TRUE); g_free(req); if (input) g_string_free(input, TRUE); @@ -837,7 +857,7 @@ rspamd_client_command_v3(struct rspamd_client_connection *conn, /* Closing boundary */ rspamd_printf_gstring(mp_body, "--%s--\r\n", boundary); - g_free(metadata_json); + g_free(metadata_buf); /* Set body */ body = rspamd_fstring_new_init(mp_body->str, mp_body->len); @@ -850,7 +870,12 @@ rspamd_client_command_v3(struct rspamd_client_connection *conn, "multipart/form-data; boundary=%s", boundary); /* Add Accept headers */ - rspamd_http_message_add_header(req->msg, "Accept", "application/json"); + if (msgpack) { + rspamd_http_message_add_header(req->msg, "Accept", "application/msgpack"); + } + else { + rspamd_http_message_add_header(req->msg, "Accept", "application/json"); + } if (compressed) { rspamd_http_message_add_header(req->msg, "Accept-Encoding", "zstd"); } diff --git a/src/client/rspamdclient.h b/src/client/rspamdclient.h index 1f41ac327a..7f5eb38d2b 100644 --- a/src/client/rspamdclient.h +++ b/src/client/rspamdclient.h @@ -95,7 +95,8 @@ gboolean rspamd_client_command( /** * Send a v3 multipart/form-data command. - * Metadata is sent as a JSON part, message as an octet-stream part. + * Metadata is sent as a JSON (or msgpack if msgpack=TRUE) part, + * message as an octet-stream part. * Response is multipart/mixed with "result" (JSON/msgpack) and optional "body" parts. */ gboolean rspamd_client_command_v3( @@ -106,6 +107,7 @@ gboolean rspamd_client_command_v3( rspamd_client_callback cb, gpointer ud, gboolean compressed, + gboolean msgpack, const char *filename, GError **err); diff --git a/test/functional/cases/001_merged/430_checkv3.robot b/test/functional/cases/001_merged/430_checkv3.robot index 76790e66f5..92c66a3f62 100644 --- a/test/functional/cases/001_merged/430_checkv3.robot +++ b/test/functional/cases/001_merged/430_checkv3.robot @@ -53,3 +53,27 @@ checkv3 malformed boundary [Documentation] Send body with wrong boundary, expect HTTP 500 (400 error mapped to 5xx) Scan File V3 Expect Error ${GTUBE} 500 ... content_type_override=multipart/form-data; boundary=wrong-boundary-does-not-match + +checkv3 via rspamc with zstd compression + [Documentation] Scan via rspamc --protocol-v3 (zstd compression enabled by default) + ${result} = Run Rspamc -p -h ${RSPAMD_LOCAL_ADDR}:${RSPAMD_PORT_NORMAL} --protocol-v3 + ... --settings=${SETTINGS_NOSYMBOLS} ${GTUBE} + Check Rspamc ${result} GTUBE ( + +checkv3 via rspamc encrypted + [Documentation] Scan via rspamc --protocol-v3 with httpcrypt encryption + ${result} = Run Rspamc -p -h ${RSPAMD_LOCAL_ADDR}:${RSPAMD_PORT_NORMAL} --protocol-v3 + ... --key ${RSPAMD_KEY_PUB1} --settings=${SETTINGS_NOSYMBOLS} ${GTUBE} + Check Rspamc ${result} GTUBE ( + +checkv3 via rspamc with msgpack metadata + [Documentation] Scan via rspamc --protocol-v3 --msgpack (msgpack metadata and response) + ${result} = Run Rspamc -p -h ${RSPAMD_LOCAL_ADDR}:${RSPAMD_PORT_NORMAL} --protocol-v3 + ... --msgpack --settings=${SETTINGS_NOSYMBOLS} ${GTUBE} + Check Rspamc ${result} GTUBE ( + +checkv3 via rspamc encrypted with msgpack + [Documentation] Scan via rspamc --protocol-v3 --msgpack --key (encrypted + msgpack) + ${result} = Run Rspamc -p -h ${RSPAMD_LOCAL_ADDR}:${RSPAMD_PORT_NORMAL} --protocol-v3 + ... --msgpack --key ${RSPAMD_KEY_PUB1} --settings=${SETTINGS_NOSYMBOLS} ${GTUBE} + Check Rspamc ${result} GTUBE (