1 /* capture_filter_edit.cpp
2 *
3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <gerald@wireshark.org>
5 * Copyright 1998 Gerald Combs
6 *
7 * SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10 #include "config.h"
11
12 #include <glib.h>
13
14 #include <epan/proto.h>
15
16 #include "capture_opts.h"
17
18 #include <ui/capture_globals.h>
19 #include <ui/filter_files.h>
20 #include <wsutil/utf8_entities.h>
21
22 #include <ui/qt/widgets/capture_filter_edit.h>
23 #include "capture_filter_syntax_worker.h"
24 #include "filter_dialog.h"
25 #include <ui/qt/widgets/stock_icon_tool_button.h>
26 #include "wireshark_application.h"
27
28 #include <QComboBox>
29 #include <QCompleter>
30 #include <QMenu>
31 #include <QMessageBox>
32 #include <QPainter>
33 #include <QStringListModel>
34 #include <QStyleOptionFrame>
35
36 #include <ui/qt/utils/qt_ui_utils.h>
37
38 // To do:
39 // - This duplicates some DisplayFilterEdit code.
40 // - We need simplified (button- and dropdown-free) versions for use in dialogs and field-only checking.
41
42 static const QString libpcap_primitive_chars_ = "-0123456789abcdefghijklmnopqrstuvwxyz";
43
44 // Primitives are from pcap-filter.manmisc
45 static const QStringList libpcap_primitives_ = QStringList()
46 // "Abbreviations for..."
47 << "ether proto"
48 << "ip" << "ip6" << "arp" << "rarp" << "atalk" << "aarp" << "decnet" << "iso" << "stp" << "ipx" << "netbeui"
49 << "moprc" << "mopdl"
50 // ip proto
51 << "tcp" << "udp" << "icmp"
52 // iso proto
53 << "clnp" << "esis" << "isis"
54 // IS‐IS PDU types
55 << "l1" << "l2" << "iih" << "lsp" << "snp" << "csnp" << "psnp"
56
57 // grep -E '^\.IP "\\fB.*\\f(R"|P)$' pcap-filter.manmisc | sed -e 's/^\.IP "\\fB/<< "/' -e 's/ *\\f.*/"/' | sort -u
58 << "action"
59 << "clnp"
60 << "decnet dst"
61 << "decnet host"
62 << "decnet src"
63 << "dir"
64 << "dst host"
65 << "dst net"
66 << "dst port"
67 << "dst portrange"
68 << "ether broadcast"
69 << "ether dst"
70 << "ether host"
71 << "ether multicast"
72 << "ether src"
73 << "gateway"
74 << "greater"
75 << "host"
76 << "ifname"
77 << "ip broadcast"
78 << "ip multicast"
79 << "ip proto"
80 << "ip protochain"
81 << "ip6 multicast"
82 << "ip6 proto"
83 << "ip6 protochain"
84 << "iso proto"
85 << "l1"
86 << "lat"
87 << "less"
88 << "mpls"
89 << "net"
90 << "on"
91 << "port"
92 << "portrange"
93 << "reason"
94 << "rnr"
95 << "rset"
96 << "rulenum"
97 << "ruleset"
98 << "src host"
99 << "src net"
100 << "src port"
101 << "src portrange"
102 << "srnr"
103 << "subrulenum"
104 << "subtype"
105 << "type"
106 << "vlan"
107 << "wlan addr1"
108 << "wlan addr2"
109 << "wlan addr3"
110 << "wlan addr4"
111 << "wlan ra"
112 << "wlan ta"
113 ;
114
CaptureFilterEdit(QWidget * parent,bool plain)115 CaptureFilterEdit::CaptureFilterEdit(QWidget *parent, bool plain) :
116 SyntaxLineEdit(parent),
117 plain_(plain),
118 field_name_only_(false),
119 enable_save_action_(false),
120 save_action_(NULL),
121 remove_action_(NULL),
122 actions_(Q_NULLPTR),
123 bookmark_button_(NULL),
124 clear_button_(NULL),
125 apply_button_(NULL)
126 {
127 setAccessibleName(tr("Capture filter entry"));
128
129 completion_model_ = new QStringListModel(this);
130 setCompleter(new QCompleter(completion_model_, this));
131 setCompletionTokenChars(libpcap_primitive_chars_);
132
133 setConflict(false);
134
135 if (!plain_) {
136 bookmark_button_ = new StockIconToolButton(this, "x-capture-filter-bookmark");
137 bookmark_button_->setCursor(Qt::ArrowCursor);
138 bookmark_button_->setMenu(new QMenu(bookmark_button_));
139 bookmark_button_->setPopupMode(QToolButton::InstantPopup);
140 bookmark_button_->setToolTip(tr("Manage saved bookmarks."));
141 bookmark_button_->setIconSize(QSize(14, 14));
142 bookmark_button_->setStyleSheet(
143 "QToolButton {"
144 " border: none;"
145 " background: transparent;" // Disables platform style on Windows.
146 " padding: 0 0 0 0;"
147 "}"
148 "QToolButton::menu-indicator { image: none; }"
149 );
150 connect(bookmark_button_, &StockIconToolButton::clicked, this, &CaptureFilterEdit::bookmarkClicked);
151
152 clear_button_ = new StockIconToolButton(this, "x-filter-clear");
153 clear_button_->setCursor(Qt::ArrowCursor);
154 clear_button_->setToolTip(QString());
155 clear_button_->setIconSize(QSize(14, 14));
156 clear_button_->setStyleSheet(
157 "QToolButton {"
158 " border: none;"
159 " background: transparent;" // Disables platform style on Windows.
160 " padding: 0 0 0 0;"
161 " margin-left: 1px;"
162 "}"
163 );
164 connect(clear_button_, &StockIconToolButton::clicked, this, &CaptureFilterEdit::clearFilter);
165 }
166
167 connect(this, &CaptureFilterEdit::textChanged, this,
168 static_cast<void (CaptureFilterEdit::*)(const QString &)>(&CaptureFilterEdit::checkFilter));
169
170 #if 0
171 // Disable the apply button for now
172 if (!plain_) {
173 apply_button_ = new StockIconToolButton(this, "x-filter-apply");
174 apply_button_->setCursor(Qt::ArrowCursor);
175 apply_button_->setEnabled(false);
176 apply_button_->setToolTip(tr("Apply this filter string to the display."));
177 apply_button_->setIconSize(QSize(24, 14));
178 apply_button_->setStyleSheet(
179 "QToolButton {"
180 " border: none;"
181 " background: transparent;" // Disables platform style on Windows.
182 " padding: 0 0 0 0;"
183 "}"
184 );
185 connect(apply_button_, &StockIconToolButton::clicked, this, &CaptureFilterEdit::applyCaptureFilter);
186 }
187 #endif
188 connect(this, &CaptureFilterEdit::returnPressed, this, &CaptureFilterEdit::applyCaptureFilter);
189
190 int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
191 QSize bksz;
192 if (bookmark_button_) bksz = bookmark_button_->sizeHint();
193 QSize cbsz;
194 if (clear_button_) cbsz = clear_button_->sizeHint();
195 QSize apsz;
196 if (apply_button_) apsz = apply_button_->sizeHint();
197
198 setStyleSheet(QString(
199 "CaptureFilterEdit {"
200 " padding-left: %1px;"
201 " margin-left: %2px;"
202 " margin-right: %3px;"
203 "}"
204 )
205 .arg(frameWidth + 1)
206 .arg(bksz.width())
207 .arg(cbsz.width() + apsz.width() + frameWidth + 1)
208 );
209
210 QComboBox *cf_combo = qobject_cast<QComboBox *>(parent);
211 if (cf_combo) {
212 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
213 connect(cf_combo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::textActivated),
214 this, &CaptureFilterEdit::textEdited);
215 #else
216 connect(cf_combo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
217 this, &CaptureFilterEdit::textEdited);
218 #endif
219 }
220
221 syntax_thread_ = new QThread;
222 syntax_worker_ = new CaptureFilterSyntaxWorker;
223 syntax_worker_->moveToThread(syntax_thread_);
224 connect(wsApp, &WiresharkApplication::appInitialized, this, &CaptureFilterEdit::updateBookmarkMenu);
225 connect(wsApp, &WiresharkApplication::captureFilterListChanged, this, &CaptureFilterEdit::updateBookmarkMenu);
226 connect(syntax_thread_, &QThread::started, this,
227 static_cast<void (CaptureFilterEdit::*)()>(&CaptureFilterEdit::checkFilter));
228 connect(syntax_worker_, &CaptureFilterSyntaxWorker::syntaxResult,
229 this, &CaptureFilterEdit::setFilterSyntaxState);
230 connect(this, &CaptureFilterEdit::captureFilterChanged, syntax_worker_, &CaptureFilterSyntaxWorker::checkFilter);
231 syntax_thread_->start();
232 updateBookmarkMenu();
233 }
234
~CaptureFilterEdit()235 CaptureFilterEdit::~CaptureFilterEdit()
236 {
237 syntax_thread_->quit();
238 syntax_thread_->wait();
239 delete syntax_thread_;
240 delete syntax_worker_;
241 }
242
paintEvent(QPaintEvent * evt)243 void CaptureFilterEdit::paintEvent(QPaintEvent *evt) {
244 SyntaxLineEdit::paintEvent(evt);
245
246 if (bookmark_button_) {
247 // Draw the right border by hand. We could try to do this in the
248 // style sheet but it's a pain.
249 #ifdef Q_OS_MAC
250 QColor divider_color = Qt::gray;
251 #else
252 QColor divider_color = palette().shadow().color();
253 #endif
254 QPainter painter(this);
255 painter.setPen(divider_color);
256 QRect cr = contentsRect();
257 QSize bksz = bookmark_button_->size();
258 painter.drawLine(bksz.width(), cr.top(), bksz.width(), cr.bottom());
259 }
260 }
261
resizeEvent(QResizeEvent *)262 void CaptureFilterEdit::resizeEvent(QResizeEvent *)
263 {
264 QSize cbsz;
265 if (clear_button_) cbsz = clear_button_->sizeHint();
266 QSize apsz;
267 if (apply_button_) apsz = apply_button_->sizeHint();
268
269 int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
270 if (clear_button_) {
271 clear_button_->move(contentsRect().right() - frameWidth - cbsz.width() - apsz.width(),
272 contentsRect().top());
273 clear_button_->setMinimumHeight(contentsRect().height());
274 clear_button_->setMaximumHeight(contentsRect().height());
275 }
276 if (apply_button_) {
277 apply_button_->move(contentsRect().right() - frameWidth - apsz.width(),
278 contentsRect().top());
279 apply_button_->setMinimumHeight(contentsRect().height());
280 apply_button_->setMaximumHeight(contentsRect().height());
281 }
282 if (bookmark_button_) {
283 bookmark_button_->setMinimumHeight(contentsRect().height());
284 bookmark_button_->setMaximumHeight(contentsRect().height());
285 }
286 }
287
setConflict(bool conflict)288 void CaptureFilterEdit::setConflict(bool conflict)
289 {
290 if (conflict) {
291 //: This is a very long concept that needs to fit into a short space.
292 placeholder_text_ = tr("Multiple filters selected. Override them here or leave this blank to preserve them.");
293 setToolTip(tr("<p>The interfaces you have selected have different capture filters."
294 " Typing a filter here will override them. Doing nothing will"
295 " preserve them.</p>"));
296 } else {
297 placeholder_text_ = QString(tr("Enter a capture filter %1")).arg(UTF8_HORIZONTAL_ELLIPSIS);
298 setToolTip(QString());
299 }
300 setPlaceholderText(placeholder_text_);
301 }
302
303 // XXX Make this private along with setConflict.
getSelectedFilter()304 QPair<const QString, bool> CaptureFilterEdit::getSelectedFilter()
305 {
306 QString user_filter;
307 bool filter_conflict = false;
308 #ifdef HAVE_LIBPCAP
309 int selected_devices = 0;
310
311 for (guint i = 0; i < global_capture_opts.all_ifaces->len; i++) {
312 interface_t *device = &g_array_index(global_capture_opts.all_ifaces, interface_t, i);
313 if (device->selected) {
314 selected_devices++;
315 if (selected_devices == 1) {
316 user_filter = device->cfilter;
317 } else {
318 if (user_filter.compare(device->cfilter)) {
319 filter_conflict = true;
320 }
321 }
322 }
323 }
324 #endif // HAVE_LIBPCAP
325 return QPair<const QString, bool>(user_filter, filter_conflict);
326 }
327
checkFilter(const QString & filter)328 void CaptureFilterEdit::checkFilter(const QString& filter)
329 {
330 if (text().length() == 0 && actions_ && actions_->checkedAction())
331 actions_->checkedAction()->setChecked(false);
332
333 setSyntaxState(Busy);
334 wsApp->popStatus(WiresharkApplication::FilterSyntax);
335 setToolTip(QString());
336 bool empty = filter.isEmpty();
337
338 setConflict(false);
339 if (bookmark_button_) {
340 bool match = false;
341
342 FilterListModel model(FilterListModel::Capture);
343 QModelIndex idx = model.findByExpression(text());
344 if (idx.isValid()) {
345 match = true;
346
347 bookmark_button_->setStockIcon("x-filter-matching-bookmark");
348 if (remove_action_) {
349 remove_action_->setData(text());
350 remove_action_->setEnabled(true);
351 }
352 } else {
353 bookmark_button_->setStockIcon("x-capture-filter-bookmark");
354 if (remove_action_) {
355 remove_action_->setEnabled(false);
356 }
357 }
358
359 enable_save_action_ = (!match && !filter.isEmpty());
360 if (save_action_) {
361 save_action_->setEnabled(false);
362 }
363 }
364
365 if (apply_button_) {
366 apply_button_->setEnabled(false);
367 }
368
369 if (clear_button_) {
370 clear_button_->setVisible(!empty);
371 }
372
373 if (empty) {
374 setFilterSyntaxState(filter, Empty, QString());
375 } else {
376 emit captureFilterChanged(filter);
377 }
378 }
379
checkFilter()380 void CaptureFilterEdit::checkFilter()
381 {
382 checkFilter(text());
383 }
384
updateBookmarkMenu()385 void CaptureFilterEdit::updateBookmarkMenu()
386 {
387 if (!bookmark_button_)
388 return;
389
390 QMenu *bb_menu = bookmark_button_->menu();
391 bb_menu->clear();
392
393 save_action_ = bb_menu->addAction(tr("Save this filter"));
394 connect(save_action_, &QAction::triggered, this, &CaptureFilterEdit::saveFilter);
395 remove_action_ = bb_menu->addAction(tr("Remove this filter"));
396 connect(remove_action_, &QAction::triggered, this, &CaptureFilterEdit::removeFilter);
397 QAction *manage_action = bb_menu->addAction(tr("Manage Capture Filters"));
398 connect(manage_action, &QAction::triggered, this, &CaptureFilterEdit::showFilters);
399 bb_menu->addSeparator();
400
401 FilterListModel model(FilterListModel::Capture);
402 QModelIndex idx = model.findByExpression(text());
403
404 int one_em = bb_menu->fontMetrics().height();
405
406 if (! actions_)
407 actions_ = new QActionGroup(this);
408
409 for (int row = 0; row < model.rowCount(); row++)
410 {
411 QModelIndex nameIdx = model.index(row, FilterListModel::ColumnName);
412 QString name = nameIdx.data().toString();
413 QString expr = model.index(row, FilterListModel::ColumnExpression).data().toString();
414 QString prep_text = QString("%1: %2").arg(name).arg(expr);
415
416 prep_text = bb_menu->fontMetrics().elidedText(prep_text, Qt::ElideRight, one_em * 40);
417 QAction * prep_action = bb_menu->addAction(prep_text);
418 prep_action->setCheckable(true);
419 if (nameIdx == idx)
420 prep_action->setChecked(true);
421
422 actions_->addAction(prep_action);
423 prep_action->setProperty("capture_filter", expr);
424 connect(prep_action, &QAction::triggered, this, &CaptureFilterEdit::prepareFilter);
425 }
426
427 checkFilter();
428 }
429
setFilterSyntaxState(QString filter,int state,QString err_msg)430 void CaptureFilterEdit::setFilterSyntaxState(QString filter, int state, QString err_msg)
431 {
432 if (filter.compare(text()) == 0) { // The user hasn't changed the filter
433 setSyntaxState((SyntaxState)state);
434 if (!err_msg.isEmpty()) {
435 wsApp->pushStatus(WiresharkApplication::FilterSyntax, err_msg);
436 setToolTip(err_msg);
437 }
438 }
439
440 bool valid = (state != Invalid);
441
442 if (valid) {
443 if (save_action_) {
444 save_action_->setEnabled(enable_save_action_);
445 }
446 if (apply_button_) {
447 apply_button_->setEnabled(true);
448 }
449 }
450
451 emit captureFilterSyntaxChanged(valid);
452 }
453
bookmarkClicked()454 void CaptureFilterEdit::bookmarkClicked()
455 {
456 emit addBookmark(text());
457 }
458
clearFilter()459 void CaptureFilterEdit::clearFilter()
460 {
461 clear();
462 emit textEdited(text());
463 }
464
buildCompletionList(const QString & primitive_word)465 void CaptureFilterEdit::buildCompletionList(const QString &primitive_word)
466 {
467 if (primitive_word.length() < 1) {
468 completion_model_->setStringList(QStringList());
469 return;
470 }
471
472 // Grab matching capture filters from our parent combo and from the
473 // saved capture filters file. Skip ones that look like single fields
474 // and assume they will be added below.
475 QStringList complex_list;
476 QComboBox *cf_combo = qobject_cast<QComboBox *>(parent());
477 if (cf_combo) {
478 for (int i = 0; i < cf_combo->count() ; i++) {
479 QString recent_filter = cf_combo->itemText(i);
480
481 if (isComplexFilter(recent_filter)) {
482 complex_list << recent_filter;
483 }
484 }
485 }
486 FilterListModel model(FilterListModel::Capture);
487 for (int row = 0; row < model.rowCount(); row++)
488 {
489 QString saved_filter = model.index(row, FilterListModel::ColumnExpression).data().toString();
490
491 if (isComplexFilter(saved_filter) && !complex_list.contains(saved_filter)) {
492 complex_list << saved_filter;
493 }
494 }
495
496 // libpcap has a small number of primitives so we just add the whole list
497 // sans the current word.
498 QStringList primitive_list = libpcap_primitives_;
499 primitive_list.removeAll(primitive_word);
500
501 completion_model_->setStringList(complex_list + primitive_list);
502 completer()->setCompletionPrefix(primitive_word);
503 }
504
applyCaptureFilter()505 void CaptureFilterEdit::applyCaptureFilter()
506 {
507 if (syntaxState() == Invalid) {
508 return;
509 }
510
511 emit startCapture();
512 }
513
saveFilter()514 void CaptureFilterEdit::saveFilter()
515 {
516 FilterDialog *capture_filter_dlg = new FilterDialog(window(), FilterDialog::CaptureFilter, text());
517 capture_filter_dlg->setWindowModality(Qt::ApplicationModal);
518 capture_filter_dlg->setAttribute(Qt::WA_DeleteOnClose);
519 capture_filter_dlg->show();
520 }
521
removeFilter()522 void CaptureFilterEdit::removeFilter()
523 {
524 if (! actions_ || ! actions_->checkedAction())
525 return;
526
527 QAction *ra = actions_->checkedAction();
528 if (ra->property("capture_filter").toString().isEmpty())
529 return;
530
531 QString remove_filter = ra->property("capture_filter").toString();
532
533 FilterListModel model(FilterListModel::Capture);
534 QModelIndex idx = model.findByExpression(remove_filter);
535 if (idx.isValid())
536 {
537 model.removeFilter(idx);
538 model.saveList();
539 }
540
541 updateBookmarkMenu();
542 }
543
showFilters()544 void CaptureFilterEdit::showFilters()
545 {
546 FilterDialog *capture_filter_dlg = new FilterDialog(window(), FilterDialog::CaptureFilter);
547 capture_filter_dlg->setWindowModality(Qt::ApplicationModal);
548 capture_filter_dlg->setAttribute(Qt::WA_DeleteOnClose);
549 capture_filter_dlg->show();
550 }
551
prepareFilter()552 void CaptureFilterEdit::prepareFilter()
553 {
554 QAction *pa = qobject_cast<QAction*>(sender());
555 if (! pa || pa->property("capture_filter").toString().isEmpty())
556 return;
557
558 QString filter = pa->property("capture_filter").toString();
559 setText(filter);
560
561 emit textEdited(filter);
562 }
563