mumble-voip_mumble/src/mumble/ConnectDialog.cpp

1924 lines
56 KiB
C++

// Copyright 2007-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
#include "ConnectDialog.h"
#ifdef USE_ZEROCONF
# include "Zeroconf.h"
#endif
#include "Channel.h"
#include "Database.h"
#include "ServerHandler.h"
#include "ServerResolver.h"
#include "Utils.h"
#include "WebFetch.h"
#include "Global.h"
#include <QSettings>
#include <QtCore/QMimeData>
#include <QtCore/QUrlQuery>
#include <QtCore/QtEndian>
#include <QtGui/QClipboard>
#include <QtGui/QDesktopServices>
#include <QtGui/QPainter>
#include <QtNetwork/QUdpSocket>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QShortcut>
#include <QtXml/QDomDocument>
#include <boost/accumulators/statistics/extended_p_square.hpp>
#include <boost/array.hpp>
#ifdef Q_OS_WIN
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <shlobj.h>
#endif
#include <QRandomGenerator>
#include <algorithm>
QMap< QString, QIcon > ServerItem::qmIcons;
QList< PublicInfo > ConnectDialog::qlPublicServers;
QString ConnectDialog::qsUserCountry, ConnectDialog::qsUserCountryCode, ConnectDialog::qsUserContinentCode;
Timer ConnectDialog::tPublicServers;
PingStats::PingStats() {
init();
}
PingStats::~PingStats() {
delete asQuantile;
}
void PingStats::init() {
boost::array< double, 3 > probs = { { 0.75, 0.80, 0.95 } };
asQuantile = new asQuantileType(boost::accumulators::tag::extended_p_square::probabilities = probs);
dPing = 0.0;
uiPing = 0;
uiPingSort = 0;
uiUsers = 0;
uiMaxUsers = 0;
uiBandwidth = 0;
uiSent = 0;
uiRecv = 0;
m_version = Version::UNKNOWN;
}
void PingStats::reset() {
delete asQuantile;
init();
}
ServerViewDelegate::ServerViewDelegate(QObject *p) : QStyledItemDelegate(p) {
}
ServerViewDelegate::~ServerViewDelegate() {
}
void ServerViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
// Allow a ServerItem's BackgroundRole to override the current theme's default color.
QVariant bg = index.data(Qt::BackgroundRole);
if (bg.isValid()) {
painter->fillRect(option.rect, bg.value< QBrush >());
}
QStyledItemDelegate::paint(painter, option, index);
}
ServerView::ServerView(QWidget *p) : QTreeWidget(p) {
siFavorite = new ServerItem(tr("Favorite"), ServerItem::FavoriteType);
addTopLevelItem(siFavorite);
siFavorite->setExpanded(true);
siFavorite->setHidden(true);
#ifdef USE_ZEROCONF
siLAN = new ServerItem(tr("LAN"), ServerItem::LANType);
addTopLevelItem(siLAN);
siLAN->setExpanded(true);
siLAN->setHidden(true);
#else
siLAN = nullptr;
#endif
if (!Global::get().s.bDisablePublicList) {
siPublic = new ServerItem(tr("Public Internet"), ServerItem::PublicType);
siPublic->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
addTopLevelItem(siPublic);
siPublic->setExpanded(false);
} else {
qWarning() << "Public list disabled";
siPublic = nullptr;
}
}
ServerView::~ServerView() {
delete siFavorite;
delete siLAN;
delete siPublic;
}
QMimeData *ServerView::mimeData(const QList< QTreeWidgetItem * > mimeitems) const {
if (mimeitems.isEmpty())
return nullptr;
ServerItem *si = static_cast< ServerItem * >(mimeitems.first());
return si->toMimeData();
}
QStringList ServerView::mimeTypes() const {
QStringList qsl;
qsl << QStringList(QLatin1String("text/uri-list"));
qsl << QStringList(QLatin1String("text/plain"));
return qsl;
}
Qt::DropActions ServerView::supportedDropActions() const {
return Qt::CopyAction | Qt::LinkAction;
}
/* Extract and append (2), (3) etc to the end of a servers name if it is cloned. */
void ServerView::fixupName(ServerItem *si) {
QString name = si->qsName;
int tag = 1;
QRegExp tmatch(QLatin1String("(.+)\\((\\d+)\\)"));
tmatch.setMinimal(true);
if (tmatch.exactMatch(name)) {
name = tmatch.capturedTexts().at(1).trimmed();
tag = tmatch.capturedTexts().at(2).toInt();
}
bool found;
QString cmpname;
do {
found = false;
if (tag > 1)
cmpname = name + QString::fromLatin1(" (%1)").arg(tag);
else
cmpname = name;
foreach (ServerItem *f, siFavorite->qlChildren)
if (f->qsName == cmpname)
found = true;
++tag;
} while (found);
si->qsName = cmpname;
}
bool ServerView::dropMimeData(QTreeWidgetItem *, int, const QMimeData *mime, Qt::DropAction) {
ServerItem *si = ServerItem::fromMimeData(mime);
if (!si)
return false;
fixupName(si);
qobject_cast< ConnectDialog * >(parent())->qlItems << si;
siFavorite->addServerItem(si);
qobject_cast< ConnectDialog * >(parent())->startDns(si);
setCurrentItem(si);
return true;
}
void ServerItem::init() {
// Without this, columncount is wrong.
setData(0, Qt::DisplayRole, QVariant());
setData(1, Qt::DisplayRole, QVariant());
setData(2, Qt::DisplayRole, QVariant());
emitDataChanged();
}
ServerItem::ServerItem(const FavoriteServer &fs) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
siParent = nullptr;
bParent = false;
itType = FavoriteType;
qsName = fs.qsName;
usPort = fs.usPort;
qsUsername = fs.qsUsername;
qsPassword = fs.qsPassword;
qsUrl = fs.qsUrl;
bCA = false;
#ifdef USE_ZEROCONF
if (fs.qsHostname.startsWith(QLatin1Char('@'))) {
zeroconfHost = fs.qsHostname.mid(1);
zeroconfRecord = BonjourRecord(zeroconfHost, QLatin1String("_mumble._tcp."), QLatin1String("local."));
} else {
qsHostname = fs.qsHostname;
}
#else
qsHostname = fs.qsHostname;
#endif
init();
}
ServerItem::ServerItem(const PublicInfo &pi) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
siParent = nullptr;
bParent = false;
itType = PublicType;
qsName = pi.qsName;
qsHostname = pi.qsIp;
usPort = pi.usPort;
qsUrl = pi.quUrl.toString();
qsCountry = pi.qsCountry;
qsCountryCode = pi.qsCountryCode;
qsContinentCode = pi.qsContinentCode;
bCA = pi.bCA;
init();
}
ServerItem::ServerItem(const QString &name, const QString &host, unsigned short port, const QString &username,
const QString &password)
: QTreeWidgetItem(QTreeWidgetItem::UserType) {
siParent = nullptr;
bParent = false;
itType = FavoriteType;
qsName = name;
usPort = port;
qsUsername = username;
qsPassword = password;
bCA = false;
#ifdef USE_ZEROCONF
if (host.startsWith(QLatin1Char('@'))) {
zeroconfHost = host.mid(1);
zeroconfRecord = BonjourRecord(zeroconfHost, QLatin1String("_mumble._tcp."), QLatin1String("local."));
} else {
qsHostname = host;
}
#else
qsHostname = host;
#endif
init();
}
#ifdef USE_ZEROCONF
ServerItem::ServerItem(const BonjourRecord &br) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
siParent = nullptr;
bParent = false;
itType = LANType;
qsName = br.serviceName;
zeroconfHost = qsName;
zeroconfRecord = br;
usPort = 0;
bCA = false;
init();
}
#endif
ServerItem::ServerItem(const QString &name, ItemType itype) {
siParent = nullptr;
bParent = true;
qsName = name;
itType = itype;
setFlags(flags() & ~Qt::ItemIsDragEnabled);
bCA = false;
init();
}
ServerItem::ServerItem(const ServerItem *si) {
siParent = nullptr;
bParent = false;
itType = FavoriteType;
qsName = si->qsName;
qsHostname = si->qsHostname;
usPort = si->usPort;
qsUsername = si->qsUsername;
qsPassword = si->qsPassword;
qsCountry = si->qsCountry;
qsCountryCode = si->qsCountryCode;
qsContinentCode = si->qsContinentCode;
qsUrl = si->qsUrl;
#ifdef USE_ZEROCONF
zeroconfHost = si->zeroconfHost;
zeroconfRecord = si->zeroconfRecord;
#endif
qlAddresses = si->qlAddresses;
bCA = si->bCA;
m_version = si->m_version;
uiPing = si->uiPing;
uiPingSort = si->uiPing;
uiUsers = si->uiUsers;
uiMaxUsers = si->uiMaxUsers;
uiBandwidth = si->uiBandwidth;
uiSent = si->uiSent;
dPing = si->dPing;
*asQuantile = *si->asQuantile;
}
ServerItem::~ServerItem() {
if (siParent) {
siParent->qlChildren.removeAll(this);
if (siParent->bParent && siParent->qlChildren.isEmpty())
siParent->setHidden(true);
}
// This is just for cleanup when exiting the dialog, it won't stop pending DNS for the children.
foreach (ServerItem *si, qlChildren)
delete si;
}
ServerItem *ServerItem::fromMimeData(const QMimeData *mime, bool default_name, QWidget *p, bool convertHttpUrls) {
if (mime->hasFormat(QLatin1String("OriginatedInMumble")))
return nullptr;
QUrl url;
if (mime->hasUrls() && !mime->urls().isEmpty())
url = mime->urls().at(0);
else if (mime->hasText())
url = QUrl::fromEncoded(mime->text().toUtf8());
QString qsFile = url.toLocalFile();
if (!qsFile.isEmpty()) {
QFile f(qsFile);
// Make sure we don't accidentally read something big the user
// happened to have in his clipboard. We only want to look
// at small link files.
if (f.open(QIODevice::ReadOnly) && f.size() < 10240) {
QByteArray qba = f.readAll();
f.close();
url = QUrl::fromEncoded(qba, QUrl::StrictMode);
if (!url.isValid()) {
// Windows internet shortcut files (.url) are an ini with an URL value
QSettings qs(qsFile, QSettings::IniFormat);
url =
QUrl::fromEncoded(qs.value(QLatin1String("InternetShortcut/URL")).toByteArray(), QUrl::StrictMode);
}
}
}
if (default_name) {
QUrlQuery query(url);
if (!query.hasQueryItem(QLatin1String("title"))) {
query.addQueryItem(QLatin1String("title"), url.host());
}
}
if (!url.isValid()) {
return nullptr;
}
// An URL from text without a scheme will have the hostname text
// in the QUrl scheme and no hostname. We do not want to use that.
if (url.host().isEmpty()) {
return nullptr;
}
// Some communication programs automatically create http links from domains.
// When a user sends another user a domain to connect to, and http is added wrongly,
// we do our best to remove it again.
if (convertHttpUrls && (url.scheme() == QLatin1String("http") || url.scheme() == QLatin1String("https"))) {
url.setScheme(QLatin1String("mumble"));
}
return fromUrl(url, p);
}
ServerItem *ServerItem::fromUrl(QUrl url, QWidget *p) {
if (!url.isValid() || (url.scheme() != QLatin1String("mumble"))) {
return nullptr;
}
QUrlQuery query(url);
if (url.userName().isEmpty()) {
if (Global::get().s.qsUsername.isEmpty()) {
bool ok;
QString defUserName = QInputDialog::getText(p, ConnectDialog::tr("Adding host %1").arg(url.host()),
ConnectDialog::tr("Enter username"), QLineEdit::Normal,
Global::get().s.qsUsername, &ok)
.trimmed();
if (!ok)
return nullptr;
if (defUserName.isEmpty())
return nullptr;
Global::get().s.qsUsername = defUserName;
}
url.setUserName(Global::get().s.qsUsername);
}
ServerItem *si =
new ServerItem(query.queryItemValue(QLatin1String("title")), url.host(),
static_cast< unsigned short >(url.port(DEFAULT_MUMBLE_PORT)), url.userName(), url.password());
if (query.hasQueryItem(QLatin1String("url")))
si->qsUrl = query.queryItemValue(QLatin1String("url"));
return si;
}
QVariant ServerItem::data(int column, int role) const {
if (bParent) {
if (column == 0) {
switch (role) {
case Qt::DisplayRole:
return qsName;
case Qt::DecorationRole:
if (itType == FavoriteType)
return loadIcon(QLatin1String("skin:emblems/emblem-favorite.svg"));
else if (itType == LANType)
return loadIcon(QLatin1String("skin:places/network-workgroup.svg"));
else
return loadIcon(QLatin1String("skin:categories/applications-internet.svg"));
}
}
} else {
if (role == Qt::DecorationRole && column == 0) {
QString flag;
if (!qsCountryCode.isEmpty()) {
flag = QString::fromLatin1(":/flags/%1.svg").arg(qsCountryCode);
if (!QFileInfo(flag).exists()) {
flag = QLatin1String("skin:categories/applications-internet.svg");
}
} else {
flag = QLatin1String("skin:categories/applications-internet.svg");
}
return loadIcon(flag);
}
if (role == Qt::DisplayRole) {
switch (column) {
case 0:
return qsName;
case 1:
return (dPing > 0.0) ? QString::number(uiPing) : QVariant();
case 2:
return uiUsers ? QString::fromLatin1("%1/%2 ").arg(uiUsers).arg(uiMaxUsers) : QVariant();
}
} else if (role == Qt::ToolTipRole) {
QStringList ipv4List;
QStringList ipv6List;
foreach (const ServerAddress &addr, qlAddresses) {
const QString address = addr.host.toString(false).toHtmlEscaped();
if (addr.host.isV6()) {
ipv6List << address;
} else {
ipv4List << address;
}
}
QString ipv4 = "-";
QString ipv6 = "-";
if (!ipv4List.isEmpty()) {
ipv4 = ipv4List.join(QLatin1String(", "));
}
if (!ipv6List.isEmpty()) {
ipv6 = ipv6List.join(QLatin1String(", "));
}
double ploss = 100.0;
if (uiSent > 0)
ploss = (uiSent - std::min(uiRecv, uiSent)) * 100. / uiSent;
QString qs;
qs += QLatin1String("<table>")
+ QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Servername"), qsName.toHtmlEscaped())
+ QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Hostname"), qsHostname.toHtmlEscaped());
#ifdef USE_ZEROCONF
if (!zeroconfHost.isEmpty())
qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Bonjour name"), zeroconfHost.toHtmlEscaped());
#endif
qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Port"))
.arg(usPort)
+ QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("IPv4 address"), ipv4)
+ QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("IPv6 address"), ipv6);
if (!qsUrl.isEmpty())
qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Website"), qsUrl.toHtmlEscaped());
if (uiSent > 0) {
qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Packet loss"), QString::fromLatin1("%1% (%2/%3)")
.arg(ploss, 0, 'f', 1)
.arg(uiSent - std::min(uiRecv, uiSent))
.arg(uiSent));
if (uiRecv > 0) {
qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Ping (80%)"),
ConnectDialog::tr("%1 ms").arg(
boost::accumulators::extended_p_square(*asQuantile)[1] / 1000., 0, 'f', 2))
+ QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Ping (95%)"),
ConnectDialog::tr("%1 ms").arg(
boost::accumulators::extended_p_square(*asQuantile)[2] / 1000., 0, 'f', 2))
+ QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Bandwidth"),
ConnectDialog::tr("%1 kbit/s").arg(uiBandwidth / 1000))
+ QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Users"),
QString::fromLatin1("%1/%2").arg(uiUsers).arg(uiMaxUsers))
+ QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
.arg(ConnectDialog::tr("Version"))
.arg(Version::toString(m_version));
}
}
qs += QLatin1String("</table>");
return qs;
} else if (role == Qt::BackgroundRole) {
if (bCA) {
QColor qc(Qt::green);
qc.setAlpha(32);
return qc;
}
} else if (role == Qt::AccessibleTextRole) {
return QString("%1 %2").arg(ConnectDialog::tr("Server")).arg(qsName);
}
}
return QTreeWidgetItem::data(column, role);
}
void ServerItem::addServerItem(ServerItem *childitem) {
Q_ASSERT(!childitem->siParent);
childitem->siParent = this;
qlChildren.append(childitem);
addChild(childitem);
// Public servers must initially be hidden for the search to work properly
// They will be set to visible later on
if (childitem->itType == PublicType) {
childitem->setHidden(true);
}
if (bParent && (itType != PublicType) && isHidden()) {
setHidden(false);
}
}
void ServerItem::setDatas(double elapsed, quint32 users, quint32 maxusers) {
if (elapsed == 0.0) {
emitDataChanged();
return;
}
(*asQuantile)(static_cast< double >(elapsed));
dPing = boost::accumulators::extended_p_square(*asQuantile)[0];
if (dPing == 0.0)
dPing = elapsed;
quint32 ping = static_cast< quint32 >(lround(dPing / 1000.));
uiRecv = static_cast< quint32 >(boost::accumulators::count(*asQuantile));
bool changed = (ping != uiPing) || (users != uiUsers) || (maxusers != uiMaxUsers);
uiUsers = users;
uiMaxUsers = maxusers;
uiPing = ping;
double grace = qMax(5000., 50. * uiPingSort);
double diff = fabs(1000. * uiPingSort - dPing);
if ((uiPingSort == 0) || ((uiSent >= 10) && (diff >= grace)))
uiPingSort = ping;
if (changed)
emitDataChanged();
}
FavoriteServer ServerItem::toFavoriteServer() const {
FavoriteServer fs;
fs.qsName = qsName;
#ifdef USE_ZEROCONF
if (!zeroconfHost.isEmpty())
fs.qsHostname = QLatin1Char('@') + zeroconfHost;
else
fs.qsHostname = qsHostname;
#else
fs.qsHostname = qsHostname;
#endif
fs.usPort = usPort;
fs.qsUsername = qsUsername;
fs.qsPassword = qsPassword;
fs.qsUrl = qsUrl;
return fs;
}
/**
* This function turns a ServerItem object into a QMimeData object holding a URL to the server.
*/
QMimeData *ServerItem::toMimeData() const {
QMimeData *mime = ServerItem::toMimeData(qsName, qsHostname, usPort);
if (itType == FavoriteType)
mime->setData(QLatin1String("OriginatedInMumble"), QByteArray());
return mime;
}
/**
* This function creates a QMimeData object containing a URL to the server at host and port. name is passed in the
* query string as "title", which is used for adding a server to favorites. channel may be omitted, but if specified it
* should be in the format of "/path/to/channel".
*/
QMimeData *ServerItem::toMimeData(const QString &name, const QString &host, unsigned short port,
const QString &channel) {
QUrl url;
url.setScheme(QLatin1String("mumble"));
url.setHost(host);
if (port != DEFAULT_MUMBLE_PORT)
url.setPort(port);
url.setPath(channel);
QUrlQuery query;
query.addQueryItem(QLatin1String("title"), name);
query.addQueryItem(QLatin1String("version"), QLatin1String("1.2.0"));
url.setQuery(query);
QString qs = QLatin1String(url.toEncoded());
QMimeData *mime = new QMimeData;
#ifdef Q_OS_WIN
QString contents = QString::fromLatin1("[InternetShortcut]\r\nURL=%1\r\n").arg(qs);
QString urlname = QString::fromLatin1("%1.url").arg(name);
FILEGROUPDESCRIPTORA fgda;
ZeroMemory(&fgda, sizeof(fgda));
fgda.cItems = 1;
fgda.fgd[0].dwFlags = FD_LINKUI | FD_FILESIZE;
fgda.fgd[0].nFileSizeLow = contents.length();
strcpy_s(fgda.fgd[0].cFileName, MAX_PATH, urlname.toLocal8Bit().constData());
mime->setData(QLatin1String("FileGroupDescriptor"),
QByteArray(reinterpret_cast< const char * >(&fgda), sizeof(fgda)));
FILEGROUPDESCRIPTORW fgdw;
ZeroMemory(&fgdw, sizeof(fgdw));
fgdw.cItems = 1;
fgdw.fgd[0].dwFlags = FD_LINKUI | FD_FILESIZE;
fgdw.fgd[0].nFileSizeLow = contents.length();
wcscpy_s(fgdw.fgd[0].cFileName, MAX_PATH, urlname.toStdWString().c_str());
mime->setData(QLatin1String("FileGroupDescriptorW"),
QByteArray(reinterpret_cast< const char * >(&fgdw), sizeof(fgdw)));
mime->setData(QString::fromWCharArray(CFSTR_FILECONTENTS), contents.toLocal8Bit());
DWORD context[4];
context[0] = 0;
context[1] = 1;
context[2] = 0;
context[3] = 0;
mime->setData(QLatin1String("DragContext"),
QByteArray(reinterpret_cast< const char * >(&context[0]), sizeof(context)));
DWORD dropaction;
dropaction = DROPEFFECT_LINK;
mime->setData(QString::fromWCharArray(CFSTR_PREFERREDDROPEFFECT),
QByteArray(reinterpret_cast< const char * >(&dropaction), sizeof(dropaction)));
#endif
QList< QUrl > urls;
urls << url;
mime->setUrls(urls);
mime->setText(qs);
mime->setHtml(QString::fromLatin1("<a href=\"%1\">%2</a>").arg(qs).arg(name.toHtmlEscaped()));
return mime;
}
bool ServerItem::operator<(const QTreeWidgetItem &o) const {
const ServerItem &other = static_cast< const ServerItem & >(o);
const QTreeWidget *w = treeWidget();
const int column = w ? w->sortColumn() : 0;
if (itType != other.itType) {
const bool inverse = w ? (w->header()->sortIndicatorOrder() == Qt::DescendingOrder) : false;
bool less;
if (itType == FavoriteType)
less = true;
else if ((itType == LANType) && (other.itType == PublicType))
less = true;
else
less = false;
return less ^ inverse;
}
if (bParent) {
const bool inverse = w ? (w->header()->sortIndicatorOrder() == Qt::DescendingOrder) : false;
return (qsName < other.qsName) ^ inverse;
}
if (column == 0) {
QString a = qsName.toLower();
QString b = other.qsName.toLower();
QRegExp re(QLatin1String("[^0-9a-z]"));
a.remove(re);
b.remove(re);
return a < b;
} else if (column == 1) {
quint32 a = uiPingSort ? uiPingSort : UINT_MAX;
quint32 b = other.uiPingSort ? other.uiPingSort : UINT_MAX;
return a < b;
} else if (column == 2) {
return uiUsers < other.uiUsers;
}
return false;
}
QIcon ServerItem::loadIcon(const QString &name) {
if (!qmIcons.contains(name))
qmIcons.insert(name, QIcon(name));
return qmIcons.value(name);
}
ConnectDialogEdit::ConnectDialogEdit(QWidget *p, const QString &name, const QString &host, const QString &user,
unsigned short port, const QString &password)
: QDialog(p) {
setupUi(this);
init();
bCustomLabel = !name.simplified().isEmpty();
qleName->setText(name);
qleServer->setText(host);
qleUsername->setText(user);
qlePort->setText(QString::number(port));
qlePassword->setText(password);
validate();
}
ConnectDialogEdit::ConnectDialogEdit(QWidget *parent) : QDialog(parent) {
setupUi(this);
setWindowTitle(tr("Add Server"));
init();
if (!updateFromClipboard()) {
// If connected to a server assume the user wants to add it
if (Global::get().sh && Global::get().sh->isRunning()) {
QString host, name, user, pw;
unsigned short port = DEFAULT_MUMBLE_PORT;
Global::get().sh->getConnectionInfo(host, port, user, pw);
Channel *c = Channel::get(Channel::ROOT_ID);
if (c && c->qsName != QLatin1String("Root")) {
name = c->qsName;
}
showNotice(tr("You are currently connected to a server.\nDo you want to fill the dialog with the "
"connection data of this server?\nHost: %1 Port: %2")
.arg(host)
.arg(port));
m_si = new ServerItem(name, host, port, user, pw);
}
}
qleUsername->setText(Global::get().s.qsUsername);
}
void ConnectDialogEdit::init() {
m_si = nullptr;
usPort = 0;
bOk = true;
bCustomLabel = false;
qwInlineNotice->hide();
qlePort->setValidator(new QIntValidator(1, 65535, qlePort));
qlePort->setText(QString::number(DEFAULT_MUMBLE_PORT));
qlePassword->setEchoMode(QLineEdit::Password);
connect(qleName, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
connect(qleServer, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
connect(qlePort, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
connect(qleUsername, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
connect(qlePassword, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
validate();
}
ConnectDialogEdit::~ConnectDialogEdit() {
delete m_si;
}
void ConnectDialogEdit::showNotice(const QString &text) {
QLabel *label = qwInlineNotice->findChild< QLabel * >(QLatin1String("qlPasteNotice"));
Q_ASSERT(label);
label->setText(text);
qwInlineNotice->show();
adjustSize();
}
bool ConnectDialogEdit::updateFromClipboard() {
delete m_si;
m_si = ServerItem::fromMimeData(QApplication::clipboard()->mimeData(), false, nullptr, true);
if (m_si) {
showNotice(
tr("You have an URL in your clipboard.\nDo you want to fill the dialog with this data?\nHost: %1 Port: %2")
.arg(m_si->qsHostname)
.arg(m_si->usPort));
return true;
} else {
qwInlineNotice->hide();
adjustSize();
return false;
}
}
void ConnectDialogEdit::on_qbFill_clicked() {
Q_ASSERT(m_si);
qwInlineNotice->hide();
adjustSize();
qleName->setText(m_si->qsName);
qleServer->setText(m_si->qsHostname);
qleUsername->setText(m_si->qsUsername);
qlePort->setText(QString::number(m_si->usPort));
qlePassword->setText(m_si->qsPassword);
delete m_si;
m_si = nullptr;
}
void ConnectDialogEdit::on_qbDiscard_clicked() {
qwInlineNotice->hide();
adjustSize();
}
void ConnectDialogEdit::on_qleName_textEdited(const QString &name) {
if (bCustomLabel) {
// If empty, then reset to automatic label.
// NOTE(nik@jnstw.us): You may be tempted to set qleName to qleServer, but that results in the odd
// UI behavior that clearing the field doesn't clear it; it'll immediately equal qleServer. Instead,
// leave it empty and let it update the next time qleServer updates. Code in accept will default it
// to qleServer if it isn't updated beforehand.
if (name.simplified().isEmpty()) {
bCustomLabel = false;
}
} else {
// If manually edited, set to Custom
bCustomLabel = true;
}
}
void ConnectDialogEdit::on_qleServer_textEdited(const QString &server) {
// If using automatic label, update it
if (!bCustomLabel) {
qleName->setText(server);
}
}
void ConnectDialogEdit::validate() {
qsName = qleName->text().simplified();
qsHostname = qleServer->text().simplified();
usPort = qlePort->text().toUShort();
qsUsername = qleUsername->text().simplified();
qsPassword = qlePassword->text();
// For bonjour hosts disable the port field as it's auto-detected
qlePort->setDisabled(!qsHostname.isEmpty() && qsHostname.startsWith(QLatin1Char('@')));
// For SuperUser show password edit
if (qsUsername.toLower() == QLatin1String("superuser")) {
qliPassword->setVisible(true);
qlePassword->setVisible(true);
qcbShowPassword->setVisible(true);
adjustSize();
} else if (qsPassword.isEmpty()) {
qliPassword->setVisible(false);
qlePassword->setVisible(false);
qcbShowPassword->setVisible(false);
adjustSize();
}
bOk = !qsHostname.isEmpty() && !qsUsername.isEmpty() && usPort;
qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(bOk);
}
void ConnectDialogEdit::accept() {
validate();
if (bOk) {
QString server = qleServer->text().simplified();
// If the user accidentally added a schema or path part, drop it now.
// We can't do so during editing as that is quite jarring.
const int schemaPos = server.indexOf(QLatin1String("://"));
if (schemaPos != -1) {
server.remove(0, schemaPos + 3);
}
const int pathPos = server.indexOf(QLatin1Char('/'));
if (pathPos != -1) {
server.resize(pathPos);
}
qleServer->setText(server);
if (qleName->text().simplified().isEmpty() || !bCustomLabel) {
qleName->setText(server);
}
QDialog::accept();
}
}
void ConnectDialogEdit::on_qcbShowPassword_toggled(bool checked) {
qlePassword->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
}
ConnectDialog::ConnectDialog(QWidget *p, bool autoconnect) : QDialog(p), bAutoConnect(autoconnect) {
setupUi(this);
#ifdef Q_OS_MAC
setWindowModality(Qt::WindowModal);
#endif
bPublicInit = false;
siAutoConnect = nullptr;
bAllowPing = Global::get().s.ptProxyType == Settings::NoProxy;
bAllowHostLookup = Global::get().s.ptProxyType == Settings::NoProxy;
bAllowZeroconf = Global::get().s.ptProxyType == Settings::NoProxy;
bAllowFilters = Global::get().s.ptProxyType == Settings::NoProxy;
if (tPublicServers.elapsed() >= 60 * 24 * 1000000ULL) {
qlPublicServers.clear();
}
qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
qdbbButtonBox->button(QDialogButtonBox::Ok)->setText(tr("C&onnect"));
qdbbButtonBox->button(QDialogButtonBox::Ok)->setFocusPolicy(Qt::TabFocus);
qdbbButtonBox->button(QDialogButtonBox::Cancel)->setFocusPolicy(Qt::TabFocus);
qdbbButtonBox->button(QDialogButtonBox::Cancel)->setAutoDefault(false);
QPushButton *qpbAdd = new QPushButton(tr("&Add New..."), this);
qpbAdd->setDefault(false);
qpbAdd->setAutoDefault(false);
connect(qpbAdd, SIGNAL(clicked()), qaFavoriteAddNew, SIGNAL(triggered()));
qdbbButtonBox->addButton(qpbAdd, QDialogButtonBox::ActionRole);
qpbEdit = new QPushButton(tr("&Edit..."), this);
qpbEdit->setEnabled(false);
qpbEdit->setDefault(false);
qpbEdit->setAutoDefault(false);
connect(qpbEdit, SIGNAL(clicked()), qaFavoriteEdit, SIGNAL(triggered()));
qdbbButtonBox->addButton(qpbEdit, QDialogButtonBox::ActionRole);
qpbAdd->setHidden(Global::get().s.disableConnectDialogEditing);
qpbEdit->setHidden(Global::get().s.disableConnectDialogEditing);
qtwServers->setItemDelegate(new ServerViewDelegate());
if (!Global::get().s.bDisablePublicList) {
const QIcon qiFlag = ServerItem::loadIcon(QLatin1String("skin:categories/applications-internet.svg"));
// Add continents and 'Unknown' to the location combobox
qcbSearchLocation->addItem(qiFlag, tr("All"), QLatin1String("all"));
qcbSearchLocation->addItem(qiFlag, tr("Africa"), QLatin1String("af"));
qcbSearchLocation->addItem(qiFlag, tr("Asia"), QLatin1String("as"));
qcbSearchLocation->addItem(qiFlag, tr("Europe"), QLatin1String("eu"));
qcbSearchLocation->addItem(qiFlag, tr("North America"), QLatin1String("na"));
qcbSearchLocation->addItem(qiFlag, tr("Oceania"), QLatin1String("oc"));
qcbSearchLocation->addItem(qiFlag, tr("South America"), QLatin1String("sa"));
qcbSearchLocation->addItem(qiFlag, tr("Unknown"), QLatin1String(""));
addCountriesToSearchLocation();
}
qgbSearch->setVisible(false);
// Hide ping and user count if we are not allowed to ping.
if (!bAllowPing) {
qtwServers->setColumnCount(1);
}
qtwServers->sortItems(1, Qt::AscendingOrder);
qtwServers->header()->setSectionResizeMode(0, QHeaderView::Stretch);
if (qtwServers->columnCount() >= 2) {
qtwServers->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
}
if (qtwServers->columnCount() >= 3) {
qtwServers->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
}
connect(qtwServers->header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), this,
SLOT(OnSortChanged(int, Qt::SortOrder)));
if (bAllowFilters) {
switch (Global::get().s.ssFilter) {
case Settings::ShowPopulated:
qcbFilter->setCurrentText(tr("Show Populated"));
break;
case Settings::ShowAll:
qcbFilter->setCurrentText(tr("Show All"));
break;
default:
qcbFilter->setCurrentText(tr("Show Reachable"));
break;
}
} else {
qcbFilter->setEnabled(false);
}
qmPopup = new QMenu(this);
QList< QTreeWidgetItem * > ql;
QList< FavoriteServer > favorites = Global::get().db->getFavorites();
foreach (const FavoriteServer &fs, favorites) {
ServerItem *si = new ServerItem(fs);
qlItems << si;
startDns(si);
qtwServers->siFavorite->addServerItem(si);
}
#ifdef USE_ZEROCONF
if (bAllowZeroconf && Global::get().zeroconf && Global::get().zeroconf->isOk()) {
connect(Global::get().zeroconf, &Zeroconf::recordsChanged, this, &ConnectDialog::onUpdateLanList);
connect(Global::get().zeroconf, &Zeroconf::recordResolved, this, &ConnectDialog::onResolved);
connect(Global::get().zeroconf, &Zeroconf::resolveError, this, &ConnectDialog::onLanResolveError);
onUpdateLanList(Global::get().zeroconf->currentRecords());
Global::get().zeroconf->startBrowser(QLatin1String("_mumble._tcp"));
}
#endif
qtPingTick = new QTimer(this);
connect(qtPingTick, SIGNAL(timeout()), this, SLOT(timeTick()));
qusSocket4 = new QUdpSocket(this);
qusSocket6 = new QUdpSocket(this);
bIPv4 = qusSocket4->bind(QHostAddress(QHostAddress::Any), 0);
bIPv6 = qusSocket6->bind(QHostAddress(QHostAddress::AnyIPv6), 0);
connect(qusSocket4, SIGNAL(readyRead()), this, SLOT(udpReply()));
connect(qusSocket6, SIGNAL(readyRead()), this, SLOT(udpReply()));
if (qtwServers->siFavorite->isHidden() && (!qtwServers->siLAN || qtwServers->siLAN->isHidden())
&& qtwServers->siPublic) {
qtwServers->siPublic->setExpanded(true);
}
iPingIndex = -1;
qtPingTick->start(50);
new QShortcut(QKeySequence(QKeySequence::Copy), this, SLOT(on_qaFavoriteCopy_triggered()));
new QShortcut(QKeySequence(QKeySequence::Paste), this, SLOT(on_qaFavoritePaste_triggered()));
qtwServers->setCurrentItem(nullptr);
bLastFound = false;
qmPingCache = Global::get().db->getPingCache();
if (!Global::get().s.qbaConnectDialogGeometry.isEmpty())
restoreGeometry(Global::get().s.qbaConnectDialogGeometry);
if (!Global::get().s.qbaConnectDialogHeader.isEmpty())
qtwServers->header()->restoreState(Global::get().s.qbaConnectDialogHeader);
setTabOrder(qtwServers, qleSearchServername);
setTabOrder(qleSearchServername, qcbSearchLocation);
setTabOrder(qcbSearchLocation, qcbFilter);
setTabOrder(qcbFilter, qpbAdd);
setTabOrder(qpbAdd, qpbEdit);
setTabOrder(qpbEdit, qdbbButtonBox->button(QDialogButtonBox::Ok));
setTabOrder(qdbbButtonBox->button(QDialogButtonBox::Ok), qtwServers);
}
ConnectDialog::~ConnectDialog() {
#ifdef USE_ZEROCONF
if (bAllowZeroconf && Global::get().zeroconf && Global::get().zeroconf->isOk()) {
Global::get().zeroconf->stopBrowser();
Global::get().zeroconf->cleanupResolvers();
}
#endif
ServerItem::qmIcons.clear();
QList< FavoriteServer > ql;
qmPingCache.clear();
foreach (ServerItem *si, qlItems) {
if (si->uiPing)
qmPingCache.insert(UnresolvedServerAddress(si->qsHostname, si->usPort), si->uiPing);
if (si->itType != ServerItem::FavoriteType)
continue;
ql << si->toFavoriteServer();
}
Global::get().db->setFavorites(ql);
Global::get().db->setPingCache(qmPingCache);
Global::get().s.qbaConnectDialogHeader = qtwServers->header()->saveState();
Global::get().s.qbaConnectDialogGeometry = saveGeometry();
}
void ConnectDialog::accept() {
ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
if (!si || (bAllowHostLookup && si->qlAddresses.isEmpty()) || si->qsHostname.isEmpty()) {
qWarning() << "Invalid server";
return;
}
qsPassword = si->qsPassword;
qsServer = si->qsHostname;
usPort = si->usPort;
if (si->qsUsername.isEmpty()) {
bool ok;
QString defUserName = QInputDialog::getText(this, tr("Connecting to %1").arg(si->qsName), tr("Enter username"),
QLineEdit::Normal, Global::get().s.qsUsername, &ok)
.trimmed();
if (!ok)
return;
Global::get().s.qsUsername = si->qsUsername = defUserName;
}
qsUsername = si->qsUsername;
Global::get().s.qsLastServer = si->qsName;
QDialog::accept();
}
void ConnectDialog::OnSortChanged(int logicalIndex, Qt::SortOrder) {
if (logicalIndex != 2) {
return;
}
foreach (ServerItem *si, qlItems) {
if (si->uiPing && (si->uiPing != si->uiPingSort)) {
si->uiPingSort = si->uiPing;
si->setDatas();
}
}
}
void ConnectDialog::on_qaFavoriteAdd_triggered() {
ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
if (!si || (si->itType == ServerItem::FavoriteType))
return;
si = new ServerItem(si);
qtwServers->fixupName(si);
qlItems << si;
qtwServers->siFavorite->addServerItem(si);
qtwServers->setCurrentItem(si);
startDns(si);
}
void ConnectDialog::on_qaFavoriteAddNew_triggered() {
ConnectDialogEdit *cde = new ConnectDialogEdit(this);
if (cde->exec() == QDialog::Accepted) {
ServerItem *si = new ServerItem(cde->qsName, cde->qsHostname, cde->usPort, cde->qsUsername, cde->qsPassword);
qlItems << si;
qtwServers->siFavorite->addServerItem(si);
qtwServers->setCurrentItem(si);
startDns(si);
qdbbButtonBox->button(QDialogButtonBox::Ok)->setFocus();
}
delete cde;
}
void ConnectDialog::on_qaFavoriteEdit_triggered() {
ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
if (!si || (si->itType != ServerItem::FavoriteType))
return;
QString host;
#ifdef USE_ZEROCONF
if (!si->zeroconfHost.isEmpty())
host = QLatin1Char('@') + si->zeroconfHost;
else
host = si->qsHostname;
#else
host = si->qsHostname;
#endif
ConnectDialogEdit *cde = new ConnectDialogEdit(this, si->qsName, host, si->qsUsername, si->usPort, si->qsPassword);
if (cde->exec() == QDialog::Accepted) {
si->qsName = cde->qsName;
si->qsUsername = cde->qsUsername;
si->qsPassword = cde->qsPassword;
if ((cde->qsHostname != host) || (cde->usPort != si->usPort)) {
stopDns(si);
si->qlAddresses.clear();
si->reset();
si->usPort = cde->usPort;
#ifdef USE_ZEROCONF
if (cde->qsHostname.startsWith(QLatin1Char('@'))) {
si->qsHostname = QString();
si->zeroconfHost = cde->qsHostname.mid(1);
si->zeroconfRecord =
BonjourRecord(si->zeroconfHost, QLatin1String("_mumble._tcp."), QLatin1String("local."));
} else {
si->qsHostname = cde->qsHostname;
si->zeroconfHost = QString();
si->zeroconfRecord = BonjourRecord();
}
#else
si->qsHostname = cde->qsHostname;
#endif
startDns(si);
}
si->setDatas();
}
delete cde;
}
void ConnectDialog::on_qaFavoriteRemove_triggered() {
ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
if (!si || (si->itType != ServerItem::FavoriteType))
return;
stopDns(si);
qlItems.removeAll(si);
delete si;
}
void ConnectDialog::on_qaFavoriteCopy_triggered() {
ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
if (!si)
return;
QApplication::clipboard()->setMimeData(si->toMimeData());
}
void ConnectDialog::on_qaFavoritePaste_triggered() {
ServerItem *si = ServerItem::fromMimeData(QApplication::clipboard()->mimeData());
if (!si)
return;
qlItems << si;
qtwServers->siFavorite->addServerItem(si);
qtwServers->setCurrentItem(si);
startDns(si);
}
void ConnectDialog::on_qaUrl_triggered() {
auto *si = static_cast< const ServerItem * >(qtwServers->currentItem());
if (!si || si->qsUrl.isEmpty()) {
return;
}
const QStringList allowedSchemes = { QLatin1String("http"), QLatin1String("https") };
const auto url = QUrl(si->qsUrl);
if (allowedSchemes.contains(url.scheme())) {
QDesktopServices::openUrl(url);
} else {
// Inform user that the requested URL has been blocked
QMessageBox msgBox;
msgBox.setText(QObject::tr("<b>Blocked URL scheme \"%1\"</b>").arg(url.scheme()));
msgBox.setInformativeText(QObject::tr("The URL uses a scheme that has been blocked for security reasons."));
msgBox.setDetailedText(QObject::tr("Blocked URL: \"%1\"").arg(url.toString()));
msgBox.setIcon(QMessageBox::Warning);
msgBox.exec();
}
}
void ConnectDialog::on_qtwServers_customContextMenuRequested(const QPoint &mpos) {
ServerItem *si = static_cast< ServerItem * >(qtwServers->itemAt(mpos));
qmPopup->clear();
if (si && si->bParent) {
si = nullptr;
}
if (si) {
if (!Global::get().s.disableConnectDialogEditing) {
if (si->itType == ServerItem::FavoriteType) {
qmPopup->addAction(qaFavoriteEdit);
qmPopup->addAction(qaFavoriteRemove);
} else {
qmPopup->addAction(qaFavoriteAdd);
}
}
if (!si->qsUrl.isEmpty()) {
qmPopup->addAction(qaUrl);
}
}
qmPopup->popup(qtwServers->viewport()->mapToGlobal(mpos), nullptr);
}
void ConnectDialog::on_qtwServers_itemDoubleClicked(QTreeWidgetItem *item, int) {
qtwServers->setCurrentItem(item);
accept();
}
void ConnectDialog::on_qtwServers_currentItemChanged(QTreeWidgetItem *item, QTreeWidgetItem *) {
ServerItem *si = static_cast< ServerItem * >(item);
if (si->siParent == qtwServers->siFavorite) {
qpbEdit->setEnabled(true);
} else {
qpbEdit->setEnabled(false);
}
bool bOk = !si->qlAddresses.isEmpty();
if (!bAllowHostLookup) {
bOk = true;
}
qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(bOk);
bLastFound = true;
}
void ConnectDialog::on_qtwServers_itemExpanded(QTreeWidgetItem *item) {
if (qtwServers->siPublic && item == qtwServers->siPublic) {
if (!Global::get().s.bPingServersDialogViewed) {
// Ask the user for consent to ping the servers. If the user does
// not give consent, disable the public server list and return.
int result = QMessageBox::question(
this, tr("Consent to the transmission of private data"),
tr("<p>To measure the latency (ping) of public servers and determine the number of active users, "
"your IP address must be transmitted to each public server.</p>"
"<p>Do you consent to the transmission of your IP address? If you answer no, the public server "
"list will be deactivated. However, you can reactivate it at any time in the network settings.</p>"),
QMessageBox::Yes | QMessageBox::No);
Global::get().s.bPingServersDialogViewed = true;
if (result == QMessageBox::No) {
Global::get().s.bDisablePublicList = true;
item->setExpanded(false);
item->setHidden(true);
return;
}
}
qgbSearch->setVisible(true);
initList();
fillList();
}
ServerItem *p = static_cast< ServerItem * >(item);
foreach (ServerItem *si, p->qlChildren) { startDns(si); }
}
void ConnectDialog::on_qtwServers_itemCollapsed(QTreeWidgetItem *item) {
if (qtwServers->siPublic && item == qtwServers->siPublic) {
qgbSearch->setVisible(false);
}
}
void ConnectDialog::initList() {
if (bPublicInit || (qlPublicServers.count() > 0))
return;
bPublicInit = true;
QUrl url;
url.setPath(QLatin1String("/v1/list"));
QUrlQuery query;
query.addQueryItem(QLatin1String("version"), Version::getRelease());
url.setQuery(query);
WebFetch::fetch(QLatin1String("publist"), url, this, SLOT(fetched(QByteArray, QUrl, QMap< QString, QString >)));
}
#ifdef USE_ZEROCONF
void ConnectDialog::onResolved(const BonjourRecord record, const QString host, const uint16_t port) {
qlBonjourActive.removeAll(record);
foreach (ServerItem *si, qlItems) {
if (si->zeroconfRecord == record) {
unsigned short usport = static_cast< unsigned short >(port);
if ((host != si->qsHostname) || (usport != si->usPort)) {
stopDns(si);
si->usPort = static_cast< unsigned short >(port);
si->qsHostname = host;
startDns(si);
}
}
}
}
void ConnectDialog::onUpdateLanList(const QList< BonjourRecord > &list) {
QSet< ServerItem * > items;
# if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QSet< ServerItem * > old =
QSet< ServerItem * >(qtwServers->siLAN->qlChildren.begin(), qtwServers->siLAN->qlChildren.end());
# else
// In Qt 5.14 QList::toSet() has been deprecated as there exists a dedicated constructor of QSet for this now
QSet< ServerItem * > old = qtwServers->siLAN->qlChildren.toSet();
# endif
foreach (const BonjourRecord &record, list) {
bool found = false;
foreach (ServerItem *si, old) {
if (si->zeroconfRecord == record) {
items.insert(si);
found = true;
break;
}
}
if (!found) {
ServerItem *si = new ServerItem(record);
qlItems << si;
Global::get().zeroconf->startResolver(record);
startDns(si);
qtwServers->siLAN->addServerItem(si);
}
}
QSet< ServerItem * > remove = old.subtract(items);
foreach (ServerItem *si, remove) {
stopDns(si);
qlItems.removeAll(si);
delete si;
}
}
void ConnectDialog::onLanResolveError(const BonjourRecord record) {
qlBonjourActive.removeAll(record);
}
#endif
void ConnectDialog::fillList() {
QList< QTreeWidgetItem * > ql;
QList< QTreeWidgetItem * > qlNew;
foreach (const PublicInfo &pi, qlPublicServers) {
bool found = false;
foreach (ServerItem *si, qlItems) {
if ((pi.qsIp == si->qsHostname) && (pi.usPort == si->usPort)) {
si->qsCountry = pi.qsCountry;
si->qsCountryCode = pi.qsCountryCode;
si->qsContinentCode = pi.qsContinentCode;
si->qsUrl = pi.quUrl.toString();
si->bCA = pi.bCA;
si->setDatas();
if (si->itType == ServerItem::PublicType)
found = true;
}
}
if (!found)
ql << new ServerItem(pi);
}
while (!ql.isEmpty()) {
ServerItem *si = static_cast< ServerItem * >(ql.takeAt(QRandomGenerator::global()->bounded(0, ql.count())));
qlNew << si;
qlItems << si;
}
foreach (QTreeWidgetItem *qtwi, qlNew) {
ServerItem *si = static_cast< ServerItem * >(qtwi);
qtwServers->siPublic->addServerItem(si);
filterServer(si);
startDns(si);
}
}
void ConnectDialog::timeTick() {
if (!bLastFound && !Global::get().s.qsLastServer.isEmpty()) {
QList< QTreeWidgetItem * > items =
qtwServers->findItems(Global::get().s.qsLastServer, Qt::MatchExactly | Qt::MatchRecursive);
if (!items.isEmpty()) {
bLastFound = true;
qtwServers->setCurrentItem(items.at(0));
if (Global::get().s.bAutoConnect && bAutoConnect) {
siAutoConnect = static_cast< ServerItem * >(items.at(0));
if (!siAutoConnect->qlAddresses.isEmpty()) {
accept();
return;
} else if (!bAllowHostLookup) {
accept();
return;
}
}
}
}
if (bAllowHostLookup) {
// Start DNS Lookup of first unknown hostname
foreach (const UnresolvedServerAddress &unresolved, qlDNSLookup) {
if (qsDNSActive.contains(unresolved)) {
continue;
}
qlDNSLookup.removeAll(unresolved);
qlDNSLookup.append(unresolved);
qsDNSActive.insert(unresolved);
ServerResolver *sr = new ServerResolver();
QObject::connect(sr, SIGNAL(resolved()), this, SLOT(lookedUp()));
sr->resolve(unresolved.hostname, unresolved.port);
break;
}
}
ServerItem *current = static_cast< ServerItem * >(qtwServers->currentItem());
ServerItem *hover =
static_cast< ServerItem * >(qtwServers->itemAt(qtwServers->viewport()->mapFromGlobal(QCursor::pos())));
ServerItem *si = nullptr;
if (tCurrent.elapsed() >= 1000000ULL)
si = current;
if (!si && (tHover.elapsed() >= 1000000ULL))
si = hover;
if (si) {
QString hostname = si->qsHostname.toLower();
unsigned short port = si->usPort;
UnresolvedServerAddress unresolved(hostname, port);
if (si->qlAddresses.isEmpty()) {
if (!hostname.isEmpty()) {
qlDNSLookup.removeAll(unresolved);
qlDNSLookup.prepend(unresolved);
}
si = nullptr;
}
}
if (!si) {
if (qlItems.isEmpty())
return;
bool expanded;
do {
++iPingIndex;
if (iPingIndex >= qlItems.count()) {
if (tRestart.isElapsed(1000000ULL))
iPingIndex = 0;
else
return;
}
si = qlItems.at(iPingIndex);
ServerItem *p = si->siParent;
expanded = true;
while (p && expanded) {
expanded = expanded && p->isExpanded();
p = p->siParent;
}
} while (si->qlAddresses.isEmpty() || !expanded);
}
if (si == current)
tCurrent.restart();
if (si == hover)
tHover.restart();
for (const ServerAddress &addr : si->qlAddresses) {
sendPing(addr.host.toAddress(), addr.port, si->m_version);
}
}
void ConnectDialog::filterPublicServerList() const {
if (!Global::get().s.bDisablePublicList) {
foreach (ServerItem *const si, qtwServers->siPublic->qlChildren) { filterServer(si); }
}
}
void ConnectDialog::filterServer(ServerItem *const si) const {
if (!si->qsName.contains(qsSearchServername, Qt::CaseInsensitive)) {
si->setHidden(true);
return;
}
if (qsSearchLocation != QLatin1String("all")) {
if (qsSearchLocation != si->qsCountry && qsSearchLocation != si->qsContinentCode) {
si->setHidden(true);
return;
}
}
if (Global::get().s.ssFilter == Settings::ShowReachable && si->dPing == 0.0) {
si->setHidden(true);
return;
} else if (Global::get().s.ssFilter == Settings::ShowPopulated && si->uiUsers == 0) {
si->setHidden(true);
return;
}
si->setHidden(false);
}
void ConnectDialog::addCountriesToSearchLocation() const {
QMap< QString, QString > qmCountries;
foreach (const PublicInfo &pi, qlPublicServers) {
if (pi.qsCountry != tr("Unknown") && !qmCountries.contains(pi.qsCountry)) {
qmCountries.insert(pi.qsCountry, pi.qsCountryCode);
}
}
foreach (auto location, qmCountries.keys()) {
// Set Icon, Text and Data
qcbSearchLocation->addItem(
ServerItem::loadIcon(QString::fromLatin1(":/flags/%1.svg").arg(qmCountries.value(location))), location,
location);
}
}
void ConnectDialog::startDns(ServerItem *si) {
if (!bAllowHostLookup) {
return;
}
QString hostname = si->qsHostname.toLower();
unsigned short port = si->usPort;
UnresolvedServerAddress unresolved(hostname, port);
if (si->qlAddresses.isEmpty()) {
// Determine if qsHostname is an IP address
// or a hostname. If it is an IP address, we
// can treat it as resolved as-is.
QHostAddress qha(si->qsHostname);
bool hostnameIsIPAddress = !qha.isNull();
if (hostnameIsIPAddress) {
si->qlAddresses.append(ServerAddress(HostAddress(qha), port));
} else {
si->qlAddresses = qhDNSCache.value(unresolved);
}
}
if (qtwServers->currentItem() == si)
qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(!si->qlAddresses.isEmpty());
if (!si->qlAddresses.isEmpty()) {
foreach (const ServerAddress &addr, si->qlAddresses) { qhPings[addr].insert(si); }
return;
}
#ifdef USE_ZEROCONF
if (bAllowZeroconf && si->qsHostname.isEmpty() && !si->zeroconfRecord.serviceName.isEmpty()) {
if (!qlBonjourActive.contains(si->zeroconfRecord)) {
Global::get().zeroconf->startResolver(si->zeroconfRecord);
qlBonjourActive.append(si->zeroconfRecord);
}
return;
}
#endif
if (!qhDNSWait.contains(unresolved)) {
if (si->itType == ServerItem::PublicType)
qlDNSLookup.append(unresolved);
else
qlDNSLookup.prepend(unresolved);
}
qhDNSWait[unresolved].insert(si);
}
void ConnectDialog::stopDns(ServerItem *si) {
if (!bAllowHostLookup) {
return;
}
foreach (const ServerAddress &addr, si->qlAddresses) {
if (qhPings.contains(addr)) {
qhPings[addr].remove(si);
if (qhPings[addr].isEmpty()) {
qhPings.remove(addr);
qhPingRand.remove(addr);
}
}
}
QString hostname = si->qsHostname.toLower();
unsigned short port = si->usPort;
UnresolvedServerAddress unresolved(hostname, port);
if (qhDNSWait.contains(unresolved)) {
qhDNSWait[unresolved].remove(si);
if (qhDNSWait[unresolved].isEmpty()) {
qhDNSWait.remove(unresolved);
qlDNSLookup.removeAll(unresolved);
}
}
}
void ConnectDialog::lookedUp() {
ServerResolver *sr = qobject_cast< ServerResolver * >(QObject::sender());
sr->deleteLater();
QString hostname = sr->hostname().toLower();
unsigned short port = sr->port();
UnresolvedServerAddress unresolved(hostname, port);
qsDNSActive.remove(unresolved);
// An error occurred, or no records were found.
if (sr->records().size() == 0) {
return;
}
QSet< ServerAddress > qs;
foreach (ServerResolverRecord record, sr->records()) {
foreach (const HostAddress &ha, record.addresses()) { qs.insert(ServerAddress(ha, record.port())); }
}
QSet< ServerItem * > waiting = qhDNSWait[unresolved];
foreach (ServerItem *si, waiting) {
foreach (const ServerAddress &addr, qs) { qhPings[addr].insert(si); }
si->qlAddresses = qs.values();
}
qlDNSLookup.removeAll(unresolved);
qhDNSCache.insert(unresolved, qs.values());
qhDNSWait.remove(unresolved);
foreach (ServerItem *si, waiting) {
if (si == qtwServers->currentItem()) {
on_qtwServers_currentItemChanged(si, si);
if (si == siAutoConnect)
accept();
}
}
if (bAllowPing) {
for (const ServerAddress &addr : qs) {
sendPing(addr.host.toAddress(), addr.port, Version::UNKNOWN);
}
}
}
void ConnectDialog::sendPing(const QHostAddress &host, unsigned short port, Version::full_t protocolVersion) {
ServerAddress addr(HostAddress(host), port);
quint64 uiRand;
if (qhPingRand.contains(addr)) {
uiRand = qhPingRand.value(addr);
} else {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
uiRand = QRandomGenerator::global()->generate64() << 32;
#else
// Qt 5.10 introduces the QRandomGenerator class and in Qt 5.15 qrand got deprecated in its favor
uiRand = (static_cast< quint64 >(qrand()) << 32) | static_cast< quint64 >(qrand());
#endif
qhPingRand.insert(addr, uiRand);
}
Mumble::Protocol::PingData pingData;
// "Encrypt" the timestamp so that server's can't spoof the returned timestamp (easily) to fake a better ping
pingData.timestamp = tPing.elapsed() ^ uiRand;
pingData.requestAdditionalInformation = true;
if (!writePing(host, port, protocolVersion, pingData)) {
return;
}
if (protocolVersion == Version::UNKNOWN) {
// Also attempt to use new ping format in case we are pinging a server that only knows the new format
writePing(host, port, Mumble::Protocol::PROTOBUF_INTRODUCTION_VERSION, pingData);
}
const QSet< ServerItem * > &qs = qhPings.value(addr);
foreach (ServerItem *si, qs)
++si->uiSent;
}
bool ConnectDialog::writePing(const QHostAddress &host, unsigned short port, Version::full_t protocolVersion,
const Mumble::Protocol::PingData &pingData) {
m_udpPingEncoder.setProtocolVersion(protocolVersion);
gsl::span< const Mumble::Protocol::byte > encodedPacket = m_udpPingEncoder.encodePingPacket(pingData);
if (bIPv4 && host.protocol() == QAbstractSocket::IPv4Protocol) {
qusSocket4->writeDatagram(reinterpret_cast< const char * >(encodedPacket.data()),
static_cast< qint64 >(encodedPacket.size()), host, port);
} else if (bIPv6 && host.protocol() == QAbstractSocket::IPv6Protocol) {
qusSocket6->writeDatagram(reinterpret_cast< const char * >(encodedPacket.data()),
static_cast< qint64 >(encodedPacket.size()), host, port);
} else {
return false;
}
return true;
}
void ConnectDialog::udpReply() {
QUdpSocket *sock = qobject_cast< QUdpSocket * >(sender());
while (sock->hasPendingDatagrams()) {
QHostAddress host;
unsigned short port;
gsl::span< Mumble::Protocol::byte > buffer = m_udpDecoder.getBuffer();
std::size_t len = static_cast< std::size_t >(sock->readDatagram(
reinterpret_cast< char * >(buffer.data()), static_cast< int >(buffer.size()), &host, &port));
// Pings are special in that they can be decoded in the new or the old format, if the protocol version is set to
// the old format (which UNKNOWN does). Thus by setting the version to UNKNOWN, we effectively enable to decode
// either format. We have to reset it to this value every time, since the call to decode may set the protocol
// version to a more recent version (if a ping in new format is detected).
m_udpDecoder.setProtocolVersion(Version::UNKNOWN);
if (m_udpDecoder.decodePing(buffer.subspan(0, len))
&& m_udpDecoder.getMessageType() == Mumble::Protocol::UDPMessageType::Ping) {
if (host.scopeId() == QLatin1String("0"))
host.setScopeId(QLatin1String(""));
ServerAddress address(HostAddress(host), port);
if (qhPings.contains(address)) {
Mumble::Protocol::PingData pingData = m_udpDecoder.getPingData();
quint64 elapsed = tPing.elapsed() - (pingData.timestamp ^ qhPingRand.value(address));
for (ServerItem *si : qhPings.value(address)) {
si->m_version = pingData.serverVersion;
quint32 users = pingData.userCount;
quint32 maxusers = pingData.maxUserCount;
si->uiBandwidth = pingData.maxBandwidthPerUser;
if (!si->uiPingSort)
si->uiPingSort = qmPingCache.value(UnresolvedServerAddress(si->qsHostname, si->usPort));
si->setDatas(static_cast< double >(elapsed), users, maxusers);
if (si->itType == ServerItem::PublicType) {
filterServer(si);
}
}
}
}
}
}
void ConnectDialog::fetched(QByteArray xmlData, QUrl, QMap< QString, QString > headers) {
if (xmlData.isNull()) {
QMessageBox::warning(this, QLatin1String("Mumble"), tr("Failed to fetch server list"), QMessageBox::Ok);
return;
}
QDomDocument doc;
doc.setContent(xmlData);
qlPublicServers.clear();
qsUserCountry = headers.value(QLatin1String("Geo-Country"));
qsUserCountryCode = headers.value(QLatin1String("Geo-Country-Code")).toLower();
qsUserContinentCode = headers.value(QLatin1String("Geo-Continent-Code")).toLower();
QDomElement root = doc.documentElement();
QDomNode n = root.firstChild();
while (!n.isNull()) {
QDomElement e = n.toElement();
if (!e.isNull()) {
if (e.tagName() == QLatin1String("server")) {
PublicInfo pi;
pi.qsName = e.attribute(QLatin1String("name"));
pi.quUrl = e.attribute(QLatin1String("url"));
pi.qsIp = e.attribute(QLatin1String("ip"));
pi.usPort = e.attribute(QLatin1String("port")).toUShort();
pi.qsCountry = e.attribute(QLatin1String("country"), tr("Unknown"));
pi.qsCountryCode = e.attribute(QLatin1String("country_code")).toLower();
pi.qsContinentCode = e.attribute(QLatin1String("continent_code")).toLower();
pi.bCA = e.attribute(QLatin1String("ca")).toInt() ? true : false;
qlPublicServers << pi;
}
}
n = n.nextSibling();
}
addCountriesToSearchLocation();
tPublicServers.restart();
fillList();
}
void ConnectDialog::on_qleSearchServername_textChanged(const QString &searchServername) {
qsSearchServername = searchServername;
filterPublicServerList();
}
void ConnectDialog::on_qcbSearchLocation_currentIndexChanged(int searchLocationIndex) {
qsSearchLocation = qcbSearchLocation->itemData(searchLocationIndex).toString();
filterPublicServerList();
}
void ConnectDialog::on_qcbFilter_currentIndexChanged(int filterIndex) {
const QString filter = qcbFilter->itemText(filterIndex);
if (filter == tr("Show All")) {
Global::get().s.ssFilter = Settings::ShowAll;
} else if (filter == tr("Show Reachable")) {
Global::get().s.ssFilter = Settings::ShowReachable;
} else if (filter == tr("Show Populated")) {
Global::get().s.ssFilter = Settings::ShowPopulated;
}
filterPublicServerList();
}