1 /* follow_stream_dialog.cpp
3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <gerald@wireshark.org>
5 * Copyright 1998 Gerald Combs
7 * SPDX-License-Identifier: GPL-2.0-or-later
10 #include "follow_stream_dialog.h"
11 #include <ui_follow_stream_dialog.h>
13 #include "main_application.h"
14 #include "main_window.h"
16 #include "frame_tvbuff.h"
17 #include "epan/follow.h"
18 #include "epan/prefs.h"
19 #include "epan/addr_resolv.h"
20 #include "epan/charsets.h"
21 #include "epan/epan_dissect.h"
24 #include "ui/alert_box.h"
25 #include "ui/simple_dialog.h"
26 #include <ui/recent.h>
27 #include <wsutil/utf8_entities.h>
28 #include <wsutil/ws_assert.h>
30 #include "wsutil/file_util.h"
31 #include "wsutil/str_util.h"
32 #include "wsutil/filesystem.h"
34 #include "ws_symbol_export.h"
36 #include <ui/qt/utils/color_utils.h>
37 #include <ui/qt/utils/qt_ui_utils.h>
39 #include "progress_frame.h"
41 #include "ui/qt/widgets/wireshark_file_dialog.h"
43 #include <QElapsedTimer>
45 #include <QMessageBox>
47 #include <QPrintDialog>
53 // - Show text while tapping.
54 // - Instead of calling QMessageBox, display the error message in the text
55 // box and disable the appropriate controls.
56 // - Add a progress bar and connect captureCaptureUpdateContinue to it
58 // Matches SplashOverlay.
59 static int info_update_freq_
= 100;
61 // Handle the loop breaking notification properly
62 static QMutex loop_break_mutex
;
64 // Indicates that a Follow Stream is currently running
65 static bool isReadRunning
;
67 Q_DECLARE_METATYPE(bytes_show_type
)
69 FollowStreamDialog::FollowStreamDialog(QWidget
&parent
, CaptureFile
&cf
, int proto_id
) :
70 WiresharkDialog(parent
, cf
),
71 ui(new Ui::FollowStreamDialog
),
74 client_buffer_count_(0),
75 server_buffer_count_(0),
76 client_packet_count_(0),
77 server_packet_count_(0),
81 use_regex_find_(false),
83 previous_sub_stream_num_(0)
86 loadGeometry(parent
.width() * 2 / 3, parent
.height());
88 ui
->streamNumberSpinBox
->setStyleSheet("QSpinBox { min-width: 2em; }");
89 ui
->subStreamNumberSpinBox
->setStyleSheet("QSpinBox { min-width: 2em; }");
90 ui
->streamNumberSpinBox
->setKeyboardTracking(false);
91 ui
->subStreamNumberSpinBox
->setKeyboardTracking(false);
93 follower_
= get_follow_by_proto_id(proto_id
);
94 if (follower_
== NULL
) {
95 ws_assert_not_reached();
98 memset(&follow_info_
, 0, sizeof(follow_info_
));
99 follow_info_
.show_stream
= BOTH_HOSTS
;
100 follow_info_
.substream_id
= SUBSTREAM_UNUSED
;
102 nstime_set_zero(&last_ts_
);
104 ui
->teStreamContent
->installEventFilter(this);
106 connect(ui
->leFind
, SIGNAL(useRegexFind(bool)), this, SLOT(useRegexFind(bool)));
108 QComboBox
*cbcs
= ui
->cbCharset
;
109 cbcs
->blockSignals(true);
110 cbcs
->addItem(tr("ASCII"), SHOW_ASCII
);
111 cbcs
->addItem(tr("C Arrays"), SHOW_CARRAY
);
112 cbcs
->addItem(tr("EBCDIC"), SHOW_EBCDIC
);
113 cbcs
->addItem(tr("Hex Dump"), SHOW_HEXDUMP
);
114 cbcs
->addItem(tr("Raw"), SHOW_RAW
);
115 // UTF-8 is guaranteed to exist as a QTextCodec
116 cbcs
->addItem(tr("UTF-8"), SHOW_CODEC
);
117 cbcs
->addItem(tr("YAML"), SHOW_YAML
);
118 cbcs
->setCurrentIndex(cbcs
->findData(recent
.gui_follow_show
));
119 cbcs
->blockSignals(false);
121 ui
->deltaComboBox
->setCurrentIndex(recent
.gui_follow_delta
);
123 b_filter_out_
= ui
->buttonBox
->addButton(tr("Filter Out This Stream"), QDialogButtonBox::ActionRole
);
124 connect(b_filter_out_
, &QPushButton::clicked
, this, &FollowStreamDialog::filterOut
);
126 b_print_
= ui
->buttonBox
->addButton(tr("Print"), QDialogButtonBox::ActionRole
);
127 connect(b_print_
, &QPushButton::clicked
, this, &FollowStreamDialog::printStream
);
129 b_save_
= ui
->buttonBox
->addButton(tr("Save as…"), QDialogButtonBox::ActionRole
);
130 connect(b_save_
, &QPushButton::clicked
, this, &FollowStreamDialog::saveAs
);
132 b_back_
= ui
->buttonBox
->addButton(tr("Back"), QDialogButtonBox::ActionRole
);
133 connect(b_back_
, &QPushButton::clicked
, this, &FollowStreamDialog::backButton
);
135 ProgressFrame::addToButtonBox(ui
->buttonBox
, &parent
);
137 connect(ui
->cbDirections
, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged
),
138 this, &FollowStreamDialog::cbDirectionsCurrentIndexChanged
);
139 connect(ui
->cbCharset
, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged
),
140 this, &FollowStreamDialog::cbCharsetCurrentIndexChanged
);
141 connect(ui
->deltaComboBox
, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged
),
142 this, &FollowStreamDialog::deltaComboBoxCurrentIndexChanged
);
144 connect(ui
->streamNumberSpinBox
, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged
),
145 this, &FollowStreamDialog::streamNumberSpinBoxValueChanged
);
146 connect(ui
->subStreamNumberSpinBox
, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged
),
147 this, &FollowStreamDialog::subStreamNumberSpinBoxValueChanged
);
149 connect(ui
->buttonBox
, &QDialogButtonBox::helpRequested
, this, &FollowStreamDialog::helpButton
);
150 connect(ui
->teStreamContent
, &FollowStreamText::mouseMovedToPacket
,
151 this, &FollowStreamDialog::fillHintLabel
);
152 connect(ui
->teStreamContent
, &FollowStreamText::mouseClickedOnPacket
,
153 this, &FollowStreamDialog::goToPacketForTextPos
);
155 connect(ui
->bFind
, &QPushButton::clicked
, this, &FollowStreamDialog::bFindClicked
);
156 connect(ui
->leFind
, &FindLineEdit::returnPressed
, this, &FollowStreamDialog::leFindReturnPressed
);
158 connect(ui
->buttonBox
, &QDialogButtonBox::rejected
, this, &FollowStreamDialog::buttonBoxRejected
);
163 FollowStreamDialog::~FollowStreamDialog()
166 resetStream(); // Frees payload
169 void FollowStreamDialog::addCodecs(const QMap
<QString
, QTextCodec
*> &codecMap
)
171 // Make the combobox respect max visible items?
172 //ui->cbCharset->setStyleSheet("QComboBox { combobox-popup: 0;}");
173 ui
->cbCharset
->insertSeparator(ui
->cbCharset
->count());
174 for (const auto &codec
: codecMap
) {
175 // This is already in the menu and handled separately
176 if (codec
->name() != "US-ASCII" && codec
->name() != "UTF-8")
177 ui
->cbCharset
->addItem(tr(codec
->name()), SHOW_CODEC
);
181 void FollowStreamDialog::printStream()
183 #ifndef QT_NO_PRINTER
184 QPrinter
printer(QPrinter::HighResolution
);
185 QPrintDialog
dialog(&printer
, this);
186 if (dialog
.exec() == QDialog::Accepted
)
187 ui
->teStreamContent
->print(&printer
);
191 void FollowStreamDialog::fillHintLabel(int pkt
)
195 bool is_stratoshark
= strcmp(get_configuration_namespace(), "Stratoshark") == 0;
197 if (is_stratoshark
) {
199 hint
= tr("Event %1. ").arg(pkt
);
202 hint
+= tr("%Ln <span style=\"color: %1; background-color:%2\">reads</span>, ", "", client_packet_count_
)
203 .arg(ColorUtils::fromColorT(prefs
.st_client_fg
).name(),
204 ColorUtils::fromColorT(prefs
.st_client_bg
).name())
205 + tr("%Ln <span style=\"color: %1; background-color:%2\">writes</span>, ", "", server_packet_count_
)
206 .arg(ColorUtils::fromColorT(prefs
.st_server_fg
).name(),
207 ColorUtils::fromColorT(prefs
.st_server_bg
).name())
208 + tr("%Ln turn(s).", "", turns_
);
211 hint
= tr("Packet %1. ").arg(pkt
);
214 hint
+= tr("%Ln <span style=\"color: %1; background-color:%2\">client</span> pkt(s), ", "", client_packet_count_
)
215 .arg(ColorUtils::fromColorT(prefs
.st_client_fg
).name(),
216 ColorUtils::fromColorT(prefs
.st_client_bg
).name())
217 + tr("%Ln <span style=\"color: %1; background-color:%2\">server</span> pkt(s), ", "", server_packet_count_
)
218 .arg(ColorUtils::fromColorT(prefs
.st_server_fg
).name(),
219 ColorUtils::fromColorT(prefs
.st_server_bg
).name())
220 + tr("%Ln turn(s).", "", turns_
);
224 hint
.append(tr(" Click to select."));
227 hint
.prepend("<small><i>");
228 hint
.append("</i></small>");
229 ui
->hintLabel
->setText(hint
);
232 void FollowStreamDialog::goToPacketForTextPos(int pkt
)
239 emit
goToPacket(pkt
);
243 void FollowStreamDialog::updateWidgets(bool follow_in_progress
)
245 // XXX: If follow_in_progress set cursor to Qt::BusyCursor or WaitCursor,
246 // otherwise unset cursor?
247 bool enable
= !follow_in_progress
;
249 ui
->teStreamContent
->setEnabled(true);
253 ui
->cbDirections
->setEnabled(enable
);
254 ui
->cbCharset
->setEnabled(enable
);
255 ui
->streamNumberSpinBox
->setReadOnly(!enable
);
256 if (get_follow_sub_stream_id_func(follower_
) != NULL
) {
257 ui
->subStreamNumberSpinBox
->setReadOnly(!enable
);
259 ui
->leFind
->setEnabled(enable
);
260 ui
->bFind
->setEnabled(enable
);
261 b_filter_out_
->setEnabled(enable
);
262 b_print_
->setEnabled(enable
);
263 b_save_
->setEnabled(enable
);
265 WiresharkDialog::updateWidgets();
268 void FollowStreamDialog::useRegexFind(bool use_regex
)
270 use_regex_find_
= use_regex
;
272 ui
->lFind
->setText(tr("Regex Find:"));
274 ui
->lFind
->setText(tr("Find:"));
277 // This only calls itself with go_back false, so never recurses more than once.
278 // NOLINTNEXTLINE(misc-no-recursion)
279 void FollowStreamDialog::findText(bool go_back
)
281 if (ui
->leFind
->text().isEmpty()) return;
285 QTextDocument::FindFlags options
;
286 if (ui
->caseCheckBox
->isChecked()) {
287 options
|= QTextDocument::FindCaseSensitively
;
289 if (use_regex_find_
) {
290 // https://bugreports.qt.io/browse/QTBUG-88721
291 // QPlainTextEdit::find() searches case-insensitively unless
292 // QTextDocument::FindCaseSensitively is explicitly given.
293 // This *does* apply to QRegularExpression (overriding
294 // CaseInsensitiveOption), but not QRegExp.
296 // QRegularExpression and QRegExp do not support Perl's /i, but
297 // the former at least does support the mode modifiers (?i) and
298 // (?-i), which can override QTextDocument::FindCaseSensitively.
299 QRegularExpression
regex(ui
->leFind
->text(), QRegularExpression::UseUnicodePropertiesOption
);
300 found
= ui
->teStreamContent
->find(regex
, options
);
302 found
= ui
->teStreamContent
->find(ui
->leFind
->text(), options
);
306 ui
->teStreamContent
->setFocus();
307 } else if (go_back
) {
308 ui
->teStreamContent
->moveCursor(QTextCursor::Start
);
313 void FollowStreamDialog::saveAs()
315 QString file_name
= WiresharkFileDialog::getSaveFileName(this, mainApp
->windowTitleString(tr("Save Stream Content As…")));
316 if (file_name
.isEmpty()) {
320 QFile
file(file_name
);
321 if (!file
.open(QIODevice::WriteOnly
)) {
322 open_failure_alert_box(file_name
.toUtf8().constData(), errno
, true);
326 // XXX: What if truncated_ is true? We should save the entire stream.
327 // Unconditionally save data as UTF-8 (even if data is decoded otherwise).
328 QByteArray bytes
= ui
->teStreamContent
->toPlainText().toUtf8();
329 if (recent
.gui_follow_show
== SHOW_RAW
) {
330 // The "Raw" format is currently displayed as hex data and needs to be
331 // converted to binary data. fromHex() skips over non hex characters
332 // including line breaks, which is what we want.
333 bytes
= QByteArray::fromHex(bytes
);
336 QDataStream
out(&file
);
337 out
.writeRawData(bytes
.constData(), static_cast<int>(bytes
.size()));
340 void FollowStreamDialog::helpButton()
342 mainApp
->helpTopicAction(HELP_FOLLOW_STREAM_DIALOG
);
345 void FollowStreamDialog::backButton()
350 output_filter_
= previous_filter_
;
355 void FollowStreamDialog::filterOut()
360 output_filter_
= filter_out_filter_
;
365 void FollowStreamDialog::close()
369 // Update filter - Use:
370 // previous_filter if 'Close' (passed in follow() method)
371 // filter_out_filter_ if 'Filter Out This Stream' (built by appending !current_stream to previous_filter)
372 // leave filter alone if window closed. (current stream)
373 emit
updateFilter(output_filter_
, true);
375 WiresharkDialog::close();
378 void FollowStreamDialog::cbDirectionsCurrentIndexChanged(int idx
)
383 follow_info_
.show_stream
= BOTH_HOSTS
;
386 follow_info_
.show_stream
= FROM_SERVER
;
389 follow_info_
.show_stream
= FROM_CLIENT
;
398 void FollowStreamDialog::cbCharsetCurrentIndexChanged(int idx
)
401 recent
.gui_follow_show
= ui
->cbCharset
->currentData().value
<bytes_show_type
>();
403 switch (recent
.gui_follow_show
) {
407 ui
->deltaComboBox
->setEnabled(true);
410 ui
->deltaComboBox
->setEnabled(false);
416 void FollowStreamDialog::deltaComboBoxCurrentIndexChanged(int idx
)
419 recent
.gui_follow_delta
= static_cast<follow_delta_type
>(ui
->deltaComboBox
->currentIndex());
424 void FollowStreamDialog::bFindClicked()
429 void FollowStreamDialog::leFindReturnPressed()
434 void FollowStreamDialog::streamNumberSpinBoxValueChanged(int stream_num
)
436 if (file_closed_
) return;
438 int sub_stream_num
= 0;
439 ui
->subStreamNumberSpinBox
->blockSignals(true);
440 sub_stream_num
= ui
->subStreamNumberSpinBox
->value();
441 ui
->subStreamNumberSpinBox
->blockSignals(false);
444 if (ui
->subStreamNumberSpinBox
->isVisible()) {
445 /* We need to find a suitable sub stream for the new stream */
446 follow_sub_stream_id_func sub_stream_func
;
447 sub_stream_func
= get_follow_sub_stream_id_func(follower_
);
449 if (sub_stream_func
== NULL
) {
450 // Should not happen, this field is only visible for suitable protocols.
454 unsigned sub_stream_num_new
= static_cast<unsigned>(sub_stream_num
);
455 if (sub_stream_num
< 0) {
456 // Stream ID 0 should always exist as it is used for control messages.
457 // XXX: That is only guaranteed for HTTP2. For example, in QUIC,
458 // stream ID 0 is a normal stream used by the first standard client-
459 // initiated bidirectional stream (if it exists, and it might not)
460 // and we might have a stream (connection) but only the CRYPTO
461 // stream, which does not have a (sub) stream ID.
462 // What should we do if there is a stream with no substream to
463 // follow? Right now the substream spinbox is left active and
464 // the user can change the value to no effect.
465 sub_stream_num_new
= 0;
468 ok
= sub_stream_func(static_cast<unsigned>(stream_num
), sub_stream_num_new
, false, &sub_stream_num_new
);
470 ok
= sub_stream_func(static_cast<unsigned>(stream_num
), sub_stream_num_new
, true, &sub_stream_num_new
);
473 sub_stream_num
= static_cast<int>(sub_stream_num_new
);
475 /* XXX: For HTTP and TLS, we use the TCP stream index, and really should
476 * return false if the TCP stream doesn't have HTTP or TLS. (Or we could
477 * switch to having separate HTTP and TLS stream numbers.)
482 if (stream_num
>= 0 && ok
) {
483 follow(previous_filter_
, true, stream_num
, sub_stream_num
);
484 previous_sub_stream_num_
= sub_stream_num
;
489 void FollowStreamDialog::subStreamNumberSpinBoxValueChanged(int sub_stream_num
)
491 if (file_closed_
) return;
494 ui
->streamNumberSpinBox
->blockSignals(true);
495 stream_num
= ui
->streamNumberSpinBox
->value();
496 ui
->streamNumberSpinBox
->blockSignals(false);
498 follow_sub_stream_id_func sub_stream_func
;
499 sub_stream_func
= get_follow_sub_stream_id_func(follower_
);
501 if (sub_stream_func
== NULL
) {
502 // Should not happen, this field is only visible for suitable protocols.
506 unsigned sub_stream_num_new
= static_cast<unsigned>(sub_stream_num
);
508 /* previous_sub_stream_num_ is a hack to track which buttons was pressed without event handling */
509 if (sub_stream_num
< 0) {
510 // Stream ID 0 should always exist as it is used for control messages.
511 // XXX: That is only guaranteed for HTTP2, see above.
512 sub_stream_num_new
= 0;
514 } else if (previous_sub_stream_num_
< sub_stream_num
) {
515 ok
= sub_stream_func(static_cast<unsigned>(stream_num
), sub_stream_num_new
, false, &sub_stream_num_new
);
517 ok
= sub_stream_func(static_cast<unsigned>(stream_num
), sub_stream_num_new
, true, &sub_stream_num_new
);
519 sub_stream_num
= static_cast<int>(sub_stream_num_new
);
522 follow(previous_filter_
, true, stream_num
, sub_stream_num
);
523 previous_sub_stream_num_
= sub_stream_num
;
527 void FollowStreamDialog::buttonBoxRejected()
529 // Ignore the close button if FollowStreamDialog::close() is running.
533 WiresharkDialog::reject();
536 void FollowStreamDialog::removeStreamControls()
538 ui
->horizontalLayout
->removeItem(ui
->streamNumberSpacer
);
539 ui
->streamNumberLabel
->setVisible(false);
540 ui
->streamNumberSpinBox
->setVisible(false);
541 ui
->subStreamNumberLabel
->setVisible(false);
542 ui
->subStreamNumberSpinBox
->setVisible(false);
545 void FollowStreamDialog::resetStream(void *tap_data
)
547 follow_info_t
*follow_info
= static_cast<follow_info_t
*>(tap_data
);
548 follow_reset_stream(follow_info
);
549 // If we ever draw the text while tapping (instead of only after
550 // the tap finishes), reset the GUI here too.
553 void FollowStreamDialog::resetStream()
555 FollowStreamDialog::resetStream(&follow_info_
);
558 void FollowStreamDialog::readStream()
561 // interrupt any reading already running
562 loop_break_mutex
.lock();
563 isReadRunning
= false;
564 loop_break_mutex
.unlock();
566 double scroll_ratio
= 0.0;
567 int doc_length
= ui
->teStreamContent
->verticalScrollBar()->maximum() + ui
->teStreamContent
->verticalScrollBar()->pageStep();
568 if (doc_length
> 0) {
569 scroll_ratio
= static_cast<double>(ui
->teStreamContent
->verticalScrollBar()->value()) / doc_length
;
572 ui
->teStreamContent
->clear();
573 switch (recent
.gui_follow_show
) {
578 /* We control the width and insert line breaks in these formats. */
579 ui
->teStreamContent
->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere
);
583 /* Everything else might have extremely long lines without whitespace,
584 * (SHOW_RAW almost surely so), and QTextEdit is O(N^2) trying
585 * to search for word boundaries on long lines when adding text.
587 ui
->teStreamContent
->setWordWrapMode(QTextOption::WrapAnywhere
);
590 client_buffer_count_
= 0;
591 server_buffer_count_
= 0;
592 client_packet_count_
= 0;
593 server_packet_count_
= 0;
598 ws_assert_not_reached();
603 ui
->teStreamContent
->moveCursor(QTextCursor::Start
);
605 doc_length
= ui
->teStreamContent
->verticalScrollBar()->maximum() + ui
->teStreamContent
->verticalScrollBar()->pageStep();
606 ui
->teStreamContent
->verticalScrollBar()->setValue(doc_length
* scroll_ratio
);
610 FollowStreamDialog::followStream()
615 void FollowStreamDialog::addText(QString text
, bool is_from_server
, uint32_t packet_num
, bool colorize
)
617 ui
->teStreamContent
->addText(std::move(text
), is_from_server
, packet_num
, colorize
);
620 // The following keyboard shortcuts should work (although
621 // they may not work consistently depending on focus):
622 // / (slash), Ctrl-F - Focus and highlight the search box
623 // Ctrl-G, Ctrl-N, F3 - Find next
624 // Should we make it so that typing any text starts searching?
625 bool FollowStreamDialog::eventFilter(QObject
*, QEvent
*event
)
627 if (ui
->teStreamContent
->hasFocus() && event
->type() == QEvent::KeyPress
) {
628 QKeyEvent
*keyEvent
= static_cast<QKeyEvent
*>(event
);
629 if (keyEvent
->matches(QKeySequence::SelectAll
) || keyEvent
->matches(QKeySequence::Copy
)
630 || keyEvent
->text().isEmpty()) {
633 ui
->leFind
->setFocus();
634 if (keyEvent
->matches(QKeySequence::Find
)) {
636 } else if (keyEvent
->matches(QKeySequence::FindNext
)) {
645 void FollowStreamDialog::keyPressEvent(QKeyEvent
*event
)
647 if (ui
->leFind
->hasFocus()) {
648 if (event
->key() == Qt::Key_Enter
|| event
->key() == Qt::Key_Return
) {
653 if (event
->key() == Qt::Key_Slash
|| event
->matches(QKeySequence::Find
)) {
654 ui
->leFind
->setFocus();
655 ui
->leFind
->selectAll();
660 if (event
->key() == Qt::Key_F3
|| (event
->key() == Qt::Key_N
&& event
->modifiers() & Qt::ControlModifier
)) {
665 QDialog::keyPressEvent(event
);
668 // Replaces non printable ASCII characters in the QByteArray with .
669 // Causes buffer to detach/deep copy *only* if a character has to be
671 static inline void sanitize_buffer(QByteArray
&buffer
, size_t nchars
) {
672 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
673 for (int i
= 0; i
< (int)nchars
; i
++) {
675 for (qsizetype i
= 0; i
< (qsizetype
)nchars
; i
++) {
677 if (buffer
.at(i
) == '\n' || buffer
.at(i
) == '\r' || buffer
.at(i
) == '\t')
679 if (! g_ascii_isprint((unsigned char)buffer
.at(i
))) {
685 void FollowStreamDialog::showBuffer(QByteArray
&buffer
, size_t nchars
, bool is_from_server
, uint32_t packet_num
,
686 nstime_t abs_ts
, uint32_t *global_pos
)
689 uint32_t current_pos
;
690 static const char hexchars
[16] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
691 bool show_delta
= false;
693 if (last_packet_
== 0) {
694 last_from_server_
= is_from_server
;
696 if (recent
.gui_follow_delta
== FOLLOW_DELTA_ALL
||
697 (recent
.gui_follow_delta
== FOLLOW_DELTA_TURN
&& last_from_server_
!= is_from_server
)) {
703 if (!nstime_is_zero(&abs_ts
)) {
704 // packet-tcp.c and possibly other dissectors can return a zero abs_ts when
705 // a fragment is missing.
707 nstime_delta(&delta_ts
, &abs_ts
, &last_ts_
);
708 delta
= nstime_to_sec(&delta_ts
);
712 switch (recent
.gui_follow_show
) {
716 /* If our native arch is ASCII, call: */
717 EBCDIC_to_ASCII((uint8_t*)buffer
.data(), (unsigned) nchars
);
719 ui
->teStreamContent
->addDeltaTime(delta
);
721 if (show_delta
|| last_from_server_
!= is_from_server
) {
722 addText("\n", is_from_server
, packet_num
);
724 sanitize_buffer(buffer
, nchars
);
725 addText(buffer
, is_from_server
, packet_num
);
731 /* If our native arch is EBCDIC, call:
732 * ASCII_TO_EBCDIC(buffer, nchars);
735 ui
->teStreamContent
->addDeltaTime(delta
);
737 if (show_delta
|| last_from_server_
!= is_from_server
) {
738 addText("\n", is_from_server
, packet_num
);
740 sanitize_buffer(buffer
, nchars
);
741 addText(buffer
, is_from_server
, packet_num
);
748 ui
->teStreamContent
->addDeltaTime(delta
);
750 if (show_delta
|| last_from_server_
!= is_from_server
) {
751 addText("\n", is_from_server
, packet_num
);
753 // This assumes that multibyte characters don't span packets in the
754 // stream. To handle that case properly (which might occur with fixed
755 // block sizes, e.g. transferring over TFTP, we would need to create
756 // two stateful QTextDecoders, one for each direction, presumably in
757 // on_cbCharset_currentIndexChanged()
758 QTextCodec
*codec
= QTextCodec::codecForName(ui
->cbCharset
->currentText().toUtf8());
759 addText(codec
->toUnicode(buffer
), is_from_server
, packet_num
);
765 while (current_pos
< nchars
) {
768 char *cur
= hexbuf
, *ascii_start
;
770 /* is_from_server indentation : put 4 spaces at the
771 * beginning of the string */
772 /* XXX - We might want to prepend each line with "C" or "S" instead. */
773 if (is_from_server
&& follow_info_
.show_stream
== BOTH_HOSTS
) {
777 cur
+= snprintf(cur
, 20, "%08X ", *global_pos
);
778 /* 49 is space consumed by hex chars */
779 ascii_start
= cur
+ 49 + 2;
780 for (i
= 0; i
< 16 && current_pos
+ i
< nchars
; i
++) {
782 hexchars
[(buffer
.at(current_pos
+ i
) & 0xf0) >> 4];
784 hexchars
[buffer
.at(current_pos
+ i
) & 0x0f];
789 /* Fill it up if column isn't complete */
790 while (cur
< ascii_start
)
793 /* Now dump bytes as text */
794 for (i
= 0; i
< 16 && current_pos
+ i
< nchars
; i
++) {
796 (g_ascii_isprint((unsigned char)buffer
.at(current_pos
+ i
)) ?
797 buffer
.at(current_pos
+ i
) : '.');
807 addText(hexbuf
, is_from_server
, packet_num
);
813 snprintf(initbuf
, sizeof(initbuf
), "char peer%d_%d[] = { /* Packet %u */\n",
814 is_from_server
? 1 : 0,
815 is_from_server
? server_buffer_count_
++ : client_buffer_count_
++,
817 addText(initbuf
, is_from_server
, packet_num
);
819 while (current_pos
< nchars
) {
824 for (i
= 0; i
< 8 && current_pos
+ i
< nchars
; i
++) {
825 /* Prepend entries with "0x" */
829 hexchars
[(buffer
.at(current_pos
+ i
) & 0xf0) >> 4];
831 hexchars
[buffer
.at(current_pos
+ i
) & 0x0f];
833 /* Delimit array entries with a comma */
834 if (current_pos
+ i
+ 1 < nchars
)
840 /* Terminate the array if we are at the end */
841 if (current_pos
+ i
== nchars
) {
848 hexbuf
[cur
++] = '\n';
850 addText(hexbuf
, is_from_server
, packet_num
);
858 const int base64_raw_len
= 57; // Encodes to 76 bytes, common in RFCs
861 if (last_packet_
== 0) {
862 // Header with general info about peers
863 const char *hostname0
= address_to_name(&follow_info_
.client_ip
);
864 const char *hostname1
= address_to_name(&follow_info_
.server_ip
);
866 char *port0
= get_follow_port_to_display(follower_
)(NULL
, follow_info_
.client_port
);
867 char *port1
= get_follow_port_to_display(follower_
)(NULL
, follow_info_
.server_port
);
869 addText("peers:\n", false, 0, false);
875 .arg(hostname0
, port0
), false, 0);
881 .arg(hostname1
, port1
), true, 0);
883 wmem_free(NULL
, port0
);
884 wmem_free(NULL
, port1
);
886 addText("packets:\n", false, 0, false);
889 if (packet_num
!= last_packet_
) {
890 yaml_text
.append(QStringLiteral(" - packet: %1\n")
892 yaml_text
.append(QStringLiteral(" peer: %1\n")
893 .arg(is_from_server
? 1 : 0));
894 yaml_text
.append(QStringLiteral(" index: %1\n")
895 .arg(is_from_server
? server_buffer_count_
++ : client_buffer_count_
++));
896 yaml_text
.append(QStringLiteral(" timestamp: %1.%2\n")
898 .arg(abs_ts
.nsecs
, 9, 10, QChar('0')));
899 yaml_text
.append(QStringLiteral(" data: !!binary |\n"));
901 while (current_pos
< nchars
) {
902 int len
= current_pos
+ base64_raw_len
< nchars
? base64_raw_len
: (int) nchars
- current_pos
;
903 QByteArray
base64_data(&buffer
.constData()[current_pos
], len
);
905 /* XXX: GCC 12.1 has a bogus stringop-overread warning using the Qt
906 * conversions from QByteArray to QString at -O2 and higher due to
907 * computing a branch that will never be taken.
909 #if WS_IS_AT_LEAST_GNUC_VERSION(12,1)
910 DIAG_OFF(stringop
-overread
)
912 yaml_text
+= " " + base64_data
.toBase64() + "\n";
913 #if WS_IS_AT_LEAST_GNUC_VERSION(12,1)
914 DIAG_ON(stringop
-overread
)
918 (*global_pos
) += len
;
920 addText(std::move(yaml_text
), is_from_server
, packet_num
);
926 addText(buffer
.toHex() + '\n', is_from_server
, packet_num
);
931 /* The other Show types are supported in Show Packet Bytes but
932 * not here in Follow. (XXX: Maybe some could be added?)
934 ws_assert_not_reached();
937 if (packet_num
!= last_packet_
) {
938 last_packet_
= packet_num
;
939 if (is_from_server
) {
940 server_packet_count_
++;
942 client_packet_count_
++;
944 if (last_from_server_
!= is_from_server
) {
945 last_from_server_
= is_from_server
;
951 bool FollowStreamDialog::follow(QString previous_filter
, bool use_stream_index
, unsigned stream_num
, unsigned sub_stream_num
)
953 QString follow_filter
;
954 const char *hostname0
= NULL
, *hostname1
= NULL
;
955 char *port0
= NULL
, *port1
= NULL
;
956 QString server_to_client_string
;
957 QString client_to_server_string
;
958 QString both_directions_string
;
959 bool is_follower
= false;
961 follow_stream_count_func stream_count_func
= NULL
;
965 QMessageBox::warning(this, tr("No capture file."), tr("Please make sure you have a capture file opened."));
969 if (!use_stream_index
) {
970 if (cap_file_
.capFile()->edt
== NULL
)
972 QMessageBox::warning(this, tr("Error following stream."), tr("Capture file invalid."));
975 is_follower
= proto_is_frame_protocol(cap_file_
.capFile()->edt
->pi
.layers
, proto_get_protocol_filter_name(get_follow_proto_id(follower_
)));
977 QMessageBox::warning(this, tr("Error following stream."), tr("Please make sure you have a %1 packet selected.").arg
978 (proto_get_protocol_short_name(find_protocol_by_id(get_follow_proto_id(follower_
)))));
983 /* Create a new filter that matches all packets in the TCP stream,
984 and set the display filter entry accordingly */
985 if (use_stream_index
) {
986 follow_filter
= gchar_free_to_qstring(get_follow_index_func(follower_
)(stream_num
, sub_stream_num
));
988 follow_filter
= gchar_free_to_qstring(get_follow_conv_func(follower_
)(cap_file_
.capFile()->edt
, &cap_file_
.capFile()->edt
->pi
, &stream_num
, &sub_stream_num
));
990 if (follow_filter
.isEmpty()) {
991 // XXX: This error probably has to do with tunneling (#18231), where
992 // the addresses or ports changed after the TCP or UDP layer.
993 // (The appropriate layer must be present, or else the GUI
994 // doesn't allow the option to be selected.)
995 QMessageBox::warning(this,
996 tr("Error creating filter for this stream."),
997 tr("%1 stream not found on the selected packet.").arg(proto_get_protocol_short_name(find_protocol_by_id(get_follow_proto_id(follower_
)))));
1001 previous_filter_
= previous_filter
;
1002 /* append the negation */
1003 if (!previous_filter
.isEmpty()) {
1004 filter_out_filter_
= QStringLiteral("%1 and !(%2)")
1005 .arg(previous_filter
, follow_filter
);
1009 filter_out_filter_
= QStringLiteral("!(%1)").arg(follow_filter
);
1012 follow_info_
.substream_id
= sub_stream_num
;
1014 /* data will be passed via tap callback*/
1015 if (!registerTapListener(get_follow_tap_string(follower_
), &follow_info_
,
1016 follow_filter
.toUtf8().constData(),
1017 0, FollowStreamDialog::resetStream
,
1018 get_follow_tap_handler(follower_
), NULL
)) {
1022 stream_count_func
= get_follow_stream_count_func(follower_
);
1024 if (stream_count_func
== NULL
) {
1025 removeStreamControls();
1027 stream_count
= stream_count_func();
1028 ui
->streamNumberSpinBox
->blockSignals(true);
1029 ui
->streamNumberSpinBox
->setMaximum(stream_count
-1);
1030 ui
->streamNumberSpinBox
->setValue(stream_num
);
1031 ui
->streamNumberSpinBox
->blockSignals(false);
1032 ui
->streamNumberSpinBox
->setToolTip(tr("%Ln total stream(s).", "", stream_count
));
1033 ui
->streamNumberLabel
->setToolTip(ui
->streamNumberSpinBox
->toolTip());
1036 follow_sub_stream_id_func sub_stream_func
;
1037 sub_stream_func
= get_follow_sub_stream_id_func(follower_
);
1038 if (sub_stream_func
!= NULL
) {
1039 unsigned substream_max_id
= 0;
1040 sub_stream_func(static_cast<unsigned>(stream_num
), INT32_MAX
, true, &substream_max_id
);
1041 stream_count
= static_cast<int>(substream_max_id
);
1042 ui
->subStreamNumberSpinBox
->blockSignals(true);
1043 ui
->subStreamNumberSpinBox
->setEnabled(true);
1044 ui
->subStreamNumberSpinBox
->setMaximum(stream_count
);
1045 ui
->subStreamNumberSpinBox
->setValue(sub_stream_num
);
1046 ui
->subStreamNumberSpinBox
->blockSignals(false);
1047 ui
->subStreamNumberSpinBox
->setToolTip(tr("Max sub stream ID for the selected stream: %Ln", "", stream_count
));
1048 ui
->subStreamNumberSpinBox
->setToolTip(ui
->subStreamNumberSpinBox
->toolTip());
1049 ui
->subStreamNumberSpinBox
->setVisible(true);
1050 ui
->subStreamNumberLabel
->setVisible(true);
1052 /* disable substream spin box for protocols without substreams */
1053 ui
->subStreamNumberSpinBox
->blockSignals(true);
1054 ui
->subStreamNumberSpinBox
->setEnabled(false);
1055 ui
->subStreamNumberSpinBox
->setValue(0);
1056 ui
->subStreamNumberSpinBox
->setKeyboardTracking(false);
1057 ui
->subStreamNumberSpinBox
->blockSignals(false);
1058 ui
->subStreamNumberSpinBox
->setVisible(false);
1059 ui
->subStreamNumberLabel
->setVisible(false);
1062 beginRetapPackets();
1063 updateWidgets(true);
1065 /* Run the display filter so it goes in effect - even if it's the
1066 same as the previous display filter. */
1067 /* XXX: This forces a cf_filter_packets() - but if a rescan (or something else
1068 * that sets cf->read_lock) is in progress, this will queue the filter
1069 * and return immediately. It will also cause a rescan in progress to
1070 * stop and restart with the new filter. That also applies to this rescan;
1071 * changing the main display filter (from the main window, or from, e.g.
1072 * another FollowStreamDialog) will cause this to restart and reset the
1075 * Other tapping dialogs call cf_retap_packets (which retaps but doesn't
1076 * set the main display filter, freeze the packet list, etc.), which
1077 * has somewhat different behavior when another dialog tries to retap,
1078 * but also results in the taps being reset mid tap.
1080 * Either way, we should be event driven and listening for CaptureEvents
1081 * instead of drawing after this returns. (Or like other taps, draw
1082 * periodically in a callback, provided that can be done without causing
1083 * issues with changing the Decode As type.)
1085 emit
updateFilter(follow_filter
, true);
1087 removeTapListeners();
1089 bool is_stratoshark
= strcmp(get_configuration_namespace(), "Stratoshark") == 0;
1091 if (is_stratoshark
) {
1092 server_to_client_string
=
1093 tr("Read activity(%6)")
1094 .arg(gchar_free_to_qstring(format_size(
1095 follow_info_
.bytes_written
[0],
1096 FORMAT_SIZE_UNIT_BYTES
, FORMAT_SIZE_PREFIX_SI
)));
1098 client_to_server_string
=
1099 tr("Write activity(%6)")
1100 .arg(gchar_free_to_qstring(format_size(
1101 follow_info_
.bytes_written
[1],
1102 FORMAT_SIZE_UNIT_BYTES
, FORMAT_SIZE_PREFIX_SI
)));
1104 both_directions_string
= tr("Entire I/O activity (%1)")
1105 .arg(gchar_free_to_qstring(format_size(
1106 follow_info_
.bytes_written
[0] + follow_info_
.bytes_written
[1],
1107 FORMAT_SIZE_UNIT_BYTES
, FORMAT_SIZE_PREFIX_SI
)));
1109 hostname0
= address_to_name(&follow_info_
.client_ip
);
1110 hostname1
= address_to_name(&follow_info_
.server_ip
);
1112 port0
= get_follow_port_to_display(follower_
)(NULL
, follow_info_
.client_port
);
1113 port1
= get_follow_port_to_display(follower_
)(NULL
, follow_info_
.server_port
);
1115 server_to_client_string
=
1116 QStringLiteral("%1:%2 %3 %4:%5 (%6)")
1117 .arg(hostname0
, port0
)
1118 .arg(UTF8_RIGHTWARDS_ARROW
)
1119 .arg(hostname1
, port1
)
1120 .arg(gchar_free_to_qstring(format_size(
1121 follow_info_
.bytes_written
[0],
1122 FORMAT_SIZE_UNIT_BYTES
, FORMAT_SIZE_PREFIX_SI
)));
1124 client_to_server_string
=
1125 QStringLiteral("%1:%2 %3 %4:%5 (%6)")
1126 .arg(hostname1
, port1
)
1127 .arg(UTF8_RIGHTWARDS_ARROW
)
1128 .arg(hostname0
, port0
)
1129 .arg(gchar_free_to_qstring(format_size(
1130 follow_info_
.bytes_written
[1],
1131 FORMAT_SIZE_UNIT_BYTES
, FORMAT_SIZE_PREFIX_SI
)));
1133 wmem_free(NULL
, port0
);
1134 wmem_free(NULL
, port1
);
1136 both_directions_string
= tr("Entire conversation (%1)")
1137 .arg(gchar_free_to_qstring(format_size(
1138 follow_info_
.bytes_written
[0] + follow_info_
.bytes_written
[1],
1139 FORMAT_SIZE_UNIT_BYTES
, FORMAT_SIZE_PREFIX_SI
)));
1142 setWindowSubtitle(tr("Follow %1 Stream (%2)").arg(proto_get_protocol_short_name(find_protocol_by_id(get_follow_proto_id(follower_
))),
1145 ui
->cbDirections
->blockSignals(true);
1146 ui
->cbDirections
->clear();
1147 ui
->cbDirections
->addItem(both_directions_string
);
1148 ui
->cbDirections
->addItem(client_to_server_string
);
1149 ui
->cbDirections
->addItem(server_to_client_string
);
1150 ui
->cbDirections
->blockSignals(false);
1155 updateWidgets(false);
1158 if (prefs
.restore_filter_after_following_stream
) {
1159 emit
updateFilter(previous_filter_
, true);
1165 void FollowStreamDialog::captureFileClosed()
1167 QString tooltip
= tr("File closed.");
1168 ui
->streamNumberSpinBox
->setToolTip(tooltip
);
1169 ui
->streamNumberLabel
->setToolTip(tooltip
);
1170 WiresharkDialog::captureFileClosed();
1173 void FollowStreamDialog::readFollowStream()
1175 uint32_t global_client_pos
= 0, global_server_pos
= 0;
1176 uint32_t *global_pos
;
1179 follow_record_t
*follow_record
;
1180 QElapsedTimer elapsed_timer
;
1183 elapsed_timer
.start();
1185 loop_break_mutex
.lock();
1186 isReadRunning
= true;
1187 loop_break_mutex
.unlock();
1189 for (cur
= g_list_last(follow_info_
.payload
); cur
; cur
= g_list_previous(cur
)) {
1190 if (dialogClosed() || !isReadRunning
) break;
1192 follow_record
= (follow_record_t
*)cur
->data
;
1194 if (!follow_record
->is_server
) {
1195 global_pos
= &global_client_pos
;
1196 if (follow_info_
.show_stream
== FROM_SERVER
) {
1200 global_pos
= &global_server_pos
;
1201 if (follow_info_
.show_stream
== FROM_CLIENT
) {
1207 // This will only detach / deep copy if the buffer data is
1208 // modified. Try to avoid doing that as much as possible
1209 // (and avoid new memory allocations that have to be freed).
1210 buffer
.setRawData((char*)follow_record
->data
->data
, follow_record
->data
->len
);
1213 follow_record
->data
->len
,
1214 follow_record
->is_server
,
1215 follow_record
->packet_num
,
1216 follow_record
->abs_ts
,
1218 if (elapsed_timer
.elapsed() > info_update_freq_
) {
1219 fillHintLabel(ui
->teStreamContent
->currentPacket());
1220 mainApp
->processEvents();
1221 elapsed_timer
.start();
1226 loop_break_mutex
.lock();
1227 isReadRunning
= false;
1228 loop_break_mutex
.unlock();