1150 lines
33 KiB
C++
1150 lines
33 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 <QSslConfiguration>
|
|
#include <QtCore/QtGlobal>
|
|
|
|
#ifdef Q_OS_WIN
|
|
# include "win.h"
|
|
#endif
|
|
|
|
#include "ServerHandler.h"
|
|
|
|
#include "AudioInput.h"
|
|
#include "AudioOutput.h"
|
|
#include "Cert.h"
|
|
#include "Connection.h"
|
|
#include "Database.h"
|
|
#include "HostAddress.h"
|
|
#include "MainWindow.h"
|
|
#include "Net.h"
|
|
#include "NetworkConfig.h"
|
|
#include "OSInfo.h"
|
|
#include "PacketDataStream.h"
|
|
#include "ProtoUtils.h"
|
|
#include "RichTextEditor.h"
|
|
#include "SSL.h"
|
|
#include "ServerResolver.h"
|
|
#include "ServerResolverRecord.h"
|
|
#include "User.h"
|
|
#include "Utils.h"
|
|
#include "Global.h"
|
|
|
|
#include <QPainter>
|
|
#include <QtCore/QtEndian>
|
|
#include <QtGui/QImageReader>
|
|
#include <QtNetwork/QSslConfiguration>
|
|
#include <QtNetwork/QUdpSocket>
|
|
|
|
#include <openssl/crypto.h>
|
|
|
|
#include <cassert>
|
|
|
|
#ifdef Q_OS_WIN
|
|
// <delayimp.h> is not protected with an include guard on MinGW, resulting in
|
|
// redefinitions if the PCH header is used.
|
|
// The workaround consists in including the header only if _DELAY_IMP_VER
|
|
// (defined in the header) is not defined.
|
|
# ifndef _DELAY_IMP_VER
|
|
# include <delayimp.h>
|
|
# endif
|
|
# include <qos2.h>
|
|
# include <wincrypt.h>
|
|
# include <winsock2.h>
|
|
#else
|
|
# if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
|
|
# include <netinet/in.h>
|
|
# endif
|
|
# include <netinet/ip.h>
|
|
# include <sys/socket.h>
|
|
#endif
|
|
|
|
// Init ServerHandler::nextConnectionID
|
|
int ServerHandler::nextConnectionID = -1;
|
|
QMutex ServerHandler::nextConnectionIDMutex;
|
|
|
|
ServerHandlerMessageEvent::ServerHandlerMessageEvent(const QByteArray &msg, Mumble::Protocol::TCPMessageType type,
|
|
bool flush)
|
|
: QEvent(static_cast< QEvent::Type >(SERVERSEND_EVENT)) {
|
|
qbaMsg = msg;
|
|
this->type = type;
|
|
bFlush = flush;
|
|
}
|
|
|
|
#ifdef Q_OS_WIN
|
|
static HANDLE loadQoS() {
|
|
HANDLE hQoS = nullptr;
|
|
|
|
HRESULT hr = E_FAIL;
|
|
|
|
// We don't support delay-loading QoS on MinGW. Only enable it for MSVC.
|
|
# ifdef _MSC_VER
|
|
__try {
|
|
hr = __HrLoadAllImportsForDll("qwave.dll");
|
|
}
|
|
|
|
__except (EXCEPTION_EXECUTE_HANDLER) {
|
|
hr = E_FAIL;
|
|
}
|
|
# endif
|
|
|
|
if (!SUCCEEDED(hr)) {
|
|
qWarning("ServerHandler: Failed to load qWave.dll, no QoS available");
|
|
} else {
|
|
QOS_VERSION qvVer;
|
|
qvVer.MajorVersion = 1;
|
|
qvVer.MinorVersion = 0;
|
|
|
|
if (!QOSCreateHandle(&qvVer, &hQoS)) {
|
|
qWarning("ServerHandler: Failed to create QOS2 handle");
|
|
hQoS = nullptr;
|
|
} else {
|
|
qWarning("ServerHandler: QOS2 loaded");
|
|
}
|
|
}
|
|
return hQoS;
|
|
}
|
|
#endif
|
|
|
|
ServerHandler::ServerHandler() : database(new Database(QLatin1String("ServerHandler"))) {
|
|
cConnection.reset();
|
|
qusUdp = nullptr;
|
|
bStrong = false;
|
|
usPort = 0;
|
|
bUdp = true;
|
|
tConnectionTimeoutTimer = nullptr;
|
|
m_version = Version::UNKNOWN;
|
|
iInFlightTCPPings = 0;
|
|
|
|
// assign connection ID
|
|
{
|
|
QMutexLocker lock(&nextConnectionIDMutex);
|
|
nextConnectionID++;
|
|
connectionID = nextConnectionID;
|
|
}
|
|
|
|
// Historically, the qWarning line below initialized OpenSSL for us.
|
|
// It used to have this comment:
|
|
//
|
|
// "For some strange reason, on Win32, we have to call
|
|
// supportsSsl before the cipher list is ready."
|
|
//
|
|
// Now, OpenSSL is initialized in main() via MumbleSSL::initialize(),
|
|
// but since it's handy to have the OpenSSL version available, we
|
|
// keep this one around as well.
|
|
qWarning("OpenSSL Support: %d (%s)", QSslSocket::supportsSsl(), SSLeay_version(SSLEAY_VERSION));
|
|
|
|
MumbleSSL::addSystemCA();
|
|
|
|
{
|
|
QList< QSslCipher > ciphers = MumbleSSL::ciphersFromOpenSSLCipherString(Global::get().s.qsSslCiphers);
|
|
if (ciphers.isEmpty()) {
|
|
qFatal("Invalid 'net/sslciphers' config option. Either the cipher string is invalid or none of the ciphers "
|
|
"are available:: \"%s\"",
|
|
qPrintable(Global::get().s.qsSslCiphers));
|
|
}
|
|
|
|
QSslConfiguration config = QSslConfiguration::defaultConfiguration();
|
|
config.setCiphers(ciphers);
|
|
QSslConfiguration::setDefaultConfiguration(config);
|
|
|
|
QStringList pref;
|
|
foreach (QSslCipher c, ciphers) { pref << c.name(); }
|
|
qWarning("ServerHandler: TLS cipher preference is \"%s\"", qPrintable(pref.join(QLatin1String(":"))));
|
|
}
|
|
|
|
#ifdef Q_OS_WIN
|
|
hQoS = loadQoS();
|
|
if (hQoS)
|
|
Connection::setQoS(hQoS);
|
|
#endif
|
|
|
|
connect(this, SIGNAL(pingRequested()), this, SLOT(sendPingInternal()), Qt::QueuedConnection);
|
|
}
|
|
|
|
ServerHandler::~ServerHandler() {
|
|
wait();
|
|
cConnection.reset();
|
|
#ifdef Q_OS_WIN
|
|
if (hQoS) {
|
|
QOSCloseHandle(hQoS);
|
|
Connection::setQoS(nullptr);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void ServerHandler::customEvent(QEvent *evt) {
|
|
if (evt->type() != SERVERSEND_EVENT)
|
|
return;
|
|
|
|
ServerHandlerMessageEvent *shme = static_cast< ServerHandlerMessageEvent * >(evt);
|
|
|
|
ConnectionPtr connection(cConnection);
|
|
if (connection) {
|
|
if (shme->qbaMsg.size() > 0) {
|
|
connection->sendMessage(shme->qbaMsg);
|
|
if (shme->bFlush)
|
|
connection->forceFlush();
|
|
} else {
|
|
exit(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
int ServerHandler::getConnectionID() const {
|
|
return connectionID;
|
|
}
|
|
|
|
void ServerHandler::setProtocolVersion(Version::full_t version) {
|
|
m_version = version;
|
|
|
|
m_udpPingEncoder.setProtocolVersion(version);
|
|
m_udpDecoder.setProtocolVersion(version);
|
|
m_tcpTunnelDecoder.setProtocolVersion(version);
|
|
}
|
|
|
|
void ServerHandler::udpReady() {
|
|
while (qusUdp->hasPendingDatagrams()) {
|
|
char encrypted[Mumble::Protocol::MAX_UDP_PACKET_SIZE];
|
|
unsigned int buflen = static_cast< unsigned int >(qusUdp->pendingDatagramSize());
|
|
|
|
if (buflen > Mumble::Protocol::MAX_UDP_PACKET_SIZE) {
|
|
// Discard datagrams that exceed our buffer's size as we'd have to trim them down anyways and it is not very
|
|
// likely that the data is valid in the trimmed down form.
|
|
// As we're using a maxSize of 0 it is okay to pass nullptr as the data buffer. Qt's docs (5.15) ensures
|
|
// that a maxSize of 0 means discarding the datagram.
|
|
qusUdp->readDatagram(nullptr, 0);
|
|
continue;
|
|
}
|
|
|
|
QHostAddress senderAddr;
|
|
quint16 senderPort;
|
|
qusUdp->readDatagram(encrypted, buflen, &senderAddr, &senderPort);
|
|
|
|
if (!(HostAddress(senderAddr) == HostAddress(qhaRemote)) || (senderPort != usResolvedPort))
|
|
continue;
|
|
|
|
ConnectionPtr connection(cConnection);
|
|
if (!connection)
|
|
continue;
|
|
|
|
if (!connection->csCrypt->isValid())
|
|
continue;
|
|
|
|
if (buflen < 5)
|
|
continue;
|
|
|
|
gsl::span< Mumble::Protocol::byte > buffer = m_udpDecoder.getBuffer();
|
|
|
|
// 4 bytes is the overhead of the encryption
|
|
assert(buffer.size() >= buflen - 4);
|
|
|
|
if (!connection->csCrypt->decrypt(reinterpret_cast< const unsigned char * >(encrypted), buffer.data(),
|
|
buflen)) {
|
|
if (connection->csCrypt->tLastGood.elapsed() > 5000000ULL) {
|
|
if (connection->csCrypt->tLastRequest.elapsed() > 5000000ULL) {
|
|
connection->csCrypt->tLastRequest.restart();
|
|
MumbleProto::CryptSetup mpcs;
|
|
sendMessage(mpcs);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (m_udpDecoder.decode(buffer.subspan(0, buflen - 4))) {
|
|
switch (m_udpDecoder.getMessageType()) {
|
|
case Mumble::Protocol::UDPMessageType::Ping: {
|
|
const Mumble::Protocol::PingData pingData = m_udpDecoder.getPingData();
|
|
|
|
accUDP(static_cast< double >(tTimestamp.elapsed() - pingData.timestamp) / 1000.0);
|
|
|
|
break;
|
|
}
|
|
case Mumble::Protocol::UDPMessageType::Audio: {
|
|
const Mumble::Protocol::AudioData audioData = m_udpDecoder.getAudioData();
|
|
|
|
handleVoicePacket(audioData);
|
|
break;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ServerHandler::handleVoicePacket(const Mumble::Protocol::AudioData &audioData) {
|
|
if (audioData.usedCodec != Mumble::Protocol::AudioCodec::Opus) {
|
|
qWarning("Dropping audio packet using invalid codec (not Opus): %d", static_cast< int >(audioData.usedCodec));
|
|
return;
|
|
}
|
|
|
|
ClientUser *sender = ClientUser::get(audioData.senderSession);
|
|
|
|
AudioOutputPtr ao = Global::get().ao;
|
|
if (ao && sender
|
|
&& !((audioData.targetOrContext == Mumble::Protocol::AudioContext::WHISPER) && Global::get().s.bWhisperFriends
|
|
&& sender->qsFriendName.isEmpty())) {
|
|
ao->addFrameToBuffer(sender, audioData);
|
|
}
|
|
}
|
|
|
|
void ServerHandler::sendMessage(const unsigned char *data, int len, bool force) {
|
|
static std::vector< unsigned char > crypto;
|
|
crypto.resize(static_cast< std::size_t >(len + 4));
|
|
|
|
QMutexLocker qml(&qmUdp);
|
|
|
|
if (!qusUdp)
|
|
return;
|
|
|
|
ConnectionPtr connection(cConnection);
|
|
if (!connection || !connection->csCrypt->isValid())
|
|
return;
|
|
|
|
if (!force && (NetworkConfig::TcpModeEnabled() || !bUdp)) {
|
|
QByteArray qba;
|
|
|
|
qba.resize(len + 6);
|
|
unsigned char *uc = reinterpret_cast< unsigned char * >(qba.data());
|
|
*reinterpret_cast< quint16 * >(&uc[0]) =
|
|
qToBigEndian(static_cast< quint16 >(Mumble::Protocol::TCPMessageType::UDPTunnel));
|
|
*reinterpret_cast< quint32 * >(&uc[2]) = qToBigEndian(static_cast< quint32 >(len));
|
|
memcpy(uc + 6, data, static_cast< std::size_t >(len));
|
|
|
|
QApplication::postEvent(this,
|
|
new ServerHandlerMessageEvent(qba, Mumble::Protocol::TCPMessageType::UDPTunnel, true));
|
|
} else {
|
|
if (!connection->csCrypt->encrypt(reinterpret_cast< const unsigned char * >(data), crypto.data(),
|
|
static_cast< unsigned int >(len))) {
|
|
return;
|
|
}
|
|
qusUdp->writeDatagram(reinterpret_cast< const char * >(crypto.data()), len + 4, qhaRemote, usResolvedPort);
|
|
}
|
|
}
|
|
|
|
void ServerHandler::sendProtoMessage(const ::google::protobuf::Message &msg, Mumble::Protocol::TCPMessageType type) {
|
|
QByteArray qba;
|
|
|
|
if (QThread::currentThread() != thread()) {
|
|
Connection::messageToNetwork(msg, type, qba);
|
|
ServerHandlerMessageEvent *shme = new ServerHandlerMessageEvent(qba, type, false);
|
|
QApplication::postEvent(this, shme);
|
|
} else {
|
|
ConnectionPtr connection(cConnection);
|
|
if (!connection)
|
|
return;
|
|
|
|
connection->sendMessage(msg, type, qba);
|
|
}
|
|
}
|
|
|
|
bool ServerHandler::isConnected() const {
|
|
// If the digest isn't empty, then we are currently connected to a server (the digest being a hash
|
|
// of the server's certificate)
|
|
return !qbaDigest.isEmpty();
|
|
}
|
|
|
|
bool ServerHandler::hasSynchronized() const {
|
|
return serverSynchronized;
|
|
}
|
|
|
|
void ServerHandler::setServerSynchronized(bool synchronized) {
|
|
serverSynchronized = synchronized;
|
|
}
|
|
|
|
void ServerHandler::hostnameResolved() {
|
|
ServerResolver *sr = qobject_cast< ServerResolver * >(QObject::sender());
|
|
QList< ServerResolverRecord > records = sr->records();
|
|
|
|
// Exit the ServerHandler thread's event loop with an
|
|
// error code in case our hostname lookup failed.
|
|
if (records.isEmpty()) {
|
|
exit(-1);
|
|
return;
|
|
}
|
|
|
|
// Create the list of target host:port pairs
|
|
// that the ServerHandler should try to connect to.
|
|
QList< ServerAddress > ql;
|
|
QHash< ServerAddress, QString > qh;
|
|
foreach (ServerResolverRecord record, records) {
|
|
foreach (HostAddress addr, record.addresses()) {
|
|
auto sa = ServerAddress(addr, record.port());
|
|
ql.append(sa);
|
|
qh[sa] = record.hostname();
|
|
}
|
|
}
|
|
qlAddresses = ql;
|
|
qhHostnames = qh;
|
|
|
|
// Exit the event loop with 'success' status code,
|
|
// to continue connecting to the server.
|
|
exit(0);
|
|
}
|
|
|
|
void ServerHandler::run() {
|
|
// Resolve the hostname...
|
|
{
|
|
ServerResolver sr;
|
|
QObject::connect(&sr, SIGNAL(resolved()), this, SLOT(hostnameResolved()));
|
|
sr.resolve(qsHostName, usPort);
|
|
int ret = exec();
|
|
if (ret < 0) {
|
|
qWarning("ServerHandler: failed to resolve hostname");
|
|
emit error(QAbstractSocket::HostNotFoundError, tr("Unable to resolve hostname"));
|
|
return;
|
|
}
|
|
}
|
|
|
|
QList< ServerAddress > targetAddresses(qlAddresses);
|
|
bool shouldTryNextTargetServer = true;
|
|
do {
|
|
saTargetServer = qlAddresses.takeFirst();
|
|
|
|
tConnectionTimeoutTimer = nullptr;
|
|
qbaDigest = QByteArray();
|
|
bStrong = true;
|
|
qtsSock = new QSslSocket(this);
|
|
qtsSock->setPeerVerifyName(qhHostnames[saTargetServer]);
|
|
|
|
if (!Global::get().s.bSuppressIdentity && CertWizard::validateCert(Global::get().s.kpCertificate)) {
|
|
qtsSock->setPrivateKey(Global::get().s.kpCertificate.second);
|
|
qtsSock->setLocalCertificate(Global::get().s.kpCertificate.first.at(0));
|
|
QSslConfiguration config = qtsSock->sslConfiguration();
|
|
QList< QSslCertificate > certs = config.caCertificates();
|
|
certs << Global::get().s.kpCertificate.first;
|
|
config.setCaCertificates(certs);
|
|
qtsSock->setSslConfiguration(config);
|
|
}
|
|
|
|
{
|
|
ConnectionPtr connection(new Connection(this, qtsSock));
|
|
cConnection = connection;
|
|
|
|
// Technically it isn't necessary to reset this flag here since a ServerHandler will not be used
|
|
// for multiple connections in a row but just in case that at some point it will, we'll reset the
|
|
// flag here.
|
|
serverSynchronized = false;
|
|
|
|
qlErrors.clear();
|
|
qscCert.clear();
|
|
|
|
connect(qtsSock, &QSslSocket::encrypted, this, &ServerHandler::serverConnectionConnected);
|
|
connect(qtsSock, &QSslSocket::stateChanged, this, &ServerHandler::serverConnectionStateChanged);
|
|
connect(connection.get(), &Connection::connectionClosed, this, &ServerHandler::serverConnectionClosed);
|
|
connect(connection.get(), &Connection::message, this, &ServerHandler::message);
|
|
connect(connection.get(), &Connection::handleSslErrors, this, &ServerHandler::setSslErrors);
|
|
}
|
|
bUdp = false;
|
|
|
|
|
|
#if QT_VERSION >= 0x050500
|
|
qtsSock->setProtocol(QSsl::TlsV1_0OrLater);
|
|
#elif QT_VERSION >= 0x050400
|
|
// In Qt 5.4, QSsl::SecureProtocols is equivalent
|
|
// to "TLSv1.0 or later", which we require.
|
|
qtsSock->setProtocol(QSsl::SecureProtocols);
|
|
#else
|
|
qtsSock->setProtocol(QSsl::TlsV1_0);
|
|
#endif
|
|
|
|
qtsSock->connectToHost(saTargetServer.host.toAddress(), saTargetServer.port);
|
|
|
|
tTimestamp.restart();
|
|
|
|
// Setup ping timer;
|
|
QTimer *ticker = new QTimer(this);
|
|
connect(ticker, SIGNAL(timeout()), this, SLOT(sendPing()));
|
|
ticker->start(Global::get().s.iPingIntervalMsec);
|
|
|
|
Global::get().mw->rtLast = MumbleProto::Reject_RejectType_None;
|
|
|
|
accUDP = accTCP = accClean;
|
|
|
|
m_version = Version::UNKNOWN;
|
|
qsRelease = QString();
|
|
qsOS = QString();
|
|
qsOSVersion = QString();
|
|
|
|
int ret = exec();
|
|
if (ret == -2) {
|
|
shouldTryNextTargetServer = true;
|
|
} else {
|
|
shouldTryNextTargetServer = false;
|
|
}
|
|
|
|
if (qusUdp) {
|
|
QMutexLocker qml(&qmUdp);
|
|
|
|
#ifdef Q_OS_WIN
|
|
if (hQoS) {
|
|
if (!QOSRemoveSocketFromFlow(hQoS, 0, dwFlowUDP, 0)) {
|
|
qWarning("ServerHandler: Failed to remove UDP from QoS. QOSRemoveSocketFromFlow() failed with "
|
|
"error %lu!",
|
|
GetLastError());
|
|
}
|
|
|
|
dwFlowUDP = 0;
|
|
}
|
|
#endif
|
|
delete qusUdp;
|
|
qusUdp = nullptr;
|
|
}
|
|
|
|
ticker->stop();
|
|
|
|
ConnectionPtr cptr(cConnection);
|
|
if (cptr) {
|
|
cptr->disconnectSocket(true);
|
|
}
|
|
|
|
cConnection.reset();
|
|
while (!cptr.unique()) {
|
|
msleep(100);
|
|
}
|
|
delete qtsSock;
|
|
delete tConnectionTimeoutTimer;
|
|
} while (shouldTryNextTargetServer && !qlAddresses.isEmpty());
|
|
}
|
|
|
|
#ifdef Q_OS_WIN
|
|
extern DWORD WinVerifySslCert(const QByteArray &cert);
|
|
#endif
|
|
|
|
void ServerHandler::setSslErrors(const QList< QSslError > &errors) {
|
|
ConnectionPtr connection(cConnection);
|
|
if (!connection)
|
|
return;
|
|
|
|
qscCert = connection->peerCertificateChain();
|
|
QList< QSslError > newErrors = errors;
|
|
|
|
#ifdef Q_OS_WIN
|
|
bool bRevalidate = false;
|
|
QList< QSslError > errorsToRemove;
|
|
foreach (const QSslError &e, errors) {
|
|
switch (e.error()) {
|
|
case QSslError::UnableToGetLocalIssuerCertificate:
|
|
case QSslError::SelfSignedCertificateInChain:
|
|
bRevalidate = true;
|
|
errorsToRemove << e;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bRevalidate) {
|
|
QByteArray der = qscCert.first().toDer();
|
|
DWORD errorStatus = WinVerifySslCert(der);
|
|
if (errorStatus == CERT_TRUST_NO_ERROR) {
|
|
foreach (const QSslError &e, errorsToRemove) { newErrors.removeOne(e); }
|
|
}
|
|
if (newErrors.isEmpty()) {
|
|
connection->proceedAnyway();
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bStrong = false;
|
|
if ((qscCert.size() > 0)
|
|
&& (QString::fromLatin1(qscCert.at(0).digest(QCryptographicHash::Sha1).toHex())
|
|
== database->getDigest(qsHostName, usPort)))
|
|
connection->proceedAnyway();
|
|
else
|
|
qlErrors = newErrors;
|
|
}
|
|
|
|
void ServerHandler::sendPing() {
|
|
emit pingRequested();
|
|
}
|
|
|
|
void ServerHandler::sendPingInternal() {
|
|
ConnectionPtr connection(cConnection);
|
|
if (!connection)
|
|
return;
|
|
|
|
if (qtsSock->state() != QAbstractSocket::ConnectedState) {
|
|
return;
|
|
}
|
|
|
|
// Ensure the TLS handshake has completed before sending pings.
|
|
if (!qtsSock->isEncrypted()) {
|
|
return;
|
|
}
|
|
|
|
if (Global::get().s.iMaxInFlightTCPPings > 0 && iInFlightTCPPings >= Global::get().s.iMaxInFlightTCPPings) {
|
|
serverConnectionClosed(QAbstractSocket::UnknownSocketError, tr("Server is not responding to TCP pings"));
|
|
return;
|
|
}
|
|
|
|
quint64 t = tTimestamp.elapsed();
|
|
|
|
if (qusUdp) {
|
|
Mumble::Protocol::PingData pingData;
|
|
pingData.timestamp = t;
|
|
pingData.requestAdditionalInformation = false;
|
|
|
|
m_udpPingEncoder.setProtocolVersion(m_version);
|
|
gsl::span< const Mumble::Protocol::byte > encodedPacket = m_udpPingEncoder.encodePingPacket(pingData);
|
|
|
|
sendMessage(encodedPacket.data(), static_cast< int >(encodedPacket.size()), true);
|
|
}
|
|
|
|
MumbleProto::Ping mpp;
|
|
|
|
mpp.set_timestamp(t);
|
|
mpp.set_good(connection->csCrypt->uiGood);
|
|
mpp.set_late(connection->csCrypt->uiLate);
|
|
mpp.set_lost(connection->csCrypt->uiLost);
|
|
mpp.set_resync(connection->csCrypt->uiResync);
|
|
|
|
|
|
if (boost::accumulators::count(accUDP)) {
|
|
mpp.set_udp_ping_avg(static_cast< float >(boost::accumulators::mean(accUDP)));
|
|
mpp.set_udp_ping_var(static_cast< float >(boost::accumulators::variance(accUDP)));
|
|
}
|
|
mpp.set_udp_packets(static_cast< unsigned int >(boost::accumulators::count(accUDP)));
|
|
|
|
if (boost::accumulators::count(accTCP)) {
|
|
mpp.set_tcp_ping_avg(static_cast< float >(boost::accumulators::mean(accTCP)));
|
|
mpp.set_tcp_ping_var(static_cast< float >(boost::accumulators::variance(accTCP)));
|
|
}
|
|
mpp.set_tcp_packets(static_cast< unsigned int >(boost::accumulators::count(accTCP)));
|
|
|
|
sendMessage(mpp);
|
|
|
|
iInFlightTCPPings += 1;
|
|
}
|
|
|
|
void ServerHandler::message(Mumble::Protocol::TCPMessageType type, const QByteArray &qbaMsg) {
|
|
const char *ptr = qbaMsg.constData();
|
|
if (type == Mumble::Protocol::TCPMessageType::UDPTunnel) {
|
|
// audio tunneled through tcp.
|
|
// since it could happen that we are receiving udp and tcp messages at the same time (e.g. the server used to
|
|
// send us packages via TCP but has now switched to UDP again and the first UDP packages arrive at the same time
|
|
// as the last TCP ones), we want to use a dedicated decoder for this (to make sure there is no concurrent
|
|
// access to the decoder's internal buffer).
|
|
if (m_tcpTunnelDecoder.decode(
|
|
{ reinterpret_cast< const Mumble::Protocol::byte * >(ptr), static_cast< std::size_t >(qbaMsg.size()) })
|
|
&& m_tcpTunnelDecoder.getMessageType() == Mumble::Protocol::UDPMessageType::Audio) {
|
|
handleVoicePacket(m_tcpTunnelDecoder.getAudioData());
|
|
}
|
|
} else if (type == Mumble::Protocol::TCPMessageType::Ping) {
|
|
MumbleProto::Ping msg;
|
|
if (msg.ParseFromArray(qbaMsg.constData(), qbaMsg.size())) {
|
|
ConnectionPtr connection(cConnection);
|
|
if (!connection)
|
|
return;
|
|
|
|
// Reset in-flight TCP ping counter to 0.
|
|
// We've received a ping. That means the
|
|
// connection is still OK.
|
|
iInFlightTCPPings = 0;
|
|
|
|
connection->csCrypt->uiRemoteGood = msg.good();
|
|
connection->csCrypt->uiRemoteLate = msg.late();
|
|
connection->csCrypt->uiRemoteLost = msg.lost();
|
|
connection->csCrypt->uiRemoteResync = msg.resync();
|
|
accTCP(static_cast< double >(tTimestamp.elapsed() - msg.timestamp()) / 1000.0);
|
|
|
|
if (((connection->csCrypt->uiRemoteGood == 0) || (connection->csCrypt->uiGood == 0)) && bUdp
|
|
&& (tTimestamp.elapsed() > 20000000ULL)) {
|
|
bUdp = false;
|
|
if (!NetworkConfig::TcpModeEnabled()) {
|
|
if ((connection->csCrypt->uiRemoteGood == 0) && (connection->csCrypt->uiGood == 0))
|
|
Global::get().mw->msgBox(
|
|
tr("UDP packets cannot be sent to or received from the server. Switching to TCP mode."));
|
|
else if (connection->csCrypt->uiRemoteGood == 0)
|
|
Global::get().mw->msgBox(
|
|
tr("UDP packets cannot be sent to the server. Switching to TCP mode."));
|
|
else
|
|
Global::get().mw->msgBox(
|
|
tr("UDP packets cannot be received from the server. Switching to TCP mode."));
|
|
|
|
database->setUdp(qbaDigest, false);
|
|
}
|
|
} else if (!bUdp && (connection->csCrypt->uiRemoteGood > 3) && (connection->csCrypt->uiGood > 3)) {
|
|
bUdp = true;
|
|
if (!NetworkConfig::TcpModeEnabled()) {
|
|
Global::get().mw->msgBox(
|
|
tr("UDP packets can be sent to and received from the server. Switching back to UDP mode."));
|
|
|
|
database->setUdp(qbaDigest, true);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ServerHandlerMessageEvent *shme = new ServerHandlerMessageEvent(qbaMsg, type, false);
|
|
QApplication::postEvent(Global::get().mw, shme);
|
|
}
|
|
}
|
|
|
|
void ServerHandler::disconnect() {
|
|
// Actual TCP object is in a different thread, so signal it
|
|
// The actual type of this event doesn't matter as we are only abusing the event mechanism to signal the thread to
|
|
// exit.
|
|
QByteArray qbaBuffer;
|
|
ServerHandlerMessageEvent *shme =
|
|
new ServerHandlerMessageEvent(qbaBuffer, Mumble::Protocol::TCPMessageType::Ping, false);
|
|
QApplication::postEvent(this, shme);
|
|
}
|
|
|
|
void ServerHandler::serverConnectionClosed(QAbstractSocket::SocketError err, const QString &reason) {
|
|
Connection *c = cConnection.get();
|
|
if (!c)
|
|
return;
|
|
if (c->bDisconnectedEmitted)
|
|
return;
|
|
c->bDisconnectedEmitted = true;
|
|
|
|
AudioOutputPtr ao = Global::get().ao;
|
|
if (ao)
|
|
ao->wipe();
|
|
|
|
// Try next server in the list if possible.
|
|
// Otherwise, emit disconnect and exit with
|
|
// a normal status code.
|
|
if (!qlAddresses.isEmpty()) {
|
|
if (err == QAbstractSocket::ConnectionRefusedError || err == QAbstractSocket::SocketTimeoutError) {
|
|
qWarning("ServerHandler: connection attempt to %s:%i failed: %s (%li); trying next server....",
|
|
qPrintable(saTargetServer.host.toString()), static_cast< int >(saTargetServer.port),
|
|
qPrintable(reason), static_cast< long >(err));
|
|
exit(-2);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Having 2 signals here that basically fire at the same time is wanted behavior!
|
|
// See the documentation of "aboutToDisconnect" for an explanation.
|
|
emit aboutToDisconnect(err, reason);
|
|
emit disconnected(err, reason);
|
|
|
|
exit(0);
|
|
}
|
|
|
|
void ServerHandler::serverConnectionTimeoutOnConnect() {
|
|
ConnectionPtr connection(cConnection);
|
|
if (connection)
|
|
connection->disconnectSocket(true);
|
|
|
|
serverConnectionClosed(QAbstractSocket::SocketTimeoutError, tr("Connection timed out"));
|
|
}
|
|
|
|
void ServerHandler::serverConnectionStateChanged(QAbstractSocket::SocketState state) {
|
|
if (state == QAbstractSocket::ConnectingState) {
|
|
// Start timer for connection timeout during connect after resolving is completed
|
|
tConnectionTimeoutTimer = new QTimer();
|
|
connect(tConnectionTimeoutTimer, SIGNAL(timeout()), this, SLOT(serverConnectionTimeoutOnConnect()));
|
|
tConnectionTimeoutTimer->setSingleShot(true);
|
|
tConnectionTimeoutTimer->start(Global::get().s.iConnectionTimeoutDurationMsec);
|
|
} else if (state == QAbstractSocket::ConnectedState) {
|
|
// Start TLS handshake
|
|
qtsSock->startClientEncryption();
|
|
}
|
|
}
|
|
|
|
void ServerHandler::serverConnectionConnected() {
|
|
ConnectionPtr connection(cConnection);
|
|
if (!connection)
|
|
return;
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
|
|
// The ephemeralServerKey property is only a non-null key, if forward secrecy is used.
|
|
// See also https://doc.qt.io/qt-5/qsslconfiguration.html#ephemeralServerKey
|
|
connectionUsesPerfectForwardSecrecy = !qtsSock->sslConfiguration().ephemeralServerKey().isNull();
|
|
#endif
|
|
|
|
iInFlightTCPPings = 0;
|
|
|
|
tConnectionTimeoutTimer->stop();
|
|
|
|
if (Global::get().s.bQoS)
|
|
connection->setToS();
|
|
|
|
qscCert = connection->peerCertificateChain();
|
|
qscCipher = connection->sessionCipher();
|
|
|
|
if (!qscCert.isEmpty()) {
|
|
// Get the server's immediate SSL certificate
|
|
const QSslCertificate &qsc = qscCert.first();
|
|
qbaDigest = sha1(qsc.publicKey().toDer());
|
|
bUdp = database->getUdp(qbaDigest);
|
|
} else {
|
|
// Shouldn't reach this
|
|
qCritical("Server must have a certificate. Dropping connection");
|
|
disconnect();
|
|
return;
|
|
}
|
|
|
|
MumbleProto::Version mpv;
|
|
mpv.set_release(u8(Version::getRelease()));
|
|
MumbleProto::setVersion(mpv, Version::get());
|
|
|
|
if (!Global::get().s.bHideOS) {
|
|
mpv.set_os(u8(OSInfo::getOS()));
|
|
mpv.set_os_version(u8(OSInfo::getOSDisplayableVersion()));
|
|
}
|
|
|
|
sendMessage(mpv);
|
|
|
|
MumbleProto::Authenticate mpa;
|
|
mpa.set_username(u8(qsUserName));
|
|
mpa.set_password(u8(qsPassword));
|
|
|
|
QStringList tokens = database->getTokens(qbaDigest);
|
|
foreach (const QString &qs, tokens)
|
|
mpa.add_tokens(u8(qs));
|
|
|
|
mpa.set_opus(true);
|
|
sendMessage(mpa);
|
|
|
|
{
|
|
QMutexLocker qml(&qmUdp);
|
|
|
|
qhaRemote = connection->peerAddress();
|
|
qhaLocal = connection->localAddress();
|
|
usResolvedPort = connection->peerPort();
|
|
if (qhaLocal.isNull()) {
|
|
qFatal("ServerHandler: qhaLocal is unexpectedly a null addr");
|
|
}
|
|
|
|
qusUdp = new QUdpSocket(this);
|
|
if (!qusUdp) {
|
|
qFatal("ServerHandler: qusUdp is unexpectedly a null addr");
|
|
}
|
|
if (Global::get().s.bUdpForceTcpAddr) {
|
|
qusUdp->bind(qhaLocal, 0);
|
|
} else {
|
|
if (qhaRemote.protocol() == QAbstractSocket::IPv6Protocol) {
|
|
qusUdp->bind(QHostAddress(QHostAddress::AnyIPv6), 0);
|
|
} else {
|
|
qusUdp->bind(QHostAddress(QHostAddress::Any), 0);
|
|
}
|
|
}
|
|
|
|
connect(qusUdp, SIGNAL(readyRead()), this, SLOT(udpReady()));
|
|
|
|
if (Global::get().s.bQoS) {
|
|
#if defined(Q_OS_UNIX)
|
|
int val = 0xe0;
|
|
if (setsockopt(static_cast< int >(qusUdp->socketDescriptor()), IPPROTO_IP, IP_TOS, &val, sizeof(val))) {
|
|
val = 0x80;
|
|
if (setsockopt(static_cast< int >(qusUdp->socketDescriptor()), IPPROTO_IP, IP_TOS, &val, sizeof(val)))
|
|
qWarning("ServerHandler: Failed to set TOS for UDP Socket");
|
|
}
|
|
# if defined(SO_PRIORITY)
|
|
socklen_t optlen = sizeof(val);
|
|
if (getsockopt(static_cast< int >(qusUdp->socketDescriptor()), SOL_SOCKET, SO_PRIORITY, &val, &optlen)
|
|
== 0) {
|
|
if (val == 0) {
|
|
val = 6;
|
|
setsockopt(static_cast< int >(qusUdp->socketDescriptor()), SOL_SOCKET, SO_PRIORITY, &val,
|
|
sizeof(val));
|
|
}
|
|
}
|
|
# endif
|
|
#elif defined(Q_OS_WIN)
|
|
if (hQoS) {
|
|
struct sockaddr_in addr;
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(usPort);
|
|
addr.sin_addr.s_addr = htonl(qhaRemote.toIPv4Address());
|
|
|
|
dwFlowUDP = 0;
|
|
if (!QOSAddSocketToFlow(hQoS, qusUdp->socketDescriptor(), reinterpret_cast< sockaddr * >(&addr),
|
|
QOSTrafficTypeVoice, QOS_NON_ADAPTIVE_FLOW,
|
|
reinterpret_cast< PQOS_FLOWID >(&dwFlowUDP)))
|
|
qWarning("ServerHandler: Failed to add UDP to QOS");
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
emit connected();
|
|
}
|
|
|
|
void ServerHandler::setConnectionInfo(const QString &host, unsigned short port, const QString &username,
|
|
const QString &pw) {
|
|
qsHostName = host;
|
|
usPort = port;
|
|
qsUserName = username;
|
|
qsPassword = pw;
|
|
}
|
|
|
|
void ServerHandler::getConnectionInfo(QString &host, unsigned short &port, QString &username, QString &pw) const {
|
|
host = qsHostName;
|
|
port = usPort;
|
|
username = qsUserName;
|
|
pw = qsPassword;
|
|
}
|
|
|
|
bool ServerHandler::isStrong() const {
|
|
return bStrong;
|
|
}
|
|
|
|
void ServerHandler::requestUserStats(unsigned int uiSession, bool statsOnly) {
|
|
MumbleProto::UserStats mpus;
|
|
mpus.set_session(uiSession);
|
|
mpus.set_stats_only(statsOnly);
|
|
sendMessage(mpus);
|
|
}
|
|
|
|
void ServerHandler::joinChannel(unsigned int uiSession, unsigned int channel) {
|
|
static const QStringList EMPTY;
|
|
|
|
joinChannel(uiSession, channel, EMPTY);
|
|
}
|
|
|
|
void ServerHandler::joinChannel(unsigned int uiSession, unsigned int channel,
|
|
const QStringList &temporaryAccessTokens) {
|
|
MumbleProto::UserState mpus;
|
|
mpus.set_session(uiSession);
|
|
mpus.set_channel_id(channel);
|
|
|
|
foreach (const QString &tmpToken, temporaryAccessTokens) {
|
|
mpus.add_temporary_access_tokens(tmpToken.toUtf8().constData());
|
|
}
|
|
|
|
sendMessage(mpus);
|
|
}
|
|
|
|
void ServerHandler::startListeningToChannel(unsigned int channel) {
|
|
startListeningToChannels({ channel });
|
|
}
|
|
|
|
void ServerHandler::startListeningToChannels(const QList< unsigned int > &channelIDs) {
|
|
if (channelIDs.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
MumbleProto::UserState mpus;
|
|
mpus.set_session(Global::get().uiSession);
|
|
|
|
for (unsigned int currentChannel : channelIDs) {
|
|
// The naming of the function is a bit unfortunate but what this does is to add
|
|
// the channel ID to the message field listening_channel_add
|
|
mpus.add_listening_channel_add(currentChannel);
|
|
}
|
|
|
|
sendMessage(mpus);
|
|
}
|
|
|
|
void ServerHandler::stopListeningToChannel(unsigned int channel) {
|
|
stopListeningToChannels({ channel });
|
|
}
|
|
|
|
void ServerHandler::stopListeningToChannels(const QList< unsigned int > &channelIDs) {
|
|
if (channelIDs.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
MumbleProto::UserState mpus;
|
|
mpus.set_session(Global::get().uiSession);
|
|
|
|
for (unsigned int currentChannel : channelIDs) {
|
|
// The naming of the function is a bit unfortunate but what this does is to add
|
|
// the channel ID to the message field listening_channel_remove
|
|
mpus.add_listening_channel_remove(currentChannel);
|
|
}
|
|
|
|
sendMessage(mpus);
|
|
}
|
|
|
|
void ServerHandler::createChannel(unsigned int parent_id, const QString &name, const QString &description,
|
|
unsigned int position, bool temporary, unsigned int maxUsers) {
|
|
MumbleProto::ChannelState mpcs;
|
|
mpcs.set_parent(parent_id);
|
|
mpcs.set_name(u8(name));
|
|
mpcs.set_description(u8(description));
|
|
mpcs.set_position(static_cast< int >(position));
|
|
mpcs.set_temporary(temporary);
|
|
mpcs.set_max_users(maxUsers);
|
|
sendMessage(mpcs);
|
|
}
|
|
|
|
void ServerHandler::requestBanList() {
|
|
MumbleProto::BanList mpbl;
|
|
mpbl.set_query(true);
|
|
sendMessage(mpbl);
|
|
}
|
|
|
|
void ServerHandler::requestUserList() {
|
|
MumbleProto::UserList mpul;
|
|
sendMessage(mpul);
|
|
}
|
|
|
|
void ServerHandler::requestACL(unsigned int channel) {
|
|
MumbleProto::ACL mpacl;
|
|
mpacl.set_channel_id(channel);
|
|
mpacl.set_query(true);
|
|
sendMessage(mpacl);
|
|
}
|
|
|
|
void ServerHandler::registerUser(unsigned int uiSession) {
|
|
MumbleProto::UserState mpus;
|
|
mpus.set_session(uiSession);
|
|
mpus.set_user_id(0);
|
|
sendMessage(mpus);
|
|
}
|
|
|
|
void ServerHandler::kickBanUser(unsigned int uiSession, const QString &reason, bool ban) {
|
|
MumbleProto::UserRemove mpur;
|
|
mpur.set_session(uiSession);
|
|
mpur.set_reason(u8(reason));
|
|
mpur.set_ban(ban);
|
|
sendMessage(mpur);
|
|
}
|
|
|
|
void ServerHandler::sendUserTextMessage(unsigned int uiSession, const QString &message_) {
|
|
MumbleProto::TextMessage mptm;
|
|
mptm.add_session(uiSession);
|
|
mptm.set_message(u8(message_));
|
|
sendMessage(mptm);
|
|
}
|
|
|
|
void ServerHandler::sendChannelTextMessage(unsigned int channel, const QString &message_, bool tree) {
|
|
MumbleProto::TextMessage mptm;
|
|
if (tree) {
|
|
mptm.add_tree_id(channel);
|
|
} else {
|
|
mptm.add_channel_id(channel);
|
|
|
|
if (message_ == QString::fromUtf8(Global::get().ccHappyEaster + 10))
|
|
Global::get().bHappyEaster = true;
|
|
}
|
|
mptm.set_message(u8(message_));
|
|
sendMessage(mptm);
|
|
}
|
|
|
|
void ServerHandler::setUserComment(unsigned int uiSession, const QString &comment) {
|
|
MumbleProto::UserState mpus;
|
|
mpus.set_session(uiSession);
|
|
mpus.set_comment(u8(comment));
|
|
sendMessage(mpus);
|
|
}
|
|
|
|
void ServerHandler::setUserTexture(unsigned int uiSession, const QByteArray &qba) {
|
|
QByteArray texture;
|
|
|
|
if ((m_version >= Version::fromComponents(1, 2, 2)) || qba.isEmpty()) {
|
|
texture = qba;
|
|
} else {
|
|
QByteArray raw = qba;
|
|
|
|
QBuffer qb(&raw);
|
|
qb.open(QIODevice::ReadOnly);
|
|
|
|
QImageReader qir;
|
|
qir.setDecideFormatFromContent(false);
|
|
|
|
QByteArray fmt;
|
|
if (!RichTextImage::isValidImage(qba, fmt)) {
|
|
return;
|
|
}
|
|
|
|
qir.setFormat(fmt);
|
|
qir.setDevice(&qb);
|
|
|
|
QSize sz = qir.size();
|
|
const int TEX_MAX_WIDTH = 600;
|
|
const int TEX_MAX_HEIGHT = 60;
|
|
const int TEX_RGBA_SIZE = TEX_MAX_WIDTH * TEX_MAX_HEIGHT * 4;
|
|
sz.scale(TEX_MAX_WIDTH, TEX_MAX_HEIGHT, Qt::KeepAspectRatio);
|
|
qir.setScaledSize(sz);
|
|
|
|
QImage tex = qir.read();
|
|
if (tex.isNull()) {
|
|
return;
|
|
}
|
|
|
|
raw = QByteArray(TEX_RGBA_SIZE, 0);
|
|
QImage img(reinterpret_cast< unsigned char * >(raw.data()), TEX_MAX_WIDTH, TEX_MAX_HEIGHT,
|
|
QImage::Format_ARGB32);
|
|
|
|
QPainter imgp(&img);
|
|
imgp.setRenderHint(QPainter::Antialiasing);
|
|
imgp.setRenderHint(QPainter::TextAntialiasing);
|
|
imgp.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
|
imgp.drawImage(0, 0, tex);
|
|
|
|
texture = qCompress(QByteArray(reinterpret_cast< const char * >(img.bits()), TEX_RGBA_SIZE));
|
|
}
|
|
|
|
MumbleProto::UserState mpus;
|
|
mpus.set_session(uiSession);
|
|
mpus.set_texture(blob(texture));
|
|
sendMessage(mpus);
|
|
|
|
if (!texture.isEmpty()) {
|
|
database->setBlob(sha1(texture), texture);
|
|
}
|
|
}
|
|
|
|
void ServerHandler::setTokens(const QStringList &tokens) {
|
|
MumbleProto::Authenticate msg;
|
|
foreach (const QString &qs, tokens)
|
|
msg.add_tokens(u8(qs));
|
|
sendMessage(msg);
|
|
}
|
|
|
|
void ServerHandler::removeChannel(unsigned int channel) {
|
|
MumbleProto::ChannelRemove mpcr;
|
|
mpcr.set_channel_id(channel);
|
|
sendMessage(mpcr);
|
|
}
|
|
|
|
void ServerHandler::addChannelLink(unsigned int channel, unsigned int link) {
|
|
MumbleProto::ChannelState mpcs;
|
|
mpcs.set_channel_id(channel);
|
|
mpcs.add_links_add(link);
|
|
sendMessage(mpcs);
|
|
}
|
|
|
|
void ServerHandler::removeChannelLink(unsigned int channel, unsigned int link) {
|
|
MumbleProto::ChannelState mpcs;
|
|
mpcs.set_channel_id(channel);
|
|
mpcs.add_links_remove(link);
|
|
sendMessage(mpcs);
|
|
}
|
|
|
|
void ServerHandler::requestChannelPermissions(unsigned int channel) {
|
|
MumbleProto::PermissionQuery mppq;
|
|
mppq.set_channel_id(channel);
|
|
sendMessage(mppq);
|
|
}
|
|
|
|
void ServerHandler::setSelfMuteDeafState(bool mute, bool deaf) {
|
|
MumbleProto::UserState mpus;
|
|
mpus.set_self_mute(mute);
|
|
mpus.set_self_deaf(deaf);
|
|
sendMessage(mpus);
|
|
}
|
|
|
|
void ServerHandler::announceRecordingState(bool recording) {
|
|
MumbleProto::UserState mpus;
|
|
mpus.set_recording(recording);
|
|
sendMessage(mpus);
|
|
}
|
|
|
|
QUrl ServerHandler::getServerURL(bool withPassword) const {
|
|
QUrl url;
|
|
|
|
url.setScheme(QLatin1String("mumble"));
|
|
url.setHost(qsHostName);
|
|
if (usPort != DEFAULT_MUMBLE_PORT) {
|
|
url.setPort(usPort);
|
|
}
|
|
|
|
url.setUserName(qsUserName);
|
|
|
|
if (withPassword && !qsPassword.isEmpty()) {
|
|
url.setPassword(qsPassword);
|
|
}
|
|
|
|
return url;
|
|
}
|