/src/wireshark/epan/dissectors/packet-quakeworld.c
Line | Count | Source |
1 | | /* packet-quakeworld.c |
2 | | * Routines for QuakeWorld packet dissection |
3 | | * |
4 | | * Uwe Girlich <uwe@planetquake.com> |
5 | | * http://www.idsoftware.com/q1source/q1source.zip |
6 | | * |
7 | | * Wireshark - Network traffic analyzer |
8 | | * By Gerald Combs <gerald@wireshark.org> |
9 | | * Copyright 1998 Gerald Combs |
10 | | * |
11 | | * Copied from packet-quake.c |
12 | | * |
13 | | * SPDX-License-Identifier: GPL-2.0-or-later |
14 | | */ |
15 | | |
16 | | #include "config.h" |
17 | | |
18 | | #include <stdlib.h> |
19 | | #include <epan/packet.h> |
20 | | #include <epan/prefs.h> |
21 | | #include <epan/expert.h> |
22 | | |
23 | | #include <wsutil/strtoi.h> |
24 | | |
25 | | void proto_register_quakeworld(void); |
26 | | void proto_reg_handoff_quakeworld(void); |
27 | | |
28 | | static dissector_handle_t quakeworld_handle; |
29 | | |
30 | | static int proto_quakeworld; |
31 | | |
32 | | static int hf_quakeworld_s2c; |
33 | | static int hf_quakeworld_c2s; |
34 | | static int hf_quakeworld_connectionless; |
35 | | static int hf_quakeworld_game; |
36 | | static int hf_quakeworld_connectionless_marker; |
37 | | static int hf_quakeworld_connectionless_text; |
38 | | static int hf_quakeworld_connectionless_command; |
39 | | static int hf_quakeworld_connectionless_arguments; |
40 | | static int hf_quakeworld_connectionless_connect_version; |
41 | | static int hf_quakeworld_connectionless_connect_qport; |
42 | | static int hf_quakeworld_connectionless_connect_challenge; |
43 | | static int hf_quakeworld_connectionless_connect_infostring; |
44 | | static int hf_quakeworld_connectionless_connect_infostring_key_value; |
45 | | static int hf_quakeworld_connectionless_connect_infostring_key; |
46 | | static int hf_quakeworld_connectionless_connect_infostring_value; |
47 | | static int hf_quakeworld_connectionless_rcon_password; |
48 | | static int hf_quakeworld_connectionless_rcon_command; |
49 | | static int hf_quakeworld_game_seq1; |
50 | | static int hf_quakeworld_game_rel1; |
51 | | static int hf_quakeworld_game_seq2; |
52 | | static int hf_quakeworld_game_rel2; |
53 | | static int hf_quakeworld_game_qport; |
54 | | |
55 | | static int ett_quakeworld; |
56 | | static int ett_quakeworld_connectionless; |
57 | | static int ett_quakeworld_connectionless_text; |
58 | | static int ett_quakeworld_connectionless_arguments; |
59 | | static int ett_quakeworld_connectionless_connect_infostring; |
60 | | static int ett_quakeworld_connectionless_connect_infostring_key_value; |
61 | | static int ett_quakeworld_game; |
62 | | static int ett_quakeworld_game_seq1; |
63 | | static int ett_quakeworld_game_seq2; |
64 | | static int ett_quakeworld_game_clc; |
65 | | static int ett_quakeworld_game_svc; |
66 | | |
67 | | static expert_field ei_quakeworld_connectionless_command_invalid; |
68 | | |
69 | | /* |
70 | | helper functions, they may ave to go somewhere else |
71 | | they are mostly copied without change from |
72 | | quakeworldsource/client/cmd.c |
73 | | quakeworldsource/client/common.c |
74 | | */ |
75 | | |
76 | 0 | #define MAX_TEXT_SIZE 2048 |
77 | | |
78 | | static const char * |
79 | | COM_Parse (wmem_allocator_t* allocator, const char *data, int data_len, int* token_start, int* token_len) |
80 | 70 | { |
81 | 70 | int c; |
82 | 70 | char* com_token = (char*)wmem_alloc(allocator, data_len+1); |
83 | | |
84 | 70 | com_token[0] = '\0'; |
85 | 70 | *token_start = 0; |
86 | 70 | *token_len = 0; |
87 | | |
88 | 70 | if (data == NULL) |
89 | 0 | return NULL; |
90 | | |
91 | | /* skip whitespace */ |
92 | 70 | skipwhite: |
93 | 85 | while (true) { |
94 | 85 | c = *data; |
95 | 85 | if (c == '\0') |
96 | 0 | return NULL; /* end of file; */ |
97 | 85 | if ((c != ' ') && (!g_ascii_iscntrl(c))) |
98 | 70 | break; |
99 | 15 | data++; |
100 | 15 | (*token_start)++; |
101 | 15 | } |
102 | | |
103 | | /* skip // comments */ |
104 | 70 | if ((c=='/') && (data[1]=='/')) { |
105 | 0 | while (*data && *data != '\n'){ |
106 | 0 | data++; |
107 | 0 | (*token_start)++; |
108 | 0 | } |
109 | 0 | goto skipwhite; |
110 | 0 | } |
111 | | |
112 | | /* handle quoted strings specially */ |
113 | 70 | if (c == '\"') { |
114 | 36 | data++; |
115 | 36 | (*token_start)++; |
116 | 91 | while (*token_len < data_len) { |
117 | 89 | c = *data++; |
118 | 89 | if ((c=='\"') || (c=='\0')) { |
119 | 34 | com_token[*token_len] = '\0'; |
120 | 34 | return data; |
121 | 34 | } |
122 | 55 | com_token[*token_len] = c; |
123 | 55 | (*token_len)++; |
124 | 55 | } |
125 | 36 | } |
126 | | |
127 | 36 | if (*token_len == data_len) { |
128 | 2 | com_token[*token_len] = '\0'; |
129 | 2 | return data; |
130 | 2 | } |
131 | | |
132 | | /* parse a regular word */ |
133 | 667 | do { |
134 | 667 | com_token[*token_len] = c; |
135 | 667 | data++; |
136 | 667 | (*token_len)++; |
137 | 667 | c = *data; |
138 | 667 | } while (( c != ' ') && (!g_ascii_iscntrl(c)) && (*token_len < data_len)); |
139 | | |
140 | 34 | com_token[*token_len] = '\0'; |
141 | 34 | return data; |
142 | 36 | } |
143 | | |
144 | | |
145 | 70 | #define MAX_ARGS 80 |
146 | | static int cmd_argc; |
147 | | static const char *cmd_argv[MAX_ARGS]; |
148 | | static const char *cmd_null_string = ""; |
149 | | static int cmd_argv_start[MAX_ARGS]; |
150 | | static int cmd_argv_length[MAX_ARGS]; |
151 | | |
152 | | |
153 | | |
154 | | static int |
155 | | Cmd_Argc(void) |
156 | 0 | { |
157 | 0 | return cmd_argc; |
158 | 0 | } |
159 | | |
160 | | |
161 | | static const char* |
162 | | Cmd_Argv(int arg) |
163 | 13 | { |
164 | 13 | if ( arg >= cmd_argc ) |
165 | 1 | return cmd_null_string; |
166 | 12 | return cmd_argv[arg]; |
167 | 13 | } |
168 | | |
169 | | |
170 | | static int |
171 | | Cmd_Argv_start(int arg) |
172 | 0 | { |
173 | 0 | if ( arg >= cmd_argc ) |
174 | 0 | return 0; |
175 | 0 | return cmd_argv_start[arg]; |
176 | 0 | } |
177 | | |
178 | | |
179 | | static int |
180 | | Cmd_Argv_length(int arg) |
181 | 0 | { |
182 | 0 | if ( arg >= cmd_argc ) |
183 | 0 | return 0; |
184 | 0 | return cmd_argv_length[arg]; |
185 | 0 | } |
186 | | |
187 | | |
188 | | static void |
189 | | Cmd_TokenizeString(wmem_allocator_t* allocator, const char* text, int text_len) |
190 | 13 | { |
191 | 13 | int start; |
192 | 13 | int com_token_start; |
193 | 13 | int com_token_length; |
194 | 13 | cmd_argc = 0; |
195 | | |
196 | 13 | start = 0; |
197 | 83 | while (start < text_len) { |
198 | | |
199 | | /* skip whitespace up to a \n */ |
200 | 200 | while (*text && *text <= ' ' && *text != '\n' && start < text_len) { |
201 | 125 | text++; |
202 | 125 | start++; |
203 | 125 | } |
204 | | |
205 | 75 | if (*text == '\n') { |
206 | | /* a newline separates commands in the buffer */ |
207 | 0 | text++; |
208 | 0 | break; |
209 | 0 | } |
210 | | |
211 | 75 | if ((!*text) || (start == text_len)) |
212 | 5 | return; |
213 | | |
214 | 70 | text = COM_Parse (allocator, text, text_len-start, &com_token_start, &com_token_length); |
215 | 70 | if (!text) |
216 | 0 | return; |
217 | | |
218 | 70 | if (cmd_argc < MAX_ARGS) { |
219 | 70 | cmd_argv[cmd_argc] = (char*)text; |
220 | 70 | cmd_argv_start[cmd_argc] = start + com_token_start; |
221 | 70 | cmd_argv_length[cmd_argc] = com_token_length; |
222 | 70 | cmd_argc++; |
223 | 70 | } |
224 | | |
225 | 70 | start += com_token_start + com_token_length; |
226 | 70 | } |
227 | 13 | } |
228 | | |
229 | | |
230 | | static void |
231 | | dissect_id_infostring(tvbuff_t *tvb, proto_tree* tree, |
232 | | int offset, char* infostring, |
233 | | int ett_key_value, int hf_key_value, int hf_key, int hf_value) |
234 | 0 | { |
235 | 0 | char *newpos = infostring; |
236 | 0 | bool end_of_info = false; |
237 | | |
238 | | /* to look at all the key/value pairs, we destroy infostring */ |
239 | 0 | while(!end_of_info) { |
240 | 0 | char* keypos; |
241 | 0 | char* valuepos; |
242 | 0 | int keylength; |
243 | 0 | char* keyvaluesep; |
244 | 0 | int valuelength; |
245 | 0 | char* valueend; |
246 | |
|
247 | 0 | keypos = newpos; |
248 | 0 | if (*keypos == '\0') break; |
249 | 0 | if (*keypos == '\\') keypos++; |
250 | |
|
251 | 0 | for (keylength = 0 |
252 | 0 | ; |
253 | 0 | *(keypos + keylength) != '\\' && |
254 | 0 | *(keypos + keylength) != '\0' |
255 | 0 | ; |
256 | 0 | keylength++) |
257 | 0 | ; |
258 | 0 | keyvaluesep = keypos + keylength; |
259 | 0 | if (*keyvaluesep == '\0') break; |
260 | 0 | valuepos = keyvaluesep+1; |
261 | 0 | for (valuelength = 0 |
262 | 0 | ; |
263 | 0 | *(valuepos + valuelength) != '\\' && |
264 | 0 | *(valuepos + valuelength) != '\0' |
265 | 0 | ; |
266 | 0 | valuelength++) |
267 | 0 | ; |
268 | 0 | valueend = valuepos + valuelength; |
269 | 0 | if (*valueend == '\0') { |
270 | 0 | end_of_info = true; |
271 | 0 | } |
272 | 0 | *(keyvaluesep) = '='; |
273 | 0 | *(valueend) = '\0'; |
274 | |
|
275 | 0 | if (tree) { |
276 | 0 | proto_item* sub_item; |
277 | 0 | proto_tree* sub_tree; |
278 | |
|
279 | 0 | sub_item = proto_tree_add_string(tree, |
280 | 0 | hf_key_value, |
281 | 0 | tvb, |
282 | 0 | offset + (int)(keypos-infostring), |
283 | 0 | keylength + 1 + valuelength, keypos); |
284 | 0 | sub_tree = proto_item_add_subtree( |
285 | 0 | sub_item, |
286 | 0 | ett_key_value); |
287 | 0 | *(keyvaluesep) = '\0'; |
288 | 0 | proto_tree_add_string(sub_tree, |
289 | 0 | hf_key, |
290 | 0 | tvb, |
291 | 0 | offset + (int)(keypos-infostring), |
292 | 0 | keylength, keypos); |
293 | 0 | proto_tree_add_string(sub_tree, |
294 | 0 | hf_value, |
295 | 0 | tvb, |
296 | 0 | offset + (int)(valuepos-infostring), |
297 | 0 | valuelength, valuepos); |
298 | 0 | } |
299 | 0 | newpos = valueend + 1; |
300 | 0 | } |
301 | 0 | } |
302 | | |
303 | | |
304 | | static const value_string names_direction[] = { |
305 | 43 | #define DIR_C2S 0 |
306 | | { DIR_C2S, "Client to Server" }, |
307 | 43 | #define DIR_S2C 1 |
308 | | { DIR_S2C, "Server to Client" }, |
309 | | { 0, NULL } |
310 | | }; |
311 | | |
312 | | |
313 | | /* I took this name and value directly out of the QW source. */ |
314 | 15 | #define PORT_MASTER 27500 /* Not IANA registered */ |
315 | | static range_t *gbl_quakeworldServerPorts; |
316 | | |
317 | | /* out of band message id bytes (taken out of quakeworldsource/client/protocol.h */ |
318 | | |
319 | | /* M = master, S = server, C = client, A = any */ |
320 | | /* the second character will allways be \n if the message isn't a single */ |
321 | | /* byte long (?? not true anymore?) */ |
322 | | |
323 | 0 | #define S2C_CHALLENGE 'c' |
324 | 0 | #define S2C_CONNECTION 'j' |
325 | 26 | #define A2A_PING 'k' /* respond with an A2A_ACK */ |
326 | 26 | #define A2A_ACK 'l' /* general acknowledgement without info */ |
327 | | #define A2A_NACK 'm' /* [+ comment] general failure */ |
328 | | #define A2A_ECHO 'e' /* for echoing */ |
329 | 0 | #define A2C_PRINT 'n' /* print a message on client */ |
330 | | |
331 | | #define S2M_HEARTBEAT 'a' /* + serverinfo + userlist + fraglist */ |
332 | 0 | #define A2C_CLIENT_COMMAND 'B' /* + command line */ |
333 | | #define S2M_SHUTDOWN 'C' |
334 | | |
335 | | |
336 | | static void |
337 | | dissect_quakeworld_ConnectionlessPacket(tvbuff_t *tvb, packet_info *pinfo, |
338 | | proto_tree *tree, int direction) |
339 | 13 | { |
340 | 13 | proto_tree *cl_tree; |
341 | 13 | proto_tree *text_tree = NULL; |
342 | 13 | proto_item *pi = NULL; |
343 | 13 | const char *text; |
344 | 13 | int len; |
345 | 13 | int offset; |
346 | 13 | uint32_t marker; |
347 | 13 | int command_len; |
348 | 13 | const char *command; |
349 | 13 | bool command_finished = false; |
350 | | |
351 | 13 | marker = tvb_get_ntohl(tvb, 0); |
352 | 13 | cl_tree = proto_tree_add_subtree(tree, tvb, 0, -1, ett_quakeworld_connectionless, NULL, "Connectionless"); |
353 | | |
354 | 13 | proto_tree_add_uint(cl_tree, hf_quakeworld_connectionless_marker, |
355 | 13 | tvb, 0, 4, marker); |
356 | | |
357 | | /* all the rest of the packet is just text */ |
358 | 13 | offset = 4; |
359 | | |
360 | | /* actually, we should look for a eol char and stop already there */ |
361 | 13 | proto_item *text_item; |
362 | 13 | text_item = proto_tree_add_item_ret_string_and_length(cl_tree, hf_quakeworld_connectionless_text, |
363 | 13 | tvb, offset, -1, ENC_ASCII, pinfo->pool, (const uint8_t**)&text, &len); |
364 | 13 | text_tree = proto_item_add_subtree(text_item, ett_quakeworld_connectionless_text); |
365 | | |
366 | 13 | if (direction == DIR_C2S) { |
367 | | /* client to server commands */ |
368 | 13 | const char *c; |
369 | | |
370 | 13 | Cmd_TokenizeString(pinfo->pool, text, len); |
371 | 13 | c = Cmd_Argv(0); |
372 | | |
373 | | /* client to sever commands */ |
374 | 13 | if (strcmp(c,"ping") == 0) { |
375 | 0 | command = "Ping"; |
376 | 0 | command_len = 4; |
377 | 13 | } else if (strcmp(c,"status") == 0) { |
378 | 0 | command = "Status"; |
379 | 0 | command_len = 6; |
380 | 13 | } else if (strcmp(c,"log") == 0) { |
381 | 0 | command = "Log"; |
382 | 0 | command_len = 3; |
383 | 13 | } else if (strcmp(c,"connect") == 0) { |
384 | 0 | uint32_t version = 0; |
385 | 0 | uint16_t qport = 0; |
386 | 0 | uint32_t challenge = 0; |
387 | 0 | bool version_valid = true; |
388 | 0 | bool qport_valid = true; |
389 | 0 | bool challenge_valid = true; |
390 | 0 | const char *infostring; |
391 | 0 | proto_tree *argument_tree = NULL; |
392 | 0 | command = "Connect"; |
393 | 0 | command_len = Cmd_Argv_length(0); |
394 | 0 | if (text_tree) { |
395 | 0 | proto_item *argument_item; |
396 | 0 | pi = proto_tree_add_string(text_tree, hf_quakeworld_connectionless_command, |
397 | 0 | tvb, offset, command_len, command); |
398 | 0 | argument_item = proto_tree_add_string(text_tree, |
399 | 0 | hf_quakeworld_connectionless_arguments, |
400 | 0 | tvb, offset + Cmd_Argv_start(1), len + 1 - Cmd_Argv_start(1), |
401 | 0 | text + Cmd_Argv_start(1)); |
402 | 0 | argument_tree = proto_item_add_subtree(argument_item, |
403 | 0 | ett_quakeworld_connectionless_arguments); |
404 | 0 | command_finished=true; |
405 | 0 | } |
406 | 0 | version_valid = ws_strtou32(Cmd_Argv(1), NULL, &version); |
407 | 0 | qport_valid = ws_strtou16(Cmd_Argv(2), NULL, &qport); |
408 | 0 | challenge_valid = ws_strtou32(Cmd_Argv(3), NULL, &challenge); |
409 | 0 | infostring = Cmd_Argv(4); |
410 | |
|
411 | 0 | if (text_tree && (!version_valid || !qport_valid || !challenge_valid)) |
412 | 0 | expert_add_info(pinfo, pi, &ei_quakeworld_connectionless_command_invalid); |
413 | |
|
414 | 0 | if (argument_tree) { |
415 | 0 | proto_item *info_item; |
416 | 0 | proto_tree *info_tree; |
417 | 0 | proto_tree_add_uint(argument_tree, |
418 | 0 | hf_quakeworld_connectionless_connect_version, |
419 | 0 | tvb, |
420 | 0 | offset + Cmd_Argv_start(1), |
421 | 0 | Cmd_Argv_length(1), version); |
422 | 0 | proto_tree_add_uint(argument_tree, |
423 | 0 | hf_quakeworld_connectionless_connect_qport, |
424 | 0 | tvb, |
425 | 0 | offset + Cmd_Argv_start(2), |
426 | 0 | Cmd_Argv_length(2), qport); |
427 | 0 | proto_tree_add_int(argument_tree, |
428 | 0 | hf_quakeworld_connectionless_connect_challenge, |
429 | 0 | tvb, |
430 | 0 | offset + Cmd_Argv_start(3), |
431 | 0 | Cmd_Argv_length(3), challenge); |
432 | 0 | info_item = proto_tree_add_string(argument_tree, |
433 | 0 | hf_quakeworld_connectionless_connect_infostring, |
434 | 0 | tvb, |
435 | 0 | offset + Cmd_Argv_start(4), |
436 | 0 | Cmd_Argv_length(4), infostring); |
437 | 0 | info_tree = proto_item_add_subtree( |
438 | 0 | info_item, ett_quakeworld_connectionless_connect_infostring); |
439 | 0 | dissect_id_infostring(tvb, info_tree, offset + Cmd_Argv_start(4), |
440 | 0 | wmem_strdup(pinfo->pool, infostring), |
441 | 0 | ett_quakeworld_connectionless_connect_infostring_key_value, |
442 | 0 | hf_quakeworld_connectionless_connect_infostring_key_value, |
443 | 0 | hf_quakeworld_connectionless_connect_infostring_key, |
444 | 0 | hf_quakeworld_connectionless_connect_infostring_value); |
445 | 0 | } |
446 | 13 | } else if (strcmp(c,"getchallenge") == 0) { |
447 | 0 | command = "Get Challenge"; |
448 | 0 | command_len = Cmd_Argv_length(0); |
449 | 13 | } else if (strcmp(c,"rcon") == 0) { |
450 | 0 | const char* password; |
451 | 0 | int i; |
452 | 0 | char remaining[MAX_TEXT_SIZE+1]; |
453 | 0 | proto_tree *argument_tree = NULL; |
454 | 0 | command = "Remote Command"; |
455 | 0 | command_len = Cmd_Argv_length(0); |
456 | 0 | if (text_tree) { |
457 | 0 | proto_item *argument_item; |
458 | 0 | proto_tree_add_string(text_tree, hf_quakeworld_connectionless_command, |
459 | 0 | tvb, offset, command_len, command); |
460 | 0 | argument_item = proto_tree_add_string(text_tree, |
461 | 0 | hf_quakeworld_connectionless_arguments, |
462 | 0 | tvb, offset + Cmd_Argv_start(1), len - Cmd_Argv_start(1), |
463 | 0 | text + Cmd_Argv_start(1)); |
464 | 0 | argument_tree = proto_item_add_subtree(argument_item, |
465 | 0 | ett_quakeworld_connectionless_arguments); |
466 | 0 | command_finished=true; |
467 | 0 | } |
468 | 0 | password = Cmd_Argv(1); |
469 | 0 | if (argument_tree) { |
470 | 0 | proto_tree_add_string(argument_tree, |
471 | 0 | hf_quakeworld_connectionless_rcon_password, |
472 | 0 | tvb, |
473 | 0 | offset + Cmd_Argv_start(1), |
474 | 0 | Cmd_Argv_length(1), password); |
475 | 0 | } |
476 | 0 | remaining[0] = '\0'; |
477 | 0 | for (i=2; i<Cmd_Argc() ; i++) { |
478 | 0 | (void) g_strlcat (remaining, Cmd_Argv(i), MAX_TEXT_SIZE+1); |
479 | 0 | (void) g_strlcat (remaining, " ", MAX_TEXT_SIZE+1); |
480 | 0 | } |
481 | 0 | if (text_tree) { |
482 | 0 | proto_tree_add_string(argument_tree, |
483 | 0 | hf_quakeworld_connectionless_rcon_command, |
484 | 0 | tvb, offset + Cmd_Argv_start(2), |
485 | 0 | Cmd_Argv_start(Cmd_Argc()-1) + Cmd_Argv_length(Cmd_Argc()-1) - |
486 | 0 | Cmd_Argv_start(2), |
487 | 0 | remaining); |
488 | 0 | } |
489 | 13 | } else if (c[0]==A2A_PING && ( c[1]=='\0' || c[1]=='\n')) { |
490 | 0 | command = "Ping"; |
491 | 0 | command_len = 1; |
492 | 13 | } else if (c[0]==A2A_ACK && ( c[1]=='\0' || c[1]=='\n')) { |
493 | 0 | command = "Ack"; |
494 | 0 | command_len = 1; |
495 | 13 | } else { |
496 | 13 | command = "Unknown"; |
497 | 13 | command_len = len - 1; |
498 | 13 | } |
499 | 13 | } |
500 | 0 | else { |
501 | | /* server to client commands */ |
502 | 0 | if (text[0] == S2C_CONNECTION) { |
503 | 0 | command = "Connected"; |
504 | 0 | command_len = 1; |
505 | 0 | } else if (text[0] == A2C_CLIENT_COMMAND) { |
506 | 0 | command = "Client Command"; |
507 | 0 | command_len = 1; |
508 | | /* stringz (command), stringz (localid) */ |
509 | 0 | } else if (text[0] == A2C_PRINT) { |
510 | 0 | command = "Print"; |
511 | 0 | command_len = 1; |
512 | | /* string */ |
513 | 0 | } else if (text[0] == A2A_PING) { |
514 | 0 | command = "Ping"; |
515 | 0 | command_len = 1; |
516 | 0 | } else if (text[0] == S2C_CHALLENGE) { |
517 | 0 | command = "Challenge"; |
518 | 0 | command_len = 1; |
519 | | /* string, conversion */ |
520 | 0 | } else { |
521 | 0 | command = "Unknown"; |
522 | 0 | command_len = len - 1; |
523 | 0 | } |
524 | 0 | } |
525 | | |
526 | 13 | col_append_fstr(pinfo->cinfo, COL_INFO, " %s", command); |
527 | | |
528 | 13 | if (!command_finished) { |
529 | 13 | proto_tree_add_string(text_tree, hf_quakeworld_connectionless_command, |
530 | 13 | tvb, offset, command_len, command); |
531 | 13 | } |
532 | | /*offset += len;*/ |
533 | 13 | } |
534 | | |
535 | | |
536 | | static void |
537 | | dissect_quakeworld_client_commands(tvbuff_t *tvb, packet_info *pinfo, |
538 | | proto_tree *tree) |
539 | 3 | { |
540 | | /* If I have too much time at hand, I'll fill it with all |
541 | | the information from my QWD specs: |
542 | | http://www.planetquake.com/demospecs/qwd/ |
543 | | */ |
544 | 3 | call_data_dissector(tvb, pinfo, tree); |
545 | 3 | } |
546 | | |
547 | | |
548 | | static void |
549 | | dissect_quakeworld_server_commands(tvbuff_t *tvb, packet_info *pinfo, |
550 | | proto_tree *tree) |
551 | 1 | { |
552 | | /* If I have too much time at hand, I'll fill it with all |
553 | | the information from my QWD specs: |
554 | | http://www.planetquake.com/demospecs/qwd/ |
555 | | */ |
556 | 1 | call_data_dissector(tvb, pinfo, tree); |
557 | 1 | } |
558 | | |
559 | | |
560 | | static const value_string names_reliable[] = { |
561 | | { 0, "Non Reliable" }, |
562 | | { 1, "Reliable" }, |
563 | | { 0, NULL } |
564 | | }; |
565 | | |
566 | | |
567 | | static void |
568 | | dissect_quakeworld_GamePacket(tvbuff_t *tvb, packet_info *pinfo, |
569 | | proto_tree *tree, int direction) |
570 | 5 | { |
571 | 5 | proto_tree *game_tree = NULL; |
572 | 5 | uint32_t seq1; |
573 | 5 | uint32_t seq2; |
574 | 5 | int rel1; |
575 | 5 | int rel2; |
576 | 5 | int offset; |
577 | 5 | unsigned rest_length; |
578 | | |
579 | 5 | direction = value_is_in_range(gbl_quakeworldServerPorts, pinfo->destport) ? |
580 | 4 | DIR_C2S : DIR_S2C; |
581 | | |
582 | 5 | game_tree = proto_tree_add_subtree(tree, tvb, 0, -1, ett_quakeworld_game, NULL, "Game"); |
583 | | |
584 | 5 | offset = 0; |
585 | | |
586 | 5 | seq1 = tvb_get_letohl(tvb, offset); |
587 | 5 | rel1 = seq1 & 0x80000000 ? 1 : 0; |
588 | 5 | seq1 &= ~0x80000000; |
589 | 5 | if (game_tree) { |
590 | 5 | proto_tree *seq1_tree = proto_tree_add_subtree_format(game_tree, |
591 | 5 | tvb, offset, 4, ett_quakeworld_game_seq1, NULL, "Current Sequence: %u (%s)", |
592 | 5 | seq1, val_to_str(pinfo->pool, rel1,names_reliable,"%u")); |
593 | 5 | proto_tree_add_uint(seq1_tree, hf_quakeworld_game_seq1, |
594 | 5 | tvb, offset, 4, seq1); |
595 | 5 | proto_tree_add_boolean(seq1_tree, hf_quakeworld_game_rel1, |
596 | 5 | tvb, offset+3, 1, rel1); |
597 | 5 | } |
598 | 5 | offset += 4; |
599 | | |
600 | 5 | seq2 = tvb_get_letohl(tvb, offset); |
601 | 5 | rel2 = seq2 & 0x80000000 ? 1 : 0; |
602 | 5 | seq2 &= ~0x80000000; |
603 | 5 | if (game_tree) { |
604 | 5 | proto_tree *seq2_tree = proto_tree_add_subtree_format(game_tree, |
605 | 5 | tvb, offset, 4, ett_quakeworld_game_seq2, NULL, "Acknowledge Sequence: %u (%s)", |
606 | 5 | seq2, val_to_str(pinfo->pool, rel2,names_reliable,"%u")); |
607 | 5 | proto_tree_add_uint(seq2_tree, hf_quakeworld_game_seq2, tvb, offset, 4, seq2); |
608 | 5 | proto_tree_add_boolean(seq2_tree, hf_quakeworld_game_rel2, tvb, offset+3, 1, rel2); |
609 | 5 | } |
610 | 5 | offset += 4; |
611 | | |
612 | 5 | if (direction == DIR_C2S) { |
613 | | /* client to server */ |
614 | 4 | uint16_t qport = tvb_get_letohs(tvb, offset); |
615 | 4 | if (game_tree) { |
616 | 3 | proto_tree_add_uint(game_tree, hf_quakeworld_game_qport, tvb, offset, 2, qport); |
617 | 3 | } |
618 | 4 | offset +=2; |
619 | 4 | } |
620 | | |
621 | | /* all the rest is pure game data */ |
622 | 5 | rest_length = tvb_reported_length(tvb) - offset; |
623 | 5 | if (rest_length) { |
624 | 4 | tvbuff_t *next_tvb = tvb_new_subset_remaining(tvb, offset); |
625 | 4 | proto_tree *c_tree; |
626 | | |
627 | 4 | if (direction == DIR_C2S) { |
628 | 3 | c_tree = proto_tree_add_subtree(game_tree, next_tvb, |
629 | 3 | 0, -1, ett_quakeworld_game_clc, NULL, "Client Commands"); |
630 | 3 | dissect_quakeworld_client_commands(next_tvb, pinfo, c_tree); |
631 | 3 | } |
632 | 1 | else { |
633 | 1 | c_tree = proto_tree_add_subtree(game_tree, next_tvb, |
634 | 1 | 0, -1, ett_quakeworld_game_svc, NULL, "Server Commands"); |
635 | | |
636 | 1 | dissect_quakeworld_server_commands(next_tvb, pinfo, c_tree); |
637 | 1 | } |
638 | 4 | } |
639 | 5 | } |
640 | | |
641 | | |
642 | | static int |
643 | | dissect_quakeworld(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_) |
644 | 18 | { |
645 | 18 | proto_tree *quakeworld_tree = NULL; |
646 | 18 | int direction; |
647 | | |
648 | 18 | direction = value_is_in_range(gbl_quakeworldServerPorts, pinfo->destport) ? |
649 | 17 | DIR_C2S : DIR_S2C; |
650 | | |
651 | 18 | col_set_str(pinfo->cinfo, COL_PROTOCOL, "QUAKEWORLD"); |
652 | 18 | col_add_str(pinfo->cinfo, COL_INFO, val_to_str(pinfo->pool, direction, |
653 | 18 | names_direction, "%u")); |
654 | | |
655 | 18 | if (tree) { |
656 | 18 | proto_item *quakeworld_item; |
657 | 18 | quakeworld_item = proto_tree_add_item(tree, proto_quakeworld, |
658 | 18 | tvb, 0, -1, ENC_NA); |
659 | 18 | quakeworld_tree = proto_item_add_subtree(quakeworld_item, ett_quakeworld); |
660 | 18 | proto_tree_add_uint_format(quakeworld_tree, |
661 | 18 | direction == DIR_S2C ? |
662 | 1 | hf_quakeworld_s2c : |
663 | 18 | hf_quakeworld_c2s, |
664 | 18 | tvb, 0, 0, 1, |
665 | 18 | "Direction: %s", val_to_str(pinfo->pool, direction, names_direction, "%u")); |
666 | 18 | } |
667 | | |
668 | 18 | if (tvb_get_ntohl(tvb, 0) == 0xffffffff) { |
669 | 13 | col_append_str(pinfo->cinfo, COL_INFO, " Connectionless"); |
670 | 13 | proto_tree_add_uint_format(quakeworld_tree, |
671 | 13 | hf_quakeworld_connectionless, |
672 | 13 | tvb, 0, 0, 1, |
673 | 13 | "Type: Connectionless"); |
674 | 13 | dissect_quakeworld_ConnectionlessPacket( |
675 | 13 | tvb, pinfo, quakeworld_tree, direction); |
676 | 13 | } |
677 | 5 | else { |
678 | 5 | col_append_str(pinfo->cinfo, COL_INFO, " Game"); |
679 | 5 | proto_tree_add_uint_format(quakeworld_tree, |
680 | 5 | hf_quakeworld_game, |
681 | 5 | tvb, 0, 0, 1, |
682 | 5 | "Type: Game"); |
683 | 5 | dissect_quakeworld_GamePacket( |
684 | 5 | tvb, pinfo, quakeworld_tree, direction); |
685 | 5 | } |
686 | 18 | return tvb_captured_length(tvb); |
687 | 18 | } |
688 | | |
689 | | static void |
690 | | apply_quakeworld_prefs(void) |
691 | 15 | { |
692 | | /* Port preference used to determine client/server */ |
693 | 15 | gbl_quakeworldServerPorts = prefs_get_range_value("quakeworld", "udp.port"); |
694 | 15 | } |
695 | | |
696 | | void |
697 | | proto_register_quakeworld(void) |
698 | 15 | { |
699 | 15 | expert_module_t* expert_quakeworld; |
700 | | |
701 | 15 | static hf_register_info hf[] = { |
702 | 15 | { &hf_quakeworld_c2s, |
703 | 15 | { "Client to Server", "quakeworld.c2s", |
704 | 15 | FT_UINT32, BASE_DEC, NULL, 0x0, |
705 | 15 | NULL, HFILL }}, |
706 | 15 | { &hf_quakeworld_s2c, |
707 | 15 | { "Server to Client", "quakeworld.s2c", |
708 | 15 | FT_UINT32, BASE_DEC, NULL, 0x0, |
709 | 15 | NULL, HFILL }}, |
710 | 15 | { &hf_quakeworld_connectionless, |
711 | 15 | { "Connectionless", "quakeworld.connectionless", |
712 | 15 | FT_UINT32, BASE_DEC, NULL, 0x0, |
713 | 15 | NULL, HFILL }}, |
714 | 15 | { &hf_quakeworld_game, |
715 | 15 | { "Game", "quakeworld.game", |
716 | 15 | FT_UINT32, BASE_DEC, NULL, 0x0, |
717 | 15 | NULL, HFILL }}, |
718 | 15 | { &hf_quakeworld_connectionless_marker, |
719 | 15 | { "Marker", "quakeworld.connectionless.marker", |
720 | 15 | FT_UINT32, BASE_HEX, NULL, 0x0, |
721 | 15 | NULL, HFILL }}, |
722 | 15 | { &hf_quakeworld_connectionless_text, |
723 | 15 | { "Text", "quakeworld.connectionless.text", |
724 | 15 | FT_STRINGZ, BASE_NONE, NULL, 0x0, |
725 | 15 | NULL, HFILL }}, |
726 | 15 | { &hf_quakeworld_connectionless_command, |
727 | 15 | { "Command", "quakeworld.connectionless.command", |
728 | 15 | FT_STRING, BASE_NONE, NULL, 0x0, |
729 | 15 | NULL, HFILL }}, |
730 | 15 | { &hf_quakeworld_connectionless_arguments, |
731 | 15 | { "Arguments", "quakeworld.connectionless.arguments", |
732 | 15 | FT_STRING, BASE_NONE, NULL, 0x0, |
733 | 15 | NULL, HFILL }}, |
734 | 15 | { &hf_quakeworld_connectionless_connect_version, |
735 | 15 | { "Version", "quakeworld.connectionless.connect.version", |
736 | 15 | FT_UINT32, BASE_DEC, NULL, 0x0, |
737 | 15 | "Protocol Version", HFILL }}, |
738 | 15 | { &hf_quakeworld_connectionless_connect_qport, |
739 | 15 | { "QPort", "quakeworld.connectionless.connect.qport", |
740 | 15 | FT_UINT32, BASE_DEC, NULL, 0x0, |
741 | 15 | "QPort of the client", HFILL }}, |
742 | 15 | { &hf_quakeworld_connectionless_connect_challenge, |
743 | 15 | { "Challenge", "quakeworld.connectionless.connect.challenge", |
744 | 15 | FT_INT32, BASE_DEC, NULL, 0x0, |
745 | 15 | "Challenge from the server", HFILL }}, |
746 | 15 | { &hf_quakeworld_connectionless_connect_infostring, |
747 | 15 | { "Infostring", "quakeworld.connectionless.connect.infostring", |
748 | 15 | FT_STRING, BASE_NONE, NULL, 0x0, |
749 | 15 | "Infostring with additional variables", HFILL }}, |
750 | 15 | { &hf_quakeworld_connectionless_connect_infostring_key_value, |
751 | 15 | { "Key/Value", "quakeworld.connectionless.connect.infostring.key_value", |
752 | 15 | FT_STRING, BASE_NONE, NULL, 0x0, |
753 | 15 | "Key and Value", HFILL }}, |
754 | 15 | { &hf_quakeworld_connectionless_connect_infostring_key, |
755 | 15 | { "Key", "quakeworld.connectionless.connect.infostring.key", |
756 | 15 | FT_STRING, BASE_NONE, NULL, 0x0, |
757 | 15 | "Infostring Key", HFILL }}, |
758 | 15 | { &hf_quakeworld_connectionless_connect_infostring_value, |
759 | 15 | { "Value", "quakeworld.connectionless.connect.infostring.value", |
760 | 15 | FT_STRING, BASE_NONE, NULL, 0x0, |
761 | 15 | "Infostring Value", HFILL }}, |
762 | 15 | { &hf_quakeworld_connectionless_rcon_password, |
763 | 15 | { "Password", "quakeworld.connectionless.rcon.password", |
764 | 15 | FT_STRING, BASE_NONE, NULL, 0x0, |
765 | 15 | "Rcon Password", HFILL }}, |
766 | 15 | { &hf_quakeworld_connectionless_rcon_command, |
767 | 15 | { "Command", "quakeworld.connectionless.rcon.command", |
768 | 15 | FT_STRING, BASE_NONE, NULL, 0x0, |
769 | 15 | NULL, HFILL }}, |
770 | 15 | { &hf_quakeworld_game_seq1, |
771 | 15 | { "Sequence Number", "quakeworld.game.seq1", |
772 | 15 | FT_UINT32, BASE_DEC, NULL, 0x0, |
773 | 15 | "Sequence number of the current packet", HFILL }}, |
774 | 15 | { &hf_quakeworld_game_rel1, |
775 | 15 | { "Reliable", "quakeworld.game.rel1", |
776 | 15 | FT_BOOLEAN, BASE_NONE, NULL, 0x0, |
777 | 15 | "Packet is reliable and may be retransmitted", HFILL }}, |
778 | 15 | { &hf_quakeworld_game_seq2, |
779 | 15 | { "Sequence Number", "quakeworld.game.seq2", |
780 | 15 | FT_UINT32, BASE_DEC, NULL, 0x0, |
781 | 15 | "Sequence number of the last received packet", HFILL }}, |
782 | 15 | { &hf_quakeworld_game_rel2, |
783 | 15 | { "Reliable", "quakeworld.game.rel2", |
784 | 15 | FT_BOOLEAN, BASE_NONE, NULL, 0x0, |
785 | 15 | "Packet was reliable and may be retransmitted", HFILL }}, |
786 | 15 | { &hf_quakeworld_game_qport, |
787 | 15 | { "QPort", "quakeworld.game.qport", |
788 | 15 | FT_UINT32, BASE_DEC, NULL, 0x0, |
789 | 15 | "QuakeWorld Client Port", HFILL }} |
790 | 15 | }; |
791 | 15 | static int *ett[] = { |
792 | 15 | &ett_quakeworld, |
793 | 15 | &ett_quakeworld_connectionless, |
794 | 15 | &ett_quakeworld_connectionless_text, |
795 | 15 | &ett_quakeworld_connectionless_arguments, |
796 | 15 | &ett_quakeworld_connectionless_connect_infostring, |
797 | 15 | &ett_quakeworld_connectionless_connect_infostring_key_value, |
798 | 15 | &ett_quakeworld_game, |
799 | 15 | &ett_quakeworld_game_seq1, |
800 | 15 | &ett_quakeworld_game_seq2, |
801 | 15 | &ett_quakeworld_game_clc, |
802 | 15 | &ett_quakeworld_game_svc |
803 | 15 | }; |
804 | | |
805 | 15 | static ei_register_info ei[] = { |
806 | 15 | { &ei_quakeworld_connectionless_command_invalid, { "quakeworld.connectionless.command.invalid", |
807 | 15 | PI_MALFORMED, PI_ERROR, "Invalid connectionless command", EXPFILL }} |
808 | 15 | }; |
809 | | |
810 | 15 | proto_quakeworld = proto_register_protocol("QuakeWorld Network Protocol", "QUAKEWORLD", "quakeworld"); |
811 | 15 | proto_register_field_array(proto_quakeworld, hf, array_length(hf)); |
812 | 15 | proto_register_subtree_array(ett, array_length(ett)); |
813 | | |
814 | | /* Register the dissector handle */ |
815 | 15 | quakeworld_handle = register_dissector("quakeworld", dissect_quakeworld, proto_quakeworld); |
816 | | |
817 | | /* Register a configuration option for port */ |
818 | 15 | prefs_register_protocol(proto_quakeworld, apply_quakeworld_prefs); |
819 | | |
820 | 15 | expert_quakeworld = expert_register_protocol(proto_quakeworld); |
821 | 15 | expert_register_field_array(expert_quakeworld, ei, array_length(ei)); |
822 | 15 | } |
823 | | |
824 | | |
825 | | void |
826 | | proto_reg_handoff_quakeworld(void) |
827 | 15 | { |
828 | 15 | dissector_add_uint_with_preference("udp.port", PORT_MASTER, quakeworld_handle); |
829 | 15 | apply_quakeworld_prefs(); |
830 | 15 | } |
831 | | |
832 | | /* |
833 | | * Editor modelines - https://www.wireshark.org/tools/modelines.html |
834 | | * |
835 | | * Local variables: |
836 | | * c-basic-offset: 8 |
837 | | * tab-width: 8 |
838 | | * indent-tabs-mode: t |
839 | | * End: |
840 | | * |
841 | | * vi: set shiftwidth=8 tabstop=8 noexpandtab: |
842 | | * :indentSize=8:tabSize=8:noTabs=false: |
843 | | */ |