forked from osmocom/wireshark
GRPC: Add support for gRPC-Web
Supporting both application/grpc-web and application/grpc-web-text. Add test case for grpc-web(-text). close #17939
This commit is contained in:
parent
90ddcc44ed
commit
c3dea0b98e
|
@ -130,6 +130,7 @@ Secure File Transfer Protocol (sftp)
|
||||||
Secure Host IP Configuration Protocol (SHICP)
|
Secure Host IP Configuration Protocol (SHICP)
|
||||||
USB Attached SCSI (UASP)
|
USB Attached SCSI (UASP)
|
||||||
ZBOSS NCP
|
ZBOSS NCP
|
||||||
|
gRPC Web (gRPC-Web)
|
||||||
--
|
--
|
||||||
|
|
||||||
=== Updated Protocol Support
|
=== Updated Protocol Support
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* packet-grpc.c
|
/* packet-grpc.c
|
||||||
* Routines for GRPC dissection
|
* Routines for GRPC dissection
|
||||||
* Copyright 2017, Huang Qiangxiong <qiangxiong.huang@qq.com>
|
* Copyright 2017,2022 Huang Qiangxiong <qiangxiong.huang@qq.com>
|
||||||
*
|
*
|
||||||
* Wireshark - Network traffic analyzer
|
* Wireshark - Network traffic analyzer
|
||||||
* By Gerald Combs <gerald@wireshark.org>
|
* By Gerald Combs <gerald@wireshark.org>
|
||||||
|
@ -12,10 +12,12 @@
|
||||||
/*
|
/*
|
||||||
* The information used comes from:
|
* The information used comes from:
|
||||||
* https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
|
* https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
|
||||||
|
* https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
|
||||||
*
|
*
|
||||||
* This GRPC dissector must be invoked by HTTP2 dissector.
|
* This GRPC dissector must be invoked by HTTP2 or HTTP dissector.
|
||||||
|
* The native GRPC is always over HTTP2, the GRPC-Web is over either HTTP2 or HTTP.
|
||||||
*
|
*
|
||||||
* The main task of grpc dissector includes:
|
* The main task of GRPC dissector for native GRPC includes:
|
||||||
*
|
*
|
||||||
* 1. Parse grpc message header first, if header shows message is compressed,
|
* 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()
|
* it will find grpc-encoding http2 header by invoking http2_get_header_value()
|
||||||
|
@ -47,14 +49,15 @@
|
||||||
* Content-type level subdissector can use this information to locate
|
* Content-type level subdissector can use this information to locate
|
||||||
* the request/response message type.
|
* the request/response message type.
|
||||||
*
|
*
|
||||||
*
|
* For GRPC-WEB, the ways to get information like content-type, path (request uri)
|
||||||
* TODO
|
* are different. And for GRPC-WEB-TEXT, the dissector will first decode the base64
|
||||||
* Support tap.
|
* payload and then dissect the data as GRPC-WEB.
|
||||||
* Support statistics.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <epan/conversation.h>
|
||||||
|
#include <epan/proto_data.h>
|
||||||
#include <epan/packet.h>
|
#include <epan/packet.h>
|
||||||
#include <epan/expert.h>
|
#include <epan/expert.h>
|
||||||
#include <epan/prefs.h>
|
#include <epan/prefs.h>
|
||||||
|
@ -62,6 +65,7 @@
|
||||||
#include <epan/proto_data.h>
|
#include <epan/proto_data.h>
|
||||||
#include <epan/dissectors/packet-http2.h>
|
#include <epan/dissectors/packet-http2.h>
|
||||||
|
|
||||||
|
#include "packet-http.h"
|
||||||
#include "wsutil/pint.h"
|
#include "wsutil/pint.h"
|
||||||
|
|
||||||
#define GRPC_MESSAGE_HEAD_LEN 5
|
#define GRPC_MESSAGE_HEAD_LEN 5
|
||||||
|
@ -72,6 +76,9 @@
|
||||||
/* http2 for grpc */
|
/* http2 for grpc */
|
||||||
#define HTTP2_HEADER_GRPC_ENCODING "grpc-encoding"
|
#define HTTP2_HEADER_GRPC_ENCODING "grpc-encoding"
|
||||||
|
|
||||||
|
/* calculate the size of a bytes after decoding as base64 */
|
||||||
|
#define BASE64_ENCODE_SIZE(len) ((len) / 3 * 4 + ((len) % 3 == 0 ? 0 : 4))
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Decompression of zlib encoded entities.
|
* Decompression of zlib encoded entities.
|
||||||
*/
|
*/
|
||||||
|
@ -83,20 +90,39 @@ static gboolean grpc_decompress_body = FALSE;
|
||||||
|
|
||||||
/* detect json automatically */
|
/* detect json automatically */
|
||||||
static gboolean grpc_detect_json_automatically = TRUE;
|
static gboolean grpc_detect_json_automatically = TRUE;
|
||||||
/* whether embed GRPC messages under HTTP2 protocol tree items */
|
/* whether embed GRPC messages under HTTP2 (or other) protocol tree items */
|
||||||
static gboolean grpc_embedded_under_http2 = FALSE;
|
static gboolean grpc_embedded_under_http2 = FALSE;
|
||||||
|
|
||||||
void proto_register_grpc(void);
|
void proto_register_grpc(void);
|
||||||
void proto_reg_handoff_grpc(void);
|
void proto_reg_handoff_grpc(void);
|
||||||
|
|
||||||
static int proto_grpc = -1;
|
static int proto_grpc = -1;
|
||||||
|
static int proto_http = -1;
|
||||||
|
|
||||||
/* message header */
|
/* message header */
|
||||||
|
static int hf_grpc_frame_type = -1;
|
||||||
static int hf_grpc_compressed_flag = -1;
|
static int hf_grpc_compressed_flag = -1;
|
||||||
static int hf_grpc_message_length = -1;
|
static int hf_grpc_message_length = -1;
|
||||||
/* message body */
|
/* message body */
|
||||||
static int hf_grpc_message_data = -1;
|
static int hf_grpc_message_data = -1;
|
||||||
|
|
||||||
|
/* grpc protocol type */
|
||||||
|
#define grpc_protocol_type_vals_VALUE_STRING_LIST(XXX) \
|
||||||
|
XXX(GRPC_PTYPE_GRPC, 0, "GRPC") \
|
||||||
|
XXX(GRPC_PTYPE_GRPC_WEB, 1, "GRPC-Web") \
|
||||||
|
XXX(GRPC_PTYPE_GRPC_WEB_TEXT, 2, "GRPC-Web-Text")
|
||||||
|
|
||||||
|
typedef VALUE_STRING_ENUM(grpc_protocol_type_vals) grpc_protocol_type_t;
|
||||||
|
VALUE_STRING_ARRAY(grpc_protocol_type_vals);
|
||||||
|
|
||||||
|
/* grpc frame type (grpc-web extension) */
|
||||||
|
#define grpc_frame_type_vals_VALUE_STRING_LIST(XXX) \
|
||||||
|
XXX(GRPC_FRAME_TYPE_DATA, 0, "Data") \
|
||||||
|
XXX(GRPC_FRAME_TYPE_TRAILER, 1, "Trailer")
|
||||||
|
|
||||||
|
VALUE_STRING_ENUM(grpc_frame_type_vals);
|
||||||
|
VALUE_STRING_ARRAY(grpc_frame_type_vals);
|
||||||
|
|
||||||
/* compressed flag vals */
|
/* compressed flag vals */
|
||||||
#define grpc_compressed_flag_vals_VALUE_STRING_LIST(XXX) \
|
#define grpc_compressed_flag_vals_VALUE_STRING_LIST(XXX) \
|
||||||
XXX(GRPC_NOT_COMPRESSED, 0, "Not Compressed") \
|
XXX(GRPC_NOT_COMPRESSED, 0, "Not Compressed") \
|
||||||
|
@ -115,6 +141,15 @@ static int ett_grpc_message = -1;
|
||||||
static int ett_grpc_encoded_entity = -1;
|
static int ett_grpc_encoded_entity = -1;
|
||||||
|
|
||||||
static dissector_handle_t grpc_handle;
|
static dissector_handle_t grpc_handle;
|
||||||
|
static dissector_handle_t data_text_lines_handle;
|
||||||
|
|
||||||
|
/* the information used during dissecting a grpc message */
|
||||||
|
typedef struct {
|
||||||
|
gboolean is_request; /* is request or response message */
|
||||||
|
const gchar* path; /* is http2 ":path" or http request_uri, format: "/" Service-Name "/" {method name} */
|
||||||
|
const gchar* content_type; /* is http2 or http content-type, like: application/grpc */
|
||||||
|
const gchar* encoding; /* is grpc-encoding header containing compressed method, for example "gzip" */
|
||||||
|
} grpc_context_info_t;
|
||||||
|
|
||||||
/* GRPC message type dissector table list.
|
/* GRPC message type dissector table list.
|
||||||
* Dissectors can register themselves in this table as grpc message data dissectors.
|
* Dissectors can register themselves in this table as grpc message data dissectors.
|
||||||
|
@ -128,13 +163,25 @@ static dissector_handle_t grpc_handle;
|
||||||
*/
|
*/
|
||||||
static dissector_table_t grpc_message_type_subdissector_table;
|
static dissector_table_t grpc_message_type_subdissector_table;
|
||||||
|
|
||||||
|
static grpc_protocol_type_t
|
||||||
|
get_grpc_protocol_type(const gchar* content_type) {
|
||||||
|
if (content_type != NULL) {
|
||||||
|
if (g_str_has_prefix(content_type, "application/grpc-web-text")) {
|
||||||
|
return GRPC_PTYPE_GRPC_WEB_TEXT;
|
||||||
|
} else if (g_str_has_prefix(content_type, "application/grpc-web")) {
|
||||||
|
return GRPC_PTYPE_GRPC_WEB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GRPC_PTYPE_GRPC;
|
||||||
|
}
|
||||||
|
|
||||||
/* Try to dissect grpc message according to grpc message info or http2 content_type. */
|
/* Try to dissect grpc message according to grpc message info or http2 content_type. */
|
||||||
static void
|
static void
|
||||||
dissect_body_data(proto_tree *grpc_tree, packet_info *pinfo, tvbuff_t *tvb, const gint offset,
|
dissect_body_data(proto_tree *grpc_tree, packet_info *pinfo, tvbuff_t *tvb, const gint offset,
|
||||||
gint length, gboolean continue_dissect,
|
gint length, gboolean continue_dissect,
|
||||||
const gchar* http2_path, gboolean is_request)
|
guint32 frame_type, grpc_context_info_t *grpc_ctx)
|
||||||
{
|
{
|
||||||
const gchar *http2_content_type;
|
const gchar *http2_content_type = grpc_ctx->content_type;
|
||||||
gchar *grpc_message_info;
|
gchar *grpc_message_info;
|
||||||
tvbuff_t *next_tvb;
|
tvbuff_t *next_tvb;
|
||||||
int dissected;
|
int dissected;
|
||||||
|
@ -142,12 +189,16 @@ dissect_body_data(proto_tree *grpc_tree, packet_info *pinfo, tvbuff_t *tvb, cons
|
||||||
|
|
||||||
proto_tree_add_bytes_format_value(grpc_tree, hf_grpc_message_data, tvb, offset, length, NULL, "%u bytes", length);
|
proto_tree_add_bytes_format_value(grpc_tree, hf_grpc_message_data, tvb, offset, length, NULL, "%u bytes", length);
|
||||||
|
|
||||||
|
if (frame_type == GRPC_FRAME_TYPE_TRAILER) {
|
||||||
|
call_dissector(data_text_lines_handle, tvb_new_subset_length(tvb, offset, length), pinfo, grpc_tree);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!continue_dissect) {
|
if (!continue_dissect) {
|
||||||
return; /* if uncompress failed, we don't continue dissecting. */
|
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 || grpc_ctx->path == NULL) {
|
||||||
if (http2_content_type == NULL || http2_path == NULL) {
|
|
||||||
return; /* not continue if there is not enough grpc information */
|
return; /* not continue if there is not enough grpc information */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +240,7 @@ dissect_body_data(proto_tree *grpc_tree, packet_info *pinfo, tvbuff_t *tvb, cons
|
||||||
* application/grpc,/helloworld.Greeter/SayHello,request
|
* application/grpc,/helloworld.Greeter/SayHello,request
|
||||||
*/
|
*/
|
||||||
grpc_message_info = wmem_strconcat(pinfo->pool, http2_content_type, ",",
|
grpc_message_info = wmem_strconcat(pinfo->pool, http2_content_type, ",",
|
||||||
http2_path, ",", (is_request ? "request" : "response"), NULL);
|
grpc_ctx->path, ",", (grpc_ctx->is_request ? "request" : "response"), NULL);
|
||||||
|
|
||||||
parent_tree = proto_tree_get_parent_tree(grpc_tree);
|
parent_tree = proto_tree_get_parent_tree(grpc_tree);
|
||||||
|
|
||||||
|
@ -208,11 +259,8 @@ dissect_body_data(proto_tree *grpc_tree, packet_info *pinfo, tvbuff_t *tvb, cons
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
can_uncompress_body(packet_info *pinfo, const gchar **compression_method)
|
can_uncompress_body(const gchar *grpc_encoding)
|
||||||
{
|
{
|
||||||
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 */
|
/* check http2 have a grpc-encoding header appropriate */
|
||||||
return grpc_decompress_body
|
return grpc_decompress_body
|
||||||
&& grpc_encoding != NULL
|
&& grpc_encoding != NULL
|
||||||
|
@ -223,20 +271,27 @@ can_uncompress_body(packet_info *pinfo, const gchar **compression_method)
|
||||||
to 5 + message_length according to grpc wire format definition. */
|
to 5 + message_length according to grpc wire format definition. */
|
||||||
static guint
|
static guint
|
||||||
dissect_grpc_message(tvbuff_t *tvb, guint offset, guint length, packet_info *pinfo, proto_tree *grpc_tree,
|
dissect_grpc_message(tvbuff_t *tvb, guint offset, guint length, packet_info *pinfo, proto_tree *grpc_tree,
|
||||||
const gchar* http2_path, gboolean is_request)
|
grpc_context_info_t* grpc_ctx)
|
||||||
{
|
{
|
||||||
guint32 compressed_flag, message_length;
|
guint32 frame_type, compressed_flag, message_length;
|
||||||
const gchar *compression_method;
|
const gchar *compression_method = grpc_ctx->encoding;
|
||||||
|
|
||||||
/* GRPC message format:
|
/* GRPC message format:
|
||||||
Delimited-Message -> Compressed-Flag Message-Length Message
|
Delimited-Message -> Compressed-Flag Message-Length Message
|
||||||
Compressed-Flag -> 0 / 1 # encoded as 1 byte unsigned integer
|
Compressed-Flag -> 0 / 1 # encoded as 1 byte unsigned integer
|
||||||
Message-Length -> {length of Message} # encoded as 4 byte unsigned integer
|
Message-Length -> {length of Message} # encoded as 4 byte unsigned integer
|
||||||
Message -> *{binary octet} (may be protobuf or json)
|
Message -> *{binary octet} (may be protobuf or json)
|
||||||
|
|
||||||
|
Note: GRPC-WEB extend the MSB of Compressed-Flag as frame type (0-data, 1-trailer)
|
||||||
*/
|
*/
|
||||||
|
proto_tree_add_item_ret_uint(grpc_tree, hf_grpc_frame_type, tvb, offset, 1, ENC_BIG_ENDIAN, &frame_type);
|
||||||
proto_tree_add_item_ret_uint(grpc_tree, hf_grpc_compressed_flag, tvb, offset, 1, ENC_BIG_ENDIAN, &compressed_flag);
|
proto_tree_add_item_ret_uint(grpc_tree, hf_grpc_compressed_flag, tvb, offset, 1, ENC_BIG_ENDIAN, &compressed_flag);
|
||||||
offset += 1;
|
offset += 1;
|
||||||
|
|
||||||
|
if (frame_type == GRPC_FRAME_TYPE_TRAILER) {
|
||||||
|
proto_item_append_text(proto_tree_get_parent(grpc_tree), " (Trailer)");
|
||||||
|
}
|
||||||
|
|
||||||
proto_tree_add_item(grpc_tree, hf_grpc_message_length, tvb, offset, 4, ENC_BIG_ENDIAN);
|
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) */
|
message_length = length - 5; /* should be equal to tvb_get_ntohl(tvb, offset) */
|
||||||
offset += 4;
|
offset += 4;
|
||||||
|
@ -247,7 +302,7 @@ dissect_grpc_message(tvbuff_t *tvb, guint offset, guint length, packet_info *pin
|
||||||
|
|
||||||
/* uncompressed message data if compressed_flag is set */
|
/* uncompressed message data if compressed_flag is set */
|
||||||
if (compressed_flag & GRPC_COMPRESSED) {
|
if (compressed_flag & GRPC_COMPRESSED) {
|
||||||
if (can_uncompress_body(pinfo, &compression_method)) {
|
if (can_uncompress_body(compression_method)) {
|
||||||
proto_item *compressed_proto_item = NULL;
|
proto_item *compressed_proto_item = NULL;
|
||||||
tvbuff_t *uncompressed_tvb = tvb_child_uncompress(tvb, tvb, offset, message_length);
|
tvbuff_t *uncompressed_tvb = tvb_child_uncompress(tvb, tvb, offset, message_length);
|
||||||
|
|
||||||
|
@ -261,33 +316,37 @@ dissect_grpc_message(tvbuff_t *tvb, guint offset, guint length, packet_info *pin
|
||||||
guint uncompressed_length = tvb_captured_length(uncompressed_tvb);
|
guint uncompressed_length = tvb_captured_length(uncompressed_tvb);
|
||||||
add_new_data_source(pinfo, uncompressed_tvb, "Uncompressed entity body");
|
add_new_data_source(pinfo, uncompressed_tvb, "Uncompressed entity body");
|
||||||
proto_item_append_text(compressed_proto_item, " -> %u bytes", uncompressed_length);
|
proto_item_append_text(compressed_proto_item, " -> %u bytes", uncompressed_length);
|
||||||
dissect_body_data(grpc_tree, pinfo, uncompressed_tvb, 0, uncompressed_length, TRUE,
|
dissect_body_data(grpc_tree, pinfo, uncompressed_tvb, 0, uncompressed_length, TRUE, frame_type, grpc_ctx);
|
||||||
http2_path, is_request);
|
|
||||||
} else {
|
} else {
|
||||||
proto_tree_add_expert(compressed_entity_tree, pinfo, &ei_grpc_body_decompression_failed,
|
proto_tree_add_expert(compressed_entity_tree, pinfo, &ei_grpc_body_decompression_failed,
|
||||||
tvb, offset, message_length);
|
tvb, offset, message_length);
|
||||||
dissect_body_data(grpc_tree, pinfo, tvb, offset, message_length, FALSE, http2_path, is_request);
|
dissect_body_data(grpc_tree, pinfo, tvb, offset, message_length, FALSE, frame_type, grpc_ctx);
|
||||||
}
|
}
|
||||||
} else { /* compressed flag is set, but we can not uncompressed */
|
} 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);
|
dissect_body_data(grpc_tree, pinfo, tvb, offset, message_length, FALSE, frame_type, grpc_ctx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dissect_body_data(grpc_tree, pinfo, tvb, offset, message_length, TRUE, http2_path, is_request);
|
dissect_body_data(grpc_tree, pinfo, tvb, offset, message_length, TRUE, frame_type, grpc_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
return offset + message_length;
|
return offset + message_length;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
dissect_grpc(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_)
|
dissect_grpc_common(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, grpc_context_info_t *grpc_ctx)
|
||||||
{
|
{
|
||||||
proto_item *ti;
|
proto_item *ti;
|
||||||
proto_tree *grpc_tree;
|
proto_tree *grpc_tree;
|
||||||
guint32 message_length;
|
guint32 message_length;
|
||||||
guint offset = 0;
|
guint offset = 0;
|
||||||
const gchar* http2_path;
|
|
||||||
gboolean is_request;
|
|
||||||
guint tvb_len = tvb_reported_length(tvb);
|
guint tvb_len = tvb_reported_length(tvb);
|
||||||
|
grpc_protocol_type_t proto_type;
|
||||||
|
const gchar* proto_name;
|
||||||
|
|
||||||
|
DISSECTOR_ASSERT_HINT(grpc_ctx && grpc_ctx->content_type && grpc_ctx->path, "The content_type and path of grpc context must be set.");
|
||||||
|
|
||||||
|
proto_type = get_grpc_protocol_type(grpc_ctx->content_type);
|
||||||
|
proto_name = val_to_str_const(proto_type, grpc_protocol_type_vals, "GRPC");
|
||||||
|
|
||||||
if (!grpc_embedded_under_http2 && proto_tree_get_parent_tree(tree)) {
|
if (!grpc_embedded_under_http2 && proto_tree_get_parent_tree(tree)) {
|
||||||
tree = proto_tree_get_parent_tree(tree);
|
tree = proto_tree_get_parent_tree(tree);
|
||||||
|
@ -323,39 +382,88 @@ dissect_grpc(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_
|
||||||
}
|
}
|
||||||
/* ready to add information into protocol columns and tree */
|
/* ready to add information into protocol columns and tree */
|
||||||
if (offset == 0) { /* change columns only when there is at least one grpc message will be parsed */
|
if (offset == 0) { /* change columns only when there is at least one grpc message will be parsed */
|
||||||
col_set_str(pinfo->cinfo, COL_PROTOCOL, "GRPC");
|
col_set_str(pinfo->cinfo, COL_PROTOCOL, proto_name);
|
||||||
col_append_str(pinfo->cinfo, COL_INFO, " (GRPC)");
|
col_append_fstr(pinfo->cinfo, COL_INFO, " (%s)", proto_name);
|
||||||
col_set_fence(pinfo->cinfo, COL_PROTOCOL);
|
col_set_fence(pinfo->cinfo, COL_PROTOCOL);
|
||||||
}
|
}
|
||||||
ti = proto_tree_add_item(tree, proto_grpc, tvb, offset, message_length + GRPC_MESSAGE_HEAD_LEN, ENC_NA);
|
ti = proto_tree_add_item(tree, proto_grpc, tvb, offset, message_length + GRPC_MESSAGE_HEAD_LEN, ENC_NA);
|
||||||
grpc_tree = proto_item_add_subtree(ti, ett_grpc_message);
|
grpc_tree = proto_item_add_subtree(ti, ett_grpc_message);
|
||||||
|
proto_item_set_text(ti, "%s Message", proto_name);
|
||||||
|
|
||||||
/* http2_path contains: "/" Service-Name "/" {method name} */
|
if (grpc_ctx->path) {
|
||||||
http2_path = http2_get_header_value(pinfo, HTTP2_HEADER_PATH, FALSE);
|
proto_item_append_text(ti, ": %s, %s", grpc_ctx->path, (grpc_ctx->is_request ? "Request" : "Response"));
|
||||||
is_request = (http2_path != NULL);
|
|
||||||
|
|
||||||
if (http2_path == NULL) { /* this response, so we get it from http2 request stream */
|
|
||||||
http2_path = http2_get_header_value(pinfo, HTTP2_HEADER_PATH, TRUE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (http2_path) {
|
offset = dissect_grpc_message(tvb, offset, GRPC_MESSAGE_HEAD_LEN + message_length, pinfo, grpc_tree, grpc_ctx);
|
||||||
proto_item_append_text(ti, ": %s, %s", http2_path, (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);
|
return tvb_captured_length(tvb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
dissect_grpc(tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, void* data)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
http_conv_t* http_conv;
|
||||||
|
tvbuff_t* real_data_tvb;
|
||||||
|
grpc_context_info_t grpc_ctx = { 0 };
|
||||||
|
conversation_t* conv = find_or_create_conversation(pinfo);
|
||||||
|
http_message_info_t* http_msg_info = (http_message_info_t*)data;
|
||||||
|
gboolean is_grpc_web_text = g_str_has_prefix(pinfo->match_string, "application/grpc-web-text");
|
||||||
|
|
||||||
|
if (is_grpc_web_text) {
|
||||||
|
real_data_tvb = base64_tvb_to_new_tvb(tvb, 0, tvb_reported_length(tvb));
|
||||||
|
add_new_data_source(pinfo, real_data_tvb, "Decoded base64 body");
|
||||||
|
} else {
|
||||||
|
real_data_tvb = tvb;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proto_is_frame_protocol(pinfo->layers, "http2")) {
|
||||||
|
grpc_ctx.path = http2_get_header_value(pinfo, HTTP2_HEADER_PATH, FALSE);
|
||||||
|
grpc_ctx.is_request = (grpc_ctx.path != NULL);
|
||||||
|
if (grpc_ctx.path == NULL) {
|
||||||
|
/* this must be response, so we get it from http2 request stream */
|
||||||
|
grpc_ctx.path = http2_get_header_value(pinfo, HTTP2_HEADER_PATH, TRUE);
|
||||||
|
}
|
||||||
|
grpc_ctx.content_type = http2_get_header_value(pinfo, HTTP2_HEADER_CONTENT_TYPE, FALSE);
|
||||||
|
grpc_ctx.encoding = http2_get_header_value(pinfo, HTTP2_HEADER_GRPC_ENCODING, FALSE);
|
||||||
|
}
|
||||||
|
else if (proto_is_frame_protocol(pinfo->layers, "http")) {
|
||||||
|
http_conv = (http_conv_t*)conversation_get_proto_data(conv, proto_http);
|
||||||
|
DISSECTOR_ASSERT_HINT(http_conv && http_msg_info, "Unexpected error: HTTP conversation or HTTP message info not available.");
|
||||||
|
grpc_ctx.is_request = (http_msg_info->type == HTTP_REQUEST);
|
||||||
|
grpc_ctx.path = http_conv->request_uri;
|
||||||
|
grpc_ctx.content_type = pinfo->match_string; /* only for grpc-web(-text) over http1.1 */
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* unexpected protocol error */
|
||||||
|
DISSECTOR_ASSERT_NOT_REACHED();
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = dissect_grpc_common(real_data_tvb, pinfo, tree, &grpc_ctx);
|
||||||
|
|
||||||
|
if (is_grpc_web_text) {
|
||||||
|
/* convert reassembly the lengths of offset and remaining bytes back to the base64 lengths */
|
||||||
|
pinfo->desegment_offset = BASE64_ENCODE_SIZE(pinfo->desegment_offset);
|
||||||
|
pinfo->desegment_len = BASE64_ENCODE_SIZE(pinfo->desegment_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
proto_register_grpc(void)
|
proto_register_grpc(void)
|
||||||
{
|
{
|
||||||
|
|
||||||
static hf_register_info hf[] = {
|
static hf_register_info hf[] = {
|
||||||
|
{ &hf_grpc_frame_type,
|
||||||
|
{ "Frame Type", "grpc.frame_type",
|
||||||
|
FT_UINT8, BASE_DEC, VALS(grpc_frame_type_vals), 0x80,
|
||||||
|
"The frame type of this grpc message (GRPC-WEB extension)", HFILL }
|
||||||
|
},
|
||||||
{ &hf_grpc_compressed_flag,
|
{ &hf_grpc_compressed_flag,
|
||||||
{ "Compressed Flag", "grpc.compressed_flag",
|
{ "Compressed Flag", "grpc.compressed_flag",
|
||||||
FT_UINT8, BASE_DEC, VALS(grpc_compressed_flag_vals), 0x0,
|
FT_UINT8, BASE_DEC, VALS(grpc_compressed_flag_vals), 0x01,
|
||||||
"Compressed-Flag value of 1 indicates that the binary octet sequence of Message is compressed", HFILL }
|
"Compressed-Flag value of 1 indicates that the binary octet sequence of Message is compressed", HFILL }
|
||||||
},
|
},
|
||||||
{ &hf_grpc_message_length,
|
{ &hf_grpc_message_length,
|
||||||
|
@ -409,8 +517,8 @@ proto_register_grpc(void)
|
||||||
&grpc_detect_json_automatically);
|
&grpc_detect_json_automatically);
|
||||||
|
|
||||||
prefs_register_bool_preference(grpc_module, "embedded_under_http2",
|
prefs_register_bool_preference(grpc_module, "embedded_under_http2",
|
||||||
"Embed gRPC messages under HTTP2 protocol tree items.",
|
"Embed gRPC messages under HTTP2 (or other) protocol tree items.",
|
||||||
"Embed gRPC messages under HTTP2 protocol tree items.",
|
"Embed gRPC messages under HTTP2 (or other) protocol tree items.",
|
||||||
&grpc_embedded_under_http2);
|
&grpc_embedded_under_http2);
|
||||||
|
|
||||||
prefs_register_static_text_preference(grpc_module, "service_definition",
|
prefs_register_static_text_preference(grpc_module, "service_definition",
|
||||||
|
@ -438,15 +546,22 @@ proto_reg_handoff_grpc(void)
|
||||||
"application/grpc",
|
"application/grpc",
|
||||||
"application/grpc+proto",
|
"application/grpc+proto",
|
||||||
"application/grpc+json",
|
"application/grpc+json",
|
||||||
|
"application/grpc-web",
|
||||||
|
"application/grpc-web+proto",
|
||||||
|
"application/grpc-web-text",
|
||||||
|
"application/grpc-web-text+proto",
|
||||||
NULL /* end flag */
|
NULL /* end flag */
|
||||||
};
|
};
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
/* register/deregister grpc_handle to/from tables */
|
/* register native grpc handler */
|
||||||
for (i = 0; content_types[i]; i++) {
|
for (i = 0; content_types[i]; i++) {
|
||||||
dissector_add_string("streaming_content_type", content_types[i], grpc_handle);
|
dissector_add_string("streaming_content_type", content_types[i], grpc_handle);
|
||||||
dissector_add_string("media_type", content_types[i], grpc_handle);
|
dissector_add_string("media_type", content_types[i], grpc_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proto_http = proto_get_id_by_filter_name("http");
|
||||||
|
data_text_lines_handle = find_dissector_add_dependency("data-text-lines", proto_grpc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -2085,6 +2085,10 @@ proto_reg_handoff_protobuf(void)
|
||||||
old_dissect_bytes_as_string = dissect_bytes_as_string;
|
old_dissect_bytes_as_string = dissect_bytes_as_string;
|
||||||
dissector_add_string("grpc_message_type", "application/grpc", protobuf_handle);
|
dissector_add_string("grpc_message_type", "application/grpc", protobuf_handle);
|
||||||
dissector_add_string("grpc_message_type", "application/grpc+proto", protobuf_handle);
|
dissector_add_string("grpc_message_type", "application/grpc+proto", protobuf_handle);
|
||||||
|
dissector_add_string("grpc_message_type", "application/grpc-web", protobuf_handle);
|
||||||
|
dissector_add_string("grpc_message_type", "application/grpc-web+proto", protobuf_handle);
|
||||||
|
dissector_add_string("grpc_message_type", "application/grpc-web-text", protobuf_handle);
|
||||||
|
dissector_add_string("grpc_message_type", "application/grpc-web-text+proto", protobuf_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,22 @@
|
||||||
|
// This file is from https://github.com/grpc/grpc-dotnet/blob/v2.42.x/examples/Browser/Proto/greet.proto
|
||||||
|
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package greet;
|
||||||
|
|
||||||
|
// The greeting service definition.
|
||||||
|
service Greeter {
|
||||||
|
// Sends a greeting
|
||||||
|
rpc SayHello (HelloRequest) returns (HelloReply);
|
||||||
|
rpc SayHellos (HelloRequest) returns (stream HelloReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The request message containing the user's name.
|
||||||
|
message HelloRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The response message containing the greetings
|
||||||
|
message HelloReply {
|
||||||
|
string message = 1;
|
||||||
|
}
|
|
@ -216,6 +216,146 @@ class case_dissect_grpc(subprocesstest.SubprocessTestCase):
|
||||||
))
|
))
|
||||||
self.assertEqual(self.countOutput('DATA'), 2)
|
self.assertEqual(self.countOutput('DATA'), 2)
|
||||||
|
|
||||||
|
|
||||||
|
@fixtures.mark_usefixtures('test_env')
|
||||||
|
@fixtures.uses_fixtures
|
||||||
|
class case_dissect_grpc_web(subprocesstest.SubprocessTestCase):
|
||||||
|
|
||||||
|
def test_grpc_web_unary_call_over_http1(self, cmd_tshark, features, dirs, capture_file):
|
||||||
|
'''gRPC-Web unary call over http1'''
|
||||||
|
well_know_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'well_know_types').replace('\\', '/')
|
||||||
|
user_defined_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'user_defined_types').replace('\\', '/')
|
||||||
|
self.assertRun((cmd_tshark,
|
||||||
|
'-r', capture_file('grpc_web.pcapng.gz'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(well_know_types_dir, 'FALSE'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(user_defined_types_dir, 'TRUE'),
|
||||||
|
'-o', 'protobuf.preload_protos: TRUE',
|
||||||
|
'-o', 'protobuf.pbf_as_hf: TRUE',
|
||||||
|
'-d', 'tcp.port==57226,http',
|
||||||
|
'-Y', '(tcp.stream eq 0) && (pbf.greet.HelloRequest.name == "88888888"'
|
||||||
|
'|| pbf.greet.HelloRequest.name == "99999999"'
|
||||||
|
'|| pbf.greet.HelloReply.message == "Hello 99999999")',
|
||||||
|
))
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloRequest'), 2)
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloReply'), 1)
|
||||||
|
|
||||||
|
def test_grpc_web_unary_call_over_http2(self, cmd_tshark, features, dirs, capture_file):
|
||||||
|
'''gRPC-Web unary call over http2'''
|
||||||
|
if not features.have_nghttp2:
|
||||||
|
self.skipTest('Requires nghttp2.')
|
||||||
|
well_know_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'well_know_types').replace('\\', '/')
|
||||||
|
user_defined_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'user_defined_types').replace('\\', '/')
|
||||||
|
self.assertRun((cmd_tshark,
|
||||||
|
'-r', capture_file('grpc_web.pcapng.gz'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(well_know_types_dir, 'FALSE'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(user_defined_types_dir, 'TRUE'),
|
||||||
|
'-o', 'protobuf.preload_protos: TRUE',
|
||||||
|
'-o', 'protobuf.pbf_as_hf: TRUE',
|
||||||
|
'-d', 'tcp.port==57228,http2',
|
||||||
|
'-Y', '(tcp.stream eq 1) && (pbf.greet.HelloRequest.name == "88888888"'
|
||||||
|
'|| pbf.greet.HelloRequest.name == "99999999"'
|
||||||
|
'|| pbf.greet.HelloReply.message == "Hello 99999999")',
|
||||||
|
))
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloRequest'), 2)
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloReply'), 1)
|
||||||
|
|
||||||
|
def test_grpc_web_reassembly_and_stream_over_http2(self, cmd_tshark, features, dirs, capture_file):
|
||||||
|
'''gRPC-Web data reassembly and server stream over http2'''
|
||||||
|
if not features.have_nghttp2:
|
||||||
|
self.skipTest('Requires nghttp2.')
|
||||||
|
well_know_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'well_know_types').replace('\\', '/')
|
||||||
|
user_defined_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'user_defined_types').replace('\\', '/')
|
||||||
|
self.assertRun((cmd_tshark,
|
||||||
|
'-r', capture_file('grpc_web.pcapng.gz'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(well_know_types_dir, 'FALSE'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(user_defined_types_dir, 'TRUE'),
|
||||||
|
'-o', 'protobuf.preload_protos: TRUE',
|
||||||
|
'-o', 'protobuf.pbf_as_hf: TRUE',
|
||||||
|
'-d', 'tcp.port==57228,http2',
|
||||||
|
'-Y', '(tcp.stream eq 2) && ((pbf.greet.HelloRequest.name && grpc.message_length == 80004)'
|
||||||
|
'|| (pbf.greet.HelloReply.message && (grpc.message_length == 23 || grpc.message_length == 80012)))',
|
||||||
|
))
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloRequest'), 2)
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloReply'), 4)
|
||||||
|
|
||||||
|
def test_grpc_web_text_unary_call_over_http1(self, cmd_tshark, features, dirs, capture_file):
|
||||||
|
'''gRPC-Web-Text unary call over http1'''
|
||||||
|
well_know_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'well_know_types').replace('\\', '/')
|
||||||
|
user_defined_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'user_defined_types').replace('\\', '/')
|
||||||
|
self.assertRun((cmd_tshark,
|
||||||
|
'-r', capture_file('grpc_web.pcapng.gz'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(well_know_types_dir, 'FALSE'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(user_defined_types_dir, 'TRUE'),
|
||||||
|
'-o', 'protobuf.preload_protos: TRUE',
|
||||||
|
'-o', 'protobuf.pbf_as_hf: TRUE',
|
||||||
|
'-d', 'tcp.port==57226,http',
|
||||||
|
'-Y', '(tcp.stream eq 5) && (pbf.greet.HelloRequest.name == "88888888"'
|
||||||
|
'|| pbf.greet.HelloRequest.name == "99999999"'
|
||||||
|
'|| pbf.greet.HelloReply.message == "Hello 99999999")',
|
||||||
|
))
|
||||||
|
self.assertTrue(self.grepOutput('GRPC-Web-Text'))
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloRequest'), 2)
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloReply'), 1)
|
||||||
|
|
||||||
|
def test_grpc_web_text_unary_call_over_http2(self, cmd_tshark, features, dirs, capture_file):
|
||||||
|
'''gRPC-Web-Text unary call over http2'''
|
||||||
|
if not features.have_nghttp2:
|
||||||
|
self.skipTest('Requires nghttp2.')
|
||||||
|
well_know_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'well_know_types').replace('\\', '/')
|
||||||
|
user_defined_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'user_defined_types').replace('\\', '/')
|
||||||
|
self.assertRun((cmd_tshark,
|
||||||
|
'-r', capture_file('grpc_web.pcapng.gz'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(well_know_types_dir, 'FALSE'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(user_defined_types_dir, 'TRUE'),
|
||||||
|
'-o', 'protobuf.preload_protos: TRUE',
|
||||||
|
'-o', 'protobuf.pbf_as_hf: TRUE',
|
||||||
|
'-d', 'tcp.port==57228,http2',
|
||||||
|
'-Y', '(tcp.stream eq 6) && (pbf.greet.HelloRequest.name == "88888888"'
|
||||||
|
'|| pbf.greet.HelloRequest.name == "99999999"'
|
||||||
|
'|| pbf.greet.HelloReply.message == "Hello 99999999")',
|
||||||
|
))
|
||||||
|
self.assertTrue(self.grepOutput('GRPC-Web-Text'))
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloRequest'), 2)
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloReply'), 1)
|
||||||
|
|
||||||
|
def test_grpc_web_text_reassembly_and_stream_over_http2(self, cmd_tshark, features, dirs, capture_file):
|
||||||
|
'''gRPC-Web-Text data reassembly and server stream over http2'''
|
||||||
|
if not features.have_nghttp2:
|
||||||
|
self.skipTest('Requires nghttp2.')
|
||||||
|
well_know_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'well_know_types').replace('\\', '/')
|
||||||
|
user_defined_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'user_defined_types').replace('\\', '/')
|
||||||
|
self.assertRun((cmd_tshark,
|
||||||
|
'-r', capture_file('grpc_web.pcapng.gz'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(well_know_types_dir, 'FALSE'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(user_defined_types_dir, 'TRUE'),
|
||||||
|
'-o', 'protobuf.preload_protos: TRUE',
|
||||||
|
'-o', 'protobuf.pbf_as_hf: TRUE',
|
||||||
|
'-d', 'tcp.port==57228,http2',
|
||||||
|
'-Y', '(tcp.stream eq 8) && ((pbf.greet.HelloRequest.name && grpc.message_length == 80004)'
|
||||||
|
'|| (pbf.greet.HelloReply.message && (grpc.message_length == 23 || grpc.message_length == 80012)))',
|
||||||
|
))
|
||||||
|
self.assertTrue(self.grepOutput('GRPC-Web-Text'))
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloRequest'), 2)
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloReply'), 4)
|
||||||
|
|
||||||
|
def test_grpc_web_text_reassembly_over_http1(self, cmd_tshark, features, dirs, capture_file):
|
||||||
|
'''gRPC-Web-Text data reassembly over http1'''
|
||||||
|
well_know_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'well_know_types').replace('\\', '/')
|
||||||
|
user_defined_types_dir = os.path.join(dirs.protobuf_lang_files_dir, 'user_defined_types').replace('\\', '/')
|
||||||
|
self.assertRun((cmd_tshark,
|
||||||
|
'-r', capture_file('grpc_web.pcapng.gz'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(well_know_types_dir, 'FALSE'),
|
||||||
|
'-o', 'uat:protobuf_search_paths: "{}","{}"'.format(user_defined_types_dir, 'TRUE'),
|
||||||
|
'-o', 'protobuf.preload_protos: TRUE',
|
||||||
|
'-o', 'protobuf.pbf_as_hf: TRUE',
|
||||||
|
'-d', 'tcp.port==57226,http',
|
||||||
|
'-Y', '(tcp.stream eq 7) && (grpc.message_length == 80004 || grpc.message_length == 80010)',
|
||||||
|
))
|
||||||
|
self.assertTrue(self.grepOutput('GRPC-Web-Text'))
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloRequest'), 1)
|
||||||
|
self.assertEqual(self.countOutput('greet.HelloReply'), 1)
|
||||||
|
|
||||||
|
|
||||||
@fixtures.mark_usefixtures('test_env')
|
@fixtures.mark_usefixtures('test_env')
|
||||||
@fixtures.uses_fixtures
|
@fixtures.uses_fixtures
|
||||||
class case_dissect_http(subprocesstest.SubprocessTestCase):
|
class case_dissect_http(subprocesstest.SubprocessTestCase):
|
||||||
|
|
Loading…
Reference in New Issue