1 /* wireless_timeline.cpp
2 * GUI to show an 802.11 wireless timeline of packets
3 *
4 * Wireshark - Network traffic analyzer
5 * By Gerald Combs <gerald@wireshark.org>
6 * Copyright 1998 Gerald Combs
7 *
8 * Copyright 2012 Parc Inc and Samsung Electronics
9 * Copyright 2015, 2016 & 2017 Cisco Inc
10 *
11 * SPDX-License-Identifier: GPL-2.0-or-later
12 */
13
14 #include "wireless_timeline.h"
15
16 #include <epan/packet.h>
17 #include <epan/prefs.h>
18 #include <epan/proto_data.h>
19 #include <epan/packet_info.h>
20 #include <epan/column-utils.h>
21 #include <epan/tap.h>
22
23 #include <cmath>
24
25 #include "globals.h"
26 #include <epan/dissectors/packet-ieee80211-radio.h>
27
28 #include <epan/color_filters.h>
29 #include "frame_tvbuff.h"
30
31 #include <ui/qt/utils/color_utils.h>
32 #include <ui/qt/utils/qt_ui_utils.h>
33 #include "wireshark_application.h"
34 #include <wsutil/report_message.h>
35 #include <wsutil/utf8_entities.h>
36
37 #ifdef Q_OS_WIN
38 #include "wsutil/file_util.h"
39 #include <QSysInfo>
40 #endif
41
42 #include <QPaintEvent>
43 #include <QPainter>
44 #include <QGraphicsScene>
45 #include <QToolTip>
46
47 #include "packet_list.h"
48 #include <ui/qt/models/packet_list_model.h>
49
50 /* we start rendering this number of microseconds left of the left edge - to ensure
51 * NAV lines are drawn correctly, and that small errors in time order don't prevent some
52 * frames from being rendered.
53 * These errors in time order can come from generators that record PHY rate incorrectly
54 * in some circumstances.
55 */
56 #define RENDER_EARLY 40000
57
58
59 const float fraction = 0.8F;
60 const float base = 0.1F;
61 class pcolor : public QColor
62 {
63 public:
pcolor(float red,float green,float blue)64 inline pcolor(float red, float green, float blue) : QColor(
65 (int) (255*(red * fraction + base)),
66 (int) (255*(green * fraction + base)),
67 (int) (255*(blue * fraction + base))) { }
68 };
69
reset_rgb(float rgb[TIMELINE_HEIGHT][3])70 static void reset_rgb(float rgb[TIMELINE_HEIGHT][3])
71 {
72 int i;
73 for (i = 0; i < TIMELINE_HEIGHT; i++)
74 rgb[i][0] = rgb[i][1] = rgb[i][2] = 1.0;
75 }
76
render_pixels(QPainter & p,gint x,gint width,float rgb[TIMELINE_HEIGHT][3],float ratio)77 static void render_pixels(QPainter &p, gint x, gint width, float rgb[TIMELINE_HEIGHT][3], float ratio)
78 {
79 int previous = 0, i;
80 for (i = 1; i <= TIMELINE_HEIGHT; i++) {
81 if (i != TIMELINE_HEIGHT &&
82 rgb[previous][0] == rgb[i][0] &&
83 rgb[previous][1] == rgb[i][1] &&
84 rgb[previous][2] == rgb[i][2])
85 continue;
86 if (rgb[previous][0] != 1.0 || rgb[previous][1] != 1.0 || rgb[previous][2] != 1.0) {
87 p.fillRect(QRectF(x/ratio, previous, width/ratio, i-previous), pcolor(rgb[previous][0],rgb[previous][1],rgb[previous][2]));
88 }
89 previous = i;
90 }
91 reset_rgb(rgb);
92 }
93
render_rectangle(QPainter & p,gint x,gint width,guint height,int dfilter,float r,float g,float b,float ratio)94 static void render_rectangle(QPainter &p, gint x, gint width, guint height, int dfilter, float r, float g, float b, float ratio)
95 {
96 p.fillRect(QRectF(x/ratio, TIMELINE_HEIGHT/2-height, width/ratio, dfilter ? height * 2 : height), pcolor(r,g,b));
97 }
98
accumulate_rgb(float rgb[TIMELINE_HEIGHT][3],int height,int dfilter,float width,float red,float green,float blue)99 static void accumulate_rgb(float rgb[TIMELINE_HEIGHT][3], int height, int dfilter, float width, float red, float green, float blue)
100 {
101 int i;
102 for (i = TIMELINE_HEIGHT/2-height; i < (TIMELINE_HEIGHT/2 + (dfilter ? height : 0)); i++) {
103 rgb[i][0] = rgb[i][0] - width + width * red;
104 rgb[i][1] = rgb[i][1] - width + width * green;
105 rgb[i][2] = rgb[i][2] - width + width * blue;
106 }
107 }
108
109
mousePressEvent(QMouseEvent * event)110 void WirelessTimeline::mousePressEvent(QMouseEvent *event)
111 {
112 start_x = last_x = event->localPos().x();
113 }
114
115
mouseMoveEvent(QMouseEvent * event)116 void WirelessTimeline::mouseMoveEvent(QMouseEvent *event)
117 {
118 if (event->buttons() == Qt::NoButton)
119 return;
120
121 qreal offset = event->localPos().x() - last_x;
122 last_x = event->localPos().x();
123
124 qreal shift = ((qreal) (end_tsf - start_tsf))/width() * offset;
125 start_tsf -= shift;
126 end_tsf -= shift;
127 clip_tsf();
128
129 // TODO: scroll by moving pixels and redraw only exposed area
130 // render(p, ...)
131 // then update full widget only on release.
132 update();
133 }
134
135
mouseReleaseEvent(QMouseEvent * event)136 void WirelessTimeline::mouseReleaseEvent(QMouseEvent *event)
137 {
138 QPointF localPos = event->localPos();
139 qreal offset = localPos.x() - start_x;
140
141 /* if this was a drag, ignore it */
142 if (std::abs(offset) > 3)
143 return;
144
145 /* this was a click */
146 guint num = find_packet(localPos.x());
147 if (num == 0)
148 return;
149
150 frame_data *fdata = frame_data_sequence_find(cfile.provider.frames, num);
151 if (!fdata->passed_dfilter && fdata->prev_dis_num > 0)
152 num = fdata->prev_dis_num;
153
154 cf_goto_frame(&cfile, num);
155 }
156
157
clip_tsf()158 void WirelessTimeline::clip_tsf()
159 {
160 // did we go past the start of the file?
161 if (((gint64) start_tsf) < ((gint64) first->start_tsf)) {
162 // align the start of the file at the left edge
163 guint64 shift = first->start_tsf - start_tsf;
164 start_tsf += shift;
165 end_tsf += shift;
166 }
167 if (end_tsf > last->end_tsf) {
168 guint64 shift = end_tsf - last->end_tsf;
169 start_tsf -= shift;
170 end_tsf -= shift;
171 }
172 }
173
174
selectedFrameChanged(QList<int>)175 void WirelessTimeline::selectedFrameChanged(QList<int>)
176 {
177 if (isHidden())
178 return;
179
180 if (cfile.current_frame) {
181 struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num);
182
183 guint left_margin = 0.9 * start_tsf + 0.1 * end_tsf;
184 guint right_margin = 0.1 * start_tsf + 0.9 * end_tsf;
185 guint64 half_window = (end_tsf - start_tsf)/2;
186
187 if (wr) {
188 // are we to the left of the left margin?
189 if (wr->start_tsf < left_margin) {
190 // scroll the left edge back to the left margin
191 guint64 offset = left_margin - wr->start_tsf;
192 if (offset < half_window) {
193 // small movement; keep packet to margin
194 start_tsf -= offset;
195 end_tsf -= offset;
196 } else {
197 // large movement; move packet to center of window
198 guint64 center = (wr->start_tsf + wr->end_tsf)/2;
199 start_tsf = center - half_window;
200 end_tsf = center + half_window;
201 }
202 } else if (wr->end_tsf > right_margin) {
203 guint64 offset = wr->end_tsf - right_margin;
204 if (offset < half_window) {
205 start_tsf += offset;
206 end_tsf += offset;
207 } else {
208 guint64 center = (wr->start_tsf + wr->end_tsf)/2;
209 start_tsf = center - half_window;
210 end_tsf = center + half_window;
211 }
212 }
213 clip_tsf();
214
215 update();
216 }
217 }
218 }
219
220
221 /* given an x position find which packet that corresponds to.
222 * if it's inter frame space the subsequent packet is returned */
223 guint
find_packet(qreal x_position)224 WirelessTimeline::find_packet(qreal x_position)
225 {
226 guint64 x_time = start_tsf + (x_position/width() * (end_tsf - start_tsf));
227
228 return find_packet_tsf(x_time);
229 }
230
captureFileReadStarted(capture_file * cf)231 void WirelessTimeline::captureFileReadStarted(capture_file *cf)
232 {
233 capfile = cf;
234 hide();
235 // TODO: hide or grey the toolbar controls
236 }
237
captureFileReadFinished()238 void WirelessTimeline::captureFileReadFinished()
239 {
240 /* All frames must be included in packet list */
241 if (cfile.count == 0 || g_hash_table_size(radio_packet_list) != cfile.count)
242 return;
243
244 /* check that all frames have start and end tsf time and are reasonable time order.
245 * packet timing reference seems to be off a little on some generators, which
246 * causes frequent IFS values in the range 0 to -30. Some generators emit excessive
247 * data when an FCS error happens, and this results in the duration calculation for
248 * the error frame being excessively long. This can cause larger negative IFS values
249 * (-30 to -1000) for the subsequent frame. Ignore these cases, as they don't seem
250 * to impact the GUI too badly. If the TSF reference point is set wrong (TSF at
251 * start of frame when it is at the end) then larger negative offsets are often
252 * seen. Don't display the timeline in these cases.
253 */
254 /* TODO: update GUI to handle captures with occasional frames missing TSF data */
255 /* TODO: indicate error message to the user */
256 for (guint32 n = 1; n < cfile.count; n++) {
257 struct wlan_radio *w = get_wlan_radio(n);
258 if (w->start_tsf == 0 || w->end_tsf == 0) {
259 QString err = tr("Packet number %1 does not include TSF timestamp, not showing timeline.").arg(n);
260 wsApp->pushStatus(WiresharkApplication::TemporaryStatus, err);
261 return;
262 }
263 if (w->ifs < -RENDER_EARLY) {
264 QString err = tr("Packet number %u has large negative jump in TSF, not showing timeline. Perhaps TSF reference point is set wrong?").arg(n);
265 wsApp->pushStatus(WiresharkApplication::TemporaryStatus, err);
266 return;
267 }
268 }
269
270 first = get_wlan_radio(1);
271 last = get_wlan_radio(cfile.count);
272
273 start_tsf = first->start_tsf;
274 end_tsf = last->end_tsf;
275
276 /* TODO: only reset the zoom level if the file is changed, not on redissection */
277 zoom_level = 0;
278
279 show();
280 selectedFrameChanged(QList<int>());
281 // TODO: show or ungrey the toolbar controls
282 update();
283 }
284
appInitialized()285 void WirelessTimeline::appInitialized()
286 {
287 connect(wsApp->mainWindow(), SIGNAL(framesSelected(QList<int>)), this, SLOT(selectedFrameChanged(QList<int>)));
288
289 GString *error_string;
290 error_string = register_tap_listener("wlan_radio_timeline", this, NULL, TL_REQUIRES_NOTHING, tap_timeline_reset, tap_timeline_packet, NULL/*tap_draw_cb tap_draw*/, NULL);
291 if (error_string) {
292 report_failure("Wireless Timeline - tap registration failed: %s", error_string->str);
293 g_string_free(error_string, TRUE);
294 }
295 }
296
resizeEvent(QResizeEvent *)297 void WirelessTimeline::resizeEvent(QResizeEvent*)
298 {
299 // TODO adjust scrollbar
300 }
301
302
303 // Calculate the x position on the GUI from the timestamp
position(guint64 tsf,float ratio)304 int WirelessTimeline::position(guint64 tsf, float ratio)
305 {
306 int position = -100;
307
308 if (tsf != G_MAXUINT64) {
309 position = ((double) tsf - start_tsf)*width()*ratio/(end_tsf-start_tsf);
310 }
311 return position;
312 }
313
314
WirelessTimeline(QWidget * parent)315 WirelessTimeline::WirelessTimeline(QWidget *parent) : QWidget(parent)
316 {
317 setHidden(true);
318 zoom_level = 1.0;
319 setFixedHeight(TIMELINE_HEIGHT);
320 first_packet = 1;
321 setMouseTracking(true);
322 start_x = 0;
323 last_x = 0;
324 packet_list = NULL;
325 start_tsf = 0;
326 end_tsf = 0;
327 first = NULL;
328 last = NULL;
329 capfile = NULL;
330
331 radio_packet_list = g_hash_table_new(g_direct_hash, g_direct_equal);
332 connect(wsApp, SIGNAL(appInitialized()), this, SLOT(appInitialized()));
333 }
334
~WirelessTimeline()335 WirelessTimeline::~WirelessTimeline()
336 {
337 if (radio_packet_list != NULL)
338 {
339 g_hash_table_destroy(radio_packet_list);
340 }
341 }
342
setPacketList(PacketList * packet_list)343 void WirelessTimeline::setPacketList(PacketList *packet_list)
344 {
345 this->packet_list = packet_list;
346 }
347
tap_timeline_reset(void * tapdata)348 void WirelessTimeline::tap_timeline_reset(void* tapdata)
349 {
350 WirelessTimeline* timeline = (WirelessTimeline*)tapdata;
351
352 if (timeline->radio_packet_list != NULL)
353 {
354 g_hash_table_destroy(timeline->radio_packet_list);
355 }
356 timeline->hide();
357
358 timeline->radio_packet_list = g_hash_table_new(g_direct_hash, g_direct_equal);
359 }
360
tap_timeline_packet(void * tapdata,packet_info * pinfo,epan_dissect_t * edt _U_,const void * data)361 tap_packet_status WirelessTimeline::tap_timeline_packet(void *tapdata, packet_info* pinfo, epan_dissect_t* edt _U_, const void *data)
362 {
363 WirelessTimeline* timeline = (WirelessTimeline*)tapdata;
364 const struct wlan_radio *wlan_radio_info = (const struct wlan_radio *)data;
365
366 /* Save the radio information in our own (GUI) hashtable */
367 g_hash_table_insert(timeline->radio_packet_list, GUINT_TO_POINTER(pinfo->num), (gpointer)wlan_radio_info);
368 return TAP_PACKET_DONT_REDRAW;
369 }
370
get_wlan_radio(guint32 packet_num)371 struct wlan_radio* WirelessTimeline::get_wlan_radio(guint32 packet_num)
372 {
373 return (struct wlan_radio*)g_hash_table_lookup(radio_packet_list, GUINT_TO_POINTER(packet_num));
374 }
375
doToolTip(struct wlan_radio * wr,QPoint pos,int x)376 void WirelessTimeline::doToolTip(struct wlan_radio *wr, QPoint pos, int x)
377 {
378 if (x < position(wr->start_tsf, 1.0)) {
379 QToolTip::showText(pos, QString("Inter frame space %1 " UTF8_MICRO_SIGN "s").arg(wr->ifs));
380 } else {
381 QToolTip::showText(pos, QString("Total duration %1 " UTF8_MICRO_SIGN "s\nNAV %2 " UTF8_MICRO_SIGN "s")
382 .arg(wr->end_tsf-wr->start_tsf).arg(wr->nav));
383 }
384 }
385
386
event(QEvent * event)387 bool WirelessTimeline::event(QEvent *event)
388 {
389 if (event->type() == QEvent::ToolTip) {
390 QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
391 guint packet = find_packet(helpEvent->pos().x());
392 if (packet) {
393 doToolTip(get_wlan_radio(packet), helpEvent->globalPos(), helpEvent->x());
394 } else {
395 QToolTip::hideText();
396 event->ignore();
397 }
398 return true;
399 }
400 return QWidget::event(event);
401 }
402
403
wheelEvent(QWheelEvent * event)404 void WirelessTimeline::wheelEvent(QWheelEvent *event)
405 {
406 // "Most mouse types work in steps of 15 degrees, in which case the delta
407 // value is a multiple of 120; i.e., 120 units * 1/8 = 15 degrees"
408 double steps = event->angleDelta().y() / 120.0;
409 if (steps != 0.0) {
410 zoom_level += steps;
411 if (zoom_level < 0) zoom_level = 0;
412 if (zoom_level > TIMELINE_MAX_ZOOM) zoom_level = TIMELINE_MAX_ZOOM;
413 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
414 zoom(event->position().x() / width());
415 #else
416 zoom(event->posF().x() / width());
417 #endif
418 }
419 }
420
421
bgColorizationProgress(int first,int last)422 void WirelessTimeline::bgColorizationProgress(int first, int last)
423 {
424 if (isHidden()) return;
425
426 struct wlan_radio *first_wr = get_wlan_radio(first);
427
428 struct wlan_radio *last_wr = get_wlan_radio(last-1);
429
430 int x = position(first_wr->start_tsf, 1);
431 int x_end = position(last_wr->end_tsf, 1);
432
433 update(x, 0, x_end-x+1, height());
434 }
435
436
437 // zoom at relative position 0.0 <= x_fraction <= 1.0.
zoom(double x_fraction)438 void WirelessTimeline::zoom(double x_fraction)
439 {
440 /* adjust the zoom around the selected packet */
441 guint64 file_range = last->end_tsf - first->start_tsf;
442 guint64 center = start_tsf + x_fraction * (end_tsf - start_tsf);
443 guint64 span = pow(file_range, 1.0 - zoom_level / TIMELINE_MAX_ZOOM);
444 start_tsf = center - span * x_fraction;
445 end_tsf = center + span * (1.0 - x_fraction);
446 clip_tsf();
447 update();
448 }
449
find_packet_tsf(guint64 tsf)450 int WirelessTimeline::find_packet_tsf(guint64 tsf)
451 {
452 if (cfile.count < 1)
453 return 0;
454
455 if (cfile.count < 2)
456 return 1;
457
458 guint32 min_count = 1;
459 guint32 max_count = cfile.count-1;
460
461 guint64 min_tsf = get_wlan_radio(min_count)->end_tsf;
462 guint64 max_tsf = get_wlan_radio(max_count)->end_tsf;
463
464 for (;;) {
465 if (tsf >= max_tsf)
466 return max_count+1;
467
468 if (tsf < min_tsf)
469 return min_count;
470
471 guint32 middle = (min_count + max_count)/2;
472 if (middle == min_count)
473 return middle+1;
474
475 guint64 middle_tsf = get_wlan_radio(middle)->end_tsf;
476
477 if (tsf >= middle_tsf) {
478 min_count = middle;
479 min_tsf = middle_tsf;
480 } else {
481 max_count = middle;
482 max_tsf = middle_tsf;
483 }
484 };
485 }
486
487 void
paintEvent(QPaintEvent * qpe)488 WirelessTimeline::paintEvent(QPaintEvent *qpe)
489 {
490 QPainter p(this);
491
492 // painting is done in device pixels in the x axis, get the ratio here
493 float ratio = p.device()->devicePixelRatio();
494
495 unsigned int packet;
496 double zoom;
497 int last_x=-1;
498 int left = qpe->rect().left()*ratio;
499 int right = qpe->rect().right()*ratio;
500 float rgb[TIMELINE_HEIGHT][3];
501 reset_rgb(rgb);
502
503 zoom = ((double) width())/(end_tsf - start_tsf) * ratio;
504
505 /* background is light grey */
506 p.fillRect(0, 0, width(), TIMELINE_HEIGHT, QColor(240,240,240));
507
508 /* background of packets visible in packet_list is white */
509 int top = packet_list->indexAt(QPoint(0,0)).row();
510 int bottom = packet_list->indexAt(QPoint(0,packet_list->viewport()->height())).row();
511
512 frame_data * topData = packet_list->getFDataForRow(top);
513 frame_data * botData = packet_list->getFDataForRow(bottom);
514 if (! topData || ! botData)
515 return;
516
517 int x1 = top == -1 ? 0 : position(get_wlan_radio(topData->num)->start_tsf, ratio);
518 int x2 = bottom == -1 ? width() : position(get_wlan_radio(botData->num)->end_tsf, ratio);
519 p.fillRect(QRectF(x1/ratio, 0, (x2-x1+1)/ratio, TIMELINE_HEIGHT), Qt::white);
520
521 /* background of current packet is blue */
522 if (cfile.current_frame) {
523 struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num);
524 if (wr) {
525 x1 = position(wr->start_tsf, ratio);
526 x2 = position(wr->end_tsf, ratio);
527 p.fillRect(QRectF(x1/ratio, 0, (x2-x1+1)/ratio, TIMELINE_HEIGHT), Qt::blue);
528 }
529 }
530
531 QGraphicsScene qs;
532 for (packet = find_packet_tsf(start_tsf + left/zoom - RENDER_EARLY); packet <= cfile.count; packet++) {
533 frame_data *fdata = frame_data_sequence_find(cfile.provider.frames, packet);
534 struct wlan_radio *ri = get_wlan_radio(fdata->num);
535 float x, width, red, green, blue;
536
537 if (ri == NULL) continue;
538
539 gint8 rssi = ri->aggregate ? ri->aggregate->rssi : ri->rssi;
540 guint height = (rssi+100)/2;
541 gint end_nav;
542
543 /* leave a margin above the packets so the selected packet can be seen */
544 if (height > TIMELINE_HEIGHT/2-6)
545 height = TIMELINE_HEIGHT/2-6;
546
547 /* ensure shortest packets are clearly visible */
548 if (height < 2)
549 height = 2;
550
551 /* skip frames we don't have start and end data for */
552 /* TODO: show something, so it's clear a frame is missing */
553 if (ri->start_tsf == 0 || ri->end_tsf == 0)
554 continue;
555
556 x = ((gint64) (ri->start_tsf - start_tsf))*zoom;
557 /* is there a previous anti-aliased pixel to output */
558 if (last_x >= 0 && ((int) x) != last_x) {
559 /* write it out now */
560 render_pixels(p, last_x, 1, rgb, ratio);
561 last_x = -1;
562 }
563
564 /* does this packet start past the right edge of the window? */
565 if (x >= right) {
566 break;
567 }
568
569 width = (ri->end_tsf - ri->start_tsf)*zoom;
570 if (width < 0) {
571 continue;
572 }
573
574 /* is this packet completely to the left of the displayed area? */
575 // TODO clip NAV line properly if we are displaying it
576 if ((x + width) < left)
577 continue;
578
579 /* remember the first displayed packet */
580 if (first_packet < 0)
581 first_packet = packet;
582
583 if (fdata->color_filter) {
584 const color_t *c = &((const color_filter_t *) fdata->color_filter)->fg_color;
585 red = c->red / 65535.0;
586 green = c->green / 65535.0;
587 blue = c->blue / 65535.0;
588 } else {
589 red = green = blue = 0.0;
590 }
591
592 /* record NAV field at higher magnifications */
593 end_nav = x + width + ri->nav*zoom;
594 if (zoom >= 0.01 && ri->nav && end_nav > 0) {
595 gint y = 2*(packet % (TIMELINE_HEIGHT/2));
596 qs.addLine(QLineF((x+width)/ratio, y, end_nav/ratio, y), QPen(pcolor(red,green,blue)));
597 }
598
599 /* does this rectangle fit within one pixel? */
600 if (((int) x) == ((int) (x+width))) {
601 /* accumulate it for later rendering together
602 * with all other sub pixels that fall within this
603 * pixel */
604 last_x = x;
605 accumulate_rgb(rgb, height, fdata->passed_dfilter, width, red, green, blue);
606 } else {
607 /* it spans more than 1 pixel.
608 * first accumulate the part that does fit */
609 float partial = ((int) x) + 1 - x;
610 accumulate_rgb(rgb, height, fdata->passed_dfilter, partial, red, green, blue);
611 /* and render it */
612 render_pixels(p, (int) x, 1, rgb, ratio);
613 last_x = -1;
614 x += partial;
615 width -= partial;
616 /* are there any whole pixels of width left to draw? */
617 if (width > 1.0) {
618 render_rectangle(p, x, width, height, fdata->passed_dfilter, red, green, blue, ratio);
619 x += (int) width;
620 width -= (int) width;
621 }
622 /* is there a partial pixel left */
623 if (width > 0.0) {
624 last_x = x;
625 accumulate_rgb(rgb, height, fdata->passed_dfilter, width, red, green, blue);
626 }
627 }
628 }
629
630 // draw the NAV lines last, so they appear on top of the packets
631 qs.render(&p, rect(), rect());
632 }
633