1 /*
2 * Copyright 2015-2021 The Regents of the University of California
3 * All rights reserved.
4 *
5 * This file is part of Spoofer.
6 *
7 * Spoofer is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Spoofer is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Spoofer. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 // Disable some Qt features we don't need to speed up compilation and avoid
22 // some compiler warnings.
23 #define QT_NO_MATRIX4X4
24 #define QT_NO_VECTOR3D
25 #define QT_NO_VECTOR4D
26 #define QT_NO_QUATERNION
27
28 #include <time.h>
29
30 #include "spoof_qt.h"
31 #include <QtGlobal>
32 #include <QAction>
33 #include <QtWidgets>
34 #include <QFile>
35 #include <QTextStream>
36 #include <QFileSystemWatcher>
37 #include <QSystemTrayIcon>
38 #include <QTableWidget>
39 #include <QUrl>
40 #include <QHostAddress>
41
42 #include "../../config.h"
43 #include "port.h"
44 #include "mainwindow.h"
45 #include "FileTailThread.h"
46 #include "ActionButton.h"
47 #include "PreferencesDialog.h"
48 #include "ColoredLabel.h"
49 #include "SubnetAddr.h"
50 static const char cvsid[] ATR_USED = "$Id: mainwindow.cpp,v 1.224 2021/04/28 17:39:09 kkeys Exp $";
51
52 #define HISTORYTABLE_BGCOLOR "#EEEEEE"
53
54 MainWindow::WindowOutput *MainWindow::winout = nullptr;
55 MainWindow *MainWindow::theInstance = nullptr;
56 QPlainTextEdit *MainWindow::consoleWidget = nullptr;
57 ActionButton *MainWindow::consoleButton = nullptr;
58 QString MainWindow::mainTitle(QSL("Spoofer Manager GUI"));
59
60 class SessionResults {
61 public:
62 int row;
63 int ipv;
64 uint32_t asn;
65 QString ipaddr;
66 QString routable;
67 QString privaddr;
68 QString ininternal;
69 QString inprivaddr;
SessionResults(int _ipv)70 SessionResults(int _ipv) : row(-1), ipv(_ipv), asn(0), ipaddr(),
71 routable(), privaddr(), ininternal(), inprivaddr()
72 { }
73 };
74
75 class RunResults {
76 RunResults(const RunResults&) NO_METHOD; // no copy-ctor
77 RunResults operator=(const RunResults&) NO_METHOD; // no copy-assign
78 public:
79 bool live;
80 time_t started;
81 QString logname;
82 QString report;
83 bool proberVersionMatch;
84 SessionResults *results4;
85 SessionResults *results6;
RunResults(bool _live,const QString & _logname)86 RunResults(bool _live, const QString &_logname) :
87 live(_live), started(_live ? time(nullptr) : 0), logname(_logname),
88 report(), proberVersionMatch(false), results4(), results6()
89 {
90 static QRegularExpression re(SpooferBase::proberLogRegex);
91 QRegularExpressionMatch match = re.match(logname);
92 if (match.hasMatch()) {
93 int year = match.captured(1).toInt();
94 int mon = match.captured(2).toInt();
95 int day = match.captured(3).toInt();
96 int hour = match.captured(4).toInt();
97 int min = match.captured(5).toInt();
98 int sec = match.captured(6).toInt();
99 QDateTime datetime(QDate(year, mon, day), QTime(hour, min, sec), Qt::UTC);
100 started = static_cast<time_t>(datetime.toTime_t());
101 }
102 }
~RunResults()103 ~RunResults() {
104 if (results4) delete results4;
105 if (results6) delete results6;
106 }
107 void parseProberText(const QString &text, bool showMessages = true);
108 void findResults(const QString &ipv, ProgressRow **progRow, SessionResults **results);
109 };
110
111 class LinkWidget : public ColoredLabel {
112 public:
LinkWidget(const QUrl & _url,const QString & _text,QWidget * _parent=nullptr)113 LinkWidget(const QUrl &_url, const QString &_text,
114 QWidget *_parent = nullptr) :
115 ColoredLabel(QSL("<a href='") % _url.toString(QUrl::FullyEncoded) %
116 QSL("'>") % _text.toHtmlEscaped() % QSL("</a>"), _parent)
117 {
118 this->setTextInteractionFlags(Qt::LinksAccessibleByMouse |
119 Qt::LinksAccessibleByKeyboard);
120 this->setOpenExternalLinks(true);
121 this->setStatusTip(_url.toString());
122 }
123 };
124
125 template<class T>
126 class TableRow {
127 public:
128 QLabel *title;
129 T *content;
TableRow(QGridLayout * grid,int rownum,const QString & _title,T * _content)130 TableRow(QGridLayout *grid, int rownum, const QString &_title, T *_content) :
131 title(new QLabel(_title)), content(_content)
132 {
133 grid->addWidget(title, rownum, 0, Qt::AlignTop);
134 grid->addWidget(content, rownum, 1, Qt::AlignTop);
135 }
show()136 void show() {
137 title->show();
138 content->show();
139 }
140 };
141
retainSizeWhenHidden(QWidget * w)142 static void retainSizeWhenHidden(QWidget *w)
143 {
144 QSizePolicy sp;
145 sp = w->sizePolicy();
146 sp.setRetainSizeWhenHidden(true);
147 w->setSizePolicy(sp);
148 }
149
150 class ProgressRow : public TableRow<QWidget> {
151 // +---------+-------------------+
152 // | title | content---------+ |
153 // | | | text | |
154 // | | | progbar | |
155 // | | +---------------+ |
156 // +---------+-------------------+
157 ProgressRow(const ProgressRow&) NO_METHOD; // no copy-ctor
158 ProgressRow operator=(const ProgressRow&) NO_METHOD; // no copy-assign
159 public:
160 QString name;
161 QLabel *text;
162 QProgressBar *bar;
163 SessionResults *results;
ProgressRow(QGridLayout * grid,int rownum,QString _name)164 ProgressRow(QGridLayout *grid, int rownum, QString _name) :
165 TableRow(grid, rownum, _name, new QWidget()),
166 name(_name), text(), bar(), results()
167 {
168 QVBoxLayout *vbox = new QVBoxLayout();
169 content->setLayout(vbox);
170 vbox->setContentsMargins(0,0,0,0);
171 vbox->addWidget(text = new QLabel(QSL("no results yet")));
172 vbox->addWidget(bar = new QProgressBar());
173 bar->setFormat(QSL("%v/%m"));
174 retainSizeWhenHidden(bar);
175 retainSizeWhenHidden(text);
176 retainSizeWhenHidden(title);
177 bar->hide();
178 text->hide();
179 title->hide();
180 }
start()181 void start() {
182 text->setText(QSL("untested"));
183 bar->reset();
184 bar->setMaximum(0);
185 bar->setValue(0);
186 bar->hide();
187 text->show();
188 title->show();
189 show();
190 }
stage(const QString & _text)191 void stage(const QString &_text) {
192 text->setText(_text);
193 bar->show();
194 text->show();
195 title->show();
196 }
tick(int progress,int goal)197 void tick(int progress, int goal) {
198 bar->setMaximum(goal);
199 bar->setValue(progress);
200 }
endSession()201 void endSession() {
202 text->setText(!results ? QSL("incomplete") : QSL("done"));
203 }
endProber()204 void endProber() {
205 bar->hide();
206 text->hide();
207 title->hide();
208 results = nullptr;
209 }
210 };
211
writeData(const char * data,qint64 maxSize)212 qint64 MainWindow::WindowOutput::writeData(const char *data, qint64 maxSize)
213 {
214 // Note: on Windows, this is called twice for each line of text: first with
215 // the visible text, and then with "\r\n".
216 QScrollBar *sb = _textedit->verticalScrollBar();
217 bool wasNearBottom = (sb->value() >= sb->maximum() - 5); // exact equality doesn't work
218
219 // If sequential lines start with PROGRESS_PREFIX, 2nd will overwrite 1st.
220 int chunklen = 0;
221 int len = safe_int<int>(maxSize);
222 const char *chunk = data;
223 while (chunklen < len) {
224 if (chunk[chunklen++] != '\n' && chunklen < len)
225 continue;
226 if (_cursor->atBlockStart()) { // beginning of line?
227 if (strncmp(chunk, PROGRESS_PREFIX, sizeof(PROGRESS_PREFIX)-1) == 0) {
228 if (overwritableStart >= 0)
229 _cursor->setPosition(overwritableStart, QTextCursor::KeepAnchor);
230 overwritableStart = _cursor->position();
231 } else {
232 overwritableStart = -1;
233 }
234 _cursor->insertText(QSL(" ")); // indent
235 }
236 _cursor->insertText(QString(QString::fromLocal8Bit(chunk, chunklen)));
237 chunk += chunklen;
238 len -= chunklen;
239 chunklen = 0;
240 }
241
242 if (wasNearBottom)
243 sb->setValue(sb->maximum());
244 return maxSize;
245 }
246
logHandler(QtMsgType type,const QMessageLogContext & ctx,const QString & msg)247 void MainWindow::logHandler(QtMsgType type, const QMessageLogContext &ctx,
248 const QString &msg)
249 {
250 if (type == QtCriticalMsg || type == QtFatalMsg)
251 if (!consoleWidget->isVisible())
252 consoleButton->click();
253 QTextCharFormat oldfmt = winout->setLogCharFmt(type);
254 SpooferBase::logHandler(type, ctx, msg);
255 winout->setCharFmt(oldfmt);
256 }
257
setActionTip(QAction * action,QString tip)258 static void setActionTip(QAction *action, QString tip)
259 {
260 action->setStatusTip(tip);
261 action->setToolTip(tip);
262 }
263
MainWindow(QWidget * _parent)264 MainWindow::MainWindow(QWidget *_parent) :
265 QMainWindow(_parent), SpooferUI(),
266 schedWatcher(), countdownTimer(this), centralLayout(),
267 proberInfoLabel(), proberCountdownLabel(), schedulerStatusLabel(),
268 proberBoxLayout(), firstProberMsgIdx(), nextProberMsgIdx(),
269 proberTimeRow(), progRow4(), progRow6(), currentRun(),
270 aboutAct(), exitAct(), runAct(), abortAct(),
271 pauseAct(), resumeAct(), shutdownAct(), hideGuiAct(), showGuiAct(),
272 runButton(), pauseButton(), historyWidget(), prefDialog(),
273 upgradeBox(), upgradeNotice(new QLabel()), upgradeDetails(new QLabel()),
274 upgradeAutoNotice(new QLabel()),
275 #ifdef AUTOUPGRADE_ENABLED
276 upgradeResultBox(), upgradeResultLabel(new QLabel()), upgradeResultTime(0),
277 upgradeAct(), upgradeCancelAct(), upgradeButton(), upgradeCancelButton(),
278 autoupgradeTimer(), autoupgradeTime(),
279 #endif
280 debugFlags()
281 {
282 theInstance = this;
283 }
284
285 #define CHAR_ARROW_RT "\u25B6"
286
287 #define HISTORY_COLUMNS(XX) /* "X-Macro" */ \
288 XX(date, "date", "Date and time of prober run") \
289 XX(ipv, "IPv", "Internet Protocol version") \
290 XX(ipaddr_short, "client address", "IP address of spoofer client\n(click for full IPv6 addresses)") \
291 XX(ipaddr_long, "client address", "IP address of spoofer client\n(click to abbreviate IPv6 addresses)") \
292 XX(asn, "ASN", "Autonomous System number of client") \
293 XX(privaddr, "outbound\nprivate", "Result of client sending packets to server with spoofed private addresses") \
294 XX(routable, "outbound\nroutable","Result of client sending packets to server with spoofed routable addresses") \
295 XX(inprivaddr, "inbound\nprivate", "Result of server sending packets to client with spoofed private addresses") \
296 XX(ininternal, "inbound\ninternal", "Result of server sending packets to client with spoofed internal (same subnet as client) addresses") \
297 XX(report, "report", "Summary report at website") \
298 XX(log, "log", "Prober technical log (click to hide)") \
299 XX(showlog, CHAR_ARROW_RT, "Click to show logs")
300
301 enum historyColumnId {
302 #define DECLARE_HIST_ENUM(id, name, tip) HIST_##id,
303 HISTORY_COLUMNS(DECLARE_HIST_ENUM)
304 HIST_N
305 };
306
307 static const struct {
308 QString name;
309 QString tip;
310 } historyHeader[HIST_N] = {
311 #define DEFINE_HIST_HEADER(id, name, tip) { QSL(name), QSL(tip) },
312 HISTORY_COLUMNS(DEFINE_HIST_HEADER)
313 };
314
resultWidget(const QString & raw)315 static QLabel *resultWidget(const QString &raw)
316 {
317 static QChar checkMark(0x2714);
318 static QChar xMark(0x2718);
319 QString text;
320 QLabel *widget = new ColoredLabel();
321 QPalette palette = widget->palette();
322 if (raw.compare(QSL("BLOCKED"), Qt::CaseInsensitive) == 0) {
323 text.append(checkMark).append(QSL(" "));
324 palette.setColor(QPalette::Text, Qt::darkGreen);
325 } else if (raw.compare(QSL("RECEIVED"), Qt::CaseInsensitive) == 0) {
326 text.append(xMark).append(QSL(" "));
327 palette.setColor(QPalette::Text, Qt::darkRed);
328 } else if (raw.compare(QSL("REWRITTEN"), Qt::CaseInsensitive) == 0) {
329 text.append(xMark).append(QSL(" "));
330 palette.setColor(QPalette::Text, Qt::darkYellow);
331 } else {
332 text.append(QSL("? "));
333 }
334 widget->setText(text.append(raw));
335 widget->setPalette(palette);
336 return widget;
337 }
338
scaleVerticalMargins(QLayout * layout,qreal factor)339 static void scaleVerticalMargins(QLayout *layout, qreal factor)
340 {
341 QMargins m = layout->contentsMargins();
342 m.setTop(int(m.top() * factor));
343 m.setBottom(int(m.bottom() * factor));
344 layout->setContentsMargins(m);
345 }
346
newFrame(QLayout * parentLayout,QLayout * selfLayout)347 static QFrame *newFrame(QLayout *parentLayout, QLayout *selfLayout)
348 {
349 QFrame *frame = new QFrame();
350 frame->setFrameStyle(QFrame::Panel | QFrame::Sunken);
351 parentLayout->addWidget(frame);
352 frame->setLayout(selfLayout);
353 scaleVerticalMargins(selfLayout, 0.5);
354 return frame;
355 }
356
newBanner(QLayout * parentLayout,QLayout * selfLayout)357 static QFrame *newBanner(QLayout *parentLayout, QLayout *selfLayout)
358 {
359 QFrame *banner = newFrame(parentLayout, selfLayout);
360 banner->setAutoFillBackground(true);
361 banner->setBackgroundRole(QPalette::ToolTipBase);
362 banner->setForegroundRole(QPalette::ToolTipText);
363 banner->hide();
364 return banner;
365 }
366
367 // Do extra initialization before QMainWindow::show().
init()368 void MainWindow::init()
369 {
370 QIcon spooferIcon;
371 spooferIcon.addFile(QSL(":/icons/spoofer16.png"), QSize(16,16));
372 spooferIcon.addFile(QSL(":/icons/spoofer32.png"), QSize(32,32));
373 spooferIcon.addFile(QSL(":/icons/spoofer48.png"), QSize(48,48));
374 spooferIcon.addFile(QSL(":/icons/spoofer64.png"), QSize(64,64));
375 spooferIcon.addFile(QSL(":/icons/spoofer128.png"), QSize(128,128));
376 spooferIcon.addFile(QSL(":/icons/spoofer256.png"), QSize(256,256));
377 setWindowIcon(spooferIcon);
378
379 scheduler = new QLocalSocket(this);
380
381 //========= result history table
382 historyWidget = new QTableWidget(this);
383
384 //========= text output console
385 consoleWidget = new QPlainTextEdit(this);
386
387 //========= Actions
388 QAction *prefAct = new QAction(QSL("&Preferences"), this);
389 setActionTip(prefAct, QSL("Open preferences dialog"));
390 connect(prefAct, &QAction::triggered, this, &MainWindow::openPreferences);
391
392 #if 0 // not used
393 helpAct = new QAction(QSL("&Help"), this);
394 setActionTip(helpAct, QSL("View documentation"));
395 helpAct->setShortcut(QKeySequence::HelpContents);
396 connect(helpAct, &QAction::triggered, this, &MainWindow::help);
397 #endif
398
399 aboutAct = new QAction(QSL("&About Spoofer"), this);
400 setActionTip(aboutAct, QSL("About Spoofer"));
401 connect(aboutAct, &QAction::triggered, this, &MainWindow::about);
402
403 exitAct = new QAction(QSL("&Exit GUI"), this);
404 setActionTip(exitAct, QSL("Exit this GUI (does not affect Scheduler or Prober)"));
405 exitAct->setShortcut(QKeySequence::Quit);
406 connect(exitAct, &QAction::triggered, this, &MainWindow::close);
407
408 hideGuiAct = new QAction(QSL("&Hide GUI"), this);
409 setActionTip(hideGuiAct, QSL("Hide GUI"));
410 hideGuiAct->setEnabled(true);
411 connect(hideGuiAct, &QAction::triggered, this, &MainWindow::hide);
412
413 showGuiAct = new QAction(QSL("&Show GUI"), this);
414 setActionTip(showGuiAct, QSL("Show GUI"));
415 showGuiAct->setEnabled(true);
416 connect(showGuiAct, &QAction::triggered, this, &MainWindow::show);
417
418 QAction *hideConsoleAct = new QAction(QSL("Hide &Console"), this);
419 setActionTip(hideConsoleAct, QSL("Hide console"));
420 connect(hideConsoleAct, &QAction::triggered, consoleWidget, &QPlainTextEdit::hide);
421
422 QAction *showConsoleAct = new QAction(QSL("Show &Console"), this);
423 setActionTip(showConsoleAct, QSL("Show console"));
424 connect(showConsoleAct, &QAction::triggered, consoleWidget, &QPlainTextEdit::show);
425
426 QAction *clearConsoleAct = new QAction(QSL("Clear Console"), this);
427 setActionTip(clearConsoleAct, QSL("Erase contents of console window"));
428 connect(clearConsoleAct, &QAction::triggered, consoleWidget, &QPlainTextEdit::clear);
429
430 #ifdef HIDABLE_HISTORY
431 QAction *hideHistoryAct = new QAction(QSL("Hide Result &History"), this);
432 setActionTip(hideHistoryAct, QSL("Hide results of previous tests"));
433 connect(hideHistoryAct, &QAction::triggered, historyWidget, &QTableWidget::hide);
434
435 QAction *showHistoryAct = new QAction(QSL("Show Result &History"), this);
436 setActionTip(showHistoryAct, QSL("Show results of previous tests"));
437 connect(showHistoryAct, &QAction::triggered, historyWidget, &QTableWidget::show);
438 #endif
439
440 runAct = new QAction(QSL("Start Tests"), this);
441 setActionTip(runAct, QSL("Start a Prober test run now"));
442 runAct->setShortcut(QKeySequence::New);
443 runAct->setEnabled(false);
444 connect(runAct, &QAction::triggered, this, &MainWindow::runProber);
445
446 abortAct = new QAction(QSL("Stop Tests"), this);
447 setActionTip(abortAct, QSL("Stop the current Prober test run"));
448 abortAct->setEnabled(false);
449 connect(abortAct, &QAction::triggered, this, &MainWindow::abortProber);
450
451 pauseAct = new QAction(QSL("&Pause Scheduler"), this);
452 setActionTip(pauseAct, QSL("Disable automatic scheduled Prober runs"));
453 pauseAct->setEnabled(false);
454 connect(pauseAct, &QAction::triggered,
455 this, &MainWindow::pauseScheduler);
456
457 resumeAct = new QAction(QSL("&Resume Scheduler"), this);
458 setActionTip(resumeAct, QSL("Enable automatic scheduled Prober runs"));
459 resumeAct->setEnabled(false);
460 connect(resumeAct, &QAction::triggered,
461 this, &MainWindow::resumeScheduler);
462
463 shutdownAct = new QAction(QSL("&Shutdown Scheduler"), this);
464 setActionTip(shutdownAct, QSL("Shut down Scheduler (not recommended)"));
465 shutdownAct->setEnabled(false);
466 connect(shutdownAct, &QAction::triggered,
467 this, &MainWindow::shutdownScheduler);
468
469 #ifdef AUTOUPGRADE_ENABLED
470 upgradeAct = new QAction(QSL("Upgrade"), this);
471 setActionTip(upgradeAct, QSL("Upgrade Spoofer now"));
472 upgradeAct->setEnabled(true);
473 connect(upgradeAct, &QAction::triggered, this, &MainWindow::doUpgrade);
474
475 upgradeCancelAct = new QAction(QSL("Cancel"), this);
476 setActionTip(upgradeCancelAct, QSL("Cancel automatic upgrade"));
477 upgradeCancelAct->setEnabled(true);
478 connect(upgradeCancelAct, &QAction::triggered, this, &MainWindow::cancelUpgrade);
479 #endif
480
481 //========= menubar
482 // Note: menubar->addAction() may work on some platforms, but does not
483 // work at all on OSX; only QMenus can be added to menubar.
484 // (Toolbars, OTOH, can contain actions and widgets but not menus.)
485 QMenuBar *menubar = this->menuBar();
486
487 // Note: on OSX, Qt will move the special entries "about", "exit", and
488 // "preferences" out to the OS menu unless their role is changed.
489 #ifdef Q_OS_MACOS
490 QMenu *spooferMenu = new QMenu(QSL("Preferences"));
491 spooferMenu->menuAction()->setMenuRole(QAction::NoRole);
492 #else
493 QMenu *spooferMenu = new QMenu(QSL("Spoo&fer"));
494 #endif
495 spooferMenu->addAction(prefAct);
496 spooferMenu->addAction(aboutAct);
497 spooferMenu->addAction(exitAct);
498 menubar->addMenu(spooferMenu);
499
500 QMenu *schedulerMenu = new QMenu(QSL("&Scheduler"));
501 schedulerMenu->addAction(pauseAct);
502 schedulerMenu->addAction(resumeAct);
503 schedulerMenu->addAction(shutdownAct);
504 menubar->addMenu(schedulerMenu);
505
506 QMenu *proberMenu = new QMenu(QSL("&Prober"));
507 proberMenu->addAction(runAct);
508 proberMenu->addAction(abortAct);
509 menubar->addMenu(proberMenu);
510
511 //========= status bar (for action tips)
512 statusBar()->setSizeGripEnabled(false);
513
514 //========= central widget
515 QWidget *widget = new QWidget();
516 setCentralWidget(widget);
517 centralLayout = new QVBoxLayout;
518 // centralLayout->addStrut(600); // minimum width
519 widget->setLayout(centralLayout);
520 centralLayout->setSpacing(4);
521 scaleVerticalMargins(centralLayout, 0.5);
522
523 #ifdef AUTOUPGRADE_ENABLED
524 //========= upgrade result information
525 QHBoxLayout *upgradeResultLayout = new QHBoxLayout();
526 upgradeResultBox = newBanner(centralLayout, upgradeResultLayout);
527
528 upgradeResultLayout->addWidget(upgradeResultLabel, 1);
529 upgradeResultLabel->setWordWrap(true);
530
531 auto *upgradeResultClose = new QPushButton();
532 upgradeResultLayout->addWidget(upgradeResultClose, 0, Qt::AlignRight);
533 upgradeResultClose->setIcon(style()->standardIcon(QStyle::SP_DockWidgetCloseButton));
534 upgradeResultClose->setFlat(true);
535 upgradeResultClose->setToolTip(QSL("Close this notice"));
536 connect(upgradeResultClose, &QToolButton::clicked, upgradeResultBox, &QWidget::hide);
537 #endif
538
539 //========= pending upgrade information
540 QGridLayout *upgradeLayout = new QGridLayout();
541 upgradeBox = newBanner(centralLayout, upgradeLayout);
542
543 // row 0: upgrade available
544 QVBoxLayout *upgradeNoticeLayout = new QVBoxLayout();
545 upgradeLayout->addLayout(upgradeNoticeLayout, 0, 0);
546 upgradeNoticeLayout->addWidget(upgradeNotice);
547 upgradeNotice->setWordWrap(true);
548 upgradeNoticeLayout->addWidget(upgradeDetails);
549 upgradeDetails->setWordWrap(true);
550 connect(upgradeNotice, &QLabel::linkActivated, this, &MainWindow::toggleUpgradeDetails);
551 upgradeDetails->setTextInteractionFlags(Qt::TextSelectableByMouse);
552 upgradeDetails->hide();
553 #ifdef AUTOUPGRADE_ENABLED
554 upgradeButton = new ActionButton(upgradeAct, nullptr, this);
555 upgradeLayout->addWidget(upgradeButton, 0, 1, Qt::AlignRight | Qt::AlignVCenter);
556 // row 1: automatic upgrade
557 upgradeLayout->addWidget(upgradeAutoNotice, 1, 0);
558 upgradeAutoNotice->setWordWrap(true);
559 upgradeCancelButton = new ActionButton(upgradeCancelAct, nullptr, this);
560 upgradeLayout->addWidget(upgradeCancelButton, 1, 1, Qt::AlignRight | Qt::AlignVCenter);
561 upgradeLayout->setColumnStretch(0, 1);
562
563 connect(&autoupgradeTimer, &QTimer::timeout, this, &MainWindow::showUpgradeTimer);
564 #endif
565
566 //========= scheduler information
567 QHBoxLayout *siLayout = new QHBoxLayout();
568 /* QFrame *siBox = */ newFrame(centralLayout, siLayout);
569 siLayout->addWidget(new QLabel(QSL("Scheduler:"), this));
570 siLayout->addWidget(schedulerStatusLabel = new QLabel(QSL("unknown"), this), 1);
571 siLayout->addWidget(pauseButton = new ActionButton(pauseAct, resumeAct, this));
572
573 //========= prober information
574 proberBoxLayout = new QVBoxLayout();
575 /* QFrame *proberBox = */ newFrame(centralLayout, proberBoxLayout);
576
577 // prober grid
578 QGridLayout *proberGrid = new QGridLayout;
579 proberGrid->setColumnStretch(1, 1);
580 proberBoxLayout->addLayout(proberGrid);
581 scaleVerticalMargins(proberGrid, 0.5);
582 int rownum = -1;
583
584 // prober grid first line
585 proberGrid->addWidget(new QLabel(QSL("Prober:"), this), ++rownum, 0, Qt::AlignVCenter);
586 QHBoxLayout *p0 = new QHBoxLayout();
587 proberGrid->addLayout(p0, rownum, 1, Qt::AlignVCenter);
588 p0->addWidget(proberInfoLabel = new QLabel(QSL("schedule unknown"), this), 0);
589 p0->addWidget(proberCountdownLabel = new QLabel(QSL(""), this), 1);
590 p0->addWidget(runButton = new ActionButton(runAct, nullptr, this));
591 connect(&countdownTimer, &QTimer::timeout, this, &MainWindow::setCountdownLabel);
592
593 // prober time and progress bars
594 proberTimeRow = new TableRow<QLabel>(proberGrid, ++rownum, QSL(""), new QLabel());
595 progRow4 = new ProgressRow(proberGrid, ++rownum, QSL("IPv4 progress:"));
596 progRow6 = new ProgressRow(proberGrid, ++rownum, QSL("IPv6 progress:"));
597
598 // index for future prober messages
599 nextProberMsgIdx = firstProberMsgIdx = proberBoxLayout->count();
600
601 //========= history buttons
602 QHBoxLayout *historyButtons = new QHBoxLayout();
603 centralLayout->addLayout(historyButtons);
604
605 #ifdef HIDABLE_HISTORY
606 //========= show/hide history button
607 historyWidget->hide();
608 ActionButton *historyButton = historyWidget->isVisible() ?
609 new ActionButton(hideHistoryAct, showHistoryAct, this) :
610 new ActionButton(showHistoryAct, hideHistoryAct, this);
611 historyButtons->addWidget(historyButton, 0, Qt::AlignLeft);
612 #else
613 historyButtons->addWidget(new QLabel(QSL("Result history:")));
614 #endif
615
616 //========= show/hide blank history checkbox
617 QCheckBox *hideBlankTestsButton = new QCheckBox(QSL("Hide old blank tests"), this);
618 hideBlankTestsButton->setChecked(true);
619 connect(hideBlankTestsButton, &QCheckBox::toggled, this, &MainWindow::hideBlankTests);
620 historyButtons->addWidget(hideBlankTestsButton, 0, Qt::AlignRight);
621
622 //========= result history table
623 centralLayout->addWidget(historyWidget, 2);
624 historyWidget->setSelectionMode(QAbstractItemView::NoSelection);
625 historyWidget->setColumnCount(HIST_N);
626 for (int i = 0; i < HIST_N; i++) {
627 QTableWidgetItem *item = new QTableWidgetItem(historyHeader[i].name);
628 if (!historyHeader[i].tip.isEmpty())
629 item->setToolTip(historyHeader[i].tip);
630 historyWidget->setHorizontalHeaderItem(i, item);
631 }
632
633 int hsize = QFontInfo(historyWidget->font()).pixelSize();
634 historyWidget->setMinimumHeight(12*hsize);
635 historyWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
636 historyWidget->verticalHeader()->setVisible(false);
637 historyWidget->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
638 QHeaderView *hh = historyWidget->horizontalHeader();
639 hh->setSectionResizeMode(QHeaderView::ResizeToContents);
640 hh->setSectionsClickable(true);
641 connect(hh, &QHeaderView::sectionClicked,
642 this, &MainWindow::handleHistoryColumnClick);
643 hh->setSectionHidden(HIST_log, true);
644 hh->setSectionHidden(HIST_ipaddr_long, true);
645 hh->setMinimumSectionSize(0);
646
647 {
648 // There ought to be a better way to do this...
649 hh->setSectionHidden(HIST_showlog, true); // don't count showlog
650 historyWidget->insertRow(0); // add dummy row to calculate width
651 addHistoryCell(0, HIST_date, Qt::AlignLeft, new QLabel(QSL("0000-00-00 00:00:00")));
652 addHistoryCell(0, HIST_ipv, Qt::AlignHCenter, new QLabel(QSL("0")));
653 addHistoryCell(0, HIST_ipaddr_short, Qt::AlignLeft, new QLabel(QSL("000:000:000:000::/64")));
654 addHistoryCell(0, HIST_asn, Qt::AlignRight, new QLabel(QSL("0000")));
655 addHistoryCell(0, HIST_privaddr, Qt::AlignHCenter, resultWidget(QSL("rewritten")));
656 addHistoryCell(0, HIST_routable, Qt::AlignHCenter, resultWidget(QSL("rewritten")));
657 addHistoryCell(0, HIST_inprivaddr, Qt::AlignHCenter, resultWidget(QSL("rewritten")));
658 addHistoryCell(0, HIST_ininternal, Qt::AlignHCenter, resultWidget(QSL("rewritten")));
659 addHistoryCell(0, HIST_report, Qt::AlignHCenter, new QLabel(QSL("report")));
660 historyWidget->resizeColumnsToContents();
661 int histWidth = 0;
662 for (int i = 0; i < HIST_N; i++)
663 if (!hh->isSectionHidden(i))
664 histWidth += historyWidget->columnWidth(i);
665 histWidth += historyWidget->verticalScrollBar()->sizeHint().width();
666 historyWidget->setMinimumWidth(histWidth);
667 historyWidget->removeRow(0); // remove dummy row
668 hh->setSectionHidden(HIST_showlog, false);
669 }
670
671 //========= console buttons
672 QHBoxLayout *consoleButtons = new QHBoxLayout();
673 centralLayout->addLayout(consoleButtons);
674
675 //========= show/hide console button
676 consoleWidget->hide();
677 consoleButton = new ActionButton(showConsoleAct, hideConsoleAct, this);
678 // If console visibility could be changed by something other than the
679 // button, we'd have to detect that using a custom class Foo with an
680 // eventFilter() that catches Show and Hide events, and then
681 // consoleWidget->installEventFilter(Foo).
682 consoleButtons->addWidget(consoleButton, 0, Qt::AlignLeft);
683
684 //========= clear console button
685 ActionButton *clearConsoleButton = new ActionButton(clearConsoleAct, nullptr, this);
686 clearConsoleButton->hide();
687 consoleButtons->addWidget(clearConsoleButton, 1, Qt::AlignLeft);
688 connect(hideConsoleAct, &QAction::triggered, clearConsoleButton, &QPushButton::hide);
689 connect(showConsoleAct, &QAction::triggered, clearConsoleButton, &QPushButton::show);
690
691 //========= text output console
692 centralLayout->addWidget(consoleWidget, 5);
693 consoleWidget->setReadOnly(true);
694 int csize = QFontInfo(consoleWidget->font()).pixelSize();
695 consoleWidget->setMinimumHeight(15*csize);
696 consoleWidget->setMaximumBlockCount(10000);
697 // Create a cursor for appending, independent of the default cursor which
698 // can be moved by the user (e.g., to copy a selection).
699 winout = new WindowOutput(consoleWidget);
700 outdev.setDevice(winout, QSL(""));
701 errdev.setDevice(winout, QSL(""));
702 qInstallMessageHandler(MainWindow::logHandler);
703
704 //========= stretch
705 // keep other stuff from stretching when history and console are hidden
706 centralLayout->addStretch(0);
707
708
709
710
711 qDebug() << "### cwd: " << qPrintable(QDir::toNativeSeparators(QDir::currentPath()));
712
713 //========= other
714 this->setWindowTitle(mainTitle);
715 this->setEnabled(false); // until event loop is started
716
717 showStatus();
718
719 #if 0
720 if (QSystemTrayIcon::isSystemTrayAvailable()) {
721 qDebug() << "### System tray is available";
722 QIcon *icon = new QIcon(QSL("/home/kkeys/WIP/spoofer/cropped/spoofer32.png")); // XXX
723 QSystemTrayIcon *trayicon = new QSystemTrayIcon(*icon, 0);
724 QMenu *traymenu = new QMenu(this);
725 traymenu->addAction(aboutAct);
726 traymenu->addAction(pauseAct);
727 traymenu->addAction(resumeAct);
728 traymenu->addAction(showGuiAct);
729 traymenu->addAction(hideGuiAct);
730 traymenu->addAction(exitAct);
731 trayicon->setContextMenu(traymenu);
732 trayicon->show();
733 } else {
734 qDebug() << "### System tray is NOT available";
735 }
736 #endif
737
738 // Turn off the table grid, but give each cell a margin. The bgcolor
739 // will appear in the cell margins as well as the space outside the table.
740 historyWidget->setShowGrid(false);
741 qApp->setStyleSheet(QSL(
742 "QTableWidget { background: " HISTORYTABLE_BGCOLOR "; }"
743 "QTableWidget:item { background: white; margin: 1px; }"
744 ));
745
746 this->setEnabled(true);
747 }
748
handleHistoryColumnClick(int logicalIndex)749 void MainWindow::handleHistoryColumnClick(int logicalIndex)
750 {
751 QHeaderView *hh = historyWidget->horizontalHeader();
752 if (logicalIndex == HIST_log) {
753 hh->setSectionHidden(HIST_log, true);
754 hh->setSectionHidden(HIST_showlog, false);
755 } else if (logicalIndex == HIST_showlog) {
756 hh->setSectionHidden(HIST_log, false);
757 hh->setSectionHidden(HIST_showlog, true);
758 } else if (logicalIndex == HIST_ipaddr_short) {
759 hh->setSectionHidden(HIST_ipaddr_long, false);
760 hh->setSectionHidden(HIST_ipaddr_short, true);
761 } else if (logicalIndex == HIST_ipaddr_long) {
762 hh->setSectionHidden(HIST_ipaddr_long, true);
763 hh->setSectionHidden(HIST_ipaddr_short, false);
764 }
765 }
766
767 // Before doing anything that might alter the main window (e.g., by calling
768 // logHandler()) or trigger QApplication::exit(), wait until the MainWindow is
769 // visible (the Show event) and the event loop has started, plus a delay
770 // for mysterious other stuff that can cause display glitches if we didn't
771 // wait for it.
showEvent(QShowEvent * ev)772 void MainWindow::showEvent(QShowEvent *ev)
773 {
774 Q_UNUSED(ev);
775 static bool started = false;
776 if (!started)
777 QTimer::singleShot(1, this, SLOT(initEvents()));
778 started = true;
779 }
780
initEvents()781 void MainWindow::initEvents()
782 {
783 QLabel *label = new QLabel(QSL("Loading result history..."), historyWidget);
784 label->raise();
785 label->show();
786 label->setContentsMargins(10,10,10,10);
787 label->setAutoFillBackground(true);
788 label->adjustSize();
789 int pw = historyWidget->size().width();
790 int ph = historyWidget->size().height();
791 int lw = label->size().width();
792 int lh = label->size().height();
793 label->setGeometry((pw-lw)/2, (ph-lh)/2, lw, lh); // center
794 repaint();
795 qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
796
797 loadHistoricLogs();
798
799 delete label;
800 repaint();
801 qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
802
803 if (!connectScheduler(true))
804 watchForSchedulerRestart();
805
806 this->setEnabled(true);
807 }
808
connectScheduler(bool privileged)809 bool MainWindow::connectScheduler(bool privileged)
810 {
811 connect(scheduler, &QLocalSocket::connected,
812 this, &MainWindow::schedConnected);
813 connect(scheduler, &QLocalSocket::disconnected,
814 this, &MainWindow::schedDisconnected);
815 connect(scheduler, QLOCALSOCKET_ERROR_OCCURRED,
816 this, &MainWindow::schedError);
817 connect(scheduler, &QLocalSocket::readyRead,
818 this, &MainWindow::readScheduler);
819 return connectToScheduler(privileged);
820 }
821
opAllowed(const Config::MemberBase & cfgItem)822 bool MainWindow::opAllowed(const Config::MemberBase &cfgItem)
823 {
824 return (scheduler && scheduler->state() == QLocalSocket::ConnectedState) &&
825 (connectionIsPrivileged || cfgItem.variant().toBool());
826 }
827
additionalArgs()828 QStringList MainWindow::additionalArgs() {
829 QStringList list;
830 list << QSL("-qwindowgeometry") <<
831 QSL("+") % QString::number(x()) % QSL("+") % QString::number(y());
832 return list;
833 }
834
openPreferences()835 void MainWindow::openPreferences()
836 {
837 if (!prefDialog) {
838 prefDialog = new PreferencesDialog(this, this->scheduler,
839 opAllowed(config->unprivPref));
840 connect(prefDialog, &QDialog::finished, this, &MainWindow::closePreferences);
841 }
842 prefDialog->show();
843 prefDialog->raise();
844 prefDialog->activateWindow();
845 }
846
closePreferences()847 void MainWindow::closePreferences()
848 {
849 if (prefDialog) {
850 disconnect(prefDialog, nullptr, nullptr, nullptr);
851 prefDialog->deleteLater();
852 prefDialog = nullptr;
853 }
854 }
855
toggleUpgradeDetails(const QString & link)856 void MainWindow::toggleUpgradeDetails(const QString &link)
857 {
858 Q_UNUSED(link);
859 upgradeDetails->setVisible(!upgradeDetails->isVisible());
860 }
861
promptForUpgrade()862 void MainWindow::promptForUpgrade()
863 {
864 upgradeBox->hide();
865
866 QString notice(QSL("<div>Spoofer upgrade %1: %2 "
867 "<a href='#'>(details)</a>.</div>")
868 .arg(upgradeInfo->mandatory ? QSL("required") : QSL("available"))
869 .arg(upgradeInfo->vstr.toHtmlEscaped()));
870 if (!upgradeInfo->warning.isNull())
871 notice += QSL("<div style='color:red'>") %
872 upgradeInfo->warning.toHtmlEscaped() % QSL("</div>");
873 upgradeNotice->setText(notice);
874
875 QString details;
876 #ifndef UPGRADE_WITHOUT_DOWNLOAD
877 details.append(QSL("<div>URL: ") % upgradeInfo->file.toHtmlEscaped() % QSL("</div>"));
878 #endif
879 #ifdef UPGRADE_CMD
880 details.append(QSL("<div>command: <code>") % QSL(UPGRADE_CMD).toHtmlEscaped() % QSL("</code></div>"));
881 #endif
882 upgradeDetails->setText(details);
883 upgradeDetails->hide();
884
885 #ifdef AUTOUPGRADE_ENABLED
886 time_t now;
887 time(&now);
888 if (now - upgradeResultTime > 60)
889 upgradeResultBox->hide();
890 if (upgradeInfo->autoTime >= 0) {
891 autoupgradeTime = now + upgradeInfo->autoTime;
892 autoupgradeTimer.start(1000); // every second
893 showUpgradeTimer();
894 }
895 upgradeAutoNotice->setVisible(upgradeInfo->autoTime >= 0);
896 upgradeCancelButton->setVisible(upgradeInfo->autoTime >= 0);
897 upgradeButton->show();
898 #endif
899 upgradeNotice->show();
900 upgradeBox->show();
901 if (upgradeInfo->autoTime >= 0)
902 runAct->setEnabled(false);
903 }
904
905 #ifdef AUTOUPGRADE_ENABLED
showUpgradeTimer()906 void MainWindow::showUpgradeTimer()
907 {
908 time_t now;
909 time(&now);
910 upgradeAutoNotice->setText(QSL("Spoofer will upgrade automatically in ") %
911 QString::number(autoupgradeTime - now) % QSL(" seconds."));
912 }
913
doUpgrade()914 void MainWindow::doUpgrade()
915 {
916 scheduler->write("upgrade\n");
917 cancelUpgradePrompt();
918 }
919
cancelUpgrade()920 void MainWindow::cancelUpgrade()
921 {
922 scheduler->write("cancel\n");
923 cancelUpgradePrompt();
924 }
925
showUpgradeProgress(const QString & text)926 void MainWindow::showUpgradeProgress(const QString &text)
927 {
928 qDebug() << "showUpgradeProgress" << text;
929 upgradeAutoNotice->setText(text);
930 upgradeAutoNotice->show();
931 upgradeCancelButton->show();
932 upgradeBox->show();
933
934 upgradeResultBox->hide();
935 upgradeNotice->hide();
936 upgradeDetails->hide();
937 upgradeButton->hide();
938 runAct->setEnabled(false);
939 }
940
showUpgradeResult(const QString & text)941 void MainWindow::showUpgradeResult(const QString &text)
942 {
943 qDebug() << "showUpgradeResult" << text;
944 upgradeResultLabel->setText(text);
945 upgradeResultBox->show();
946 time(&upgradeResultTime);
947
948 upgradeAutoNotice->hide();
949 upgradeCancelButton->hide();
950 upgradeBox->hide();
951 showStatus();
952 }
953 #endif // AUTOUPGRADE_ENABLED
954
cancelUpgradePrompt()955 void MainWindow::cancelUpgradePrompt()
956 {
957 #ifdef AUTOUPGRADE_ENABLED
958 autoupgradeTimer.stop();
959 #endif // AUTOUPGRADE_ENABLED
960 upgradeBox->hide();
961 }
962
help()963 void MainWindow::help() {
964 qDebug() << "### signal: help";
965 }
966
about()967 void MainWindow::about() {
968 static QString text;
969 if (text.isNull()) {
970 text +=
971 QSL("<h1>" PACKAGE_NAME "</h1>") %
972 QSL("<span style='white-space: nowrap;'>") %
973 QSL(PACKAGE_DESC ", version " PACKAGE_VERSION "<br />") %
974 QSL("</span>") %
975 QSL(PACKAGE_LONGDESC "<br /><br />") %
976 QSL("<span style='white-space: nowrap;'>") %
977 QSL(COPYRIGHT).replace(QSL("; "), QSL("<br />")) %
978 QSL("</span>") %
979 QSL("<br /><br />") %
980 QSL("<a href='" PACKAGE_URL "'>" PACKAGE_URL "</a>" "<br />");
981 // QSL("contact: " PACKAGE_BUGREPORT "<br />");
982 }
983 QMessageBox::about(this, QSL("About Spoofer"), text);
984 }
985
configChanged()986 void MainWindow::configChanged()
987 {
988 if (prefDialog)
989 prefDialog->warn(QSL("Warning: settings have changed since this window opened."));
990 }
991
needConfig()992 void MainWindow::needConfig()
993 {
994 openPreferences();
995 }
996
schedConnected()997 void MainWindow::schedConnected()
998 {
999 spout << "Connected to scheduler." << Qt_endl;
1000 connect(qApp, &QApplication::aboutToQuit, scheduler, &QLocalSocket::close);
1001 schedulerPaused = false; // until told otherwise
1002 schedulerNeedsConfig = false; // until told otherwise
1003 if (fileTail) // stale
1004 fileTail->requestInterruption();
1005 showStatus();
1006 if (schedWatcher) delete schedWatcher;
1007 schedWatcher = nullptr;
1008 }
1009
schedCleanup(bool isError)1010 void MainWindow::schedCleanup(bool isError)
1011 {
1012 if (prefDialog) {
1013 prefDialog->warn(QSL("Disconnected from Scheduler. Settings can not be changed."));
1014 prefDialog->disable();
1015 }
1016 disconnect(qApp, nullptr, scheduler, nullptr);
1017 disconnect(scheduler, nullptr, nullptr, nullptr);
1018 if (isError) scheduler->abort();
1019 scheduler->close();
1020 scheduler->deleteLater();
1021 scheduler = new QLocalSocket(this);
1022 if (fileTail) {
1023 // Without the scheduler to tell us SC_PROBER_FINISHED, we'll assume
1024 // prober is done after a period of inactivity in the log.
1025 fileTail->setTimeout(10000);
1026 }
1027 cancelUpgradePrompt();
1028 showStatus();
1029 #ifndef EVERYONE_IS_PRIVILEGED
1030 if (isError && connectionIsPrivileged)
1031 if (connectScheduler(false)) return;
1032 #endif
1033 watchForSchedulerRestart();
1034 }
1035
schedError()1036 void MainWindow::schedError()
1037 {
1038 qCritical() << "Scheduler error:" << qPrintable(scheduler->errorString());
1039 schedCleanup(true);
1040 }
1041
showStatus()1042 void MainWindow::showStatus()
1043 {
1044 statusBar()->clearMessage();
1045 if (fileTail) {
1046 runAct->setEnabled(false);
1047 abortAct->setEnabled(opAllowed(config->unprivTest));
1048 } else {
1049 runAct->setEnabled(opAllowed(config->unprivTest) && !upgradeAutoNotice->isVisible());
1050 abortAct->setEnabled(false);
1051 }
1052 runButton->setAction(fileTail ? abortAct : runAct);
1053 bool connected = scheduler && (scheduler->state() == QLocalSocket::ConnectedState);
1054 schedulerStatusLabel->setText(
1055 !connected ? QSL("not connected") :
1056 schedulerNeedsConfig ? QSL("pending configuration") :
1057 schedulerPaused ? QSL("paused") :
1058 QSL("ready"));
1059 proberInfoLabel->setEnabled(scheduler && scheduler->isOpen());
1060 proberCountdownLabel->setEnabled(scheduler && scheduler->isOpen());
1061 shutdownAct->setEnabled(scheduler && scheduler->isOpen());
1062 pauseAct->setEnabled(opAllowed(config->unprivPref) && !schedulerPaused);
1063 resumeAct->setEnabled(opAllowed(config->unprivPref) && schedulerPaused);
1064 if (schedulerPaused)
1065 pauseButton->setAction(resumeAct, pauseAct);
1066 else
1067 pauseButton->setAction(pauseAct, resumeAct);
1068 }
1069
finishProber()1070 bool MainWindow::finishProber()
1071 {
1072 if (!SpooferUI::finishProber())
1073 return false;
1074 showStatus();
1075 // show results even if prober didn't exit properly
1076 proberTimeRow->title->setText(QSL("Last run:"));
1077 progRow4->endProber();
1078 progRow6->endProber();
1079
1080 bool complete = false;
1081 if (currentRun) {
1082 complete = addHistoryReport(currentRun);
1083 delete currentRun;
1084 currentRun = nullptr;
1085 }
1086 if (!complete)
1087 proberTimeRow->content->setText(
1088 proberTimeRow->content->text() % QSL(" (incomplete)"));
1089 return true;
1090 }
1091
setCountdownUnit(time_t when,time_t now,int count,int secs,QString unit,QString units)1092 bool MainWindow::setCountdownUnit(time_t when, time_t now, int count, int secs,
1093 QString unit, QString units)
1094 {
1095 if ((when - now) > count * secs) {
1096 time_t n = (when - now + secs/2 - 1) / secs;
1097 proberCountdownLabel->setText(
1098 QSL("(in about %1 %2)").arg(n).arg(n == 1 ? unit : units));
1099 time_t timeout = std::min(
1100 (when - now + secs/2) % secs + 1, // the nearest X.5 units before when
1101 (when - now) - count * secs); // count units before when
1102 // qDebug() << "countdown: " << ftime(nullptr, &now) << ftime(nullptr, &when);
1103 // qDebug() << "countdown: label" << proberCountdownLabel->text() << ", diff" << when-now << ", new timeout" << timeout;
1104 countdownTimer.start(1000 * static_cast<int>(timeout));
1105 return true;
1106 } else {
1107 return false;
1108 }
1109 }
1110
setCountdownLabel()1111 void MainWindow::setCountdownLabel()
1112 {
1113 time_t when = nextProberStart.when;
1114 if (!when) {
1115 countdownTimer.stop();
1116 proberCountdownLabel->setText(QString());
1117 return;
1118 }
1119 time_t now;
1120 time(&now);
1121 if (!setCountdownUnit(when, now, 2, 86400, QSL("day"), QSL("days")) &&
1122 !setCountdownUnit(when, now, 2, 3600, QSL("hour"), QSL("hours")) &&
1123 !setCountdownUnit(when, now, 1, 60, QSL("minute"), QSL("minutes")))
1124 {
1125 proberCountdownLabel->setText(QSL("(in less than 1 minute)"));
1126 countdownTimer.stop();
1127 }
1128 }
1129
printNextProberStart()1130 void MainWindow::printNextProberStart()
1131 {
1132 time_t when = nextProberStart.when;
1133 proberInfoLabel->setText(!when ? QSL("none scheduled") :
1134 QSL("next scheduled for ") % ftime(QString(), &when));
1135 setCountdownLabel();
1136 SpooferUI::printNextProberStart();
1137 }
1138
startFileTail(QString logname)1139 void MainWindow::startFileTail(QString logname)
1140 {
1141 clearProberMessages(); // clear old prober messages
1142 proberInfoLabel->setText(QSL("in progress"));
1143 proberCountdownLabel->setText(QSL(""));
1144
1145 fileTail = new FileTailThread(logname);
1146 connect(fileTail, &FileTailThread::dataReady,
1147 this, &MainWindow::handleProberText,
1148 Qt::BlockingQueuedConnection);
1149 // Blocking avoids races between dataReady and finished
1150 connect(fileTail, &FileTailThread::finished,
1151 this, &MainWindow::finishProber);
1152 fileTail->start();
1153 currentRun = new RunResults(true, logname);
1154 showStatus();
1155 // hide any old results
1156 progRow4->start();
1157 progRow6->start();
1158 initRunDisplay(currentRun, QSL("Started:"));
1159 historyWidget->setRowHidden(0, false); // "live" row is visible by default
1160 }
1161
hideBlankTests(bool checked)1162 void MainWindow::hideBlankTests(bool checked)
1163 {
1164 if (!historyWidget->rowCount()) return;
1165 bool liveSpan = fileTail ? historyWidget->rowSpan(0, HIST_date) : 0;
1166 for (int i = 0; i < historyWidget->rowCount(); i++) {
1167 if (!historyWidget->cellWidget(i, HIST_asn))
1168 historyWidget->setRowHidden(i, checked && i >= liveSpan);
1169 }
1170 historyWidget->resizeColumnsToContents();
1171 }
1172
loadHistoricLogs()1173 void MainWindow::loadHistoricLogs()
1174 {
1175 QDir dir(config->dataDir());
1176
1177 QStringList lognames = dir.entryList(QStringList() << proberLogGlob,
1178 QDir::Files, QDir::Name);
1179
1180 for (int i = 0; i < lognames.size(); i++) {
1181 bool showMessages = (i == lognames.size() - 1);
1182 QString logname = dir.filePath(lognames.at(i));
1183 QFile file(logname);
1184 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1185 qWarning() << "Failed to load log" <<
1186 qPrintable(QDir::toNativeSeparators(logname)) << ":" <<
1187 qPrintable(getLastErrmsg());
1188 continue;
1189 }
1190 RunResults historicRun(false, logname);
1191 initRunDisplay(&historicRun, QSL("Last run:"));
1192 historyWidget->setRowHidden(0, true); // hide until endHistorySession()
1193 QTextStream in(&file);
1194 while (!in.atEnd()) {
1195 historicRun.parseProberText(in.read(4096), showMessages);
1196 }
1197 addHistoryReport(&historicRun);
1198 }
1199 }
1200
findResults(const QString & ipv,ProgressRow ** progRow,SessionResults ** results)1201 void RunResults::findResults(const QString &ipv, ProgressRow **progRow, SessionResults **results)
1202 {
1203 *progRow = nullptr;
1204 if (results) *results = nullptr;
1205 if (ipv.at(0) == QChar::fromLatin1('6')) {
1206 if (!this->results6)
1207 this->results6 = new SessionResults(6);
1208 if (results)
1209 *results = this->results6;
1210 if (this->live) {
1211 *progRow = MainWindow::instance()->progRow6;
1212 (*progRow)->results = this->results6;
1213 }
1214 } else {
1215 if (!this->results4)
1216 this->results4 = new SessionResults(4);
1217 if (results)
1218 *results = this->results4;
1219 if (this->live) {
1220 *progRow = MainWindow::instance()->progRow4;
1221 (*progRow)->results = this->results4;
1222 }
1223 }
1224 }
1225
addHistoryCell(int row,int column,Qt::Alignment halign,QLabel * label)1226 void MainWindow::addHistoryCell(int row, int column, Qt::Alignment halign, QLabel *label)
1227 {
1228 ColoredLabel *cl = dynamic_cast<ColoredLabel*>(label);
1229 if (cl && currentRun && currentRun->live) {
1230 // Fade from yellow to transparent in 10s
1231 QPropertyAnimation *anim = new QPropertyAnimation(cl, "bgcolor");
1232 anim->setDuration(10000);
1233 anim->setStartValue(QColor(255,255,0,255)); // opaque yellow
1234 anim->setEndValue(QColor(255,255,0,0)); // transparent yellow
1235 anim->start(QAbstractAnimation::DeleteWhenStopped);
1236 }
1237 label->setAlignment(halign | Qt::AlignVCenter);
1238 label->setContentsMargins(2,2,2,2);
1239 historyWidget->setCellWidget(row, column, label);
1240 }
1241
addHistoryReport(const RunResults * runResults)1242 bool MainWindow::addHistoryReport(const RunResults *runResults)
1243 {
1244 if (runResults->report.isEmpty()) return false;
1245 addHistoryCell(0, HIST_report, Qt::AlignHCenter, new LinkWidget(QUrl(runResults->report), QSL("report")));
1246 return true;
1247 }
1248
initRunDisplay(const RunResults * runResults,const QString & timeTitle)1249 void MainWindow::initRunDisplay(const RunResults *runResults, const QString &timeTitle)
1250 {
1251 debugFlags.clear();
1252
1253 proberTimeRow->title->setText(timeTitle);
1254 proberTimeRow->content->setText(ftime(QString(), &runResults->started));
1255 proberTimeRow->show();
1256
1257 historyWidget->insertRow(0);
1258
1259 QLabel *timeLabel =
1260 new ColoredLabel(ftime(QSL("yyyy-MM-dd HH:mm:ss"), &runResults->started));
1261 // Time zone would be a waste of column space (especially on Windows where
1262 // it's not abbreviated), so we put it in a tool tip instead.
1263 timeLabel->setToolTip(ftime(QSL("t"), &runResults->started));
1264 addHistoryCell(0, HIST_date, Qt::AlignLeft, timeLabel);
1265
1266 LinkWidget *loglink = new LinkWidget(QUrl::fromLocalFile(runResults->logname), QSL("log"));
1267 addHistoryCell(0, HIST_log, Qt::AlignHCenter, loglink);
1268
1269 // The showlog column's bgcolor matches the table's bg and borders, so the
1270 // column is effectively invisible. (And, just in case that doesn't work,
1271 // we can still eliminate the column's internal borders with setSpan.)
1272 auto *showlog = new QLabel();
1273 showlog->setStyleSheet(QSL("background: " HISTORYTABLE_BGCOLOR ";"));
1274 historyWidget->setCellWidget(0, HIST_showlog, showlog);
1275 if (historyWidget->rowCount() > 1)
1276 historyWidget->setSpan(0, HIST_showlog, historyWidget->rowCount(), 1);
1277
1278 historyWidget->resizeColumnsToContents();
1279 }
1280
addHistorySession(SessionResults * results)1281 void MainWindow::addHistorySession(SessionResults *results)
1282 {
1283 if (!results) return;
1284 if (results->row >= 0) return; // already added
1285
1286 if (!historyWidget->cellWidget(0, HIST_ipv)) {
1287 // first session of the run; we will fill in the base row
1288 results->row = 0;
1289 } else {
1290 // Nth session of the run; we will insert a new row
1291 results->row = historyWidget->rowSpan(0, HIST_date);
1292 historyWidget->insertRow(results->row);
1293
1294 // make some columns of row 0 span down into the new row
1295 historyWidget->setSpan(0, HIST_date, results->row + 1, 1);
1296 historyWidget->setSpan(0, HIST_report, results->row + 1, 1);
1297 historyWidget->setSpan(0, HIST_log, results->row + 1, 1);
1298 }
1299
1300 QString ipv = QString::number(results->ipv);
1301 ipv.append(debugFlags);
1302 addHistoryCell(results->row, HIST_ipv, Qt::AlignHCenter, new ColoredLabel(ipv));
1303
1304 historyWidget->setRowHidden(results->row, !fileTail);
1305 if (fileTail)
1306 historyWidget->resizeColumnsToContents();
1307 }
1308
endHistorySession(SessionResults * results)1309 void MainWindow::endHistorySession(SessionResults *results)
1310 {
1311 if (!results) return;
1312
1313 addHistoryCell(results->row, HIST_asn, Qt::AlignRight,
1314 new ColoredLabel(results->asn ? QString::number(results->asn) : QString()));
1315 addHistoryCell(results->row, HIST_privaddr, Qt::AlignHCenter,
1316 resultWidget(results->privaddr));
1317 addHistoryCell(results->row, HIST_routable, Qt::AlignHCenter,
1318 resultWidget(results->routable));
1319 if (!results->inprivaddr.isNull())
1320 addHistoryCell(results->row, HIST_inprivaddr, Qt::AlignHCenter,
1321 resultWidget(results->inprivaddr));
1322 if (!results->ininternal.isNull())
1323 addHistoryCell(results->row, HIST_ininternal, Qt::AlignHCenter,
1324 resultWidget(results->ininternal));
1325
1326 historyWidget->setRowHidden(results->row, false); // row with results should always be visible
1327 historyWidget->resizeColumnsToContents();
1328 results->row = -1;
1329 }
1330
1331 // Delete all but the n most recent prober messages
clearProberMessages(int n)1332 void MainWindow::clearProberMessages(int n)
1333 {
1334 #ifdef AUTOUPGRADE_ENABLED
1335 upgradeResultBox->hide();
1336 #endif
1337 while (this->nextProberMsgIdx > this->firstProberMsgIdx + n) {
1338 QLayoutItem *item =
1339 this->proberBoxLayout->takeAt(this->firstProberMsgIdx);
1340 if (item && item->widget()) { // should never be false
1341 item->widget()->hide();
1342 delete item;
1343 }
1344 this->nextProberMsgIdx--;
1345 }
1346 }
1347
displayProberMessage(const QString & text,bool enabled,const QColor & color)1348 void MainWindow::displayProberMessage(const QString &text, bool enabled,
1349 const QColor &color)
1350 {
1351 clearProberMessages(4); // Keep only the last 4 messages
1352
1353 QLabel *label = new QLabel(text);
1354 label->setTextFormat(Qt::PlainText);
1355 label->setWordWrap(true);
1356 label->setTextInteractionFlags(Qt::TextSelectableByMouse);
1357 if (color.isValid()) {
1358 QPalette pal = label->palette();
1359 pal.setColor(QPalette::Active, QPalette::WindowText, color);
1360 pal.setColor(QPalette::Inactive, QPalette::WindowText, color);
1361 label->setPalette(pal);
1362 }
1363 label->setEnabled(enabled);
1364 label->setContentsMargins(20,0,0,0);
1365 this->proberBoxLayout->insertWidget(this->nextProberMsgIdx++, label);
1366 }
1367
proberError(const QString & text)1368 void MainWindow::proberError(const QString &text)
1369 {
1370 if (!currentRun) // not part of an already-in-progress run
1371 clearProberMessages();
1372 displayProberMessage(QSL("Prober error: ") + text, true, Qt::darkRed);
1373 }
1374
parseProberText(const QString & text,bool showMessages)1375 void RunResults::parseProberText(const QString &text, bool showMessages)
1376 {
1377 static QString oldtext;
1378 static int offset = 0;
1379 // NB: If format changes, don't forget to update openwrt-files/spoofer
1380 static QString basePattern = QSL(
1381 // prober version, if it exactly matches ours
1382 ">> \\Q" PACKAGE_DESC "\\E version (?<proberversion>\\Q" PACKAGE_VERSION "\\E)\n|"
1383 // debugging flags
1384 ">> (?<debug>standaloneMode|pretendMode|useDevServer)\n|"
1385 // start session
1386 "# ServerMessage \\(IPv(?<startv>[46])\\):\n"
1387 "(# (?!hello:)\\S.*\n)*" // e.g. "textmsg"
1388 "# hello:\n"
1389 "(# (?!clientip:)\\S.*\n)*"
1390 "(?:# clientip: (?<addr>[0-9a-fA-F.:]+)\n)?|"
1391 // result summary (note: ", egress" substrings and optional ingress
1392 // lines were added in prober 1.2.0. "Ingress"/"egress" changed to
1393 // "inbound"/"outbound" in 1.4.3.)
1394 ">> IPv(?<endv>[46]) Result Summary:\n"
1395 ">> +ASN: (?<asn>\\d+)\n"
1396 ">> +Spoofed private addresses(, (egress|outbound))?: (?<privaddr>\\w+)\n"
1397 ">> +Spoofed routable addresses(, (egress|outbound))?: (?<routable>\\w+)\n"
1398 "(?:>> +Spoofed private addresses, (ingress|inbound): (?<inprivaddr>\\w+)\n)?"
1399 "(?:>> +Spoofed internal addresses, (ingress|inbound): (?<ininternal>\\w+)\n)?|"
1400 // report url
1401 "Your test results:\\s+(?<report>\\S+)\n|"
1402 // server or scheduler message
1403 "(?<msg>[*][*][*] (?<msgpfx>(?:IPv[46] server )?"
1404 "(?<msgtype>[Ee]rror|[Ww]arning|[Nn]otice)): (?<msgtxt>.*))\n"
1405 );
1406
1407 static QRegularExpression baseRe(basePattern);
1408
1409 // omitting this from baseRe speeds up loadHistoricLogs()
1410 static QRegularExpression liveRe(basePattern + QSL("|"
1411 // start stage
1412 ">> Running IPv(?<stagev>[46]) (?<stagetext>test \\d+:.*)\n|"
1413 // stage progress
1414 "\\Q" PROGRESS_PREFIX "\\E (?<progresstext>.*)\n|"
1415 ));
1416
1417 const QString *subject = &text;
1418 if (!oldtext.isNull()) {
1419 // append new text to old partially-matched text
1420 oldtext += text;
1421 subject = &oldtext;
1422 }
1423
1424 QRegularExpression *re = this->live ? &liveRe : &baseRe;
1425 QRegularExpressionMatchIterator it = re->globalMatch(*subject, offset,
1426 QRegularExpression::PartialPreferFirstMatch);
1427 while (it.hasNext()) {
1428 QRegularExpressionMatch match = it.next();
1429 if (match.hasPartialMatch()) {
1430 // save partially matched text and offset to try again later
1431 offset = match.capturedStart(0);
1432 if (oldtext.isNull())
1433 oldtext = text;
1434 return;
1435 }
1436 ProgressRow *progRow = nullptr;
1437 SessionResults *results = nullptr;
1438 if (!match.captured(QSL("proberversion")).isNull()) {
1439 this->proberVersionMatch = true;
1440 } else if (!match.captured(QSL("debug")).isNull()) {
1441 if (match.captured(QSL("debug")).compare(QSL("standaloneMode")) == 0)
1442 MainWindow::instance()->debugFlags.append(QSL("S"));
1443 if (match.captured(QSL("debug")).compare(QSL("pretendMode")) == 0)
1444 MainWindow::instance()->debugFlags.append(QSL("P"));
1445 if (match.captured(QSL("debug")).compare(QSL("useDevServer")) == 0)
1446 MainWindow::instance()->debugFlags.append(QSL("T"));
1447 } else if (!match.captured(QSL("startv")).isNull()) {
1448 this->findResults(match.captured(QSL("startv")), &progRow, &results);
1449 MainWindow::instance()->addHistorySession(results);
1450 if (!match.captured(QSL("addr")).isNull()) {
1451 results->ipaddr = match.captured(QSL("addr"));
1452 QLabel *longlabel = new ColoredLabel(results->ipaddr);
1453 QLabel *shortlabel;
1454 if (results->ipv == 6) {
1455 QString shortaddr(SubnetAddr(QHostAddress(results->ipaddr), 64).prefix().toString());
1456 shortlabel = new ColoredLabel(shortaddr);
1457 } else {
1458 shortlabel = new ColoredLabel(results->ipaddr);
1459 }
1460 MainWindow::instance()->addHistoryCell(results->row, HIST_ipaddr_short,
1461 Qt::AlignLeft, shortlabel);
1462 MainWindow::instance()->addHistoryCell(results->row, HIST_ipaddr_long,
1463 Qt::AlignLeft, longlabel);
1464 }
1465 } else if (!match.captured(QSL("endv")).isNull()) {
1466 this->findResults(match.captured(QSL("endv")), &progRow, &results);
1467 results->asn = match.captured(QSL("asn")).toUInt();
1468 results->privaddr = match.captured(QSL("privaddr"));
1469 results->routable = match.captured(QSL("routable"));
1470 results->inprivaddr = match.captured(QSL("inprivaddr"));
1471 results->ininternal = match.captured(QSL("ininternal"));
1472 if (this->live) progRow->endSession();
1473 MainWindow::instance()->endHistorySession(results);
1474 } else if (!match.captured(QSL("report")).isNull()) {
1475 this->report = match.captured(QSL("report"));
1476 } else if ((this->live || (showMessages && this->proberVersionMatch)) &&
1477 !match.captured(QSL("msgtype")).isNull())
1478 {
1479 // The proberVersionMatch requirement prevents us from displaying
1480 // a misleading out-of-date message like "new version available"
1481 // when the client has been upgraded since the logged prober run.
1482 QString msgtype = match.captured(QSL("msgtype")).toLower();
1483 QString msgtxt = match.captured(QSL("msgtxt"));
1484 if (msgtxt.startsWith(QSL("Upgrade available:"))) {
1485 // message will be handled by SC_UPGRADE_AVAILABLE
1486 } else {
1487 QColor color;
1488 if (msgtype.compare(QSL("error"), Qt::CaseInsensitive) == 0)
1489 color = Qt::darkRed;
1490 else if (msgtype.compare(QSL("warning"), Qt::CaseInsensitive) == 0)
1491 color = Qt::darkMagenta;
1492 MainWindow::instance()->displayProberMessage(match.captured(QSL("msg")), this->live, color);
1493 }
1494 } else if (!this->live) {
1495 // Do nothing. Remaining matches are only for live runs.
1496 } else if (!match.captured(QSL("stagev")).isNull()) {
1497 this->findResults(match.captured(QSL("stagev")), &progRow, nullptr);
1498 progRow->stage(match.captured(QSL("stagetext")));
1499 } else if (!match.captured(QSL("progresstext")).isNull()) {
1500 QString progText = match.captured(QSL("progresstext"));
1501 QRegularExpression progRe(QSL("IPv(?<v>[46]): (\\d+\\+\\d+/)?(?<tries>\\d+)/(?<goal>\\d+)"));
1502 QRegularExpressionMatchIterator progIt = progRe.globalMatch(progText);
1503 while (progIt.hasNext()) {
1504 QRegularExpressionMatch progMatch = progIt.next();
1505 int tries = progMatch.captured(QSL("tries")).toInt();
1506 int goal = progMatch.captured(QSL("goal")).toInt();
1507 this->findResults(progMatch.captured(QSL("v")), &progRow, nullptr);
1508 progRow->tick(tries, goal);
1509 }
1510 }
1511 }
1512 oldtext.clear();
1513 offset = 0;
1514 }
1515
handleProberText(QString * text)1516 void MainWindow::handleProberText(QString *text)
1517 {
1518 currentRun->parseProberText(*text);
1519 QTextCharFormat oldfmt = winout->setProberCharFmt();
1520 SpooferUI::handleProberText(text);
1521 winout->setCharFmt(oldfmt);
1522 }
1523
schedDisconnected()1524 void MainWindow::schedDisconnected()
1525 {
1526 qWarning() << "Scheduler disconnected";
1527 schedCleanup(false);
1528 }
1529
watchForSchedulerRestart()1530 void MainWindow::watchForSchedulerRestart()
1531 {
1532 // When scheduler starts, it will update the settings and create a lock
1533 // file in dataDir. So we watch for changes in one of those.
1534 QString file = config->isFile() ? config->fileName() : QString();
1535 QString dir = config->dataDir();
1536 if (!schedWatcher) {
1537 schedWatcher = new QFileSystemWatcher();
1538 connect(schedWatcher, &QFileSystemWatcher::fileChanged,
1539 this, &MainWindow::reconnectScheduler);
1540 connect(schedWatcher, &QFileSystemWatcher::directoryChanged,
1541 this, &MainWindow::reconnectScheduler);
1542 }
1543
1544 // Note: after a change is signaled for a watched path, that path may or
1545 // may not still be in the watch list.
1546 if (!file.isNull())
1547 schedWatcher->addPath(file);
1548
1549 if (schedWatcher->files().isEmpty() && !dir.isNull())
1550 schedWatcher->addPath(dir);
1551
1552 if (schedWatcher->files().isEmpty() && schedWatcher->directories().isEmpty()) {
1553 delete schedWatcher;
1554 schedWatcher = nullptr;
1555 qDebug() << "watching failed!";
1556 return;
1557 }
1558 spout << "Waiting for scheduler to start..." << Qt_endl;
1559 qDebug() << "watching: " << schedWatcher->files() << " " << schedWatcher->directories();
1560 }
1561
reconnectScheduler()1562 void MainWindow::reconnectScheduler()
1563 {
1564 qDebug() << "reconnectScheduler";
1565 QThread::msleep(1000); // give scheduler a chance to get going
1566 if (!connectScheduler(true))
1567 watchForSchedulerRestart();
1568 }
1569
runProber()1570 void MainWindow::runProber() {
1571 qDebug() << "### signal: run";
1572 if (!fileTail) {
1573 scheduler->write("run\n");
1574 } else {
1575 qDebug() << "### prober is already running";
1576 }
1577 }
1578
abortProber()1579 void MainWindow::abortProber() {
1580 qDebug() << "### signal: abort";
1581 if (fileTail) {
1582 scheduler->write("abort\n");
1583 } else {
1584 qDebug() << "### prober is not running";
1585 }
1586 }
1587
pauseScheduler()1588 void MainWindow::pauseScheduler() {
1589 qDebug() << "### signal: pause";
1590 scheduler->write("pause\n");
1591 }
1592
resumeScheduler()1593 void MainWindow::resumeScheduler() {
1594 qDebug() << "### signal: resume";
1595 scheduler->write("resume\n");
1596 }
1597
shutdownScheduler()1598 void MainWindow::shutdownScheduler() {
1599 QMessageBox mb;
1600 mb.setIcon(QMessageBox::Warning);
1601 mb.setText(QSL("<b>Shut down Spoofer scheduler?</b>"));
1602 mb.setInformativeText(QSL("Once the scheduler is shut down, this GUI will "
1603 "not be able to restart it or do anything else; and it may restart "
1604 "automatically at next reboot depending on its configuration. Use "
1605 "\"Pause Scheduler\" instead to disable scheduled tests in a way that "
1606 "can be easily re-enabled and will persist across reboots."));
1607 mb.setStandardButtons(QMessageBox::No | QMessageBox::Yes);
1608 if (mb.exec() == QMessageBox::Yes)
1609 scheduler->write("shutdown\n");
1610 }
1611
1612