Add GRPC dissector
authorHuang Qiangxiong <qiangxiong.huang@qq.com>
Mon, 31 Jul 2017 15:22:59 +0000 (15:22 +0000)
committerMichael Mann <mmann78@netscape.net>
Tue, 26 Sep 2017 11:26:01 +0000 (11:26 +0000)
GRPC dissector register it self to media_type dissector table using
patterns "application/grpc", "application/grpc+proto" and
"application/grpc+json".

GRPC stack (at least in grpc-java) can send JSON over GRPC using
content-type = "application/grpc" which normally means default protobuf
format.  A preference is added to detect the message body, if it starts
with '{', and ends with '}', will force to use JSON subdissector instead
of searching in 'grpc_message_type' table.

Ping-Bug: 13932
Change-Id: I910961ca06370e678d19b78cac533ca566d87628
Reviewed-on: https://code.wireshark.org/review/22891
Petri-Dish: Michael Mann <mmann78@netscape.net>
Tested-by: Petri Dish Buildbot <buildbot-no-reply@wireshark.org>
Reviewed-by: Michael Mann <mmann78@netscape.net>
docbook/release-notes.asciidoc
epan/dissectors/CMakeLists.txt
epan/dissectors/Makefile.am
epan/dissectors/packet-grpc.c [new file with mode: 0644]
epan/dissectors/packet-json.c

index 1060e2802701b171e7737f854d21126278725950..f9bbda96ef8960b94b0923ec209bb613987a2643 100644 (file)
@@ -69,6 +69,7 @@ Wi-Fi Device Provisioning Protocol
 PFCP (Packet Forwarding Control Protocol)
 Tibia
 Broadcom tags (Broadcom Ethernet switch management frames)
+GRPC (gRPC)
 --sort-and-group--
 
 === Updated Protocol Support
