/* packet-grpc.c * Routines for GRPC dissection * Copyright 2017, Huang Qiangxiong * * Wireshark - Network traffic analyzer * By Gerald Combs * 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 #include #include #include #include #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: */