forked from osmocom/wireshark
545 lines
19 KiB
C++
545 lines
19 KiB
C++
/* funnel_statistics.cpp
|
|
*
|
|
* Wireshark - Network traffic analyzer
|
|
* By Gerald Combs <gerald@wireshark.org>
|
|
* Copyright 1998 Gerald Combs
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <glib.h>
|
|
|
|
#include "epan/color_filters.h"
|
|
#include "file.h"
|
|
|
|
#include "epan/funnel.h"
|
|
#include "epan/prefs.h"
|
|
|
|
#include <wsutil/wslog.h>
|
|
|
|
#include "ui/progress_dlg.h"
|
|
#include "ui/simple_dialog.h"
|
|
#include <ui/qt/main_window.h>
|
|
|
|
#include "funnel_statistics.h"
|
|
#include "funnel_string_dialog.h"
|
|
#include "funnel_text_dialog.h"
|
|
|
|
#include <QAction>
|
|
#include <QClipboard>
|
|
#include <QDebug>
|
|
#include <QDesktopServices>
|
|
#include <QMenu>
|
|
#include <QUrl>
|
|
|
|
#include "main_application.h"
|
|
|
|
// To do:
|
|
// - Handle menu paths. Do we create a new path (GTK+) or use the base element?
|
|
// - Add a FunnelGraphDialog class?
|
|
|
|
extern "C" {
|
|
static struct _funnel_text_window_t* text_window_new(funnel_ops_id_t *ops_id, const char* title);
|
|
static void string_dialog_new(funnel_ops_id_t *ops_id, const gchar* title, const gchar** field_names, const gchar** field_values, funnel_dlg_cb_t dialog_cb, void* dialog_cb_data, funnel_dlg_cb_data_free_t dialog_cb_data_free);
|
|
|
|
static void funnel_statistics_logger(const gchar *, enum ws_log_level, const gchar *message, gpointer);
|
|
static void funnel_statistics_retap_packets(funnel_ops_id_t *ops_id);
|
|
static void funnel_statistics_copy_to_clipboard(GString *text);
|
|
static const gchar *funnel_statistics_get_filter(funnel_ops_id_t *ops_id);
|
|
static void funnel_statistics_set_filter(funnel_ops_id_t *ops_id, const char* filter_string);
|
|
static gchar* funnel_statistics_get_color_filter_slot(guint8 filter_num);
|
|
static void funnel_statistics_set_color_filter_slot(guint8 filter_num, const gchar* filter_string);
|
|
static gboolean funnel_statistics_open_file(funnel_ops_id_t *ops_id, const char* fname, const char* filter, char**);
|
|
static void funnel_statistics_reload_packets(funnel_ops_id_t *ops_id);
|
|
static void funnel_statistics_redissect_packets(funnel_ops_id_t *ops_id);
|
|
static void funnel_statistics_reload_lua_plugins(funnel_ops_id_t *ops_id);
|
|
static void funnel_statistics_apply_filter(funnel_ops_id_t *ops_id);
|
|
static gboolean browser_open_url(const gchar *url);
|
|
static void browser_open_data_file(const gchar *filename);
|
|
static struct progdlg *progress_window_new(funnel_ops_id_t *ops_id, const gchar* title, const gchar* task, gboolean terminate_is_stop, gboolean *stop_flag);
|
|
static void progress_window_update(struct progdlg *progress_dialog, float percentage, const gchar* status);
|
|
static void progress_window_destroy(struct progdlg *progress_dialog);
|
|
}
|
|
|
|
FunnelAction::FunnelAction(QString title, funnel_menu_callback callback, gpointer callback_data, gboolean retap, QObject *parent = nullptr) :
|
|
QAction(parent),
|
|
title_(title),
|
|
callback_(callback),
|
|
callback_data_(callback_data),
|
|
retap_(retap)
|
|
{
|
|
// Use "&&" to get a real ampersand in the menu item.
|
|
title.replace('&', "&&");
|
|
|
|
setText(title);
|
|
setObjectName(FunnelStatistics::actionName());
|
|
packetRequiredFields_ = QSet<QString>();
|
|
}
|
|
|
|
FunnelAction::FunnelAction(QString title, funnel_packet_menu_callback callback, gpointer callback_data, gboolean retap, const char *packet_required_fields, QObject *parent = nullptr) :
|
|
QAction(parent),
|
|
title_(title),
|
|
callback_data_(callback_data),
|
|
retap_(retap),
|
|
packetCallback_(callback),
|
|
packetRequiredFields_(QSet<QString>())
|
|
{
|
|
// Use "&&" to get a real ampersand in the menu item.
|
|
title.replace('&', "&&");
|
|
|
|
QStringList menuComponents = title.split(QString("/"));
|
|
// Set the menu's text to the rightmost component, set the path to being everything to the left:
|
|
setText("(empty)");
|
|
packetSubmenu_ = "";
|
|
if (!menuComponents.isEmpty())
|
|
{
|
|
setText(menuComponents.last());
|
|
menuComponents.removeLast();
|
|
packetSubmenu_ = menuComponents.join("/");
|
|
}
|
|
|
|
setObjectName(FunnelStatistics::actionName());
|
|
setPacketRequiredFields(packet_required_fields);
|
|
}
|
|
|
|
FunnelAction::~FunnelAction(){
|
|
}
|
|
|
|
funnel_menu_callback FunnelAction::callback() const {
|
|
return callback_;
|
|
}
|
|
|
|
QString FunnelAction::title() const {
|
|
return title_;
|
|
}
|
|
|
|
void FunnelAction::triggerCallback() {
|
|
if (callback_) {
|
|
callback_(callback_data_);
|
|
}
|
|
}
|
|
|
|
void FunnelAction::setPacketCallback(funnel_packet_menu_callback packet_callback) {
|
|
packetCallback_ = packet_callback;
|
|
}
|
|
|
|
void FunnelAction::setPacketRequiredFields(const char *required_fields_str) {
|
|
packetRequiredFields_.clear();
|
|
// If multiple fields are required to be present, they're split by commas
|
|
// Also remove leading and trailing spaces, in case someone writes
|
|
// "http, dns" instead of "http,dns"
|
|
QString requiredFieldsJoined = QString(required_fields_str);
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
|
QStringList requiredFieldsSplit = requiredFieldsJoined.split(",", Qt::SkipEmptyParts);
|
|
#else
|
|
QStringList requiredFieldsSplit = requiredFieldsJoined.split(",", QString::SkipEmptyParts);
|
|
#endif
|
|
foreach (QString requiredField, requiredFieldsSplit) {
|
|
QString trimmedFieldName = requiredField.trimmed();
|
|
if (! trimmedFieldName.isEmpty()) {
|
|
packetRequiredFields_.insert(trimmedFieldName);
|
|
}
|
|
}
|
|
}
|
|
|
|
const QSet<QString> FunnelAction::getPacketRequiredFields() {
|
|
return packetRequiredFields_;
|
|
}
|
|
|
|
|
|
void FunnelAction::setPacketData(GPtrArray* finfos) {
|
|
packetData_ = finfos;
|
|
}
|
|
|
|
void FunnelAction::addToMenu(QMenu * ctx_menu, QHash<QString, QMenu *> menuTextToMenus) {
|
|
QString submenusText = this->getPacketSubmenus();
|
|
if (submenusText.isEmpty()) {
|
|
ctx_menu->addAction(this);
|
|
} else {
|
|
// If the action has a submenu, ensure that the
|
|
// the full submenu chain exists:
|
|
QStringList menuComponents = submenusText.split("/");
|
|
QString menuSubComponentsStringPrior = NULL;
|
|
for (int menuIndex=0; menuIndex < menuComponents.size(); menuIndex++) {
|
|
QStringList menuSubComponents = menuComponents.mid(0, menuIndex+1);
|
|
QString menuSubComponentsString = menuSubComponents.join("/");
|
|
if (!menuTextToMenus.contains(menuSubComponentsString)) {
|
|
// Create a new menu object under the prior object
|
|
QMenu *previousSubmenu = menuTextToMenus.value(menuSubComponentsStringPrior);
|
|
QMenu *submenu = previousSubmenu->addMenu(menuComponents.at(menuIndex));
|
|
menuTextToMenus.insert(menuSubComponentsString, submenu);
|
|
}
|
|
menuSubComponentsStringPrior = menuSubComponentsString;
|
|
}
|
|
// Then add the action to the relevant submenu
|
|
QMenu *parentMenu = menuTextToMenus.value(submenusText);
|
|
parentMenu->addAction(this);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
void FunnelAction::triggerPacketCallback() {
|
|
if (packetCallback_) {
|
|
packetCallback_(callback_data_, packetData_);
|
|
}
|
|
}
|
|
|
|
bool FunnelAction::retap() {
|
|
if (retap_) return true;
|
|
return false;
|
|
}
|
|
|
|
QString FunnelAction::getPacketSubmenus() {
|
|
return packetSubmenu_;
|
|
}
|
|
|
|
static QHash<int, QList<FunnelAction *> > funnel_actions_;
|
|
const QString FunnelStatistics::action_name_ = "FunnelStatisticsAction";
|
|
static gboolean menus_registered = FALSE;
|
|
|
|
struct _funnel_ops_id_t {
|
|
FunnelStatistics *funnel_statistics;
|
|
};
|
|
|
|
FunnelStatistics::FunnelStatistics(QObject *parent, CaptureFile &cf) :
|
|
QObject(parent),
|
|
capture_file_(cf),
|
|
prepared_filter_(QString())
|
|
{
|
|
funnel_ops_ = new(struct _funnel_ops_t);
|
|
memset(funnel_ops_, 0, sizeof(struct _funnel_ops_t));
|
|
funnel_ops_id_ = new(struct _funnel_ops_id_t);
|
|
|
|
funnel_ops_id_->funnel_statistics = this;
|
|
funnel_ops_->ops_id = funnel_ops_id_;
|
|
funnel_ops_->new_text_window = text_window_new;
|
|
funnel_ops_->set_text = text_window_set_text;
|
|
funnel_ops_->append_text = text_window_append;
|
|
funnel_ops_->prepend_text = text_window_prepend;
|
|
funnel_ops_->clear_text = text_window_clear;
|
|
funnel_ops_->get_text = text_window_get_text;
|
|
funnel_ops_->set_close_cb = text_window_set_close_cb;
|
|
funnel_ops_->set_editable = text_window_set_editable;
|
|
funnel_ops_->destroy_text_window = text_window_destroy;
|
|
funnel_ops_->add_button = text_window_add_button;
|
|
funnel_ops_->new_dialog = string_dialog_new;
|
|
funnel_ops_->close_dialogs = string_dialogs_close;
|
|
funnel_ops_->logger = funnel_statistics_logger;
|
|
funnel_ops_->retap_packets = funnel_statistics_retap_packets;
|
|
funnel_ops_->copy_to_clipboard = funnel_statistics_copy_to_clipboard;
|
|
funnel_ops_->get_filter = funnel_statistics_get_filter;
|
|
funnel_ops_->set_filter = funnel_statistics_set_filter;
|
|
funnel_ops_->get_color_filter_slot = funnel_statistics_get_color_filter_slot;
|
|
funnel_ops_->set_color_filter_slot = funnel_statistics_set_color_filter_slot;
|
|
funnel_ops_->open_file = funnel_statistics_open_file;
|
|
funnel_ops_->reload_packets = funnel_statistics_reload_packets;
|
|
funnel_ops_->redissect_packets = funnel_statistics_redissect_packets;
|
|
funnel_ops_->reload_lua_plugins = funnel_statistics_reload_lua_plugins;
|
|
funnel_ops_->apply_filter = funnel_statistics_apply_filter;
|
|
funnel_ops_->browser_open_url = browser_open_url;
|
|
funnel_ops_->browser_open_data_file = browser_open_data_file;
|
|
funnel_ops_->new_progress_window = progress_window_new;
|
|
funnel_ops_->update_progress = progress_window_update;
|
|
funnel_ops_->destroy_progress_window = progress_window_destroy;
|
|
|
|
funnel_set_funnel_ops(funnel_ops_);
|
|
}
|
|
|
|
FunnelStatistics::~FunnelStatistics()
|
|
{
|
|
// At this point we're probably closing the program and will shortly
|
|
// call epan_cleanup, which calls ProgDlg__gc and TextWindow__gc.
|
|
// They in turn depend on funnel_ops_ being valid.
|
|
memset(funnel_ops_id_, 0, sizeof(struct _funnel_ops_id_t));
|
|
memset(funnel_ops_, 0, sizeof(struct _funnel_ops_t));
|
|
// delete(funnel_ops_id_);
|
|
// delete(funnel_ops_);
|
|
}
|
|
|
|
void FunnelStatistics::retapPackets()
|
|
{
|
|
capture_file_.retapPackets();
|
|
}
|
|
|
|
struct progdlg *FunnelStatistics::progressDialogNew(const gchar *task_title, const gchar *item_title, gboolean terminate_is_stop, gboolean *stop_flag)
|
|
{
|
|
return create_progress_dlg(parent(), task_title, item_title, terminate_is_stop, stop_flag);
|
|
}
|
|
|
|
const char *FunnelStatistics::displayFilter()
|
|
{
|
|
return display_filter_.constData();
|
|
}
|
|
|
|
void FunnelStatistics::emitSetDisplayFilter(const QString filter)
|
|
{
|
|
prepared_filter_ = filter;
|
|
emit setDisplayFilter(filter, FilterAction::ActionPrepare, FilterAction::ActionTypePlain);
|
|
}
|
|
|
|
void FunnelStatistics::reloadPackets()
|
|
{
|
|
capture_file_.reload();
|
|
}
|
|
|
|
void FunnelStatistics::redissectPackets()
|
|
{
|
|
// This will trigger a packet redissection.
|
|
mainApp->emitAppSignal(MainApplication::PacketDissectionChanged);
|
|
}
|
|
|
|
void FunnelStatistics::reloadLuaPlugins()
|
|
{
|
|
mainApp->reloadLuaPluginsDelayed();
|
|
}
|
|
|
|
void FunnelStatistics::emitApplyDisplayFilter()
|
|
{
|
|
emit setDisplayFilter(prepared_filter_, FilterAction::ActionApply, FilterAction::ActionTypePlain);
|
|
}
|
|
|
|
void FunnelStatistics::emitOpenCaptureFile(QString cf_path, QString filter)
|
|
{
|
|
emit openCaptureFile(cf_path, filter);
|
|
}
|
|
|
|
void FunnelStatistics::funnelActionTriggered()
|
|
{
|
|
FunnelAction *funnel_action = dynamic_cast<FunnelAction *>(sender());
|
|
if (!funnel_action) return;
|
|
|
|
funnel_action->triggerCallback();
|
|
}
|
|
|
|
void FunnelStatistics::displayFilterTextChanged(const QString &filter)
|
|
{
|
|
display_filter_ = filter.toUtf8();
|
|
}
|
|
|
|
struct _funnel_text_window_t* text_window_new(funnel_ops_id_t *ops_id, const char* title)
|
|
{
|
|
return FunnelTextDialog::textWindowNew(qobject_cast<QWidget *>(ops_id->funnel_statistics->parent()), title);
|
|
}
|
|
|
|
void string_dialog_new(funnel_ops_id_t *ops_id, const gchar* title, const gchar** field_names, const gchar** field_values, funnel_dlg_cb_t dialog_cb, void* dialog_cb_data, funnel_dlg_cb_data_free_t dialog_cb_data_free)
|
|
{
|
|
QList<QPair<QString, QString>> field_list;
|
|
for (int i = 0; field_names[i]; i++) {
|
|
QPair<QString, QString> field = QPair<QString, QString>(QString(field_names[i]), QString(""));
|
|
if (field_values != NULL && field_values[i])
|
|
{
|
|
field.second = QString(field_values[i]);
|
|
}
|
|
|
|
field_list << field;
|
|
}
|
|
FunnelStringDialog::stringDialogNew(qobject_cast<QWidget *>(ops_id->funnel_statistics->parent()), title, field_list, dialog_cb, dialog_cb_data, dialog_cb_data_free);
|
|
}
|
|
|
|
void funnel_statistics_logger(const gchar *log_domain,
|
|
enum ws_log_level log_level,
|
|
const gchar *message,
|
|
gpointer) {
|
|
ws_log(log_domain, log_level, "%s", message);
|
|
}
|
|
|
|
void funnel_statistics_retap_packets(funnel_ops_id_t *ops_id) {
|
|
if (!ops_id || !ops_id->funnel_statistics) return;
|
|
|
|
ops_id->funnel_statistics->retapPackets();
|
|
}
|
|
|
|
void funnel_statistics_copy_to_clipboard(GString *text) {
|
|
mainApp->clipboard()->setText(text->str);
|
|
}
|
|
|
|
const gchar *funnel_statistics_get_filter(funnel_ops_id_t *ops_id) {
|
|
if (!ops_id || !ops_id->funnel_statistics) return nullptr;
|
|
|
|
return ops_id->funnel_statistics->displayFilter();
|
|
}
|
|
|
|
void funnel_statistics_set_filter(funnel_ops_id_t *ops_id, const char* filter_string) {
|
|
if (!ops_id || !ops_id->funnel_statistics) return;
|
|
|
|
ops_id->funnel_statistics->emitSetDisplayFilter(filter_string);
|
|
}
|
|
|
|
gchar* funnel_statistics_get_color_filter_slot(guint8 filter_num) {
|
|
return color_filters_get_tmp(filter_num);
|
|
}
|
|
|
|
void funnel_statistics_set_color_filter_slot(guint8 filter_num, const gchar* filter_string) {
|
|
gchar *err_msg = nullptr;
|
|
if (!color_filters_set_tmp(filter_num, filter_string, FALSE, &err_msg)) {
|
|
simple_dialog(ESD_TYPE_ERROR, ESD_BTN_OK, "%s", err_msg);
|
|
g_free(err_msg);
|
|
}
|
|
}
|
|
|
|
gboolean funnel_statistics_open_file(funnel_ops_id_t *ops_id, const char* fname, const char* filter, char**) {
|
|
// XXX We need to return a proper error value. We should probably move
|
|
// MainWindow::openCaptureFile to CaptureFile and add error handling
|
|
// there.
|
|
if (!ops_id || !ops_id->funnel_statistics) return FALSE;
|
|
|
|
QString cf_name(fname);
|
|
QString cf_filter(filter);
|
|
ops_id->funnel_statistics->emitOpenCaptureFile(cf_name, cf_filter);
|
|
return TRUE;
|
|
}
|
|
|
|
void funnel_statistics_reload_packets(funnel_ops_id_t *ops_id) {
|
|
if (!ops_id || !ops_id->funnel_statistics) return;
|
|
|
|
ops_id->funnel_statistics->reloadPackets();
|
|
}
|
|
|
|
void funnel_statistics_redissect_packets(funnel_ops_id_t *ops_id) {
|
|
if (!ops_id || !ops_id->funnel_statistics) return;
|
|
|
|
ops_id->funnel_statistics->redissectPackets();
|
|
}
|
|
|
|
void funnel_statistics_reload_lua_plugins(funnel_ops_id_t *ops_id) {
|
|
if (!ops_id || !ops_id->funnel_statistics) return;
|
|
|
|
ops_id->funnel_statistics->reloadLuaPlugins();
|
|
}
|
|
|
|
void funnel_statistics_apply_filter(funnel_ops_id_t *ops_id) {
|
|
if (!ops_id || !ops_id->funnel_statistics) return;
|
|
|
|
ops_id->funnel_statistics->emitApplyDisplayFilter();
|
|
}
|
|
|
|
gboolean browser_open_url(const gchar *url) {
|
|
return QDesktopServices::openUrl(QUrl(url)) ? TRUE : FALSE;
|
|
}
|
|
|
|
void browser_open_data_file(const gchar *filename) {
|
|
QDesktopServices::openUrl(QUrl::fromLocalFile(filename));
|
|
}
|
|
|
|
struct progdlg *progress_window_new(funnel_ops_id_t *ops_id, const gchar* task_title, const gchar* item_title, gboolean terminate_is_stop, gboolean *stop_flag) {
|
|
if (!ops_id || !ops_id->funnel_statistics) return nullptr;
|
|
|
|
return ops_id->funnel_statistics->progressDialogNew(task_title, item_title, terminate_is_stop, stop_flag);
|
|
}
|
|
|
|
void progress_window_update(struct progdlg *progress_dialog, float percentage, const gchar* status) {
|
|
update_progress_dlg(progress_dialog, percentage, status);
|
|
}
|
|
|
|
void progress_window_destroy(progdlg *progress_dialog) {
|
|
destroy_progress_dlg(progress_dialog);
|
|
}
|
|
|
|
extern "C" {
|
|
|
|
void register_tap_listener_qt_funnel(void);
|
|
|
|
static void register_menu_cb(const char *name,
|
|
register_stat_group_t group,
|
|
funnel_menu_callback callback,
|
|
gpointer callback_data,
|
|
gboolean retap)
|
|
{
|
|
FunnelAction *funnel_action = new FunnelAction(name, callback, callback_data, retap, mainApp);
|
|
if (menus_registered) {
|
|
mainApp->appendDynamicMenuGroupItem(group, funnel_action);
|
|
} else {
|
|
mainApp->addDynamicMenuGroupItem(group, funnel_action);
|
|
}
|
|
if (!funnel_actions_.contains(group)) {
|
|
funnel_actions_[group] = QList<FunnelAction *>();
|
|
}
|
|
funnel_actions_[group] << funnel_action;
|
|
}
|
|
|
|
/*
|
|
* Callback used to register packet menus in the GUI.
|
|
*
|
|
* Creates a new FunnelAction with the Lua
|
|
* callback and stores it in the Wireshark GUI with
|
|
* appendPacketMenu() so it can be retrieved when
|
|
* the packet's context menu is open.
|
|
*
|
|
* @param name packet menu item's name
|
|
* @param required_fields fields required to be present for the packet menu to be displayed
|
|
* @param callback function called when the menu item is invoked. The function must take one argument and return nothing.
|
|
* @param callback_data Lua state for the callback function
|
|
* @param retap whether or not to rescan all packets
|
|
*/
|
|
static void register_packet_menu_cb(const char *name,
|
|
const char *required_fields,
|
|
funnel_packet_menu_callback callback,
|
|
gpointer callback_data,
|
|
gboolean retap)
|
|
{
|
|
FunnelAction *funnel_action = new FunnelAction(name, callback, callback_data, retap, required_fields, mainApp);
|
|
MainWindow * mainwindow = qobject_cast<MainWindow *>(mainApp->mainWindow());
|
|
if (mainwindow) {
|
|
mainwindow->appendPacketMenu(funnel_action);
|
|
}
|
|
}
|
|
|
|
static void deregister_menu_cb(funnel_menu_callback callback)
|
|
{
|
|
foreach (int group, funnel_actions_.keys()) {
|
|
QList<FunnelAction *>::iterator it = funnel_actions_[group].begin();
|
|
while (it != funnel_actions_[group].end()) {
|
|
FunnelAction *funnel_action = *it;
|
|
if (funnel_action->callback() == callback) {
|
|
// Must set back to title to find the correct sub-menu in Tools
|
|
funnel_action->setText(funnel_action->title());
|
|
mainApp->removeDynamicMenuGroupItem(group, funnel_action);
|
|
it = funnel_actions_[group].erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
register_tap_listener_qt_funnel(void)
|
|
{
|
|
funnel_register_all_menus(register_menu_cb);
|
|
menus_registered = TRUE;
|
|
}
|
|
|
|
void
|
|
funnel_statistics_reload_menus(void)
|
|
{
|
|
funnel_reload_menus(deregister_menu_cb, register_menu_cb);
|
|
funnel_statistics_load_packet_menus();
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns whether the packet menus have been modified since they were last registered
|
|
*
|
|
* @return TRUE if the packet menus were modified since the last registration
|
|
*/
|
|
gboolean
|
|
funnel_statistics_packet_menus_modified(void)
|
|
{
|
|
return funnel_packet_menus_modified();
|
|
}
|
|
|
|
/*
|
|
* Loads all registered_packet_menus into the
|
|
* Wireshark GUI.
|
|
*/
|
|
void
|
|
funnel_statistics_load_packet_menus(void)
|
|
{
|
|
funnel_register_all_packet_menus(register_packet_menu_cb);
|
|
}
|
|
|
|
} // extern "C"
|