// Copyright 2009-2021 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 . #include "CrashReporter.h" #include "EnvUtils.h" #include "NetworkConfig.h" #include "OSInfo.h" #include "Global.h" #include #include #include #include #include CrashReporter::CrashReporter(QWidget *p) : QDialog(p) { setWindowTitle(tr("Mumble Crash Report")); QVBoxLayout *vbl = new QVBoxLayout(this); QLabel *l; l = new QLabel(tr("

We're terribly sorry, but it seems Mumble has crashed. Do you want to send a crash report " "to the Mumble developers?

" "

The crash report contains a partial copy of Mumble's memory at the time it crashed, and will " "help the developers fix the problem.

")); vbl->addWidget(l); QHBoxLayout *hbl = new QHBoxLayout(); qleEmail = new QLineEdit(Global::get().qs->value(QLatin1String("crashemail")).toString()); l = new QLabel(tr("Email address (optional)")); l->setBuddy(qleEmail); hbl->addWidget(l); hbl->addWidget(qleEmail, 1); vbl->addLayout(hbl); qteDescription = new QTextEdit(); l->setBuddy(qteDescription); l = new QLabel(tr("Please describe briefly, in English, what you were doing at the time of the crash")); vbl->addWidget(l); vbl->addWidget(qteDescription, 1); QPushButton *pbOk = new QPushButton(tr("Send Report")); pbOk->setDefault(true); QPushButton *pbCancel = new QPushButton(tr("Don't send report")); pbCancel->setAutoDefault(false); QDialogButtonBox *dbb = new QDialogButtonBox(Qt::Horizontal); dbb->addButton(pbOk, QDialogButtonBox::AcceptRole); dbb->addButton(pbCancel, QDialogButtonBox::RejectRole); connect(dbb, SIGNAL(accepted()), this, SLOT(accept())); connect(dbb, SIGNAL(rejected()), this, SLOT(reject())); vbl->addWidget(dbb); qelLoop = new QEventLoop(this); qpdProgress = nullptr; qnrReply = nullptr; } CrashReporter::~CrashReporter() { Global::get().qs->setValue(QLatin1String("crashemail"), qleEmail->text()); delete qnrReply; } void CrashReporter::uploadFinished() { qpdProgress->reset(); QVariant varStatus = qnrReply->attribute(QNetworkRequest::HttpStatusCodeAttribute); if (!varStatus.isValid()) { qWarning() << "CrashReporter.cpp: Invalid HTTP code attribute"; } int httpStatusCode = varStatus.toInt(); if (httpStatusCode != 0) { // The 2xx HTTP status codes are considered success if (httpStatusCode >= 200 && httpStatusCode < 300) { QMessageBox::information(nullptr, tr("Crash upload successful"), tr("Thank you for helping make Mumble better!")); } else { QMessageBox::critical(nullptr, tr("Crash upload failed"), tr("HTTP error %1: \"%2\"") .arg(httpStatusCode) .arg(qnrReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString())); } } else { QMessageBox::critical(nullptr, tr("Crash upload failed"), tr("Internal error encountered in CrashReporter.cpp: Received network reply does not " "contain an HTTP status code." " Please inform a developer about error code %1") .arg(qnrReply->error())); } qelLoop->exit(0); } void CrashReporter::uploadProgress(qint64 sent, qint64 total) { qpdProgress->setMaximum(static_cast< int >(total)); qpdProgress->setValue(static_cast< int >(sent)); } void CrashReporter::run() { QByteArray qbaDumpContents; QFile qfCrashDump(Global::get().qdBasePath.filePath(QLatin1String("mumble.dmp"))); if (!qfCrashDump.exists()) return; qfCrashDump.open(QIODevice::ReadOnly); #if defined(Q_OS_WIN) /* On Windows, the .dmp file is a real minidump. */ if (qfCrashDump.peek(4) != "MDMP") return; qbaDumpContents = qfCrashDump.readAll(); #elif defined(Q_OS_MAC) /* * On OSX, the .dmp file is simply a dummy file that we * use to find the *real* crash dump, made by the OSX * built in crash reporter. */ QFileInfo qfiDump(qfCrashDump); QDateTime qdtModification = qfiDump.lastModified(); /* Find the real crash report. */ QDir qdCrashReports(QDir::home().absolutePath() + QLatin1String("/Library/Logs/DiagnosticReports/")); if (!qdCrashReports.exists()) { qdCrashReports.setPath(QDir::home().absolutePath() + QLatin1String("/Library/Logs/CrashReporter/")); } QStringList qslFilters; qslFilters << QString::fromLatin1("Mumble_*.crash"); qdCrashReports.setNameFilters(qslFilters); qdCrashReports.setSorting(QDir::Time); QFileInfoList qfilEntries = qdCrashReports.entryInfoList(); /* * Figure out if our delta is sufficiently close to the Apple crash dump, or * if something weird happened. */ foreach (QFileInfo fi, qfilEntries) { qint64 delta = qAbs< qint64 >(qdtModification.secsTo(fi.lastModified())); if (delta < 8) { QFile f(fi.absoluteFilePath()); f.open(QIODevice::ReadOnly); qbaDumpContents = f.readAll(); break; } } #endif QString details; #ifdef Q_OS_WIN { QTemporaryFile qtf; if (qtf.open()) { qtf.close(); QProcess qp; QStringList qsl; qsl << QLatin1String("/t"); qsl << qtf.fileName(); QString app = QLatin1String("dxdiag.exe"); QString systemRoot = EnvUtils::getenv(QLatin1String("SystemRoot")); if (systemRoot.count() > 0) { app = QDir::fromNativeSeparators(systemRoot + QLatin1String("/System32/dxdiag.exe")); } qp.start(app, qsl); if (qp.waitForFinished(30000)) { if (qtf.open()) { QByteArray qba = qtf.readAll(); details = QString::fromLocal8Bit(qba); } } else { details = QLatin1String("Failed to run dxdiag"); } qp.kill(); } } #endif if (qbaDumpContents.isEmpty()) { qWarning("CrashReporter: Empty crash dump file, not reporting."); return; } if (exec() == QDialog::Accepted) { qpdProgress = new QProgressDialog(tr("Uploading crash report"), tr("Abort upload"), 0, 100, this); qpdProgress->setMinimumDuration(500); qpdProgress->setValue(0); connect(qpdProgress, SIGNAL(canceled()), qelLoop, SLOT(quit())); QString boundary = QString::fromLatin1("---------------------------%1").arg(QDateTime::currentDateTime().toTime_t()); QString os = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; " "name=\"os\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2 %3\r\n") .arg(boundary, OSInfo::getOS(), OSInfo::getOSVersion()); QString ver = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; " "name=\"ver\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2 %3\r\n") .arg(boundary, QLatin1String(MUMTEXT(MUMBLE_VERSION)), QLatin1String(MUMBLE_RELEASE)); QString email = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; " "name=\"email\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2\r\n") .arg(boundary, qleEmail->text()); QString descr = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; " "name=\"desc\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2\r\n") .arg(boundary, qteDescription->toPlainText()); QString det = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; " "name=\"details\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2\r\n") .arg(boundary, details); QString head = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; name=\"dump\"; " "filename=\"mumble.dmp\"\r\nContent-Type: binary/octet-stream\r\n\r\n") .arg(boundary); QString end = QString::fromLatin1("\r\n--%1--\r\n").arg(boundary); QByteArray post = os.toUtf8() + ver.toUtf8() + email.toUtf8() + descr.toUtf8() + det.toUtf8() + head.toUtf8() + qbaDumpContents + end.toUtf8(); QUrl url(QLatin1String("https://crash-report.mumble.info/v1/report")); QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, QString::fromLatin1("multipart/form-data; boundary=%1").arg(boundary)); req.setHeader(QNetworkRequest::ContentLengthHeader, QString::number(post.size())); Network::prepareRequest(req); qnrReply = Global::get().nam->post(req, post); connect(qnrReply, SIGNAL(finished()), this, SLOT(uploadFinished())); connect(qnrReply, SIGNAL(uploadProgress(qint64, qint64)), this, SLOT(uploadProgress(qint64, qint64))); qelLoop->exec(QEventLoop::DialogExec); } if (!qfCrashDump.remove()) qWarning("CrashReporeter: Unable to remove crash file."); }