moonlight-qt/app/backend/nvhttp.cpp

522 lines
17 KiB
C++
Raw Normal View History

2018-04-28 22:39:50 +00:00
#include "nvhttp.h"
2018-06-24 07:14:23 +00:00
#include <Limelight.h>
2018-04-28 22:39:50 +00:00
2018-04-29 07:55:18 +00:00
#include <QDebug>
2018-04-28 22:39:50 +00:00
#include <QUuid>
#include <QtNetwork/QNetworkReply>
2018-04-29 00:08:05 +00:00
#include <QEventLoop>
#include <QTimer>
#include <QXmlStreamReader>
2018-04-29 06:29:45 +00:00
#include <QSslKey>
2018-06-27 06:49:44 +00:00
#include <QImageReader>
#include <QtEndian>
2018-11-04 22:13:56 +00:00
#include <QNetworkProxy>
2018-04-28 22:39:50 +00:00
#define FAST_FAIL_TIMEOUT_MS 2000
2018-04-29 00:08:05 +00:00
#define REQUEST_TIMEOUT_MS 5000
2019-01-06 22:35:33 +00:00
#define LAUNCH_TIMEOUT_MS 120000
#define RESUME_TIMEOUT_MS 30000
#define QUIT_TIMEOUT_MS 30000
2018-04-29 00:08:05 +00:00
2018-12-22 02:08:07 +00:00
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
m_ServerCert(serverCert)
2018-04-28 22:39:50 +00:00
{
m_BaseUrlHttp.setScheme("http");
m_BaseUrlHttps.setScheme("https");
m_BaseUrlHttp.setPort(47989);
m_BaseUrlHttps.setPort(47984);
2018-11-04 22:13:56 +00:00
2019-08-01 05:07:20 +00:00
setAddress(address);
2018-11-04 22:13:56 +00:00
// Never use a proxy server
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
m_Nam.setProxy(noProxy);
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
2018-04-28 22:39:50 +00:00
}
2018-12-23 03:55:28 +00:00
void NvHTTP::setServerCert(QSslCertificate serverCert)
{
m_ServerCert = serverCert;
}
2019-08-01 05:07:20 +00:00
void NvHTTP::setAddress(QString address)
{
Q_ASSERT(!address.isEmpty());
m_Address = address;
m_BaseUrlHttp.setHost(address);
m_BaseUrlHttps.setHost(address);
}
QString NvHTTP::address()
{
return m_Address;
}
2018-04-29 02:01:00 +00:00
QVector<int>
2018-07-06 06:12:55 +00:00
NvHTTP::parseQuad(QString quad)
2018-04-29 02:01:00 +00:00
{
QVector<int> ret;
// Return an empty vector for old GFE versions
// that were missing GfeVersion.
if (quad.isEmpty()) {
return ret;
}
QStringList parts = quad.split(".");
2021-03-03 00:14:15 +00:00
ret.reserve(parts.length());
for (int i = 0; i < parts.length(); i++)
2018-04-29 02:01:00 +00:00
{
ret.append(parts.at(i).toInt());
}
2018-04-29 05:14:27 +00:00
return ret;
2018-04-29 02:01:00 +00:00
}
2018-04-29 00:08:05 +00:00
int
NvHTTP::getCurrentGame(QString serverInfo)
{
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
// has the semantics that its name would indicate. To contain the effects of this change as much
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
QString serverState = getXmlString(serverInfo, "state");
if (serverState != nullptr && serverState.endsWith("_SERVER_BUSY"))
2018-04-29 02:01:00 +00:00
{
2018-04-29 00:08:05 +00:00
return getXmlString(serverInfo, "currentgame").toInt();
}
2018-04-29 02:01:00 +00:00
else
{
2018-04-29 00:08:05 +00:00
return 0;
}
}
QString
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
2018-04-29 00:08:05 +00:00
{
QString serverInfo;
2018-12-23 03:55:28 +00:00
// Check if we have a pinned cert for this host yet
if (!m_ServerCert.isNull())
2018-04-29 00:08:05 +00:00
{
2018-12-23 03:55:28 +00:00
try
2018-04-29 00:08:05 +00:00
{
2018-12-23 03:55:28 +00:00
// Always try HTTPS first, since it properly reports
// pairing status (and a few other attributes).
serverInfo = openConnectionToString(m_BaseUrlHttps,
2018-04-29 00:08:05 +00:00
"serverinfo",
nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
logLevel);
2018-12-23 03:55:28 +00:00
// Throws if the request failed
2018-04-29 00:08:05 +00:00
verifyResponseStatus(serverInfo);
}
2018-12-23 03:55:28 +00:00
catch (const GfeHttpResponseException& e)
2018-04-29 05:14:27 +00:00
{
2018-12-23 03:55:28 +00:00
if (e.getStatusCode() == 401)
{
// Certificate validation error, fallback to HTTP
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
2018-12-23 03:55:28 +00:00
logLevel);
verifyResponseStatus(serverInfo);
}
else
{
// Rethrow real errors
throw e;
}
2018-04-29 05:14:27 +00:00
}
2018-04-29 00:08:05 +00:00
}
2018-12-23 03:55:28 +00:00
else
{
// Only use HTTP prior to pairing
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
2018-12-23 03:55:28 +00:00
logLevel);
verifyResponseStatus(serverInfo);
}
2018-04-29 00:08:05 +00:00
return serverInfo;
}
2018-06-24 07:14:23 +00:00
void
NvHTTP::launchApp(int appId,
PSTREAM_CONFIGURATION streamConfig,
bool sops,
bool localAudio,
int gamepadMask)
{
2018-07-22 02:16:08 +00:00
int riKeyId;
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
riKeyId = qFromBigEndian(riKeyId);
2018-06-24 07:14:23 +00:00
QString response =
openConnectionToString(m_BaseUrlHttps,
"launch",
"appid="+QString::number(appId)+
"&mode="+QString::number(streamConfig->width)+"x"+
QString::number(streamConfig->height)+"x"+
// Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 0 to ensure the correct resolution is set. We
// used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3.
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
2018-06-24 07:14:23 +00:00
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
2018-07-22 02:16:08 +00:00
"&rikeyid="+QString::number(riKeyId)+
2018-06-24 07:14:23 +00:00
(streamConfig->enableHdr ?
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
"")+
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
2018-06-24 07:14:23 +00:00
"&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask),
2019-01-06 22:35:33 +00:00
LAUNCH_TIMEOUT_MS);
2018-06-24 07:14:23 +00:00
// Throws if the request failed
verifyResponseStatus(response);
}
void
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
{
2018-07-22 02:16:08 +00:00
int riKeyId;
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
riKeyId = qFromBigEndian(riKeyId);
2018-06-24 07:14:23 +00:00
QString response =
openConnectionToString(m_BaseUrlHttps,
"resume",
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
2018-07-22 02:16:08 +00:00
"&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
2019-01-06 22:35:33 +00:00
RESUME_TIMEOUT_MS);
2018-06-24 07:14:23 +00:00
// Throws if the request failed
verifyResponseStatus(response);
}
void
NvHTTP::quitApp()
{
QString response =
openConnectionToString(m_BaseUrlHttps,
"cancel",
nullptr,
2019-01-06 22:35:33 +00:00
QUIT_TIMEOUT_MS);
2018-06-24 07:14:23 +00:00
// Throws if the request failed
verifyResponseStatus(response);
// Newer GFE versions will just return success even if quitting fails
// if we're not the original requestor.
2019-01-20 05:32:35 +00:00
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
2018-06-24 07:14:23 +00:00
// Generate a synthetic GfeResponseException letting the caller know
// that they can't kill someone else's stream.
throw GfeHttpResponseException(599, "");
}
}
QVector<NvDisplayMode>
NvHTTP::getDisplayModeList(QString serverInfo)
{
QXmlStreamReader xmlReader(serverInfo);
QVector<NvDisplayMode> modes;
while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) {
auto name = xmlReader.name();
if (name == QString("DisplayMode")) {
modes.append(NvDisplayMode());
}
else if (name == QString("Width")) {
modes.last().width = xmlReader.readElementText().toInt();
}
else if (name == QString("Height")) {
modes.last().height = xmlReader.readElementText().toInt();
}
else if (name == QString("RefreshRate")) {
modes.last().refreshRate = xmlReader.readElementText().toInt();
}
}
}
return modes;
}
2018-06-27 06:39:28 +00:00
QVector<NvApp>
NvHTTP::getAppList()
{
QString appxml = openConnectionToString(m_BaseUrlHttps,
"applist",
nullptr,
2019-01-06 22:35:33 +00:00
REQUEST_TIMEOUT_MS,
2019-01-20 05:32:35 +00:00
NvLogLevel::NVLL_ERROR);
2018-06-27 06:39:28 +00:00
verifyResponseStatus(appxml);
QXmlStreamReader xmlReader(appxml);
QVector<NvApp> apps;
while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) {
auto name = xmlReader.name();
if (name == QString("App")) {
2018-06-27 06:39:28 +00:00
// We must have a valid app before advancing to the next one
if (!apps.isEmpty() && !apps.last().isInitialized()) {
qWarning() << "Invalid applist XML";
Q_ASSERT(false);
return QVector<NvApp>();
}
apps.append(NvApp());
}
else if (name == QString("AppTitle")) {
2018-06-27 06:39:28 +00:00
apps.last().name = xmlReader.readElementText();
}
else if (name == QString("ID")) {
2018-06-27 06:39:28 +00:00
apps.last().id = xmlReader.readElementText().toInt();
}
else if (name == QString("IsHdrSupported")) {
2018-06-27 06:39:28 +00:00
apps.last().hdrSupported = xmlReader.readElementText() == "1";
}
else if (name == QString("IsAppCollectorGame")) {
apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
}
2018-06-27 06:39:28 +00:00
}
}
return apps;
}
2018-04-29 00:08:05 +00:00
void
NvHTTP::verifyResponseStatus(QString xml)
{
QXmlStreamReader xmlReader(xml);
while (xmlReader.readNextStartElement())
{
if (xmlReader.name() == QString("root"))
2018-04-29 00:08:05 +00:00
{
// Status code can be 0xFFFFFFFF in some rare cases on GFE 3.20.3, and
// QString::toInt() will fail in that case, so use QString::toUInt()
// and cast the result to an int instead.
int statusCode = (int)xmlReader.attributes().value("status_code").toUInt();
2018-04-29 00:08:05 +00:00
if (statusCode == 200)
{
// Successful
return;
}
else
{
QString statusMessage = xmlReader.attributes().value("status_message").toString();
if (statusCode != 401) {
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
qWarning() << "Request failed:" << statusCode << statusMessage;
}
if (statusCode == -1 && statusMessage == "Invalid") {
// Special case handling an audio capture error which GFE doesn't
// provide any useful status message for.
statusCode = 418;
2020-11-21 19:15:54 +00:00
statusMessage = tr("Missing audio capture device. Reinstalling GeForce Experience should resolve this error.");
}
2018-04-29 00:08:05 +00:00
throw GfeHttpResponseException(statusCode, statusMessage);
}
}
}
}
2018-06-27 06:49:44 +00:00
QImage
NvHTTP::getBoxArt(int appId)
{
QNetworkReply* reply = openConnection(m_BaseUrlHttps,
"appasset",
"appid="+QString::number(appId)+
"&AssetType=2&AssetIdx=0",
2019-01-06 22:35:33 +00:00
REQUEST_TIMEOUT_MS,
2019-01-20 05:32:35 +00:00
NvLogLevel::NVLL_VERBOSE);
2018-06-27 06:49:44 +00:00
QImage image = QImageReader(reply).read();
delete reply;
return image;
}
2018-04-29 08:48:41 +00:00
QByteArray
NvHTTP::getXmlStringFromHex(QString xml,
QString tagName)
{
QString str = getXmlString(xml, tagName);
if (str == nullptr)
{
return nullptr;
}
return QByteArray::fromHex(str.toLatin1());
}
2018-04-29 00:08:05 +00:00
QString
NvHTTP::getXmlString(QString xml,
QString tagName)
{
QXmlStreamReader xmlReader(xml);
2018-04-29 07:55:18 +00:00
while (!xmlReader.atEnd())
2018-04-29 00:08:05 +00:00
{
2018-04-29 07:55:18 +00:00
if (xmlReader.readNext() != QXmlStreamReader::StartElement)
{
continue;
}
2018-04-29 00:08:05 +00:00
if (xmlReader.name() == tagName)
{
2018-04-29 07:55:18 +00:00
return xmlReader.readElementText();
2018-04-29 00:08:05 +00:00
}
}
return nullptr;
}
void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors)
{
bool ignoreErrors = true;
if (m_ServerCert.isNull()) {
// We should never make an HTTPS request without a cert
Q_ASSERT(!m_ServerCert.isNull());
return;
}
2021-03-03 00:14:15 +00:00
for (const QSslError& error : errors) {
if (m_ServerCert != error.certificate()) {
ignoreErrors = false;
break;
}
}
if (ignoreErrors) {
reply->ignoreSslErrors(errors);
}
}
2018-04-29 00:08:05 +00:00
QString
NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
2019-01-06 22:35:33 +00:00
int timeoutMs,
NvLogLevel logLevel)
2018-04-29 00:08:05 +00:00
{
2019-01-06 22:35:33 +00:00
QNetworkReply* reply = openConnection(baseUrl, command, arguments, timeoutMs, logLevel);
2018-04-29 00:08:05 +00:00
QString ret;
2018-07-27 05:15:52 +00:00
QTextStream stream(reply);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
stream.setEncoding(QStringConverter::Utf8);
#else
2018-07-27 05:15:52 +00:00
stream.setCodec("UTF-8");
#endif
2018-07-27 05:15:52 +00:00
ret = stream.readAll();
2018-04-29 00:08:05 +00:00
delete reply;
return ret;
}
2018-04-28 22:39:50 +00:00
QNetworkReply*
NvHTTP::openConnection(QUrl baseUrl,
QString command,
QString arguments,
2019-01-06 22:35:33 +00:00
int timeoutMs,
NvLogLevel logLevel)
2018-04-28 22:39:50 +00:00
{
2018-04-29 00:08:05 +00:00
// Build a URL for the request
2018-04-28 22:39:50 +00:00
QUrl url(baseUrl);
2018-04-29 05:14:27 +00:00
url.setPath("/" + command);
// Use a common UID for Moonlight clients to allow them to quit
// games for each other (otherwise GFE gets screwed up and it requires
// manual intervention to solve).
url.setQuery("uniqueid=0123456789ABCDEF&uuid=" +
QUuid::createUuid().toRfc4122().toHex() +
2018-04-29 05:14:27 +00:00
((arguments != nullptr) ? ("&" + arguments) : ""));
2018-04-28 22:39:50 +00:00
QNetworkRequest request(url);
2018-04-29 06:29:45 +00:00
// Add our client certificate
2018-06-27 02:01:40 +00:00
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
2018-04-29 06:29:45 +00:00
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Disable HTTP/2 (GFE 3.22 doesn't like it) and Qt 6 enables it by default
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
2019-12-21 20:58:45 +00:00
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
2019-12-21 20:58:45 +00:00
2018-04-29 06:29:45 +00:00
QNetworkReply* reply = m_Nam.get(request);
2018-04-29 00:08:05 +00:00
// Run the request with a timeout if requested
QEventLoop loop;
2021-03-03 00:14:15 +00:00
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
2019-01-06 22:35:33 +00:00
if (timeoutMs) {
2021-03-03 00:14:15 +00:00
QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit);
2018-04-29 00:08:05 +00:00
}
2019-01-20 05:32:35 +00:00
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
qInfo() << "Executing request:" << url.toString();
}
2018-04-29 00:08:05 +00:00
loop.exec(QEventLoop::ExcludeUserInputEvents);
// Abort the request if it timed out
if (!reply->isFinished())
{
2019-01-20 05:32:35 +00:00
if (logLevel >= NvLogLevel::NVLL_ERROR) {
qWarning() << "Aborting timed out request for" << url.toString();
}
2018-04-29 00:08:05 +00:00
reply->abort();
}
2018-06-24 06:46:16 +00:00
// We must clear out cached authentication and connections or
// GFE will puke next time
m_Nam.clearAccessCache();
2018-04-29 00:08:05 +00:00
// Handle error
if (reply->error() != QNetworkReply::NoError)
2018-04-29 00:08:05 +00:00
{
2019-01-20 05:32:35 +00:00
if (logLevel >= NvLogLevel::NVLL_ERROR) {
qWarning() << command << " request failed with error " << reply->error();
}
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
// This will trigger falling back to HTTP for the serverinfo query
// then pairing again to get the updated certificate.
GfeHttpResponseException exception(401, "Server certificate mismatch");
delete reply;
throw exception;
}
else if (reply->error() == QNetworkReply::OperationCanceledError) {
2019-01-06 22:35:33 +00:00
QtNetworkReplyException exception(QNetworkReply::TimeoutError, "Request timed out");
delete reply;
throw exception;
}
else {
QtNetworkReplyException exception(reply->error(), reply->errorString());
delete reply;
throw exception;
}
2018-04-29 00:08:05 +00:00
}
2018-04-28 22:39:50 +00:00
return reply;
}