]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Feature] rspamc: Add --msgpack flag for v3 protocol
authorVsevolod Stakhov <vsevolod@rspamd.com>
Sat, 7 Feb 2026 15:55:53 +0000 (15:55 +0000)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Sat, 7 Feb 2026 15:55:53 +0000 (15:55 +0000)
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.

src/client/rspamc.cxx
src/client/rspamdclient.c
src/client/rspamdclient.h
test/functional/cases/001_merged/430_checkv3.robot

index b367e183a9be5c542590581b656bd3204aba5efc..3fce08d560fcff9e671036783dad2e1e91a2c847 100644 (file)
@@ -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);
                }
index 0c2a87c9694af4be11f3d70e24905587ca72a4a8..e304de36e2ee303b826bdc5e13d5b196b16581f4 100644 (file)
@@ -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");
        }
index 1f41ac327a76c377108a0d8fd06cfd299e83a858..7f5eb38d2b9c84453ae64f4e5b148cd65efb9d6a 100644 (file)
@@ -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);
 
index 76790e66f5fb21028712993daa0b68f62e8552dc..92c66a3f62c1325fd4216ca1b9a17c4dba4bd294 100644 (file)
@@ -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 (