index cca5b48c1a562e1b3444f10420f01bd8e2d619cf..57da2fdb853143fd0907ef0e27052e1fdac052d5 100644 (file)
@@ -994,6 +994,7 @@ set(DISSECTOR_SRC
        packet-gprs-llc.c
        packet-gprscdr.c
        packet-gre.c
+       packet-grpc.c
        packet-gsm_a_bssmap.c
        packet-gsm_a_common.c
        packet-gsm_a_dtap.c
index 0b3e7a9be330dcfb8ce3a300fb2591b03bcefd3f..4291eedcfdea3e4ecbd624557a21ca2a55d05e7a 100644 (file)
@@ -649,6 +649,7 @@ DISSECTOR_SRC = \
        packet-gprs-llc.c       \
        packet-gprscdr.c        \
        packet-gre.c            \
+       packet-grpc.c           \
        packet-gsm_a_bssmap.c   \
        packet-gsm_a_common.c   \
        packet-gsm_a_dtap.c     \
diff --git a/epan/dissectors/packet-grpc.c b/epan/dissectors/packet-grpc.c
new file mode 100644 (file)
index 0000000..9fc0f5e
--- /dev/null
@@ -0,0 +1,437 @@
+/* packet-grpc.c
+* Routines for GRPC dissection
+* Copyright 2017, Huang Qiangxiong <qiangxiong.huang@qq.com>
+*
+* Wireshark - Network traffic analyzer
+* By Gerald Combs <gerald@wireshark.org>
+* Copyright 1998 Gerald Combs
+*
+* 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; either version 2 of the License, or
+* (at your option) any later version.
+*
+* 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.,
+* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+/*
+* The information used comes from:
+* https://grpc.io/docs/guides/wire.html
+*
+* This GRPC dissector must be invoked by HTTP2 dissector.
+*
+* The main task of grpc dissector includes:
+*
+* 1. Parse grpc message header first, if header shows message is compressed,
+*    it will find grpc-encoding http2 header by invoking http2_get_header_value()
+*    and uncompress the following message body according to the value of
+*    grpc-encoding header. After that grpc dissector call subdissector
+*    to dissect the (uncompressed) data of message body.
+*
+* 2. GRPC dissector will create and maintain a new dissector table named
+*    'grpc_message_type'. It allows dissection of a grpc message body.
+*    The pattern format used by this table has two levels:
+*
+*    1) Request/Response level pattern, which includes request
+*       grpc-method-path (equals to http2 ':path' header value) and
+*       direction (request or response), the format:
+*           http2-content-type "," http2-path "," direction
+*       direction = "request" / "response",    for example:
+*           "application/grpc,/helloworld.Greeter/SayHello,request"
+*       The "helloworld.Greeter" is  grpc_package "." grpc_service
+*
+*    2) Content-type level pattern, which just takes http2-content-type
+*       as pattern (for example, "application/grpc",
+*       "application/grpc+proto" and "application/grpc+json").
+*
+*    GRPC dissector will try to call request/response message level
+*    subdissector first. If not found, then try content-type level
+*    dissectors. grpc dissector will always transmit grpc message
+*    information - (http2-content-type "," http2-path "," direction ) to
+*    subdissector in (void *data) parameter of dissect handler.
+*    Content-type level subdissector can use this information to locate
+*    the request/response message type.
+*
+*
+* TODO
+*   Support tap.
+*   Support statistics.
+*/
+
+#include "config.h"
+
+#include <epan/packet.h>
+#include <epan/expert.h>
+#include <epan/prefs.h>
+#include <epan/proto_data.h>
+#include <epan/dissectors/packet-http2.h>
+
+#include "wsutil/pint.h"
+
+#define GRPC_MESSAGE_HEAD_LEN 5
+
+/* http2 standard headers */
+#define HTTP2_HEADER_PATH ":path"
+#define HTTP2_HEADER_CONTENT_TYPE "content-type"
+/* http2 for grpc */
+#define HTTP2_HEADER_GRPC_ENCODING "grpc-encoding"
+
+/*
+* Decompression of zlib encoded entities.
+*/
+#ifdef HAVE_ZLIB
+static gboolean grpc_decompress_body = TRUE;
+#else
+static gboolean grpc_decompress_body = FALSE;
+#endif
+
+/* detect json automaticlly */
+static gboolean grpc_detect_json_automatically = TRUE;
+
+void proto_register_grpc(void);
+void proto_reg_handoff_grpc(void);
+
+static int proto_grpc = -1;
+
+/* message header */
+static int hf_grpc_compressed_flag = -1;
+static int hf_grpc_message_length = -1;
+/* message body */
+static int hf_grpc_message_data = -1;
+
+/* compressed flag vals */
+#define grpc_compressed_flag_vals_VALUE_STRING_LIST(XXX)    \
+    XXX(GRPC_NOT_COMPRESSED, 0, "Not Compressed")  \
+    XXX(GRPC_COMPRESSED, 1, "Compressed")
+
+VALUE_STRING_ENUM(grpc_compressed_flag_vals);
+VALUE_STRING_ARRAY(grpc_compressed_flag_vals);
+
+/* expert */
+static expert_field ei_grpc_body_decompression_failed = EI_INIT;
+static expert_field ei_grpc_body_malformed = EI_INIT;
+
+/* trees */
+static int ett_grpc = -1;
+static int ett_grpc_message = -1;
+static int ett_grpc_encoded_entity = -1;
+
+static dissector_handle_t grpc_handle;
+
+/* GRPC message type dissector table list.
+* Dissectors can register themselves in this table as grpc message data dissectors.
+* Dissectors registered in this table may use pattern that
+* contains content-type,grpc-method-path(http2_path),request/reponse info, like:
+*     application/grpc,/helloworld.Greeter/SayHello,request
+* or just contains content-type:
+*     application/grpc
+*     application/grpc+proto
+*     application/grpc+json
+*/
+static dissector_table_t grpc_message_type_subdissector_table;
+
+/* Try to dissect grpc message according to grpc message info or http2 content_type. */
+static void
+dissect_body_data(proto_tree *grpc_tree, packet_info *pinfo, tvbuff_t *tvb, const gint offset,
+    gint length, gboolean continue_dissect,
+    const gchar* http2_path, gboolean is_request)
+{
+    const gchar *http2_content_type;
+    gchar *grpc_message_info;
+    tvbuff_t *next_tvb;
+    int dissected;
+    proto_tree *parent_tree;
+
+    proto_tree_add_bytes_format_value(grpc_tree, hf_grpc_message_data, tvb, offset, length, NULL, "%u bytes", length);
+
+    if (!continue_dissect) {
+        return; /* if uncompress failed, we don't continue dissecting. */
+    }
+
+    http2_content_type = http2_get_header_value(pinfo, HTTP2_HEADER_CONTENT_TYPE, FALSE);
+    if (http2_content_type == NULL || http2_path == NULL) {
+        return; /* not continue if there is not enough grpc information */
+    }
+
+    next_tvb = tvb_new_subset_length(tvb, offset, length);
+
+    /* Try to detect body as json first.
+    * Current grpc-java version sends json on grpc with content-type = application/grpc
+    * insteadof application/grpc+json, so we may detect to dissect message with default
+    * content-type application/grpc by json dissector insteadof protobuf dissector.
+    */
+    if (grpc_detect_json_automatically && length > 3
+        && tvb_get_guint8(next_tvb, 0) == '{')  /* start with '{' */
+    {
+        guint32 end_bytes = tvb_get_guint24(next_tvb, length - 3, ENC_BIG_ENDIAN);
+        if ((end_bytes & 0x0000FF) == '}'   /* end with '}' */
+            || (end_bytes & 0x00FF00) == '}' /* or "}\n" */
+            || (end_bytes & 0xFF0000) == '}') /* or "}\n\r" or " }\r\n" */
+        {
+            /* We just replace content-type with "application/grpc+json" insteadof calling
+            JSON dissector directly. Because someone may want to use his own dissector to
+            parse json insteadof default json dissector. */
+            http2_content_type = "application/grpc+json";
+        }
+    }
+
+    /* Since message data (like protobuf) may be not a self-describing protocol, we need
+    * provide grpc service-name, method-name and request or response type to subdissector.
+    * According to these information, subdissector may find correct message definition
+    * from IDL file like ".proto".
+    *
+    * We define a string format to carry these information. The benefit using string is
+    * the grpc message information might be used by the other Lua dissector in the future.
+    * The grpc message information format is:
+    *   http2_content_type "," http2_path "," ("request" / "response")
+    * Acording to grpc wire format guide, it will be:
+    *   "application/grpc" [("+proto" / "+json" / {custom})] "," "/" service-name "/" method-name "/" "," ("request" / "response")
+    * For example:
+    *   application/grpc,/helloworld.Greeter/SayHello,request
+    */
+    grpc_message_info = wmem_strconcat(wmem_packet_scope(), http2_content_type, ",",
+        http2_path, ",", (is_request ? "request" : "response"), NULL);
+
+    parent_tree = proto_tree_get_parent_tree(grpc_tree);
+
+    /* Protobuf dissector may be implemented that each request or response message
+    * of a method is defined as an individual dissector, so we try dissect using
+    * grpc_message_info first.
+    */
+    dissected = dissector_try_string(grpc_message_type_subdissector_table, grpc_message_info,
+        next_tvb, pinfo, parent_tree, grpc_message_info);
+
+    if (dissected == 0) {
+        /* not dissected yet, we try common subdissector again. */
+        dissector_try_string(grpc_message_type_subdissector_table, http2_content_type,
+            next_tvb, pinfo, parent_tree, grpc_message_info);
+    }
+}
+
+static gboolean
+can_uncompress_body(packet_info *pinfo, const gchar **compression_method)
+{
+    const gchar *grpc_encoding = http2_get_header_value(pinfo, HTTP2_HEADER_GRPC_ENCODING, FALSE);
+    *compression_method = grpc_encoding;
+
+    /* check http2 have a grpc-encoding header appropriate */
+    return grpc_decompress_body
+        && grpc_encoding != NULL
+        && (strcmp(grpc_encoding, "gzip") == 0 || strcmp(grpc_encoding, "deflate") == 0);
+}
+
+/* Dissect a grpc message. The caller needs to guarantee that the length is equal
+to 5 + message_length according to grpc wire format definition. */
+static guint
+dissect_grpc_message(tvbuff_t *tvb, guint offset, guint length, packet_info *pinfo, proto_tree *grpc_tree,
+                     const gchar* http2_path, gboolean is_request)
+{
+    guint32 compressed_flag, message_length;
+    const gchar *compression_method;
+
+    /* GRPC message format:
+    Delimited-Message -> Compressed-Flag Message-Length Message
+    Compressed-Flag -> 0 / 1 # encoded as 1 byte unsigned integer
+    Message-Length -> {length of Message} # encoded as 4 byte unsigned integer
+    Message -> *{binary octet} (may be protobuf or json)
+    */
+    proto_tree_add_item_ret_uint(grpc_tree, hf_grpc_compressed_flag, tvb, offset, 1, ENC_BIG_ENDIAN, &compressed_flag);
+    offset += 1;
+
+    proto_tree_add_item(grpc_tree, hf_grpc_message_length, tvb, offset, 4, ENC_BIG_ENDIAN);
+    message_length = length - 5;  /* should be equal to tvb_get_ntohl(tvb, offset) */
+    offset += 4;
+
+    /* uncompressed message data if compressed_flag is set */
+    if (compressed_flag & GRPC_COMPRESSED) {
+        if (can_uncompress_body(pinfo, &compression_method)) {
+            proto_item *compressed_proto_item = NULL;
+            tvbuff_t *uncompressed_tvb = tvb_child_uncompress(tvb, tvb, offset, message_length);
+
+            proto_tree *compressed_entity_tree = proto_tree_add_subtree_format(
+                grpc_tree, tvb, offset, message_length, ett_grpc_encoded_entity,
+                &compressed_proto_item, "Message-encoded entity body (%s): %u bytes",
+                compression_method == NULL ? "unknown" : compression_method, message_length
+            );
+
+            if (uncompressed_tvb != NULL) {
+                guint uncompressed_length = tvb_captured_length(uncompressed_tvb);
+                add_new_data_source(pinfo, uncompressed_tvb, "Uncompressed entity body");
+                proto_item_append_text(compressed_proto_item, " -> %u bytes", uncompressed_length);
+                dissect_body_data(grpc_tree, pinfo, uncompressed_tvb, 0, uncompressed_length, TRUE,
+                    http2_path, is_request);
+            } else {
+                proto_tree_add_expert(compressed_entity_tree, pinfo, &ei_grpc_body_decompression_failed,
+                    tvb, offset, message_length);
+                dissect_body_data(grpc_tree, pinfo, tvb, offset, message_length, FALSE, http2_path, is_request);
+            }
+        } else { /* compressed flag is set, but we can not uncompressed */
+            dissect_body_data(grpc_tree, pinfo, tvb, offset, message_length, FALSE, http2_path, is_request);
+        }
+    } else {
+        dissect_body_data(grpc_tree, pinfo, tvb, offset, message_length, TRUE, http2_path, is_request);
+    }
+
+    return length;
+}
+
+static int
+dissect_grpc(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_)
+{
+    proto_item *ti;
+    proto_tree *grpc_tree;
+    guint32 message_length;
+    guint offset = 0;
+    const gchar* http2_path;
+    gboolean is_request;
+    guint tvb_len = tvb_reported_length(tvb);
+
+    col_set_str(pinfo->cinfo, COL_PROTOCOL, "GRPC");
+    col_append_str(pinfo->cinfo, COL_INFO, " (GRPC)");
+
+    /* http2 had reassembled the http2.data.data, so we need not reassemble again.
+    reassembled http2.data.data may contain one or more grpc messages. */
+    while (offset < tvb_len)
+    {
+        ti = proto_tree_add_item(tree, proto_grpc, tvb, offset, -1, ENC_NA);
+        grpc_tree = proto_item_add_subtree(ti, ett_grpc_message);
+
+        if (tvb_len - offset < GRPC_MESSAGE_HEAD_LEN) {
+            /* need at least 5 bytes for dissecting a grpc message */
+
+            proto_tree_add_expert_format(grpc_tree, pinfo, &ei_grpc_body_malformed, tvb, offset, -1,
+                     "Malformed message data: only %u bytes left, need at least %u bytes.", tvb_len - offset, GRPC_MESSAGE_HEAD_LEN);
+            break;
+        }
+
+        message_length = tvb_get_ntohl(tvb, offset + 1);
+        if (tvb_len - offset < GRPC_MESSAGE_HEAD_LEN + message_length) {
+            /* remaining bytes are not enough for dissecting the message body */
+
+            proto_tree_add_expert_format(grpc_tree, pinfo, &ei_grpc_body_malformed, tvb, offset, -1,
+                     "Malformed message data: only %u bytes left, need at least %u bytes.", tvb_len - offset, GRPC_MESSAGE_HEAD_LEN);
+            break;
+        }
+        proto_item_set_len(ti, message_length + GRPC_MESSAGE_HEAD_LEN);
+
+        /* http2_path contains: "/" Service-Name "/" {method name} */
+        http2_path = http2_get_header_value(pinfo, HTTP2_HEADER_PATH, FALSE);
+        is_request = (http2_path != NULL);
+
+        if (http2_path == NULL) { /* this reponse, so we get it from http2 request streaam */
+            http2_path = http2_get_header_value(pinfo, HTTP2_HEADER_PATH, TRUE);
+        }
+
+        if (http2_path) {
+            proto_item_append_text(ti, ": %s, %s", http2_path, (is_request ? "Request" : "Response"));
+        } else {
+            proto_item_append_text(ti, ": %s", (is_request ? "Request" : "Response"));
+        }
+
+        offset = dissect_grpc_message(tvb, offset, GRPC_MESSAGE_HEAD_LEN + message_length, pinfo, grpc_tree, http2_path, is_request);
+    }
+
+    return tvb_captured_length(tvb);
+}
+
+void
+proto_register_grpc(void)
+{
+
+    static hf_register_info hf[] = {
+        { &hf_grpc_compressed_flag,
+        { "Compressed Flag", "grpc.compressed_flag",
+        FT_UINT8, BASE_DEC, VALS(grpc_compressed_flag_vals), 0x0,
+        "Compressed-Flag value of 1 indicates that the binary octet sequence of Message is compressed", HFILL }
+        },
+        { &hf_grpc_message_length,
+        { "Message Length", "grpc.message_length",
+        FT_UINT32, BASE_DEC, NULL, 0x0,
+        "The length (32 bits) of message payload (not include itself)", HFILL }
+        },
+        { &hf_grpc_message_data,
+        { "Message Data", "grpc.message_data",
+        FT_BYTES, BASE_NONE, NULL, 0x0,
+        NULL, HFILL }
+        }
+    };
+
+    static gint *ett[] = {
+        &ett_grpc,
+        &ett_grpc_message,
+        &ett_grpc_encoded_entity
+    };
+
+    /* Setup protocol expert items */
+    static ei_register_info ei[] = {
+        { &ei_grpc_body_decompression_failed,
+        { "grpc.body_decompression_failed", PI_UNDECODED, PI_WARN,
+        "Body decompression failed", EXPFILL }
+        },
+        { &ei_grpc_body_malformed,
+        { "grpc.body_malformed", PI_UNDECODED, PI_WARN,
+        "Malformed message data", EXPFILL }
+        }
+    };
+
+    module_t *grpc_module;
+    expert_module_t *expert_grpc;
+
+    proto_grpc = proto_register_protocol("GRPC Message", "GRPC", "grpc");
+
+    proto_register_field_array(proto_grpc, hf, array_length(hf));
+    proto_register_subtree_array(ett, array_length(ett));
+
+    grpc_module = prefs_register_protocol(proto_grpc, NULL);
+
+    prefs_register_bool_preference(grpc_module, "detect_json_automaticlly",
+        "Always check whether the message is JSON regardless of content-type.",
+        "Normally application/grpc message is protobuf, "
+        "but sometime the true message is json. "
+        "If this option in on, we always check whether the message is JSON "
+        "(body starts with '{' and ends with '}') regardless of "
+        "grpc_message_type_subdissector_table settings (which dissect grpc "
+        "message according to content-type).",
+        &grpc_detect_json_automatically);
+
+    expert_grpc = expert_register_protocol(proto_grpc);
+    expert_register_field_array(expert_grpc, ei, array_length(ei));
+
+    grpc_handle = register_dissector("grpc", dissect_grpc, proto_grpc);
+
+    /*
+    * Dissectors can register themselves in this table as grpc message
+    * subdissector. Default it support json, protobuf.
+    */
+    grpc_message_type_subdissector_table =
+        register_dissector_table("grpc_message_type",
+            "GRPC message type", proto_grpc, FT_STRING, BASE_NONE);
+}
+
+void
+proto_reg_handoff_grpc(void)
+{
+    dissector_add_string("media_type", "application/grpc", grpc_handle);
+    dissector_add_string("media_type", "application/grpc+proto", grpc_handle);
+    dissector_add_string("media_type", "application/grpc+json", grpc_handle);
+}
+
+/*
+* Editor modelines  -  http://www.wireshark.org/tools/modelines.html
+*
+* Local variables:
+* c-basic-offset: 4
+* tab-width: 8
+* indent-tabs-mode: nil
+* End:
+*
+* vi: set shiftwidth=4 tabstop=8 expandtab:
+* :indentSize=4:tabSize=8:noTabs=true:
+*/
index c172a64a4270bfbacf6b73074d4ad1b1068b5a36..538d845563c05bb2ed7ef2b3dc26983902e1eb70 100644 (file)
@@ -663,6 +663,7 @@ proto_reg_handoff_json(void)
        dissector_add_string("media_type", "application/json-rpc", json_handle); /* JSON-RPC over HTTP */
        dissector_add_string("media_type", "application/jsonrequest", json_handle); /* JSON-RPC over HTTP */
        dissector_add_string("media_type", "application/dds-web+json", json_handle); /* DDS Web Integration Service over HTTP */
+       dissector_add_string("grpc_message_type", "application/grpc+json", json_handle);
 
        text_lines_handle = find_dissector_add_dependency("data-text-lines", proto_json);
 }