diff --git a/nvhttp.cpp b/nvhttp.cpp index d3b50ed2..8caf423a 100644 --- a/nvhttp.cpp +++ b/nvhttp.cpp @@ -2,8 +2,14 @@ #include #include +#include +#include +#include -NvHTTP::NvHTTP(QString address) +#define REQUEST_TIMEOUT_MS 5000 + +NvHTTP::NvHTTP(QString address) : + m_Address(address) { m_BaseUrlHttp.setScheme("http"); m_BaseUrlHttps.setScheme("https"); @@ -13,21 +19,191 @@ NvHTTP::NvHTTP(QString address) m_BaseUrlHttps.setPort(47984); } +NvComputer +NvHTTP::getComputerInfo() +{ + NvComputer computer; + QString serverInfo = getServerInfo(); + + computer.m_Name = getXmlString(serverInfo, "hostname"); + if (computer.m_Name == nullptr) + { + computer.m_Name = "UNKNOWN"; + } + + computer.m_Uuid = getXmlString(serverInfo, "uniqueid"); + computer.m_MacAddress = getXmlString(serverInfo, "mac"); + + // If there's no LocalIP field, use the address we hit the server on + computer.m_LocalAddress = getXmlString(serverInfo, "LocalIP"); + if (computer.m_LocalAddress == nullptr) + { + computer.m_LocalAddress = m_Address; + } + + // If there's no ExternalIP field, use the address we hit the server on + computer.m_RemoteAddress = getXmlString(serverInfo, "ExternalIP"); + if (computer.m_RemoteAddress == nullptr) + { + computer.m_RemoteAddress = m_Address; + } + + computer.m_PairState = getXmlString(serverInfo, "PairStatus") == "1" ? + NvComputer::PS_PAIRED : NvComputer::PS_NOT_PAIRED; + + computer.m_RunningGameId = getCurrentGame(serverInfo); + + computer.m_State = NvComputer::CS_ONLINE; + + return computer; +} + +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")) { + return getXmlString(serverInfo, "currentgame").toInt(); + } + else { + return 0; + } +} + +QString +NvHTTP::getServerInfo() +{ + QString serverInfo; + + try + { + // Always try HTTPS first, since it properly reports + // pairing status (and a few other attributes). + serverInfo = openConnectionToString(m_BaseUrlHttps, + "serverinfo", + nullptr, + true); + // Throws if the request failed + verifyResponseStatus(serverInfo); + } + catch (GfeHttpResponseException& e) + { + if (e.getStatusCode() == 401) + { + // Certificate validation error, fallback to HTTP + serverInfo = openConnectionToString(m_BaseUrlHttps, + "serverinfo", + nullptr, + true); + verifyResponseStatus(serverInfo); + } + } + + return serverInfo; +} + +void +NvHTTP::verifyResponseStatus(QString xml) +{ + QXmlStreamReader xmlReader(xml); + + while (xmlReader.readNextStartElement()) + { + if (xmlReader.name() == "root") + { + int statusCode = xmlReader.attributes().value("status_code").toInt(); + if (statusCode == 200) + { + // Successful + return; + } + else + { + QString statusMessage = xmlReader.attributes().value("status_message").toString(); + qDebug() << "Request failed: " << statusCode << " " << statusMessage; + throw GfeHttpResponseException(statusCode, statusMessage); + } + } + } +} + +QString +NvHTTP::getXmlString(QString xml, + QString tagName) +{ + QXmlStreamReader xmlReader(xml); + + while (xmlReader.readNextStartElement()) + { + if (xmlReader.name() == tagName) + { + return xmlReader.text().toString(); + } + } + + return nullptr; +} + +QString +NvHTTP::openConnectionToString(QUrl baseUrl, + QString command, + QString arguments, + bool enableTimeout) +{ + QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout); + QString ret; + + ret = QTextStream(reply).readAll(); + delete reply; + + return ret; +} + QNetworkReply* NvHTTP::openConnection(QUrl baseUrl, QString command, QString arguments, bool enableTimeout) { + // Build a URL for the request QUrl url(baseUrl); - url.setPath(command + "?uniqueid=" + "0" + "&uuid=" + QUuid::createUuid().toString() + ((arguments != nullptr) ? (arguments + "&") : "")); QNetworkReply* reply = m_Nam.get(QNetworkRequest(url)); + + // Ignore self-signed certificate errors (since GFE uses them) reply->ignoreSslErrors(QList{ QSslError::SelfSignedCertificate }); + // Run the request with a timeout if requested + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + if (enableTimeout) + { + QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit())); + } + qDebug() << "Executing request: " << url.toString(); + loop.exec(QEventLoop::ExcludeUserInputEvents); + + // Abort the request if it timed out + if (!reply->isFinished()) + { + qDebug() << "Aborting timed out request for " << url.toString(); + reply->abort(); + } + + // Handle error + if (reply->error() != QNetworkReply::NoError) + { + qDebug() << command << " request for failed with error " << reply->error(); + delete reply; + throw new GfeHttpResponseException(reply->error(), reply->errorString()); + } + return reply; } diff --git a/nvhttp.h b/nvhttp.h index 04017331..f0a3fe5f 100644 --- a/nvhttp.h +++ b/nvhttp.h @@ -3,6 +3,65 @@ #include #include +class GfeHttpResponseException : public std::exception +{ +public: + GfeHttpResponseException(int statusCode, QString message) : + m_StatusCode(statusCode), + m_StatusMessage(message) + { + + } + + const char* what() const throw() + { + return m_StatusMessage.toStdString().c_str(); + } + + const char* getStatusMessage() + { + return m_StatusMessage.toStdString().c_str(); + } + + int getStatusCode() + { + return m_StatusCode; + } + +private: + int m_StatusCode; + QString m_StatusMessage; +}; + +class NvComputer +{ +public: + NvComputer() {} + + enum PairState + { + PS_UNKNOWN, + PS_PAIRED, + PS_NOT_PAIRED + }; + + enum ComputerState + { + CS_UNKNOWN, + CS_ONLINE, + CS_OFFLINE + }; + + QString m_Name; + QString m_Uuid; + QString m_MacAddress; + QString m_LocalAddress; + QString m_RemoteAddress; + int m_RunningGameId; + PairState m_PairState; + ComputerState m_State; +}; + class NvHTTP { public: @@ -15,6 +74,29 @@ private: QString arguments, bool enableTimeout); + NvComputer + getComputerInfo(); + + int + getCurrentGame(QString serverInfo); + + QString + getServerInfo(); + + void + verifyResponseStatus(QString xml); + + QString + getXmlString(QString xml, + QString tagName); + + QString + openConnectionToString(QUrl baseUrl, + QString command, + QString arguments, + bool enableTimeout); + + QString m_Address; QUrl m_BaseUrlHttp; QUrl m_BaseUrlHttps; QNetworkAccessManager m_Nam;