/src/wireshark/epan/dissectors/packet-p4rpc.c
Line | Count | Source |
1 | | /* |
2 | | * packet-p4rpc.c |
3 | | * |
4 | | * A Perforce P4RPC protocol packet dissector for Wireshark |
5 | | * Mark Wittenberg <mwittenberg@perforce.com>, <markw-perforce@wittenberg.us> |
6 | | * Copyright 2012-2022 Mark Wittenberg |
7 | | * Copyright 2023-2025 Perforce Software Inc. |
8 | | * Updates 2023-2024 by Tim Brazil and Jason Gibson |
9 | | * |
10 | | * It supports P4RPC over TLS as well as over TCP. |
11 | | * |
12 | | * Wireshark - Network traffic analyzer |
13 | | * By Gerald Combs <gerald@wireshark.org> |
14 | | * Copyright 1998 Gerald Combs |
15 | | * |
16 | | * SPDX-License-Identifier: GPL-2.0-or-later |
17 | | * |
18 | | * NB: C99 supports // comments, so I'm using them |
19 | | */ |
20 | | |
21 | | #include <wireshark.h> |
22 | | #include <epan/packet.h> |
23 | | #include <epan/prefs.h> |
24 | | #include <epan/dissectors/packet-tcp.h> |
25 | | |
26 | | // for TLS |
27 | | #include <epan/tvbuff.h> |
28 | | #include <epan/dissectors/packet-tls.h> |
29 | | |
30 | | // for expert info |
31 | | #include <epan/expert.h> |
32 | | |
33 | | #include <ws_symbol_export.h> |
34 | | #include <wsutil/plugins.h> |
35 | | #include <wsutil/str_util.h> |
36 | | #include <wsutil/nstime.h> |
37 | | #include <wsutil/to_str.h> |
38 | | #include <wsutil/strtoi.h> |
39 | | #include <epan/proto.h> |
40 | | |
41 | | // for prefs |
42 | | #include <wsutil/report_message.h> |
43 | | |
44 | | #include <locale.h> |
45 | | |
46 | | #include "config.h" |
47 | | |
48 | | /* |
49 | | * To decrypt TLS, don't set both ports to the same value; |
50 | | * that might prevent TLS from decrypting the packets |
51 | | * before P4RPC tries to dissect them. |
52 | | * Set the TLS port to the server port number; |
53 | | * TLS will automatically decrypt TLS packets |
54 | | * if the TCP port is not equal to the TLS port. |
55 | | * After decryption, if a packet's src or dst port |
56 | | * matches the "tls.port" then P4RPC will decrypt |
57 | | * the packet. |
58 | | * |
59 | | * These values are just the initial default value; |
60 | | * users can change the defaults via |
61 | | * Preferences -> Protocols -> P4RPC |
62 | | * and changing either the TCP or TLS ports |
63 | | * in the "TCP port" or "TLS port" text fields. |
64 | | * |
65 | | * P4RPC preferences can be more easily reached by right-clicking |
66 | | * on a P4RPC packet (if there is one) and choosing |
67 | | * Protocol Preferences -> P4RPC -> Open P4RPC Preferences |
68 | | * |
69 | | * Users can also right-click on an individual packet |
70 | | * and choose "Decode As..." but the same rules apply. |
71 | | * |
72 | | * If you don't care about supporting TLS then |
73 | | * set the TCP port to the desired port and set |
74 | | * the TLS port to anything else. |
75 | | */ |
76 | | #define P4RPC_TCP_PORT 1666 // Not IANA registered |
77 | | #define P4RPC_TLS_PORT 1667 // Not IANA registered |
78 | | |
79 | | /* |
80 | | * checksum(length) 1 |
81 | | * length 4 |
82 | | * |
83 | | * Every message starts with a header |
84 | | * and must contain at least one message. |
85 | | * The smallest message has: |
86 | | * - (1) empty name string + NUL terminator byte |
87 | | * - (4) val len = 0 |
88 | | * - (1) trailing NUL byte |
89 | | * ie, 6 bytes. |
90 | | */ |
91 | 7.72k | #define MSG_HEADER_LEN 5 |
92 | 2.75k | #define MSG_MIN_PARAM_LEN 6 |
93 | 5.27k | #define MSG_MIN_LEN (MSG_HEADER_LEN + MSG_MIN_PARAM_LEN) |
94 | 2.41k | #define MSG_MAX_LEN 0x1FFFFFFF |
95 | | |
96 | | // for expert message |
97 | | #define MSG_MIN_LEN_STR "11" |
98 | 16 | #define MSG_MAX_LEN_STR "536870911" |
99 | | |
100 | | // Normally set by Wireshark builds |
101 | | #ifndef VERSION |
102 | | # define VERSION "1.0.7" |
103 | | #endif |
104 | | |
105 | | /* |
106 | | * holds the total number of messages and pdus |
107 | | * in a reassembled packet |
108 | | */ |
109 | | typedef struct pdu_info { |
110 | | unsigned int num_msgs; |
111 | | unsigned int num_pdus; |
112 | | } pdu_info; |
113 | | |
114 | | static dissector_handle_t p4rpc_handle; |
115 | | |
116 | | // for tls |
117 | | static dissector_handle_t p4rpc_tls_handle; |
118 | | |
119 | | /* |
120 | | * If it is false then the parameters will be decoded |
121 | | * only as a "name=value" string. |
122 | | * |
123 | | * If prefs_varval_tree is true then the name=val parameters |
124 | | * in each message will be decoded as a tree (in addition |
125 | | * to as a "name=value" string), with separate child nodes |
126 | | * for each of the name, value length, and value. |
127 | | * |
128 | | * This options can be set in the P4RPC preferences dialog in |
129 | | * Preferences -> Protocols -> P4RPC |
130 | | * in the "Show message parameters as a tree" checkbox. |
131 | | */ |
132 | | typedef struct { |
133 | | unsigned int prev_tcp_port; |
134 | | unsigned int cur_tcp_port; |
135 | | unsigned int prev_tls_port; |
136 | | unsigned int cur_tls_port; |
137 | | bool prev_heur_enable; |
138 | | bool cur_heur_enable; |
139 | | bool prefer_tls; // enable TLS |
140 | | bool varval_tree; // enable displaying var=val as a tree |
141 | | bool time_utc; // display time fields in UTC rather than local time zone |
142 | | bool clear_info; // clear the INFO col and append count, funcs; else append count |
143 | | bool show_nul; // show the terminating NUL of a variable |
144 | | } p4prefs_t; |
145 | | |
146 | | /* |
147 | | * Our global preferences |
148 | | */ |
149 | | static p4prefs_t p4prefs = { |
150 | | P4RPC_TCP_PORT, // prev tcp port |
151 | | P4RPC_TCP_PORT, // cur tcp port |
152 | | P4RPC_TLS_PORT, // prev tls port |
153 | | P4RPC_TLS_PORT, // cur tls port |
154 | | true, // prev heur enable |
155 | | true, // cur heur enable |
156 | | true, // prefer_tls |
157 | | true, // varval_tree |
158 | | true, // time_utc |
159 | | true, // clear_info |
160 | | false // show_nul |
161 | | }; |
162 | | |
163 | | static char *decimal_point = "."; |
164 | | |
165 | | static int proto_p4rpc; // main tree proto |
166 | | static int ett_p4rpc; // first registered subtree |
167 | | static int ett_argtree; // second registered subtree |
168 | | static int ett_haverec; // third registered subtree |
169 | | static int ett_sync_time; // fourth registered subtree |
170 | | |
171 | | // header indices |
172 | | static int hf_p4rpc_checksum_len; |
173 | | static int hf_p4rpc_len; |
174 | | static int hf_p4rpc_varbytes; |
175 | | |
176 | | // name=value subtree indices |
177 | | static int hf_p4rpc_varname; |
178 | | static int hf_p4rpc_varvallen; |
179 | | static int hf_p4rpc_varval; |
180 | | static int hf_p4rpc_sync_time; |
181 | | static int hf_p4rpc_have_client_path; |
182 | | static int hf_p4rpc_have_depot_path; |
183 | | static int hf_p4rpc_have_file_rev; |
184 | | static int hf_p4rpc_have_file_type; |
185 | | static int hf_p4rpc_have_file_datetime; |
186 | | static int hf_p4rpc_func; // func value |
187 | | static int hf_p4rpc_handle; // handle value |
188 | | static int hf_p4rpc_action; // action value |
189 | | static int hf_p4rpc_confirm; // confirm value |
190 | | static int hf_p4rpc_num_msgs; // num msgs in packet |
191 | | static int hf_p4rpc_nul; // NUL terminator byte after param value |
192 | | |
193 | | // expert fields (for packet warnings, errors, etc) |
194 | | static expert_field ei_p4rpc_msg_short; |
195 | | static expert_field ei_p4rpc_msg_len_cksum; |
196 | | static expert_field ei_p4rpc_msg_len; |
197 | | static expert_field ei_p4rpc_msg_val_len; |
198 | | static expert_field ei_p4rpc_msg_val_nul; |
199 | | static expert_field ei_p4rpc_timestr_non_numeric; |
200 | | |
201 | | void proto_register_p4rpc(void); |
202 | | void proto_reg_handoff_p4rpc(void); |
203 | | |
204 | | /* |
205 | | * Time field number of displayed digits after decimal point (max 9) |
206 | | * All time fields are integers, so we set TIME_PREC to 0 |
207 | | */ |
208 | 0 | #define TIME_PREC 0 |
209 | | |
210 | | // used for calculating the checksum of the message length field |
211 | | static uint8_t |
212 | | calc_checksum( uint32_t val ) |
213 | 503 | { |
214 | 503 | return ( (val & 0xFF) ^ ((val >> 8) & 0xFF) ^ ((val >> 16) & 0xFF) ^ ((val >> 24) & 0xFF) ); |
215 | 503 | } |
216 | | |
217 | | /* |
218 | | * Generate an ISO-8601 datetime string to append to the field representation. |
219 | | * - Fill "obuf" with the ISO-8601 datetime representation of "secs" |
220 | | * with " (" prepended and ")" appended |
221 | | * - "secs" is the number of seconds since the epoch |
222 | | * - "secs" is an integer, so "obuf" will not contain a fractional part |
223 | | * - "len" must be the size of "obuf" and must be at least NSTIME_ISO8601_BUFSIZE+4 |
224 | | */ |
225 | | static bool |
226 | | p4rpc_secs_to_8601_str( char *obuf, unsigned int len, uint64_t secs, int precision ) |
227 | 0 | { |
228 | 0 | if( len < NSTIME_ISO8601_BUFSIZE+4 ) { |
229 | 0 | obuf[0] = '\0'; // ensure it's terminated |
230 | |
|
231 | 0 | return false; |
232 | 0 | } |
233 | | |
234 | 0 | nstime_t when = { secs, 0 }; |
235 | |
|
236 | 0 | obuf[0] = ' '; |
237 | 0 | obuf[1] = '('; |
238 | |
|
239 | 0 | format_nstime_as_iso8601( obuf+2, NSTIME_ISO8601_BUFSIZE, &when, |
240 | 0 | decimal_point, !p4prefs.time_utc, precision ); |
241 | |
|
242 | 0 | (void) g_strlcat( obuf, ")", NSTIME_ISO8601_BUFSIZE+4 ); // close our parens |
243 | |
|
244 | 0 | return true; |
245 | 0 | } |
246 | | |
247 | | /* |
248 | | * Generate an ISO-8601 datetime string to append to the field representation. |
249 | | * - Fill "obuf" with the ISO-8601 datetime representation of "secs" |
250 | | * with " (" prepended and ")" appended |
251 | | * - "ibuf" is the ASCII string representation of the seconds since the epoch |
252 | | * - "obuf" will not contain a fractional part |
253 | | * - "len" must be the size of "obuf" and must be at least NSTIME_ISO8601_BUFSIZE+4 |
254 | | * - "secs" will be set to the integer value of the "ibuf" string |
255 | | * - return true on success, false on error |
256 | | */ |
257 | | static bool |
258 | | p4rpc_secs_str_to_8601_str( char *obuf, unsigned int len, const char *ibuf, |
259 | | bool *non_numeric, uint64_t *secs, int precision ) |
260 | 0 | { |
261 | 0 | obuf[0] = '\0'; // ensure it's terminated |
262 | 0 | *secs = 0; |
263 | |
|
264 | 0 | nstime_t nst; |
265 | 0 | const char *endptr = NULL; |
266 | |
|
267 | 0 | if( (endptr = unix_epoch_to_nstime(&nst, ibuf)) ) { |
268 | 0 | if( non_numeric && *endptr ) |
269 | 0 | *non_numeric = true; |
270 | |
|
271 | 0 | *secs = nst.secs; |
272 | 0 | return p4rpc_secs_to_8601_str( obuf, len, nst.secs, precision ); |
273 | 0 | } |
274 | | |
275 | 0 | return false; |
276 | 0 | } |
277 | | |
278 | | /* |
279 | | * Special handling for the "haveRec" field value |
280 | | * because it's a compound structure. |
281 | | * |
282 | | * Display a "haveRec" structure as a subtree with a subitem |
283 | | * for each field of the structure. |
284 | | * |
285 | | * Append ISO-8601 datetime strings to the numeric display. |
286 | | */ |
287 | | static void |
288 | | p4rpc_decode_haverec( packet_info *pinfo, tvbuff_t *tvb, proto_item *parent_tree, |
289 | | uint32_t offset, uint32_t recsize ) |
290 | 0 | { |
291 | 0 | uint32_t cur_offset = offset; |
292 | | |
293 | | /* |
294 | | * Format of the haveRec field: |
295 | | * |
296 | | * struct haveRec { |
297 | | * char clientPath[]; |
298 | | * char depotPath[]; |
299 | | * uint32_t rev; |
300 | | * uint32_t type; |
301 | | * uint32_t date; // sometimes a uint64_t |
302 | | * }; |
303 | | */ |
304 | | |
305 | | /* |
306 | | * add a new item item to our sub-subtree |
307 | | * and attach a new "value" sub-subtree to it |
308 | | */ |
309 | 0 | proto_item *have_tree = proto_item_add_subtree( parent_tree, ett_haverec ); |
310 | | |
311 | | // add the client path to our sub-sub-subtree |
312 | 0 | uint32_t client_len; |
313 | |
|
314 | 0 | tvb_get_stringz_enc( pinfo->pool, tvb, cur_offset, &client_len, ENC_UTF_8 ); |
315 | 0 | proto_tree_add_item( have_tree, hf_p4rpc_have_client_path, tvb, cur_offset, |
316 | 0 | client_len, ENC_UTF_8 ); |
317 | 0 | cur_offset += client_len; |
318 | | |
319 | | // add the depot path to our sub-sub-subtree |
320 | 0 | uint32_t depot_len; |
321 | |
|
322 | 0 | tvb_get_stringz_enc( pinfo->pool, tvb, cur_offset, &depot_len, ENC_UTF_8 ); |
323 | 0 | proto_tree_add_item( have_tree, hf_p4rpc_have_depot_path, tvb, cur_offset, |
324 | 0 | depot_len, ENC_UTF_8 ); |
325 | 0 | cur_offset += depot_len; |
326 | | |
327 | | // add the file rev number to our sub-sub-subtree |
328 | 0 | proto_tree_add_item( have_tree, hf_p4rpc_have_file_rev, tvb, cur_offset, |
329 | 0 | 4, ENC_LITTLE_ENDIAN ); |
330 | 0 | cur_offset += 4; |
331 | | |
332 | | // add the file type to our sub-sub-subtree |
333 | 0 | proto_tree_add_item( have_tree, hf_p4rpc_have_file_type, tvb, cur_offset, |
334 | 0 | 4, ENC_LITTLE_ENDIAN ); |
335 | 0 | cur_offset += 4; |
336 | | |
337 | | /* |
338 | | * Add the file datetime to our sub-sub-subtree. |
339 | | * If there are 8 bytes remaining before the NUL terminator |
340 | | * then all 8 are the 64-bit datetime; |
341 | | * otherwise the next 4 bytes are the 32-bit datetime. |
342 | | * |
343 | | * Datetimes up to and including 03:14:07 UTC on 19 January 2038 |
344 | | * will use 4 bytes, and datetimes after that will use 8 bytes. |
345 | | * |
346 | | * A datetime of 0 means that a datetime was not set. |
347 | | */ |
348 | |
|
349 | 0 | unsigned int datetime_len = 4; |
350 | 0 | uint64_t timestamp = 0; |
351 | |
|
352 | 0 | if( cur_offset - offset + 8 <= recsize ) { |
353 | | // 64-bit timestamp |
354 | 0 | timestamp = tvb_get_letoh64( tvb, cur_offset ); |
355 | 0 | datetime_len = 8; |
356 | 0 | } else { |
357 | | // 32-bit timestamp |
358 | 0 | timestamp = tvb_get_letohl( tvb, cur_offset ); |
359 | 0 | } |
360 | |
|
361 | 0 | proto_item *date_item = proto_tree_add_item( have_tree, |
362 | 0 | hf_p4rpc_have_file_datetime, tvb, cur_offset, |
363 | 0 | datetime_len, ENC_LITTLE_ENDIAN ); |
364 | |
|
365 | 0 | if( timestamp ) { |
366 | 0 | char buf[NSTIME_ISO8601_BUFSIZE+4]; |
367 | |
|
368 | 0 | p4rpc_secs_to_8601_str( buf, sizeof(buf), timestamp, TIME_PREC ); |
369 | 0 | proto_item_append_text( date_item, "%s", buf ); |
370 | 0 | } else { |
371 | | // Annotate that a datetime was not set |
372 | 0 | proto_item_append_text( date_item, " (not set)" ); |
373 | 0 | } |
374 | 0 | } |
375 | | |
376 | | /* |
377 | | * Display a time field (decimal string) as an ISO-8601 string. |
378 | | * - Special-case "syncTime" to allow filtering as an integer. |
379 | | * - Used only when "var=val" is a subtree, because for "syncTime" |
380 | | * it will append text to that subtree. |
381 | | * - Used for time fields that are decimal strings rather than |
382 | | * 32- or 64-bit integer fields. |
383 | | */ |
384 | | static void |
385 | | p4rpc_decode_time_str( tvbuff_t *tvb, proto_item *parent_tree, |
386 | | proto_item *item, const uint8_t *varname, const uint8_t *varval, |
387 | | bool *non_numeric, uint32_t vallen, uint32_t val_offset ) |
388 | 0 | { |
389 | | /* |
390 | | * All params whose name ends in "Time" have values |
391 | | * that are a numeric string of seconds since the epoch. |
392 | | * Append the formatted datetime to the value field |
393 | | * if the value is set (ie, non-zero). |
394 | | */ |
395 | 0 | bool is_sync_time = strcmp((const char *)varname, "syncTime") == 0; |
396 | |
|
397 | 0 | char buf[NSTIME_ISO8601_BUFSIZE+4]; |
398 | 0 | uint64_t secs = 0; |
399 | |
|
400 | 0 | p4rpc_secs_str_to_8601_str( buf, sizeof(buf), |
401 | 0 | (const char *)varval, non_numeric, &secs, TIME_PREC ); |
402 | |
|
403 | 0 | if( secs ) { |
404 | 0 | proto_item_append_text( item, "%s", buf ); |
405 | 0 | proto_item_append_text( parent_tree, "%s", buf ); |
406 | 0 | } else { |
407 | | // Annotate that a datetime wasn't set |
408 | 0 | proto_item_append_text( item, " (not set)" ); |
409 | 0 | proto_item_append_text( parent_tree, " (not set)" ); |
410 | 0 | } |
411 | | |
412 | | /* |
413 | | * As a special case, allow filtering on "syncTime". |
414 | | * |
415 | | * We might want to consider allowing filtering |
416 | | * on other timestamp fields. |
417 | | * |
418 | | * These field values are decimal strings, so comparing |
419 | | * them except for something other than equality or inequality |
420 | | * requires creating a separate numeric field for each one, |
421 | | * as we have done here for "syncTime". |
422 | | */ |
423 | 0 | if( is_sync_time ) { |
424 | | // add the syncTime integer field |
425 | 0 | proto_item *sync_item_val = |
426 | 0 | proto_tree_add_uint64_format_value( parent_tree, |
427 | 0 | hf_p4rpc_sync_time, tvb, val_offset, vallen, |
428 | 0 | secs, "%" PRIu64, secs ); |
429 | | |
430 | | /* |
431 | | * Hide the syncTime integer field because we already show |
432 | | * the time string and just want to allow filtering by its |
433 | | * value as an integer. |
434 | | */ |
435 | 0 | proto_item_set_hidden( sync_item_val ); |
436 | 0 | } |
437 | 0 | } |
438 | | |
439 | | /* |
440 | | * Display a time field (decimal string) as an ISO-8601 string. |
441 | | * - Used only when "var=val" is a leaf node, not a subtree |
442 | | * - Used for time fields that are decimal strings rather than |
443 | | * 32- or 64-bit integer fields. |
444 | | * - Appends time string or "(not set)" to the leaf node. |
445 | | */ |
446 | | static void |
447 | | item_decode_time_str( proto_item *tree, const uint8_t *varval, bool *non_numeric ) |
448 | 0 | { |
449 | 0 | char buf[NSTIME_ISO8601_BUFSIZE+4]; |
450 | 0 | uint64_t secs = 0; |
451 | |
|
452 | 0 | p4rpc_secs_str_to_8601_str( buf, sizeof(buf), |
453 | 0 | (const char *)varval, non_numeric, &secs, TIME_PREC ); |
454 | |
|
455 | 0 | if( secs ) { |
456 | 0 | proto_item_append_text( tree, "%s", buf ); |
457 | 0 | } else { |
458 | | // Annotate a datetime that wasn't set |
459 | 0 | proto_item_append_text( tree, " (not set)" ); |
460 | 0 | } |
461 | 0 | } |
462 | | |
463 | | /* |
464 | | * dissect a single P4RPC message (request or response) |
465 | | * |
466 | | * Format of a P4RPC message: |
467 | | * offset len (always LE) description |
468 | | * ------ --------------- ----------- |
469 | | * 0 1 byte len XOR [see calc_checksum()] |
470 | | * 1 4 bytes len (uint32_t) |
471 | | * 5 N parameters |
472 | | * Each param: |
473 | | * * M bytes ASCIIZ param name |
474 | | * 1 byte NUL byte (the Z in ASCIIZ) |
475 | | * 4 bytes length of the data (uint32_t) |
476 | | * data_len bytes data (value) |
477 | | * 1 byte NUL |
478 | | */ |
479 | | static uint32_t |
480 | | dissect_one_p4rpc_message( tvbuff_t *tvb, uint32_t offset, uint32_t *seqno _U_, |
481 | | packet_info *pinfo, proto_tree *tree, unsigned int msg_idx, void *data _U_ ) |
482 | 350 | { |
483 | 350 | uint32_t msg_start = offset; |
484 | | |
485 | | // add an item to the parent tree and attach a subtree to it |
486 | 350 | proto_item *ti = proto_tree_add_item( tree, proto_p4rpc, tvb, 0, -1, ENC_NA ); |
487 | 350 | proto_item *ti_tree = proto_item_add_subtree( ti, ett_p4rpc ); |
488 | | |
489 | | // we need at least MSG_HEADER_LEN bytes to find the message length |
490 | 350 | if ( !tvb_bytes_exist(tvb, offset, MSG_HEADER_LEN) ) { |
491 | 19 | proto_tree_add_expert_format( ti_tree, pinfo, &ei_p4rpc_msg_short, tvb, |
492 | 19 | 0, MSG_HEADER_LEN, |
493 | 19 | "Invalid message: Fewer than %d bytes available", MSG_HEADER_LEN ); |
494 | 19 | } |
495 | | |
496 | | // add the checksum of the length field as an item to our subtree |
497 | 350 | uint8_t cksum_of_len = tvb_get_uint8( tvb, offset ); |
498 | 350 | proto_tree_add_item( ti_tree, hf_p4rpc_checksum_len, tvb, offset, 1, ENC_NA ); |
499 | 350 | offset++; |
500 | | |
501 | | // add the length field as an item to our subtree |
502 | 350 | uint32_t msg_len = tvb_get_letohl( tvb, offset ); |
503 | 350 | proto_tree_add_item( ti_tree, hf_p4rpc_len, tvb, offset, 4, ENC_LITTLE_ENDIAN ); |
504 | 350 | offset += 4; |
505 | | |
506 | | // warn if the length checksum doesn't match the actual checksum |
507 | 350 | if( cksum_of_len != calc_checksum(msg_len) ) { |
508 | 90 | proto_tree_add_expert_format( ti_tree, pinfo, &ei_p4rpc_msg_len_cksum, tvb, 0, MSG_HEADER_LEN, |
509 | 90 | "checksum mismatch: expected 0x%0X, got 0x%0X for length %d (0x%0X)", |
510 | 90 | cksum_of_len, calc_checksum(msg_len), msg_len, msg_len ); |
511 | 90 | } |
512 | | |
513 | | // warn if the message length is out of bounds |
514 | 350 | if ( msg_len < MSG_MIN_LEN || msg_len > MSG_MAX_LEN ) { |
515 | 231 | proto_tree_add_expert_format( ti_tree, pinfo, &ei_p4rpc_msg_len, tvb, 1, 4, |
516 | 231 | "Message length %d (0x%0X) not in range [%d, %d]", msg_len, msg_len, |
517 | 231 | MSG_MIN_LEN, MSG_MAX_LEN ); |
518 | 231 | } |
519 | | |
520 | | // there's something wrong if msg len exceeds the captured data |
521 | 350 | uint32_t tvb_len = tvb_captured_length( tvb ); |
522 | 350 | if( msg_len > tvb_len - MSG_HEADER_LEN ) { |
523 | 71 | proto_tree_add_expert_format( ti_tree, pinfo, &ei_p4rpc_msg_len, tvb, 1, 4, |
524 | 71 | "Message length %d (0x%0X) exceeds tvb length (%d)", |
525 | 71 | msg_len, msg_len, tvb_len - MSG_HEADER_LEN ); |
526 | | |
527 | | // don't try to dissect past the end of the captured data |
528 | 71 | msg_len = tvb_len - MSG_HEADER_LEN; |
529 | 71 | } |
530 | | |
531 | | // process each param {name, value} |
532 | 874 | while( offset - msg_start < msg_len ) |
533 | 524 | { |
534 | | // the offset to the start of the name of the parameter |
535 | 524 | uint32_t name_offset = offset; |
536 | 524 | uint32_t name_len; |
537 | | |
538 | | // get the param name |
539 | 524 | const uint8_t *varname = tvb_get_stringz_enc( pinfo->pool, tvb, offset, &name_len, ENC_UTF_8 ); |
540 | 524 | offset += name_len; // NB: name_len counts the NUL terminator byte |
541 | | |
542 | 524 | uint32_t vallen_offset = offset; // the offset to the start of the value length |
543 | | |
544 | | // get the param value length |
545 | 524 | uint32_t vallen = tvb_get_letohl( tvb, offset ); |
546 | 524 | offset += 4; |
547 | | |
548 | | // get the param value |
549 | 524 | uint32_t val_offset = offset; // the offset to the start of the value |
550 | 524 | uint8_t *varval = tvb_get_string_enc( pinfo->pool, tvb, offset, vallen, ENC_UTF_8 ); |
551 | 524 | int32_t tot_var_len = name_len + vallen + 4; |
552 | | |
553 | | // get the (expected) NUL byte |
554 | 524 | uint32_t nul_offset = offset + vallen; |
555 | 524 | uint8_t zero = tvb_get_uint8( tvb, nul_offset ); |
556 | | |
557 | 1.58k | #define ARGBUF_SZ 128 |
558 | | // make our "var=val" string |
559 | 524 | char *argbuf = wmem_alloc( pinfo->pool, ARGBUF_SZ ); |
560 | 524 | int bytes_written; |
561 | | // Pass in bufsz-1 to save space for closing bracket. |
562 | 524 | bytes_written = snprintf( argbuf, ARGBUF_SZ-1, "%s = {%s", |
563 | 524 | (*varname ? varname : (const uint8_t *)"<none>"), varval ); |
564 | | |
565 | | /* |
566 | | * Returns number of bytes that would have been written |
567 | | * (not including the null terminator) if not for the |
568 | | * buffer size limit. |
569 | | */ |
570 | 524 | if( bytes_written > ARGBUF_SZ - 2 ) { // Possibly truncated |
571 | 11 | ws_utf8_truncate( argbuf, ARGBUF_SZ-2 ); // ensure no partial char at the end |
572 | 11 | } |
573 | 524 | (void) g_strlcat( argbuf, "}", ARGBUF_SZ ); // close our bracket |
574 | | |
575 | | // used to check if a value length goes past the end of the message |
576 | 524 | uint32_t bytes_left = msg_len - (offset - msg_start) + MSG_HEADER_LEN; |
577 | | |
578 | | // is our "var=val" item a tree or just text? |
579 | 524 | if( p4prefs.varval_tree ) |
580 | 408 | { |
581 | | // add a new item item to our subtree and attach a new "var=value" sub-subtree to it |
582 | 408 | proto_item *ti_arg = proto_tree_add_item( ti_tree, proto_p4rpc, tvb, name_offset, tot_var_len, ENC_NA ); |
583 | 408 | proto_item *arg_tree = proto_item_add_subtree( ti_arg, ett_argtree ); |
584 | 408 | proto_item_set_text( arg_tree, "%s", argbuf ); |
585 | | |
586 | | // add the param name to our sub-subtree |
587 | 408 | proto_tree_add_item( arg_tree, hf_p4rpc_varname, tvb, name_offset, name_len, ENC_UTF_8 ); |
588 | | |
589 | | // add the length of the param value to our sub-subtree |
590 | 408 | proto_tree_add_item( arg_tree, hf_p4rpc_varvallen, tvb, vallen_offset, 4, ENC_LITTLE_ENDIAN ); |
591 | | |
592 | | // add the param value to our sub-subtree |
593 | 408 | proto_item *vv_rec = proto_tree_add_item( arg_tree, hf_p4rpc_varval, tvb, val_offset, vallen, ENC_NA ); |
594 | | |
595 | | /* |
596 | | * Special handling for the compound structure fields: |
597 | | * haveRec |
598 | | * TODO: |
599 | | * - Decode the other "*Rec" structure fields. |
600 | | * |
601 | | * Special handling for time fields -- append ISO-8601 datetime |
602 | | * strings to the numeric display: |
603 | | * depotTime |
604 | | * headModTime |
605 | | * headTime |
606 | | * maxLockTime |
607 | | * maxPauseTime |
608 | | * theirTime |
609 | | */ |
610 | 408 | if( strcmp((const char *)varname, "haveRec") == 0 ) { |
611 | 0 | p4rpc_decode_haverec( pinfo, tvb, arg_tree, val_offset, vallen ); |
612 | 408 | } else if( g_str_has_suffix((const char *)varname, "Time") ) { |
613 | 0 | bool non_numeric = false; |
614 | |
|
615 | 0 | p4rpc_decode_time_str( tvb, arg_tree, vv_rec, varname, |
616 | 0 | varval, &non_numeric, vallen, val_offset ); |
617 | | |
618 | | // report non-numeric char in time string |
619 | 0 | if( non_numeric ) |
620 | 0 | { |
621 | 0 | proto_tree_add_expert_format( arg_tree, pinfo, |
622 | 0 | &ei_p4rpc_timestr_non_numeric, tvb, val_offset, vallen, |
623 | 0 | "Time string contains non-numeric character" ); |
624 | 0 | } |
625 | 0 | } |
626 | | |
627 | | // decode the nul terminator |
628 | 408 | proto_item *nul_item = |
629 | 408 | proto_tree_add_item( arg_tree, hf_p4rpc_nul, tvb, nul_offset, 1, ENC_NA ); |
630 | 408 | if( !p4prefs.show_nul ) |
631 | 408 | proto_item_set_hidden( nul_item ); |
632 | | |
633 | 408 | if( zero ) |
634 | 34 | { |
635 | | // add a new subtree to the value field for the expert info |
636 | 34 | proto_item *expert_tree = proto_item_add_subtree( arg_tree, ett_argtree ); |
637 | 34 | proto_tree_add_expert_format( expert_tree, pinfo, &ei_p4rpc_msg_val_nul, |
638 | 34 | tvb, nul_offset, 1, |
639 | 34 | "Terminating NUL byte is 0x%0X instead of 0x0", |
640 | 34 | zero ); |
641 | 34 | } |
642 | | |
643 | | // does the value go past the end of the message? |
644 | 408 | if( vallen > bytes_left ) |
645 | 0 | { |
646 | 0 | proto_tree_add_expert_format( arg_tree, pinfo, &ei_p4rpc_msg_val_len, |
647 | 0 | tvb, vallen_offset, 4, |
648 | 0 | "Invalid value: length %d > %d bytes remaining in message", |
649 | 0 | vallen, bytes_left ); |
650 | | |
651 | | // don't try to read past the end |
652 | 0 | vallen = bytes_left; |
653 | 0 | } |
654 | 408 | } else { |
655 | | // add "var=value" to be a leaf item to our subtree |
656 | 116 | proto_item *arg_tree = |
657 | 116 | proto_tree_add_bytes_format( ti_tree, hf_p4rpc_varbytes, tvb, |
658 | 116 | name_offset, tot_var_len, NULL, "%s", argbuf ); |
659 | | |
660 | | /* |
661 | | * All params whose name ends in "Time" have values |
662 | | * that are a numeric string of seconds since the epoch. |
663 | | * Append the formatted datetime to the value field |
664 | | * if the value is set (ie, non-zero). |
665 | | */ |
666 | 116 | if( g_str_has_suffix((const char *)varname, "Time") ) { |
667 | 0 | bool non_numeric = false; |
668 | |
|
669 | 0 | item_decode_time_str( arg_tree, varval, &non_numeric ); |
670 | | |
671 | | // report non-numeric char in time string |
672 | 0 | if( non_numeric ) |
673 | 0 | { |
674 | 0 | proto_tree_add_expert_format( ti_tree, pinfo, |
675 | 0 | &ei_p4rpc_timestr_non_numeric, tvb, val_offset, |
676 | 0 | vallen, |
677 | 0 | "Time string contains non-numeric character" ); |
678 | 0 | } |
679 | 0 | } |
680 | | |
681 | | // decode the nul terminator |
682 | 116 | proto_item *nul_item = |
683 | 116 | proto_tree_add_item( ti_tree, hf_p4rpc_nul, tvb, nul_offset, 1, ENC_NA ); |
684 | 116 | if( !p4prefs.show_nul ) |
685 | 0 | proto_item_set_hidden( nul_item ); |
686 | | |
687 | | // report incorrect NUL terminator |
688 | 116 | if( zero ) |
689 | 0 | { |
690 | 0 | proto_tree_add_expert_format( ti_tree, pinfo, &ei_p4rpc_msg_val_nul, |
691 | 0 | tvb, nul_offset, 1, |
692 | 0 | "Invalid terminating NUL byte is 0x%0X instead of 0x0", |
693 | 0 | zero ); |
694 | 0 | } |
695 | | |
696 | | // does the value go past the end of the message? |
697 | 116 | if( vallen > bytes_left ) |
698 | 0 | { |
699 | 0 | proto_tree_add_expert_format( ti_tree, pinfo, &ei_p4rpc_msg_val_len, |
700 | 0 | tvb, vallen_offset, 4, |
701 | 0 | "Invalid value: var \"%s\" length %d > %d bytes remaining in message", |
702 | 0 | varname, vallen, bytes_left ); |
703 | | |
704 | | // don't try to read past the end |
705 | 0 | vallen = bytes_left; |
706 | 0 | } |
707 | 116 | } // var=val subtree |
708 | | |
709 | | /* |
710 | | * special "alias" handling for "func" and "handle" params, etc. |
711 | | */ |
712 | | |
713 | 524 | if( strcmp((char *)varname, "func") == 0 ) |
714 | 0 | { |
715 | 0 | if( p4prefs.clear_info ) { |
716 | | // append the current func name to the INFO column |
717 | 0 | col_append_fstr( pinfo->cinfo, COL_INFO, |
718 | 0 | (msg_idx == 0) ? "%s" : " | %s", varval ); |
719 | 0 | } |
720 | | |
721 | | // add the "func" name to the subtree (not sub-subtree) text |
722 | 0 | proto_item_append_text( ti_tree, " : [%s] [len=%d]", |
723 | 0 | varval, offset+vallen+1 ); |
724 | | |
725 | | /* |
726 | | * Special case "func" and "handle" for convenience, |
727 | | * so that a filter can be written as: |
728 | | * p4rpc.func == "sync" |
729 | | * rather than as: |
730 | | * p4rpc.var.name == "func" && p4rpc.var.val == "sync" |
731 | | * |
732 | | * Allow filtering by the value of the "func" param. |
733 | | * Don't show it because the "func" is already shown. |
734 | | */ |
735 | 0 | proto_item *func_item = proto_tree_add_item( |
736 | 0 | ti_tree, hf_p4rpc_func, tvb, val_offset, vallen, ENC_UTF_8); |
737 | 0 | proto_item_set_hidden( func_item ); |
738 | |
|
739 | 0 | } |
740 | | |
741 | 524 | if( strcmp((char *)varname, "handle") == 0 ) |
742 | 0 | { |
743 | | /* |
744 | | * Special case "func" and "handle" for convenience, |
745 | | * so that a filter can be written as: |
746 | | * p4rpc.handle == "sync" |
747 | | * rather than as: |
748 | | * p4rpc.var.name == "handle" && p4rpc.var.val == "sync" |
749 | | * Allow filtering by the value of the "handle" param. |
750 | | * Don't show it because the "handle" is already shown. |
751 | | */ |
752 | 0 | proto_item *handle_item = proto_tree_add_item( |
753 | 0 | ti_tree, hf_p4rpc_handle, tvb, val_offset, vallen, ENC_UTF_8 ); |
754 | 0 | proto_item_set_hidden( handle_item ); |
755 | 0 | } |
756 | | |
757 | 524 | if( strcmp((char *)varname, "action") == 0 ) |
758 | 0 | { |
759 | 0 | proto_item *handle_item = proto_tree_add_item( |
760 | 0 | ti_tree, hf_p4rpc_action, tvb, val_offset, vallen, ENC_UTF_8 ); |
761 | 0 | proto_item_set_hidden( handle_item ); |
762 | 0 | } |
763 | | |
764 | 524 | if( strcmp((char *)varname, "confirm") == 0 ) |
765 | 1 | { |
766 | 1 | proto_item *handle_item = proto_tree_add_item( |
767 | 1 | ti_tree, hf_p4rpc_confirm, tvb, val_offset, vallen, ENC_UTF_8 ); |
768 | 1 | proto_item_set_hidden( handle_item ); |
769 | 1 | } |
770 | | |
771 | 524 | offset += vallen + 1; // + 1 for the NUL data terminator |
772 | 524 | } |
773 | | |
774 | 350 | return offset; |
775 | 350 | } |
776 | | |
777 | | /* |
778 | | * This method dissects a fully reassembled PDU |
779 | | * - it will contain one complete P4RPC message |
780 | | */ |
781 | | static int |
782 | | dissect_p4rpc_pdu( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data ) |
783 | 325 | { |
784 | 325 | uint32_t offset = 0; |
785 | 325 | uint32_t tvb_len = tvb_captured_length( tvb ); |
786 | 325 | uint32_t seqno = 0xFFFF; |
787 | 325 | pdu_info *info = (pdu_info *) data; |
788 | | |
789 | 325 | ++info->num_pdus; // we get called once per pdu |
790 | | |
791 | | /* |
792 | | * Process every message in this PDU |
793 | | * (but a PDU is exactly one message |
794 | | * so we pass through the loop exactly once). |
795 | | */ |
796 | 675 | while( offset < tvb_len ) |
797 | 350 | { |
798 | 350 | offset = dissect_one_p4rpc_message( tvb, offset, &seqno, pinfo, tree, info->num_msgs++, data ); |
799 | 350 | } |
800 | | |
801 | 325 | return offset; |
802 | 325 | } |
803 | | |
804 | | /* |
805 | | * determine PDU length of protocol p4rpc |
806 | | */ |
807 | | static unsigned int |
808 | | get_p4rpc_pdu_len( packet_info *pinfo _U_, tvbuff_t *tvb, int offset, void *data _U_ ) |
809 | 326 | { |
810 | 326 | return tvb_get_letohl(tvb, offset+1) + MSG_HEADER_LEN; |
811 | 326 | } |
812 | | |
813 | | /* |
814 | | * The main dissecting routine: |
815 | | * - A P4RPC Protocol Data Unit (PDU) consists of one P4RPC message. |
816 | | * - A single (large) PDU may be split into multiple packets. |
817 | | * In this case Wireshark will dissect all but the last |
818 | | * of the component packets as TCP, adding "[TCP PDU reassembled in NN]" |
819 | | * to the INFO field of each packet (where NN is the number of the final |
820 | | * packet in the PDU), and dissect the final packet of the PDU |
821 | | * as the concatenation of all of the component packets. |
822 | | * |
823 | | * - "data" is a pointer to a pdu_info that is a count |
824 | | * of the numbers of messages and PDUs that have been dissected. |
825 | | * Because there is exactly one message in a PDU, these counts |
826 | | * are always equal, but if the protocol ever allows bundling |
827 | | * multiple messages in a PDU then the counts can differ. |
828 | | */ |
829 | | static int |
830 | | dissect_p4rpc( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_ ) |
831 | 66 | { |
832 | 66 | pdu_info info = { 0, 0 }; |
833 | | |
834 | 66 | col_set_str( pinfo->cinfo, COL_PROTOCOL, "P4RPC" ); |
835 | | |
836 | | // clear the INFO column if the user requested it |
837 | 66 | if( p4prefs.clear_info ) |
838 | 66 | col_clear( pinfo->cinfo, COL_INFO ); |
839 | | |
840 | | // group all of the messages in this set of frames |
841 | 66 | col_append_str( pinfo->cinfo, COL_INFO, " {" ); |
842 | | // reassemble TCP packets and call dissect_p4rpc_pdu on each pdu |
843 | 66 | tcp_dissect_pdus( tvb, pinfo, tree, true, MSG_HEADER_LEN, |
844 | 66 | get_p4rpc_pdu_len, dissect_p4rpc_pdu, &info ); |
845 | 66 | col_append_str( pinfo->cinfo, COL_INFO, "}" ); |
846 | | |
847 | | /* |
848 | | * Prefix {msgs=NN} to the INFO of the packet tree where NN |
849 | | * is the number of messages in the packet. |
850 | | * Note: If there are multiple sets of messages then the message |
851 | | * counts will be in reverse order. |
852 | | */ |
853 | 66 | col_prepend_fstr( pinfo->cinfo, COL_INFO, |
854 | 66 | (p4prefs.clear_info ? "[msgs=%d]" : "[msgs=%d] "), info.num_msgs ); |
855 | | |
856 | | /* |
857 | | * Similar to "func" and "hidden", allow filtering by number |
858 | | * of messages in a packet. |
859 | | * Hidden because we already show it in COL_INFO. |
860 | | */ |
861 | 66 | proto_item *num_msgs_item = proto_tree_add_uint( |
862 | 66 | tree, hf_p4rpc_num_msgs, tvb, 0, 0, info.num_msgs ); |
863 | 66 | proto_item_set_hidden( num_msgs_item ); |
864 | | |
865 | 66 | return tvb_captured_length( tvb ); |
866 | 66 | } |
867 | | |
868 | | /* |
869 | | * Called to dissect a decrypted tvb, so just call |
870 | | * our p4rpc dissector. |
871 | | */ |
872 | | static int |
873 | | dissect_tls_p4rpc( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data ) |
874 | 0 | { |
875 | 0 | return dissect_p4rpc( tvb, pinfo, tree, data ); |
876 | 0 | } |
877 | | |
878 | | /* |
879 | | * Return true if this is a P4RPC packet |
880 | | * else return false so that another dissector can try. |
881 | | * - if the checksum matches and length is reasonable |
882 | | * then assume that it is but that's not very definitive. |
883 | | * Don't enable USE_DEBUG unless you want to see a lot of error messages, |
884 | | * because non-P4RPC packets are very likely going to emit one. |
885 | | */ |
886 | | static bool |
887 | | p4rpc_check_heur( packet_info *pinfo _U_, tvbuff_t *tvb, int offset, void *data _U_ ) |
888 | 2.33k | { |
889 | 2.33k | uint8_t cksum_of_len = 0; |
890 | 2.33k | uint32_t msg_len = 0; |
891 | | |
892 | | // if we don't have at least our header, it isn't P4RPC |
893 | 2.33k | if( !tvb_bytes_exist(tvb, offset, MSG_HEADER_LEN) ) { |
894 | 157 | return false; |
895 | 157 | } |
896 | | |
897 | | // fetch the checksum from the tvb |
898 | 2.17k | cksum_of_len = tvb_get_uint8(tvb, offset); |
899 | | |
900 | | // not P4RPC if the message is too small or too big |
901 | 2.17k | msg_len = tvb_get_letohl(tvb, offset+1); |
902 | | |
903 | 2.17k | if( msg_len < MSG_MIN_LEN || msg_len > MSG_MAX_LEN) { |
904 | 1.42k | return false; |
905 | 1.42k | } |
906 | | |
907 | | // not P4RPC if the message length exceeds the captured data |
908 | 752 | uint32_t tvb_len = tvb_captured_length( tvb ); |
909 | 752 | if( msg_len > tvb_len - MSG_HEADER_LEN ) { |
910 | 670 | return false; |
911 | 670 | } |
912 | | |
913 | | // not P4RPC if the checksum is wrong |
914 | 82 | if( cksum_of_len != calc_checksum(msg_len) ) { |
915 | 36 | return false; |
916 | 36 | } |
917 | | |
918 | 46 | return true; |
919 | 82 | } |
920 | | |
921 | | /* |
922 | | * Check to see if this packet is P4RPC and dissect it if so, |
923 | | * else return false and let another dissector try. |
924 | | */ |
925 | | static bool |
926 | | dissect_p4rpc_heur( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data ) |
927 | 2.33k | { |
928 | 2.33k | if( !p4rpc_check_heur(pinfo, tvb, 0, data) ) |
929 | 2.28k | return false; |
930 | | |
931 | | /* |
932 | | * Assume that this is a P4RPC packet and set p4rpc as the dissector |
933 | | * for the rest of the packets in this conversation. |
934 | | */ |
935 | 46 | conversation_t *conversation = find_or_create_conversation( pinfo ); |
936 | 46 | if( conversation ) |
937 | 46 | conversation_set_dissector( conversation, p4rpc_handle ); |
938 | | |
939 | | // go ahead and dissect it |
940 | 46 | int rslt = dissect_p4rpc( tvb, pinfo, tree, NULL ); |
941 | | |
942 | 46 | return rslt > 0 ? true : false; |
943 | 2.33k | } |
944 | | |
945 | | /* |
946 | | * proto_reg_handoff_p4rpc() calls this with isInit == true |
947 | | * - set up initially, ignoring the p4prefs.pref_* members |
948 | | * prefs_cb() calls this with isInit == false |
949 | | * - change only the ports that have changes |
950 | | */ |
951 | | static void |
952 | | p4rpc_setup_ports( bool isInit ) |
953 | 16 | { |
954 | 16 | bool tcp_port_changed = p4prefs.cur_tcp_port != p4prefs.prev_tcp_port; |
955 | 16 | bool tls_port_changed = p4prefs.cur_tls_port != p4prefs.prev_tls_port; |
956 | 16 | bool heur_changed = p4prefs.cur_heur_enable != p4prefs.prev_heur_enable; |
957 | | |
958 | | /* |
959 | | * Claim tcp.port for P4RPC if: |
960 | | * - we don't prefer TLS, and |
961 | | * we're initializing or the tls port has changes |
962 | | */ |
963 | 16 | if( !p4prefs.prefer_tls && (isInit || tcp_port_changed) ) |
964 | 0 | { |
965 | 0 | if( !isInit ) |
966 | 0 | dissector_delete_uint( "tcp.port", p4prefs.prev_tcp_port, p4rpc_handle ); |
967 | 0 | dissector_add_uint( "tcp.port", p4prefs.cur_tcp_port, p4rpc_handle ); |
968 | 0 | } |
969 | | |
970 | | /* |
971 | | * Allow P4RPC to grab our TLS port (after decryption) by default |
972 | | * - if we prefer TLS, and |
973 | | * we're initializing or the tls port has changes |
974 | | */ |
975 | 16 | if( p4prefs.prefer_tls && (isInit || tls_port_changed) ) |
976 | 16 | { |
977 | 16 | if( !isInit ) |
978 | 0 | dissector_delete_uint( "tls.port", p4prefs.prev_tls_port, p4rpc_handle ); |
979 | 16 | dissector_add_uint( "tls.port", p4prefs.cur_tls_port, p4rpc_handle ); |
980 | 16 | } |
981 | | |
982 | | /* |
983 | | * Add or delete our TCP heuristic packet finder if TLS is lower priority. |
984 | | */ |
985 | 16 | if( isInit || heur_changed ) |
986 | 16 | { |
987 | | /* |
988 | | * We don't delete the heuristic dissector during initialization |
989 | | * because none could have been registered already. |
990 | | * |
991 | | * After initialization it's still possible that no heuristic |
992 | | * dissector was registered. |
993 | | * Rather than track whether or not one has already been registered |
994 | | * we always delete it, because heur_dissector_delete() is a no-op |
995 | | * if the dissector isn't registered. |
996 | | */ |
997 | 16 | if( !isInit ) |
998 | 0 | heur_dissector_delete( "tcp", dissect_p4rpc_heur, proto_p4rpc ); |
999 | | |
1000 | 16 | if( p4prefs.cur_heur_enable ) |
1001 | 16 | { |
1002 | 16 | heur_dissector_add( "tcp", dissect_p4rpc_heur, "P4RPC over TCP", "p4rpc_tcp", proto_p4rpc, HEURISTIC_ENABLE ); |
1003 | 16 | } |
1004 | 16 | } |
1005 | | |
1006 | | /* |
1007 | | * remember our current settings |
1008 | | */ |
1009 | | |
1010 | 16 | if( tcp_port_changed ) |
1011 | 0 | p4prefs.prev_tcp_port = p4prefs.cur_tcp_port; |
1012 | | |
1013 | 16 | if( tls_port_changed ) |
1014 | 0 | p4prefs.prev_tls_port = p4prefs.cur_tls_port; |
1015 | | |
1016 | 16 | if( heur_changed ) |
1017 | 0 | p4prefs.prev_heur_enable = p4prefs.cur_heur_enable; |
1018 | 16 | } |
1019 | | |
1020 | | /* |
1021 | | * Called after the user has changed preferences. |
1022 | | * We validate the settings, alert the user if appropriate, |
1023 | | * and apply those that have changed. |
1024 | | */ |
1025 | | static void |
1026 | | prefs_cb(void) |
1027 | 0 | { |
1028 | | /* |
1029 | | * Warn against making both ports the same |
1030 | | * because the P4RPC dissector may take |
1031 | | * precedence over the TLS dissector |
1032 | | */ |
1033 | 0 | if( p4prefs.prefer_tls && (p4prefs.cur_tcp_port == p4prefs.cur_tls_port) ) |
1034 | 0 | { |
1035 | 0 | report_warning( "You have set both the P4RPC TCP and TLS ports to %d.\n" |
1036 | 0 | "If this prevents Wireshark from decoding both the TLS and" |
1037 | 0 | " the P4RPC layers then set the TLS port to the server port" |
1038 | 0 | " and set the TCP port to any other port.\n" |
1039 | 0 | "\nDisabling \"Decrypt TLS before P4RPC\" might help if TLS" |
1040 | 0 | " grabs the port first and blocks P4RPC from decoding packets.", |
1041 | 0 | p4prefs.cur_tcp_port ); |
1042 | 0 | } |
1043 | |
|
1044 | 0 | p4rpc_setup_ports( false ); |
1045 | 0 | } |
1046 | | |
1047 | | /* |
1048 | | * Define our preferences pane and hook in our callback routine (above). |
1049 | | */ |
1050 | | static void |
1051 | | register_prefs(void) |
1052 | 16 | { |
1053 | | // register our callback func |
1054 | 16 | module_t *prefs_mod = prefs_register_protocol( proto_p4rpc, prefs_cb ); |
1055 | | |
1056 | | // tcp.port |
1057 | 16 | prefs_register_uint_preference( prefs_mod, "tcp.port", |
1058 | 16 | "TCP port", |
1059 | 16 | "Default P4RPC-over-TCP port.", |
1060 | 16 | 10, // decimal |
1061 | 16 | &p4prefs.cur_tcp_port ); |
1062 | | |
1063 | | // tls.port |
1064 | 16 | prefs_register_uint_preference( prefs_mod, "tls.port", |
1065 | 16 | "TLS port", |
1066 | 16 | "Default P4RPC-over-TLS port (should not be same as \"TCP port\" if \"TLS priority\" is disabled).", |
1067 | 16 | 10, // decimal |
1068 | 16 | &p4prefs.cur_tls_port ); |
1069 | | |
1070 | | // tls preference |
1071 | 16 | prefs_register_bool_preference( prefs_mod, "prefer_tls", |
1072 | 16 | "Decrypt TLS before P4RPC", |
1073 | 16 | "Always decrypt packets before P4RPC tries to dissect them.\n\n" |
1074 | 16 | "- Enabled: If traffic is TLS then then it will be decrypted and then decoded as P4RPC.\n" |
1075 | 16 | "- Disabled: Traffic on \"TCP port\" will not be decrypted even if it is TLS.\n" |
1076 | 16 | "\nRelevant only when \"TCP port\" is the same as \"TLS port\", " |
1077 | 16 | "but normally should be enabled.", |
1078 | 16 | &p4prefs.prefer_tls ); |
1079 | | |
1080 | | // tcp heuristics preference |
1081 | 16 | prefs_register_bool_preference( prefs_mod, "tcp_heur", |
1082 | 16 | "Enable P4RPC/TCP heuristic detection", |
1083 | 16 | "Enable heuristic detection of P4RPC over TCP.\n" |
1084 | 16 | "\nTry to guess at P4RPC packets on ports other than \"TCP port\".\n" |
1085 | 16 | "\nEnable this if you frequently dissect P4RPC traffic on different ports," |
1086 | 16 | " so that you won't need to use \"Decode As...\" often.\n" |
1087 | 16 | "\nP4RPC over TLS is always auto-detected when \"Decrypt TLS before P4RPC\" is enabled," |
1088 | 16 | " whether or not this option is enabled.", |
1089 | 16 | &p4prefs.cur_heur_enable ); |
1090 | | |
1091 | | // var=val tree preference |
1092 | 16 | prefs_register_bool_preference( prefs_mod, "message_params_as_tree", |
1093 | 16 | "Show message parameters as a \"var=value\" tree", |
1094 | 16 | "Show message parameters as a \"var=value\" subtree with \"var\", " |
1095 | 16 | " \"length\", and \"val\" children.\n" |
1096 | 16 | "\nIt is highly recommended to enable this, because this is required" |
1097 | 16 | " to enable filtering by parameter names and values (eg, p4rpc.var.*, etc)", |
1098 | 16 | &p4prefs.varval_tree ); |
1099 | | |
1100 | | // UTC vs local time field preference |
1101 | 16 | prefs_register_bool_preference( prefs_mod, "time_fields_in_utc", |
1102 | 16 | "Display time fields in UTC rather than in the local time zone", |
1103 | 16 | "If set, display time fields in UTC rather than in the local time zone", |
1104 | 16 | &p4prefs.time_utc ); |
1105 | | |
1106 | | // Clear INFO field preference |
1107 | 16 | prefs_register_bool_preference( prefs_mod, "clear_info", |
1108 | 16 | "Clear the INFO column and show the message counts and the request names", |
1109 | 16 | "If not set then just prepend the message counts", |
1110 | 16 | &p4prefs.clear_info ); |
1111 | | |
1112 | | // show_nul preference |
1113 | 16 | prefs_register_bool_preference( prefs_mod, "show_nul", |
1114 | 16 | "Show the NUL byte of var=val params in the protocol tree", |
1115 | 16 | "If set, add a subtree item for the ending NUL byte of a message parameter", |
1116 | 16 | &p4prefs.show_nul ); |
1117 | | |
1118 | | /* |
1119 | | * Not real preferences, just some static text to display. |
1120 | | * Added a dummy one to separate the version string a little. |
1121 | | */ |
1122 | 16 | prefs_register_static_text_preference( prefs_mod, "", "", "" ); |
1123 | 16 | prefs_register_static_text_preference( prefs_mod, "version", |
1124 | 16 | "P4RPC dissector version: " VERSION, |
1125 | 16 | VERSION ); // this last arg doesn't seem to get displayed |
1126 | 16 | } |
1127 | | |
1128 | | /* |
1129 | | * Register P4RPC dissector with its tree items and filter variables. |
1130 | | */ |
1131 | | void |
1132 | | proto_register_p4rpc(void) |
1133 | 16 | { |
1134 | 16 | static hf_register_info hf[] = { |
1135 | 16 | { |
1136 | | // basic protocol fields |
1137 | 16 | &hf_p4rpc_checksum_len, |
1138 | 16 | { |
1139 | 16 | "message cksum of len", |
1140 | 16 | "p4rpc.len.cksum", |
1141 | 16 | FT_UINT8, |
1142 | 16 | BASE_HEX, |
1143 | 16 | NULL, |
1144 | 16 | 0x0, |
1145 | 16 | "Checksum of p4rpc.msg.len (XOR of the 4 length bytes)", |
1146 | 16 | HFILL |
1147 | 16 | } |
1148 | 16 | }, |
1149 | 16 | { |
1150 | 16 | &hf_p4rpc_len, |
1151 | 16 | { |
1152 | 16 | "message len", |
1153 | 16 | "p4rpc.msg.len", |
1154 | 16 | FT_UINT32, |
1155 | 16 | BASE_DEC, |
1156 | 16 | NULL, |
1157 | 16 | 0x0, |
1158 | 16 | "Total number of bytes in this message", |
1159 | 16 | HFILL |
1160 | 16 | } |
1161 | 16 | }, |
1162 | 16 | { |
1163 | | // comprises p4rpc.var.name, p4rpc.var.val.len, p4rpc.var.val |
1164 | 16 | &hf_p4rpc_varbytes, |
1165 | 16 | { |
1166 | 16 | "param name=val", // placeholder |
1167 | 16 | "p4rpc.var.name.val", |
1168 | 16 | FT_BYTES, |
1169 | 16 | BASE_NONE, |
1170 | 16 | NULL, |
1171 | 16 | 0x0, |
1172 | 16 | "Combined var name and value", |
1173 | 16 | HFILL |
1174 | 16 | }, |
1175 | 16 | }, |
1176 | 16 | { |
1177 | 16 | &hf_p4rpc_nul, // p_id (int) |
1178 | 16 | { // hfinfo (header_field_info) |
1179 | 16 | "param NUL terminator", // name (const char *) |
1180 | 16 | "p4rpc.var.nul", // abbrev (const char *) |
1181 | 16 | FT_UINT8, // type (enum ftenum) |
1182 | 16 | BASE_HEX, // display (int) |
1183 | 16 | NULL, // strings (const void *) |
1184 | 16 | 0x0, // bitmask (uint64_t) |
1185 | 16 | "Parameter value NUL terminator byte", // blurb (const char *) |
1186 | 16 | HFILL |
1187 | 16 | } |
1188 | 16 | }, |
1189 | 16 | { |
1190 | 16 | &hf_p4rpc_func, |
1191 | 16 | { |
1192 | 16 | "param func", |
1193 | 16 | "p4rpc.func", |
1194 | 16 | FT_STRINGZ, |
1195 | 16 | BASE_NONE, |
1196 | 16 | NULL, |
1197 | 16 | 0x0, |
1198 | 16 | "Name of the requested function [required]", |
1199 | 16 | HFILL |
1200 | 16 | } |
1201 | 16 | }, |
1202 | 16 | { |
1203 | 16 | &hf_p4rpc_handle, // p_id (int) |
1204 | 16 | { // hfinfo (header_field_info) |
1205 | 16 | "param handle", // name (const char *) |
1206 | 16 | "p4rpc.handle", // abbrev (const char *) |
1207 | 16 | FT_STRINGZ, // type (enum ftenum) |
1208 | 16 | BASE_NONE, // display (int) |
1209 | 16 | NULL, // strings (const void *) |
1210 | 16 | 0x0, // bitmask (uint64_t) |
1211 | 16 | "Name of response function [optional]", // blurb (const char *) |
1212 | 16 | HFILL |
1213 | 16 | } |
1214 | 16 | }, |
1215 | 16 | { |
1216 | 16 | &hf_p4rpc_action, // p_id (int) |
1217 | 16 | { // hfinfo (header_field_info) |
1218 | 16 | "param action", // name (const char *) |
1219 | 16 | "p4rpc.action", // abbrev (const char *) |
1220 | 16 | FT_STRINGZ, // type (enum ftenum) |
1221 | 16 | BASE_NONE, // display (int) |
1222 | 16 | NULL, // strings (const void *) |
1223 | 16 | 0x0, // bitmask (uint64_t) |
1224 | 16 | "Name of action function [optional]", // blurb (const char *) |
1225 | 16 | HFILL |
1226 | 16 | } |
1227 | 16 | }, |
1228 | 16 | { |
1229 | 16 | &hf_p4rpc_confirm, // p_id (int) |
1230 | 16 | { // hfinfo (header_field_info) |
1231 | 16 | "param confirm", // name (const char *) |
1232 | 16 | "p4rpc.confirm", // abbrev (const char *) |
1233 | 16 | FT_STRINGZ, // type (enum ftenum) |
1234 | 16 | BASE_NONE, // display (int) |
1235 | 16 | NULL, // strings (const void *) |
1236 | 16 | 0x0, // bitmask (uint64_t) |
1237 | 16 | "Name of confirm function [optional]", // blurb (const char *) |
1238 | 16 | HFILL |
1239 | 16 | } |
1240 | 16 | }, |
1241 | 16 | { |
1242 | 16 | &hf_p4rpc_num_msgs, // p_id (int) |
1243 | 16 | { // hfinfo (header_field_info) |
1244 | 16 | "number of messages", // name (const char *) |
1245 | 16 | "p4rpc.num.msgs", // abbrev (const char *) |
1246 | 16 | FT_UINT32, // type (enum ftenum) |
1247 | 16 | BASE_DEC, // display (int) |
1248 | 16 | NULL, // strings (const void *) |
1249 | 16 | 0x0, // bitmask (uint64_t) |
1250 | 16 | "Number of messages in this (reassembled) packet", // blurb (const char *) |
1251 | 16 | HFILL |
1252 | 16 | } |
1253 | 16 | }, |
1254 | | |
1255 | | // name=value subtree |
1256 | 16 | { |
1257 | 16 | &hf_p4rpc_varname, |
1258 | 16 | { |
1259 | 16 | "param name", |
1260 | 16 | "p4rpc.var.name", |
1261 | 16 | FT_STRINGZ, |
1262 | 16 | BASE_NONE, |
1263 | 16 | NULL, |
1264 | 16 | 0x0, |
1265 | 16 | "Name of this parameter", |
1266 | 16 | HFILL |
1267 | 16 | } |
1268 | 16 | }, |
1269 | 16 | { |
1270 | 16 | &hf_p4rpc_varvallen, |
1271 | 16 | { |
1272 | 16 | "param value len", |
1273 | 16 | "p4rpc.var.val.len", |
1274 | 16 | FT_UINT32, |
1275 | 16 | BASE_DEC, |
1276 | 16 | NULL, |
1277 | 16 | 0x0, |
1278 | 16 | "Length of the value of this parameter", |
1279 | 16 | HFILL |
1280 | 16 | } |
1281 | 16 | }, |
1282 | 16 | { |
1283 | 16 | &hf_p4rpc_varval, |
1284 | 16 | { |
1285 | 16 | "param value", |
1286 | 16 | "p4rpc.var.val", |
1287 | 16 | FT_STRINGZTRUNC, // really FT_BYTES, but starts with a FT_STRINGZ |
1288 | 16 | BASE_NONE, |
1289 | 16 | NULL, |
1290 | 16 | 0x0, |
1291 | 16 | "Value of this parameter", |
1292 | 16 | HFILL |
1293 | 16 | } |
1294 | 16 | }, |
1295 | | |
1296 | | // syncTime item |
1297 | 16 | { |
1298 | 16 | &hf_p4rpc_sync_time, |
1299 | 16 | { |
1300 | 16 | "sync time", |
1301 | 16 | "p4rpc.synctime", |
1302 | 16 | FT_UINT64, |
1303 | 16 | BASE_DEC, |
1304 | 16 | NULL, |
1305 | 16 | 0x0, |
1306 | 16 | "Integer value of syncTime param", |
1307 | 16 | HFILL |
1308 | 16 | } |
1309 | 16 | }, |
1310 | | |
1311 | | // haveRec subtree |
1312 | 16 | { |
1313 | 16 | &hf_p4rpc_have_client_path, |
1314 | 16 | { |
1315 | 16 | "client path", |
1316 | 16 | "p4rpc.have.client.path", |
1317 | 16 | FT_STRINGZ, |
1318 | 16 | BASE_NONE, |
1319 | 16 | NULL, |
1320 | 16 | 0x0, |
1321 | 16 | "File path in client syntax", |
1322 | 16 | HFILL |
1323 | 16 | } |
1324 | 16 | }, |
1325 | 16 | { |
1326 | 16 | &hf_p4rpc_have_depot_path, |
1327 | 16 | { |
1328 | 16 | "depot path", |
1329 | 16 | "p4rpc.have.depot.path", |
1330 | 16 | FT_STRINGZ, |
1331 | 16 | BASE_NONE, |
1332 | 16 | NULL, |
1333 | 16 | 0x0, |
1334 | 16 | "File path in depot syntax", |
1335 | 16 | HFILL |
1336 | 16 | } |
1337 | 16 | }, |
1338 | 16 | { |
1339 | 16 | &hf_p4rpc_have_file_rev, |
1340 | 16 | { |
1341 | 16 | "file rev", |
1342 | 16 | "p4rpc.have.file.rev", |
1343 | 16 | FT_INT32, |
1344 | 16 | BASE_DEC, |
1345 | 16 | NULL, |
1346 | 16 | 0x0, |
1347 | 16 | "File revision number", |
1348 | 16 | HFILL |
1349 | 16 | } |
1350 | 16 | }, |
1351 | 16 | { |
1352 | 16 | &hf_p4rpc_have_file_type, |
1353 | 16 | { |
1354 | 16 | "file type", |
1355 | 16 | "p4rpc.have.file.type", |
1356 | 16 | FT_INT32, |
1357 | 16 | BASE_DEC, |
1358 | 16 | NULL, |
1359 | 16 | 0x0, |
1360 | 16 | "Internal code for the file type", |
1361 | 16 | HFILL |
1362 | 16 | } |
1363 | 16 | }, |
1364 | 16 | { |
1365 | 16 | &hf_p4rpc_have_file_datetime, |
1366 | 16 | { |
1367 | 16 | "file datetime", |
1368 | 16 | "p4rpc.have.file.datetime", |
1369 | | /* |
1370 | | * Use FT_INT64 rather than FT_ABSOLUTE_TIME |
1371 | | * so that we can display in ISO-8601 format, |
1372 | | * in either UTC or local timezone. |
1373 | | */ |
1374 | 16 | FT_INT64, // often only 32 bits |
1375 | 16 | BASE_DEC, |
1376 | 16 | NULL, |
1377 | 16 | 0x0, |
1378 | 16 | "Timestamp of this file", |
1379 | 16 | HFILL |
1380 | 16 | } |
1381 | 16 | } |
1382 | 16 | }; |
1383 | | |
1384 | | /* |
1385 | | * Register our expert info variables |
1386 | | * (message warnings, errors, corruption, etc) |
1387 | | */ |
1388 | 16 | static ei_register_info ei[] = { |
1389 | 16 | { |
1390 | 16 | &ei_p4rpc_msg_short, |
1391 | 16 | { |
1392 | 16 | "p4rpc.msg.short", |
1393 | 16 | PI_MALFORMED, |
1394 | 16 | PI_ERROR, |
1395 | 16 | "available data < 5 bytes", |
1396 | 16 | EXPFILL |
1397 | 16 | } |
1398 | 16 | }, |
1399 | 16 | { |
1400 | 16 | &ei_p4rpc_msg_len_cksum, |
1401 | 16 | { |
1402 | 16 | "p4rpc.msg.len_cksum", |
1403 | 16 | PI_CHECKSUM, |
1404 | 16 | PI_WARN, |
1405 | 16 | "message length checksum mismatch", |
1406 | 16 | EXPFILL |
1407 | 16 | } |
1408 | 16 | }, |
1409 | 16 | { |
1410 | 16 | &ei_p4rpc_msg_len, |
1411 | 16 | { |
1412 | 16 | "p4rpc.msg.msglen", |
1413 | 16 | PI_MALFORMED, |
1414 | 16 | PI_ERROR, |
1415 | 16 | "message length < " MSG_MIN_LEN_STR " or > " MSG_MAX_LEN_STR, |
1416 | 16 | EXPFILL |
1417 | 16 | } |
1418 | 16 | }, |
1419 | 16 | { |
1420 | 16 | &ei_p4rpc_msg_val_len, |
1421 | 16 | { |
1422 | 16 | "p4rpc.msg.val.len", |
1423 | 16 | PI_MALFORMED, |
1424 | 16 | PI_ERROR, |
1425 | 16 | "value length extends past end of message", |
1426 | 16 | EXPFILL |
1427 | 16 | } |
1428 | 16 | }, |
1429 | 16 | { |
1430 | 16 | &ei_p4rpc_msg_val_nul, |
1431 | 16 | { |
1432 | 16 | "p4rpc.msg.val.nul", |
1433 | 16 | PI_MALFORMED, |
1434 | 16 | PI_WARN, |
1435 | 16 | "expected NUL terminator is not zero", |
1436 | 16 | EXPFILL |
1437 | 16 | } |
1438 | 16 | }, |
1439 | 16 | { |
1440 | 16 | &ei_p4rpc_timestr_non_numeric, |
1441 | 16 | { |
1442 | 16 | "p4rpc.timestr.inval", |
1443 | 16 | PI_MALFORMED, |
1444 | 16 | PI_WARN, |
1445 | 16 | "time string contains non-numeric character", |
1446 | 16 | EXPFILL |
1447 | 16 | } |
1448 | 16 | } |
1449 | 16 | }; |
1450 | | |
1451 | | // setup subtree arrays |
1452 | 16 | static int *ett_arrays[] = { |
1453 | 16 | &ett_p4rpc, // protocol subtree array |
1454 | 16 | &ett_argtree, // protocol subtree name=value array |
1455 | 16 | &ett_sync_time, // protocol subtree datetime array |
1456 | 16 | &ett_haverec // protocol subtree haveRec value array |
1457 | 16 | }; |
1458 | | |
1459 | | // Finally, register our protocol |
1460 | 16 | proto_p4rpc = proto_register_protocol ("P4RPC (Perforce Protocol)", "P4RPC", "p4rpc"); |
1461 | | |
1462 | | // Register our protocol variables |
1463 | 16 | proto_register_field_array( proto_p4rpc, hf, array_length(hf) ); |
1464 | 16 | proto_register_subtree_array( ett_arrays, array_length(ett_arrays) ); |
1465 | | |
1466 | | // register expert variables |
1467 | 16 | expert_module_t *expert_p4rpc = NULL; |
1468 | | |
1469 | 16 | expert_p4rpc = expert_register_protocol( proto_p4rpc ); |
1470 | 16 | expert_register_field_array( expert_p4rpc, ei, array_length(ei) ); |
1471 | | |
1472 | | // register P4RPC as a TCP dissector |
1473 | 16 | p4rpc_handle = register_dissector_with_description( |
1474 | 16 | "p4rpc", // dissector name |
1475 | 16 | "P4RPC", // dissector description |
1476 | 16 | dissect_p4rpc, // dissector function |
1477 | 16 | proto_p4rpc // protocol being dissected |
1478 | 16 | ); |
1479 | | |
1480 | | /* |
1481 | | * register P4RPC as a TLS dissector |
1482 | | * |
1483 | | * We probably could just register dissect_p4rpc |
1484 | | * as the TLS dissector as well. |
1485 | | */ |
1486 | 16 | p4rpc_tls_handle = register_dissector_with_description( |
1487 | 16 | "p4rpc.tls", // dissector name |
1488 | 16 | "P4RPC over TLS", // dissector description |
1489 | 16 | dissect_tls_p4rpc, // dissector function |
1490 | 16 | proto_p4rpc // protocol being dissected |
1491 | 16 | ); |
1492 | | |
1493 | 16 | register_prefs(); |
1494 | | |
1495 | 16 | decimal_point = localeconv()->decimal_point; |
1496 | 16 | } // proto_register_p4rpc |
1497 | | |
1498 | | /* |
1499 | | * Associate our dissector with ports. |
1500 | | */ |
1501 | | void |
1502 | | proto_reg_handoff_p4rpc(void) |
1503 | 16 | { |
1504 | | /* |
1505 | | * Allow "Decode As" with any TCP packet. |
1506 | | * - need to do it only once |
1507 | | */ |
1508 | 16 | dissector_add_for_decode_as( "tcp.port", p4rpc_handle ); |
1509 | | |
1510 | | /* |
1511 | | * Allow "Decode As" with any packet on our TLS port. |
1512 | | * - need to do it only once |
1513 | | */ |
1514 | 16 | dissector_add_for_decode_as( "tls.port", p4rpc_handle ); |
1515 | | |
1516 | | // all the setup that needs to be redone when prefs are changed |
1517 | | p4rpc_setup_ports( true ); |
1518 | 16 | } |
1519 | | |
1520 | | /* |
1521 | | * Editor modelines - https://www.wireshark.org/tools/modelines.html |
1522 | | * |
1523 | | * Local variables: |
1524 | | * c-basic-offset: 4 |
1525 | | * tab-width: 8 |
1526 | | * indent-tabs-mode: nil |
1527 | | * End: |
1528 | | * |
1529 | | * vi: set shiftwidth=4 tabstop=8 expandtab: |
1530 | | * :indentSize=4:tabSize=8:noTabs=true: |
1531 | | */ |