diff options
-rw-r--r-- | debian/libwireshark0.symbols | 2 | ||||
-rw-r--r-- | doc/tshark.pod | 23 | ||||
-rw-r--r-- | docbook/CMakeLists.txt | 1 | ||||
-rw-r--r-- | docbook/wsug_graphics/ws-follow-http2-stream.png | bin | 0 -> 57209 bytes | |||
-rw-r--r-- | docbook/wsug_src/WSUG_chapter_advanced.adoc | 9 | ||||
-rw-r--r-- | epan/dissectors/packet-http2.c | 151 | ||||
-rw-r--r-- | epan/dissectors/packet-http2.h | 26 | ||||
-rw-r--r-- | epan/dissectors/packet-tcp.c | 4 | ||||
-rw-r--r-- | epan/dissectors/packet-tcp.h | 4 | ||||
-rw-r--r-- | epan/dissectors/packet-udp.c | 4 | ||||
-rw-r--r-- | epan/follow.h | 7 | ||||
-rw-r--r-- | sharkd_session.c | 3 | ||||
-rw-r--r-- | test/suite_dissection.py | 27 | ||||
-rw-r--r-- | ui/cli/tap-follow.c | 23 | ||||
-rw-r--r-- | ui/qt/follow_stream_dialog.cpp | 91 | ||||
-rw-r--r-- | ui/qt/follow_stream_dialog.h | 5 | ||||
-rw-r--r-- | ui/qt/follow_stream_dialog.ui | 16 | ||||
-rw-r--r-- | ui/qt/main_window.h | 4 | ||||
-rw-r--r-- | ui/qt/main_window.ui | 9 | ||||
-rw-r--r-- | ui/qt/main_window_slots.cpp | 15 | ||||
-rw-r--r-- | ui/qt/packet_list.cpp | 1 | ||||
-rw-r--r-- | ui/qt/proto_tree.cpp | 1 | ||||
-rw-r--r-- | ui/qt/tcp_stream_dialog.ui | 64 |
23 files changed, 418 insertions, 72 deletions
diff --git a/debian/libwireshark0.symbols b/debian/libwireshark0.symbols index 7e6e9df4c7..bcb243932e 100644 --- a/debian/libwireshark0.symbols +++ b/debian/libwireshark0.symbols @@ -898,6 +898,8 @@ libwireshark.so.0 libwireshark0 #MINVER# hfinfo_bitshift@Base 1.12.0~rc1 host_name_lookup_process@Base 1.9.1 hostlist_table_set_gui_info@Base 1.99.0 + http2_get_stream_id_ge@Base 3.1.1 + http2_get_stream_id_le@Base 3.1.1 http_tcp_dissector_add@Base 2.1.0 http_tcp_dissector_delete@Base 2.3.0 http_tcp_port_add@Base 2.1.0 diff --git a/doc/tshark.pod b/doc/tshark.pod index 803272705b..afcacf332e 100644 --- a/doc/tshark.pod +++ b/doc/tshark.pod @@ -1219,7 +1219,7 @@ Example: B<-z flow,tcp,network> will show data flow for all TCP frames =item B<-z> follow,I<prot>,I<mode>,I<filter>[I<,range>] -Displays the contents of a TCP or UDP stream between two nodes. The data +Displays the contents of a TCP or UDP stream between two nodes. The data sent by the second node is prefixed with a tab to differentiate it from the data sent by the first node. @@ -1241,10 +1241,12 @@ of each section of output plus a newline precedes each section of output. I<filter> specifies the stream to be displayed. UDP/TCP streams are selected with either the stream index or IP address plus port pairs. TLS streams are -selected with the stream index. For example: +selected with the stream index. HTTP/2 streams are selected by combination of +UDP/TCP and HTTP/2 streams indices. For example: ip-addr0:port0,ip-addr1:port1 stream-index + stream-index,substream-index I<range> optionally specifies which "chunks" of the stream should be displayed. @@ -1277,6 +1279,23 @@ display the contents of a TCP stream between 200.57.7.197 port 32891 and 4 .... +Example: B<-z "follow,http2,hex,0,1"> will display the contents of a HTTP/2 +stream on the first TCP session (index 0) with HTTP/2 Stream ID 1. + + =================================================================== + Follow: http2,hex + Filter: tcp.stream eq 0 and http2.streamid eq 1 + Node 0: 172.16.5.1:49178 + Node 1: 172.16.5.10:8443 + 00000000 00 00 2c 01 05 00 00 00 01 82 04 8b 63 c1 ac 2a ..,..... ....c..* + 00000010 27 1d 9d 57 ae a9 bf 87 41 8c 0b a2 5c 2e 2e da '..W.... A...\... + 00000020 e1 05 c7 9a 69 9f 7a 88 25 b6 50 c3 ab b6 25 c3 ....i.z. %.P...%. + 00000030 53 03 2a 2f 2a S.*/* + 00000000 00 00 22 01 04 00 00 00 01 88 5f 87 35 23 98 ac .."..... .._.5#.. + 00000010 57 54 df 61 96 c3 61 be 94 03 8a 61 2c 6a 08 2f WT.a..a. ...a,j./ + 00000020 34 a0 5b b8 21 5c 0b ea 62 d1 bf 4.[.!\.. b.. + 0000002B 00 40 00 00 00 00 00 00 01 89 50 4e 47 0d 0a 1a .@...... ..PNG... + =item B<-z> h225,counter[I<,filter>] Count ITU-T H.225 messages and their reasons. In the first column you get a diff --git a/docbook/CMakeLists.txt b/docbook/CMakeLists.txt index 12b2820090..2742ce5b2b 100644 --- a/docbook/CMakeLists.txt +++ b/docbook/CMakeLists.txt @@ -163,6 +163,7 @@ set(WSUG_GRAPHICS wsug_graphics/ws-filters.png wsug_graphics/ws-find-packet.png wsug_graphics/ws-follow-stream.png + wsug_graphics/ws-follow-http2-stream.png wsug_graphics/ws-go-menu.png wsug_graphics/ws-goto-packet.png wsug_graphics/ws-gui-colors-preferences.png diff --git a/docbook/wsug_graphics/ws-follow-http2-stream.png b/docbook/wsug_graphics/ws-follow-http2-stream.png Binary files differnew file mode 100644 index 0000000000..616dfd7fbe --- /dev/null +++ b/docbook/wsug_graphics/ws-follow-http2-stream.png diff --git a/docbook/wsug_src/WSUG_chapter_advanced.adoc b/docbook/wsug_src/WSUG_chapter_advanced.adoc index fe5bdffa50..6eb53fc329 100644 --- a/docbook/wsug_src/WSUG_chapter_advanced.adoc +++ b/docbook/wsug_src/WSUG_chapter_advanced.adoc @@ -101,6 +101,15 @@ You can switch between streams using the “Stream” selector. You can search for text by entering it in the “Find” entry box and pressing btn:[Find Next]. +.The “Follow HTTP/2 Stream” dialog box +image::wsug_graphics/ws-follow-http2-stream.png[{screenshot-attrs}] + +The HTTP/2 Stream dialog is similar to the "Follow TCP Stream" dialog, except +for an additional "Substream" dialog field. HTTP/2 Streams are identified by +a HTTP/2 Stream Index (field name `http2.streamid`) which are unique within a +TCP connection. The “Stream” selector determines the TCP connection whereas the +“Substream” selector is used to pick the HTTP/2 Stream ID. + [[ChAdvShowPacketBytes]] === Show Packet Bytes diff --git a/epan/dissectors/packet-http2.c b/epan/dissectors/packet-http2.c index f59cec3fab..0bd6698d2b 100644 --- a/epan/dissectors/packet-http2.c +++ b/epan/dissectors/packet-http2.c @@ -36,16 +36,16 @@ #ifdef HAVE_NGHTTP2 #include <epan/uat.h> - #include <nghttp2/nghttp2.h> - #endif -#include "packet-tcp.h" #include <epan/tap.h> #include <epan/stats_tree.h> #include <epan/reassemble.h> +#include <epan/follow.h> +#include <epan/addr_resolv.h> +#include "packet-tcp.h" #include "wsutil/pint.h" #include "wsutil/strtoi.h" @@ -199,11 +199,13 @@ typedef struct { nghttp2_hd_inflater *hd_inflater[2]; http2_header_repr_info_t header_repr_info[2]; wmem_map_t *per_stream_info; - guint32 current_stream_id; #endif + guint32 current_stream_id; tcp_flow_t *fwd_flow; } http2_session_t; +static GHashTable* streamid_hash = NULL; + void proto_register_http2(void); void proto_reg_handoff_http2(void); @@ -212,6 +214,7 @@ struct HTTP2Tap { }; static int http2_tap = -1; +static int http2_follow_tap = -1; static const guint8* st_str_http2 = "HTTP2"; static const guint8* st_str_http2_type = "Type"; @@ -884,6 +887,8 @@ http2_init_protocol(void) proto_register_field_array(proto_http2, hf_uat, num_header_fields); } #endif + /* Init hash table with mapping of stream id -> frames count for Follow HTTP2 */ + streamid_hash = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)g_hash_table_destroy); } static void @@ -895,6 +900,7 @@ http2_cleanup_protocol(void) { proto_add_deregistered_data(hf_uat); proto_free_deregistered_fields(); #endif + g_hash_table_destroy(streamid_hash); } static dissector_handle_t http2_handle; @@ -1881,6 +1887,109 @@ inflate_http2_header_block(tvbuff_t *tvb, packet_info *pinfo, guint offset, } #endif +static gchar* +http2_follow_conv_filter(packet_info *pinfo, guint *stream, guint *sub_stream) +{ + http2_session_t *h2session; + struct tcp_analysis *tcpd; + + if( ((pinfo->net_src.type == AT_IPv4 && pinfo->net_dst.type == AT_IPv4) || + (pinfo->net_src.type == AT_IPv6 && pinfo->net_dst.type == AT_IPv6))) + { + h2session = get_http2_session(pinfo); + tcpd = get_tcp_conversation_data(NULL, pinfo); + + if (tcpd == NULL) + return NULL; + if (h2session == NULL) + return NULL; + + *stream = tcpd->stream; + *sub_stream = h2session->current_stream_id; + return g_strdup_printf("tcp.stream eq %u and http2.streamid eq %u", tcpd->stream, h2session->current_stream_id); + } + + return NULL; +} + +static guint32 +get_http2_stream_count(guint streamid) +{ + guint32 result = 0; + guint32 key; + GHashTable *entry; + GList *entry_set, *it; + + entry = (GHashTable*)g_hash_table_lookup(streamid_hash, GUINT_TO_POINTER(streamid)); + if (entry != NULL) { + entry_set = g_hash_table_get_keys(entry); + + /* this is a doubly-linked list, g_list_sort has the same time complexity */ + for (it = entry_set; it != NULL; it = it->next) { + key = GPOINTER_TO_UINT(it->data); + result = key > result ? key : result; + } + g_list_free(entry_set); + } + + return result; +} + +static gboolean +is_http2_stream_contains(guint streamid, gint sub_stream_id) +{ + GHashTable *entry; + + entry = (GHashTable*)g_hash_table_lookup(streamid_hash, GUINT_TO_POINTER(streamid)); + if (entry == NULL) { + return FALSE; + } + + if (!g_hash_table_contains(entry, GINT_TO_POINTER(sub_stream_id))) { + return FALSE; + } + + return TRUE; +} + +gboolean +http2_get_stream_id_le(guint streamid, guint sub_stream_id, guint *sub_stream_id_out) +{ + // HTTP/2 Stream IDs are always 31 bit. + gint max_id = (gint)get_http2_stream_count(streamid); + gint id = (gint)(sub_stream_id & MASK_HTTP2_STREAMID); + if (id > max_id) { + id = max_id; + } + for (; id >= 0; id--) { + if (is_http2_stream_contains(streamid, id)) { + *sub_stream_id_out = (guint)id; + return TRUE; + } + } + return FALSE; +} + +gboolean +http2_get_stream_id_ge(guint streamid, guint sub_stream_id, guint *sub_stream_id_out) +{ + // HTTP/2 Stream IDs are always 31 bit. + gint max_id = (gint)get_http2_stream_count(streamid); + for (gint id = (gint)(sub_stream_id & MASK_HTTP2_STREAMID); id <= max_id; id++) { + if (is_http2_stream_contains(streamid, id)) { + *sub_stream_id_out = (guint)id; + return TRUE; + } + } + return FALSE; +} + +static gchar* +http2_follow_index_filter(guint stream, guint sub_stream) +{ + return g_strdup_printf("tcp.stream eq %u and http2.streamid eq %u", stream, sub_stream); +} + static guint8 dissect_http2_header_flags(tvbuff_t *tvb, packet_info *pinfo _U_, proto_tree *http2_tree, guint offset, guint8 type) { @@ -2645,6 +2754,8 @@ dissect_http2_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* dat guint16 length; guint32 streamid; struct HTTP2Tap *http2_stats; + GHashTable* entry; + struct tcp_analysis* tcpd; if(!p_get_proto_data(wmem_file_scope(), pinfo, proto_http2, 0)) { http2_header_data_t *header_data; @@ -2702,6 +2813,12 @@ dissect_http2_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* dat proto_tree_add_item(http2_tree, hf_http2_type, tvb, offset, 1, ENC_BIG_ENDIAN); type = tvb_get_guint8(tvb, offset); + gint type_idx; + const gchar *type_str = try_val_to_str_idx(type, http2_type_vals, &type_idx); + if (type_str == NULL) { + type_str = wmem_strdup_printf(wmem_packet_scope(), "Unknown type (%d)", type); + } + offset += 1; flags = dissect_http2_header_flags(tvb, pinfo, http2_tree, offset, type); @@ -2710,17 +2827,27 @@ dissect_http2_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* dat proto_tree_add_item(http2_tree, hf_http2_r, tvb, offset, 4, ENC_BIG_ENDIAN); proto_tree_add_item(http2_tree, hf_http2_streamid, tvb, offset, 4, ENC_BIG_ENDIAN); streamid = tvb_get_ntohl(tvb, offset) & MASK_HTTP2_STREAMID; - proto_item_append_text(ti, ": %s, Stream ID: %u, Length %u", val_to_str(type, http2_type_vals, "Unknown type (%d)"), streamid, length); + proto_item_append_text(ti, ": %s, Stream ID: %u, Length %u", type_str, streamid, length); offset += 4; /* append stream id after frame type on info column, like: HEADERS[1], DATA[1], HEADERS[3], DATA[3] */ - col_append_sep_fstr(pinfo->cinfo, COL_INFO, ", ", "%s[%u]", val_to_str(type, http2_type_vals, "Unknown type (%d)"), streamid); + col_append_sep_fstr(pinfo->cinfo, COL_INFO, ", ", "%s[%u]", type_str, streamid); + + /* fill hash table with stream ids and skip all unknown frames */ + tcpd = get_tcp_conversation_data(NULL, pinfo); + if (tcpd != NULL && type_idx != -1) { + entry = (GHashTable*)g_hash_table_lookup(streamid_hash, GUINT_TO_POINTER(tcpd->stream)); + if (entry == NULL) { + entry = g_hash_table_new(NULL, NULL); + g_hash_table_insert(streamid_hash, GUINT_TO_POINTER(tcpd->stream), entry); + } + + g_hash_table_add(entry, GUINT_TO_POINTER(streamid)); + } -#ifdef HAVE_NGHTTP2 /* Mark the current stream, used for per-stream processing later in the dissection */ http2_session_t *http2_session = get_http2_session(pinfo); http2_session->current_stream_id = streamid; -#endif /* Collect stats */ http2_stats = wmem_new0(wmem_packet_scope(), struct HTTP2Tap); @@ -2779,9 +2906,11 @@ dissect_http2_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* dat proto_tree_add_item(http2_tree, hf_http2_unknown, tvb, offset, -1, ENC_NA); break; } - tap_queue_packet(http2_tap, pinfo, http2_stats); + if (have_tap_listener(http2_follow_tap)) { + tap_queue_packet(http2_follow_tap, pinfo, tvb); + } return tvb_captured_length(tvb); } @@ -3386,6 +3515,10 @@ proto_register_http2(void) &addresses_ports_reassembly_table_functions); http2_tap = register_tap("http2"); + http2_follow_tap = register_tap("http2_follow"); + + register_follow_stream(proto_http2, "http2_follow", http2_follow_conv_filter, http2_follow_index_filter, tcp_follow_address_filter, + tcp_port_to_display, follow_tvb_tap_listener); } static void http2_stats_tree_init(stats_tree* st) diff --git a/epan/dissectors/packet-http2.h b/epan/dissectors/packet-http2.h index a121dcd4b3..b76f09c31f 100644 --- a/epan/dissectors/packet-http2.h +++ b/epan/dissectors/packet-http2.h @@ -1,5 +1,5 @@ /* packet-http2.h - * Routines for HTTP2 dissection + * Routines for HTTP/2 dissection * * Wireshark - Network traffic analyzer * By Gerald Combs <gerald@wireshark.org> @@ -10,6 +10,10 @@ #ifndef __PACKET_HTTP2_H__ #define __PACKET_HTTP2_H__ +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + int dissect_http2_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_ ); /** Get header value from current or the other direction stream. @@ -26,12 +30,30 @@ int dissect_http2_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* const gchar* http2_get_header_value(packet_info *pinfo, const gchar* name, gboolean the_other_direction); /** - * Get the HTTP2 Stream ID for the current PDU (typically the DATA frame). + * Get the HTTP/2 Stream ID for the current PDU (typically the DATA frame). * Only valid when called from a HTTP/2 subdissector. * Returns 0 if no HTTP/2 session was found. */ guint32 http2_get_stream_id(packet_info *pinfo); +/** + * Retrieves the HTTP/2 Stream ID which is smaller than or equal to the provided + * ID. If available, sub_stream_id_out will be set and TRUE is returned. + */ +WS_DLL_PUBLIC gboolean +http2_get_stream_id_le(guint streamid, guint sub_stream_id, guint *sub_stream_id_out); + +/** + * Retrieves the HTTP/2 Stream ID which is greater than or equal to the provided + * ID. If available, sub_stream_id_out will be set and TRUE is returned. + */ +WS_DLL_PUBLIC gboolean +http2_get_stream_id_ge(guint streamid, guint sub_stream_id, guint *sub_stream_id_out); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + #endif /* diff --git a/epan/dissectors/packet-tcp.c b/epan/dissectors/packet-tcp.c index 010a090c9c..8b74680b7d 100644 --- a/epan/dissectors/packet-tcp.c +++ b/epan/dissectors/packet-tcp.c @@ -904,7 +904,7 @@ tcp_seq_analysis_packet( void *ptr, packet_info *pinfo, epan_dissect_t *edt _U_, } -gchar *tcp_follow_conv_filter(packet_info *pinfo, guint *stream) +gchar *tcp_follow_conv_filter(packet_info *pinfo, guint *stream, guint *sub_stream _U_) { conversation_t *conv; struct tcp_analysis *tcpd; @@ -925,7 +925,7 @@ gchar *tcp_follow_conv_filter(packet_info *pinfo, guint *stream) return NULL; } -gchar *tcp_follow_index_filter(guint stream) +gchar *tcp_follow_index_filter(guint stream, guint sub_stream _U_) { return g_strdup_printf("tcp.stream eq %u", stream); } diff --git a/epan/dissectors/packet-tcp.h b/epan/dissectors/packet-tcp.h index b009522b1b..dfee9cdeb4 100644 --- a/epan/dissectors/packet-tcp.h +++ b/epan/dissectors/packet-tcp.h @@ -509,8 +509,8 @@ WS_DLL_PUBLIC guint32 get_tcp_stream_count(void); WS_DLL_PUBLIC guint32 get_mptcp_stream_count(void); /* Follow Stream functionality shared with HTTP (and SSL?) */ -extern gchar *tcp_follow_conv_filter(packet_info *pinfo, guint *stream); -extern gchar *tcp_follow_index_filter(guint stream); +extern gchar *tcp_follow_conv_filter(packet_info *pinfo, guint *stream, guint *sub_stream); +extern gchar *tcp_follow_index_filter(guint stream, guint sub_stream); extern gchar *tcp_follow_address_filter(address *src_addr, address *dst_addr, int src_port, int dst_port); #ifdef __cplusplus diff --git a/epan/dissectors/packet-udp.c b/epan/dissectors/packet-udp.c index 1e74cd7ac5..99def2fa6e 100644 --- a/epan/dissectors/packet-udp.c +++ b/epan/dissectors/packet-udp.c @@ -425,7 +425,7 @@ udp_build_filter(packet_info *pinfo) return NULL; } -static gchar *udp_follow_conv_filter(packet_info *pinfo, guint *stream) +static gchar *udp_follow_conv_filter(packet_info *pinfo, guint *stream, guint *sub_stream _U_) { conversation_t *conv; struct udp_analysis *udpd; @@ -446,7 +446,7 @@ static gchar *udp_follow_conv_filter(packet_info *pinfo, guint *stream) return NULL; } -static gchar *udp_follow_index_filter(guint stream) +static gchar *udp_follow_index_filter(guint stream, guint sub_stream _U_) { return g_strdup_printf("udp.stream eq %u", stream); } diff --git a/epan/follow.h b/epan/follow.h index f8053d139c..aaa366e0e8 100644 --- a/epan/follow.h +++ b/epan/follow.h @@ -42,7 +42,8 @@ typedef enum { FOLLOW_TCP, FOLLOW_TLS, FOLLOW_UDP, - FOLLOW_HTTP + FOLLOW_HTTP, + FOLLOW_HTTP2 } follow_type_t; /* Show Type */ @@ -99,8 +100,8 @@ typedef struct _follow_info { struct register_follow; typedef struct register_follow register_follow_t; -typedef gchar* (*follow_conv_filter_func)(packet_info *pinfo, guint *stream); -typedef gchar* (*follow_index_filter_func)(guint stream); +typedef gchar* (*follow_conv_filter_func)(packet_info *pinfo, guint *stream, guint *sub_stream); +typedef gchar* (*follow_index_filter_func)(guint stream, guint sub_stream); typedef gchar* (*follow_address_filter_func)(address* src_addr, address* dst_addr, int src_port, int dst_port); typedef gchar* (*follow_port_to_display_func)(wmem_allocator_t *allocator, guint port); diff --git a/sharkd_session.c b/sharkd_session.c index c2f7b53c90..4ca9d0af1f 100644 --- a/sharkd_session.c +++ b/sharkd_session.c @@ -2740,13 +2740,14 @@ sharkd_follower_visit_layers_cb(const void *key _U_, void *value, void *user_dat const int proto_id = get_follow_proto_id(follower); guint32 ignore_stream; + guint32 ignore_sub_stream; if (proto_is_frame_protocol(pi->layers, proto_get_protocol_filter_name(proto_id))) { const char *layer_proto = proto_get_protocol_short_name(find_protocol_by_id(proto_id)); char *follow_filter; - follow_filter = get_follow_conv_func(follower)(pi, &ignore_stream); + follow_filter = get_follow_conv_func(follower)(pi, &ignore_stream, &ignore_sub_stream); json_dumper_begin_array(&dumper); json_dumper_value_string(&dumper, layer_proto); diff --git a/test/suite_dissection.py b/test/suite_dissection.py index eac8529d03..ba7500f72e 100644 --- a/test/suite_dissection.py +++ b/test/suite_dissection.py @@ -57,6 +57,33 @@ class case_dissect_http2(subprocesstest.SubprocessTestCase): )) self.assertTrue(self.grepOutput('DATA')) + def test_http2_follow_0(self, cmd_tshark, features, dirs, capture_file): + '''Follow HTTP/2 Stream ID 0 test''' + if not features.have_nghttp2: + self.skipTest('Requires nghttp2.') + key_file = os.path.join(dirs.key_dir, 'http2-data-reassembly.keys') + self.assertRun((cmd_tshark, + '-r', capture_file('http2-data-reassembly.pcap'), + '-o', 'tls.keylog_file: {}'.format(key_file), + '-z', 'follow,http2,hex,0,0' + )) + self.assertTrue(self.grepOutput('00000000 00 00 12 04 00 00 00 00')) + self.assertFalse(self.grepOutput('00000000 00 00 2c 01 05 00 00 00')) + + def test_http2_follow_1(self, cmd_tshark, features, dirs, capture_file): + '''Follow HTTP/2 Stream ID 1 test''' + if not features.have_nghttp2: + self.skipTest('Requires nghttp2.') + key_file = os.path.join(dirs.key_dir, 'http2-data-reassembly.keys') + self.assertRun((cmd_tshark, + '-r', capture_file('http2-data-reassembly.pcap'), + '-o', 'tls.keylog_file: {}'.format(key_file), + '-z', 'follow,http2,hex,0,1' + )) + self.assertFalse(self.grepOutput('00000000 00 00 12 04 00 00 00 00')) + self.assertTrue(self.grepOutput('00000000 00 00 2c 01 05 00 00 00')) + + @fixtures.mark_usefixtures('test_env') @fixtures.uses_fixtures class case_dissect_tcp(subprocesstest.SubprocessTestCase): diff --git a/ui/cli/tap-follow.c b/ui/cli/tap-follow.c index bb6380fc5a..c4d7399fd1 100644 --- a/ui/cli/tap-follow.c +++ b/ui/cli/tap-follow.c @@ -37,6 +37,7 @@ typedef struct _cli_follow_info { /* filter */ int stream_index; + int sub_stream_index; int port[2]; address addr[2]; union { @@ -338,6 +339,13 @@ follow_arg_filter(const char **opt_argp, follow_info_t *follow_info) ((*opt_argp)[len] == 0 || (*opt_argp)[len] == ',')) { *opt_argp += len; + + /* if it's HTTP2 protocol we should read substream id otherwise it's a range parameter from follow_arg_range */ + if (cli_follow_info->sub_stream_index == -1 && sscanf(*opt_argp, ",%d%n", &cli_follow_info->sub_stream_index, &len) == 1 && + ((*opt_argp)[len] == 0 || (*opt_argp)[len] == ',')) + { + *opt_argp += len; + } } else { @@ -438,11 +446,20 @@ static void follow_stream(const char *opt_argp, void *userdata) register_follow_t* follower = (register_follow_t*)userdata; follow_index_filter_func index_filter; follow_address_filter_func address_filter; + int proto_id = get_follow_proto_id(follower); + const char* proto_filter_name = proto_get_protocol_filter_name(proto_id); opt_argp += strlen(STR_FOLLOW); - opt_argp += strlen(proto_get_protocol_filter_name(get_follow_proto_id(follower))); + opt_argp += strlen(proto_filter_name); cli_follow_info = g_new0(cli_follow_info_t, 1); + cli_follow_info->stream_index = -1; + /* use second parameter only for HTTP2 substream */ + if (strncmp(proto_filter_name, "http2", 5) == 0) { + cli_follow_info->sub_stream_index = -1; + } else { + cli_follow_info->sub_stream_index = 0; + } follow_info = g_new0(follow_info_t, 1); follow_info->gui_data = cli_follow_info; cli_follow_info->follower = follower; @@ -455,8 +472,8 @@ static void follow_stream(const char *opt_argp, void *userdata) if (cli_follow_info->stream_index >= 0) { index_filter = get_follow_index_func(follower); - follow_info->filter_out_filter = index_filter(cli_follow_info->stream_index); - if (follow_info->filter_out_filter == NULL) + follow_info->filter_out_filter = index_filter(cli_follow_info->stream_index, cli_follow_info->sub_stream_index); + if (follow_info->filter_out_filter == NULL || cli_follow_info->sub_stream_index < 0) { follow_exit("Error creating filter for this stream."); } diff --git a/ui/qt/follow_stream_dialog.cpp b/ui/qt/follow_stream_dialog.cpp index d6f96f7679..c845830420 100644 --- a/ui/qt/follow_stream_dialog.cpp +++ b/ui/qt/follow_stream_dialog.cpp @@ -16,6 +16,7 @@ #include "epan/follow.h" #include "epan/dissectors/packet-tcp.h" #include "epan/dissectors/packet-udp.h" +#include "epan/dissectors/packet-http2.h" #include "epan/prefs.h" #include "epan/addr_resolv.h" #include "epan/charsets.h" @@ -75,7 +76,8 @@ FollowStreamDialog::FollowStreamDialog(QWidget &parent, CaptureFile &cf, follow_ last_from_server_(0), turns_(0), use_regex_find_(false), - terminating_(false) + terminating_(false), + previous_sub_stream_num_(0) { ui->setupUi(this); loadGeometry(parent.width() * 2 / 3, parent.height()); @@ -94,6 +96,9 @@ FollowStreamDialog::FollowStreamDialog(QWidget &parent, CaptureFile &cf, follow_ case FOLLOW_HTTP: follower_ = get_follow_by_name("HTTP"); break; + case FOLLOW_HTTP2: + follower_ = get_follow_by_name("HTTP2"); + break; default : g_assert_not_reached(); } @@ -369,8 +374,47 @@ void FollowStreamDialog::on_streamNumberSpinBox_valueChanged(int stream_num) { if (file_closed_) return; + int sub_stream_num = 0; + ui->subStreamNumberSpinBox->blockSignals(true); + sub_stream_num = ui->subStreamNumberSpinBox->value(); + ui->subStreamNumberSpinBox->blockSignals(false); + + if (sub_stream_num < 0) { + sub_stream_num = 0; + } + if (stream_num >= 0) { - follow(previous_filter_, true, stream_num); + follow(previous_filter_, true, stream_num, sub_stream_num); + } +} + + +void FollowStreamDialog::on_subStreamNumberSpinBox_valueChanged(int sub_stream_num) +{ + if (file_closed_) return; + + int stream_num = 0; + ui->streamNumberSpinBox->blockSignals(true); + stream_num = ui->streamNumberSpinBox->value(); + ui->streamNumberSpinBox->blockSignals(false); + + guint sub_stream_num_new = static_cast<guint>(sub_stream_num); + gboolean ok; + /* previous_sub_stream_num_ is a hack to track which buttons was pressed without event handling */ + if (sub_stream_num < 0) { + // Stream ID 0 should always exist as it is used for control messages. + sub_stream_num_new = 0; + ok = TRUE; + } else if (previous_sub_stream_num_ < sub_stream_num){ + ok = http2_get_stream_id_ge(static_cast<guint>(stream_num), sub_stream_num_new, &sub_stream_num_new); + } else { + ok = http2_get_stream_id_le(static_cast<guint>(stream_num), sub_stream_num_new, &sub_stream_num_new); + } + sub_stream_num = static_cast<gint>(sub_stream_num_new); + + if (ok) { + follow(previous_filter_, true, stream_num, sub_stream_num); + previous_sub_stream_num_ = sub_stream_num; } } @@ -388,6 +432,8 @@ void FollowStreamDialog::removeStreamControls() ui->horizontalLayout->removeItem(ui->streamNumberSpacer); ui->streamNumberLabel->setVisible(false); ui->streamNumberSpinBox->setVisible(false); + ui->subStreamNumberLabel->setVisible(false); + ui->subStreamNumberSpinBox->setVisible(false); } void FollowStreamDialog::resetStream() @@ -455,6 +501,7 @@ FollowStreamDialog::readStream() case FOLLOW_TCP : case FOLLOW_UDP : case FOLLOW_HTTP : + case FOLLOW_HTTP2: case FOLLOW_TLS : ret = readFollowStream(); break; @@ -771,7 +818,7 @@ FollowStreamDialog::showBuffer(char *buffer, size_t nchars, gboolean is_from_ser return FRS_OK; } -bool FollowStreamDialog::follow(QString previous_filter, bool use_stream_index, guint stream_num) +bool FollowStreamDialog::follow(QString previous_filter, bool use_stream_index, guint stream_num, guint sub_stream_num) { QString follow_filter; const char *hostname0 = NULL, *hostname1 = NULL; @@ -815,9 +862,9 @@ bool FollowStreamDialog::follow(QString previous_filter, bool use_stream_index, /* Create a new filter that matches all packets in the TCP stream, and set the display filter entry accordingly */ if (use_stream_index) { - follow_filter = gchar_free_to_qstring(get_follow_index_func(follower_)(stream_num)); + follow_filter = gchar_free_to_qstring(get_follow_index_func(follower_)(stream_num, sub_stream_num)); } else { - follow_filter = gchar_free_to_qstring(get_follow_conv_func(follower_)(&cap_file_.capFile()->edt->pi, &stream_num)); + follow_filter = gchar_free_to_qstring(get_follow_conv_func(follower_)(&cap_file_.capFile()->edt->pi, &stream_num, &sub_stream_num)); } if (follow_filter.isEmpty()) { QMessageBox::warning(this, @@ -844,6 +891,15 @@ bool FollowStreamDialog::follow(QString previous_filter, bool use_stream_index, return false; } + /* disable substream spin box for all protocols except HTTP2 */ + ui->subStreamNumberSpinBox->blockSignals(true); + ui->subStreamNumberSpinBox->setEnabled(false); + ui->subStreamNumberSpinBox->setValue(0); + ui->subStreamNumberSpinBox->setKeyboardTracking(false); + ui->subStreamNumberSpinBox->blockSignals(false); + ui->subStreamNumberSpinBox->setVisible(false); + ui->subStreamNumberLabel->setVisible(false); + switch (follow_type_) { case FOLLOW_TCP: @@ -870,6 +926,31 @@ bool FollowStreamDialog::follow(QString previous_filter, bool use_stream_index, break; } + case FOLLOW_HTTP2: + { + int stream_count = get_tcp_stream_count(); + ui->streamNumberSpinBox->blockSignals(true); + ui->streamNumberSpinBox->setMaximum(stream_count-1); + ui->streamNumberSpinBox->setValue(stream_num); + ui->streamNumberSpinBox->blockSignals(false); + ui->streamNumberSpinBox->setToolTip(tr("%Ln total stream(s).", "", stream_count)); + ui->streamNumberLabel->setToolTip(ui->streamNumberSpinBox->toolTip()); + + guint substream_max_id = 0; + http2_get_stream_id_le(static_cast<guint>(stream_num), G_MAXINT32, &substream_max_id); + stream_count = static_cast<gint>(substream_max_id); + ui->subStreamNumberSpinBox->blockSignals(true); + ui->subStreamNumberSpinBox->setEnabled(true); + ui->subStreamNumberSpinBox->setMaximum(stream_count); + ui->subStreamNumberSpinBox->setValue(sub_stream_num); + ui->subStreamNumberSpinBox->blockSignals(false); + ui->subStreamNumberSpinBox->setToolTip(tr("%Ln total sub stream(s).", "", stream_count)); + ui->subStreamNumberSpinBox->setToolTip(ui->subStreamNumberSpinBox->toolTip()); + ui->subStreamNumberSpinBox->setVisible(true); + ui->subStreamNumberLabel->setVisible(true); + + break; + } case FOLLOW_TLS: case FOLLOW_HTTP: /* No extra handling */ diff --git a/ui/qt/follow_stream_dialog.h b/ui/qt/follow_stream_dialog.h index 4988092a6c..a2c3928fae 100644 --- a/ui/qt/follow_stream_dialog.h +++ b/ui/qt/follow_stream_dialog.h @@ -42,7 +42,7 @@ public: explicit FollowStreamDialog(QWidget &parent, CaptureFile &cf, follow_type_t type = FOLLOW_TCP); ~FollowStreamDialog(); - bool follow(QString previous_filter = QString(), bool use_stream_index = false, guint stream_num = 0); + bool follow(QString previous_filter = QString(), bool use_stream_index = false, guint stream_num = 0, guint sub_stream_num = 0); public slots: void captureEvent(CaptureEvent e); @@ -69,6 +69,7 @@ private slots: void goToPacketForTextPos(int text_pos); void on_streamNumberSpinBox_valueChanged(int stream_num); + void on_subStreamNumberSpinBox_valueChanged(int sub_stream_num); void on_buttonBox_rejected(); @@ -122,6 +123,8 @@ private: bool use_regex_find_; bool terminating_; + + int previous_sub_stream_num_; }; #endif // FOLLOW_STREAM_DIALOG_H diff --git a/ui/qt/follow_stream_dialog.ui b/ui/qt/follow_stream_dialog.ui index 066b90b857..2d70316471 100644 --- a/ui/qt/follow_stream_dialog.ui +++ b/ui/qt/follow_stream_dialog.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>594</width> - <height>620</height> + <width>609</width> + <height>600</height> </rect> </property> <property name="sizePolicy"> @@ -41,7 +41,7 @@ </widget> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,1,0,0"> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,1,0,0,0,0"> <item> <widget class="QComboBox" name="cbDirections"> <property name="sizeAdjustPolicy"> @@ -99,6 +99,16 @@ <item> <widget class="QSpinBox" name="streamNumberSpinBox"/> </item> + <item> + <widget class="QLabel" name="subStreamNumberLabel"> + <property name="text"> + <string>Substream</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="subStreamNumberSpinBox"/> + </item> </layout> </item> <item> diff --git a/ui/qt/main_window.h b/ui/qt/main_window.h index 2be67fbe16..a5d8077012 100644 --- a/ui/qt/main_window.h +++ b/ui/qt/main_window.h @@ -543,12 +543,14 @@ private slots: void on_actionAnalyzeDecodeAs_triggered(); void on_actionAnalyzeReloadLuaPlugins_triggered(); - void openFollowStreamDialog(follow_type_t type, guint stream_num, bool use_stream_index = true); + void openFollowStreamDialog(follow_type_t type, guint stream_num, guint sub_stream_num, bool use_stream_index = true); void openFollowStreamDialogForType(follow_type_t type); void on_actionAnalyzeFollowTCPStream_triggered(); void on_actionAnalyzeFollowUDPStream_triggered(); void on_actionAnalyzeFollowTLSStream_triggered(); void on_actionAnalyzeFollowHTTPStream_triggered(); + void on_actionAnalyzeFollowHTTP2Stream_triggered(); + void statCommandExpertInfo(const char *, void *); void on_actionAnalyzeExpertInfo_triggered(); diff --git a/ui/qt/main_window.ui b/ui/qt/main_window.ui index 1f03a535dc..06ffdd1df4 100644 --- a/ui/qt/main_window.ui +++ b/ui/qt/main_window.ui @@ -415,6 +415,7 @@ <addaction name="actionAnalyzeFollowUDPStream"/> <addaction name="actionAnalyzeFollowTLSStream"/> <addaction name="actionAnalyzeFollowHTTPStream"/> + <addaction name="actionAnalyzeFollowHTTP2Stream"/> </widget> <widget class="QMenu" name="menuConversationFilter"> <property name="title"> @@ -1711,6 +1712,14 @@ <string notr="true">Ctrl+Alt+Shift+H</string> </property> </action> + <action name="actionAnalyzeFollowHTTP2Stream"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>HTTP/2 Stream</string> + </property> + </action> <action name="actionStatisticsTcpStreamTcptrace"> <property name="text"> <string>Time Sequence (tcptrace)</string> diff --git a/ui/qt/main_window_slots.cpp b/ui/qt/main_window_slots.cpp index 3516730475..a164c20865 100644 --- a/ui/qt/main_window_slots.cpp +++ b/ui/qt/main_window_slots.cpp @@ -1114,7 +1114,7 @@ void MainWindow::recentActionTriggered() { void MainWindow::setMenusForSelectedPacket() { - gboolean is_ip = FALSE, is_tcp = FALSE, is_udp = FALSE, is_sctp = FALSE, is_tls = FALSE, is_rtp = FALSE, is_lte_rlc = FALSE, is_http = FALSE; + gboolean is_ip = FALSE, is_tcp = FALSE, is_udp = FALSE, is_sctp = FALSE, is_tls = FALSE, is_rtp = FALSE, is_lte_rlc = FALSE, is_http = FALSE, is_http2 = FALSE; /* Making the menu context-sensitive allows for easier selection of the desired item and has the added benefit, with large captures, of @@ -1173,6 +1173,7 @@ void MainWindow::setMenusForSelectedPacket() &is_ip, &is_tcp, &is_udp, &is_sctp, &is_tls, &is_rtp, &is_lte_rlc); is_http = proto_is_frame_protocol(capture_file_.capFile()->edt->pi.layers, "http"); + is_http2 = proto_is_frame_protocol(capture_file_.capFile()->edt->pi.layers, "http2"); } } @@ -1205,6 +1206,7 @@ void MainWindow::setMenusForSelectedPacket() main_ui_->actionAnalyzeFollowUDPStream->setEnabled(is_udp); main_ui_->actionAnalyzeFollowTLSStream->setEnabled(is_tls); main_ui_->actionAnalyzeFollowHTTPStream->setEnabled(is_http); + main_ui_->actionAnalyzeFollowHTTP2Stream->setEnabled(is_http2); foreach(QAction *cc_action, cc_actions) { cc_action->setEnabled(frame_selected); @@ -2697,7 +2699,7 @@ void MainWindow::on_actionAnalyzeReloadLuaPlugins_triggered() reloadLuaPlugins(); } -void MainWindow::openFollowStreamDialog(follow_type_t type, guint stream_num, bool use_stream_index) { +void MainWindow::openFollowStreamDialog(follow_type_t type, guint stream_num, guint sub_stream_num, bool use_stream_index) { FollowStreamDialog *fsd = new FollowStreamDialog(*this, capture_file_, type); connect(fsd, SIGNAL(updateFilter(QString, bool)), this, SLOT(filterPackets(QString, bool))); connect(fsd, SIGNAL(goToPacket(int)), packet_list_, SLOT(goToPacket(int))); @@ -2706,14 +2708,14 @@ void MainWindow::openFollowStreamDialog(follow_type_t type, guint stream_num, bo if (use_stream_index) { // If a specific conversation was requested, then ignore any previous // display filters and display all related packets. - fsd->follow("", true, stream_num); + fsd->follow("", true, stream_num, sub_stream_num); } else { fsd->follow(getFilter()); } } void MainWindow::openFollowStreamDialogForType(follow_type_t type) { - openFollowStreamDialog(type, 0, false); + openFollowStreamDialog(type, 0, 0, false); } void MainWindow::on_actionAnalyzeFollowTCPStream_triggered() @@ -2736,6 +2738,11 @@ void MainWindow::on_actionAnalyzeFollowHTTPStream_triggered() openFollowStreamDialogForType(FOLLOW_HTTP); } +void MainWindow::on_actionAnalyzeFollowHTTP2Stream_triggered() +{ + openFollowStreamDialogForType(FOLLOW_HTTP2); +} + void MainWindow::openSCTPAllAssocsDialog() { SCTPAllAssocsDialog *sctp_dialog = new SCTPAllAssocsDialog(this, capture_file_.capFile()); diff --git a/ui/qt/packet_list.cpp b/ui/qt/packet_list.cpp index 3304fe362c..bc8ffc287a 100644 --- a/ui/qt/packet_list.cpp +++ b/ui/qt/packet_list.cpp @@ -543,6 +543,7 @@ void PacketList::contextMenuEvent(QContextMenuEvent *event) submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowUDPStream")); submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowTLSStream")); submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowHTTPStream")); + submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowHTTP2Stream")); ctx_menu->addSeparator(); diff --git a/ui/qt/proto_tree.cpp b/ui/qt/proto_tree.cpp index d23c0879aa..bc72ca7b3d 100644 --- a/ui/qt/proto_tree.cpp +++ b/ui/qt/proto_tree.cpp @@ -285,6 +285,7 @@ void ProtoTree::contextMenuEvent(QContextMenuEvent *event) submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowUDPStream")); submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowTLSStream")); submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowHTTPStream")); + submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowHTTP2Stream")); ctx_menu.addSeparator(); } diff --git a/ui/qt/tcp_stream_dialog.ui b/ui/qt/tcp_stream_dialog.ui index d21074818e..d732f2665e 100644 --- a/ui/qt/tcp_stream_dialog.ui +++ b/ui/qt/tcp_stream_dialog.ui @@ -6,7 +6,7 @@ <rect> <x>0</x> <y>0</y> - <width>850</width> + <width>969</width> <height>640</height> </rect> </property> @@ -90,12 +90,12 @@ </item> <item> <widget class="QComboBox" name="graphTypeComboBox"> - <property name="frame"> - <bool>false</bool> - </property> <property name="focusPolicy"> <enum>Qt::TabFocus</enum> </property> + <property name="frame"> + <bool>false</bool> + </property> </widget> </item> <item> @@ -119,19 +119,19 @@ </widget> </item> <item> - <widget class="QDoubleSpinBox" name="maWindowSizeSpinBox" /> + <widget class="QDoubleSpinBox" name="maWindowSizeSpinBox"/> </item> <item> <widget class="QCheckBox" name="selectSACKsCheckBox"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> <property name="toolTip"> <string>Allow SACK segments as well as data packets to be selected by clicking on the graph</string> </property> <property name="text"> <string>Select SACKs</string> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> </widget> </item> <item> @@ -180,6 +180,9 @@ </item> <item> <widget class="QRadioButton" name="dragRadioButton"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> <property name="toolTip"> <string>Drag using the mouse button.</string> </property> @@ -189,9 +192,6 @@ <property name="checkable"> <bool>true</bool> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> <attribute name="buttonGroup"> <string notr="true">mouseButtonGroup</string> </attribute> @@ -199,6 +199,9 @@ </item> <item> <widget class="QRadioButton" name="zoomRadioButton"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> <property name="toolTip"> <string>Select using the mouse button.</string> </property> @@ -208,9 +211,6 @@ <property name="checkable"> <bool>true</bool> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> <attribute name="buttonGroup"> <string notr="true">mouseButtonGroup</string> </attribute> @@ -231,80 +231,80 @@ </item> <item> <widget class="QCheckBox" name="bySeqNumberCheckBox"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> <property name="toolTip"> <string>Display Round Trip Time vs Sequence Number</string> </property> <property name="text"> <string>RTT By Sequence Number</string> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> </widget> </item> <item> <widget class="QCheckBox" name="showSegLengthCheckBox"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> <property name="toolTip"> <string>Display graph of Segment Length vs Time</string> </property> <property name="text"> <string>Segment Length</string> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> </widget> </item> <item> <widget class="QCheckBox" name="showThroughputCheckBox"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> <property name="toolTip"> <string>Display graph of Mean Transmitted Bytes vs Time</string> </property> <property name="text"> <string>Throughput</string> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> </widget> </item> <item> <widget class="QCheckBox" name="showGoodputCheckBox"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> <property name="toolTip"> <string>Display graph of Mean ACKed Bytes vs Time</string> </property> <property name="text"> <string>Goodput</string> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> </widget> </item> <item> <widget class="QCheckBox" name="showRcvWinCheckBox"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> <property name="toolTip"> <string>Display graph of Receive Window Size vs Time</string> </property> <property name="text"> <string>Rcv Win</string> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> </widget> </item> <item> <widget class="QCheckBox" name="showBytesOutCheckBox"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> <property name="toolTip"> <string>Display graph of Outstanding Bytes vs Time</string> </property> <property name="text"> <string>Bytes Out</string> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> </widget> </item> <item> |