mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-06-15 21:22:40 +00:00
Rename http folder to backend to better align with the classes inside
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
#include "boxartmanager.h"
|
||||
|
||||
#include <QStandardPaths>
|
||||
#include <QImageReader>
|
||||
#include <QImageWriter>
|
||||
|
||||
BoxArtManager::BoxArtManager(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_BoxArtDir(
|
||||
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart"),
|
||||
m_PlaceholderImage(":/res/no_app_image.png")
|
||||
{
|
||||
if (!m_BoxArtDir.exists()) {
|
||||
m_BoxArtDir.mkpath(".");
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
BoxArtManager::getFilePathForBoxArt(NvComputer* computer, int appId)
|
||||
{
|
||||
QDir dir = m_BoxArtDir;
|
||||
|
||||
// Create the cache directory if it did not already exist
|
||||
if (!dir.exists(computer->uuid)) {
|
||||
dir.mkdir(computer->uuid);
|
||||
}
|
||||
|
||||
// Change to this computer's box art cache folder
|
||||
dir.cd(computer->uuid);
|
||||
|
||||
// Try to open the cached file
|
||||
return dir.filePath(QString::number(appId) + ".png");
|
||||
}
|
||||
|
||||
QImage BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
||||
{
|
||||
// Try to open the cached file
|
||||
QFile cacheFile(getFilePathForBoxArt(computer, app.id));
|
||||
if (cacheFile.open(QFile::ReadOnly)) {
|
||||
// Return what we have if it's a valid image
|
||||
QImage image = QImageReader(&cacheFile).read();
|
||||
if (!image.isNull()) {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we need to fetch asynchronously.
|
||||
// Kick off a worker on our thread pool to do just that.
|
||||
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
||||
QThreadPool::globalInstance()->start(netLoadTask);
|
||||
|
||||
// Return the placeholder then we can notify the caller
|
||||
// later when the real image is ready.
|
||||
return m_PlaceholderImage;
|
||||
}
|
||||
|
||||
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QImage image)
|
||||
{
|
||||
emit boxArtLoadComplete(computer, app, image);
|
||||
}
|
||||
|
||||
QImage BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||
{
|
||||
NvHTTP http(computer->activeAddress);
|
||||
|
||||
QImage image;
|
||||
try {
|
||||
image = http.getBoxArt(appId);
|
||||
} catch (...) {}
|
||||
|
||||
// Cache the box art on disk if it loaded
|
||||
if (!image.isNull()) {
|
||||
image.save(getFilePathForBoxArt(computer, appId));
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "computermanager.h"
|
||||
#include <QDir>
|
||||
#include <QImage>
|
||||
#include <QThreadPool>
|
||||
#include <QRunnable>
|
||||
|
||||
class BoxArtManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class NetworkBoxArtLoadTask;
|
||||
|
||||
public:
|
||||
explicit BoxArtManager(QObject *parent = nullptr);
|
||||
|
||||
QImage
|
||||
loadBoxArt(NvComputer* computer, NvApp& app);
|
||||
|
||||
signals:
|
||||
void
|
||||
boxArtLoadComplete(NvComputer* computer, NvApp app, QImage image);
|
||||
|
||||
public slots:
|
||||
|
||||
private slots:
|
||||
void
|
||||
handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QImage image);
|
||||
|
||||
private:
|
||||
QImage
|
||||
loadBoxArtFromNetwork(NvComputer* computer, int appId);
|
||||
|
||||
QString
|
||||
getFilePathForBoxArt(NvComputer* computer, int appId);
|
||||
|
||||
QDir m_BoxArtDir;
|
||||
QImage m_PlaceholderImage;
|
||||
};
|
||||
|
||||
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app)
|
||||
: m_Bam(boxArtManager),
|
||||
m_Computer(computer),
|
||||
m_App(app)
|
||||
{
|
||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QImage)),
|
||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QImage)));
|
||||
}
|
||||
|
||||
signals:
|
||||
void boxArtFetchCompleted(NvComputer* computer, NvApp app, QImage image);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
QImage image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||
if (image.isNull()) {
|
||||
// Give it another shot if it fails once
|
||||
image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||
}
|
||||
emit boxArtFetchCompleted(m_Computer, m_App, image);
|
||||
}
|
||||
|
||||
BoxArtManager* m_Bam;
|
||||
NvComputer* m_Computer;
|
||||
NvApp m_App;
|
||||
};
|
||||
@@ -0,0 +1,330 @@
|
||||
#include "computermanager.h"
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#define SER_HOSTS "hosts"
|
||||
#define SER_NAME "hostname"
|
||||
#define SER_UUID "uuid"
|
||||
#define SER_MAC "mac"
|
||||
#define SER_CODECSUPP "codecsupport"
|
||||
#define SER_LOCALADDR "localaddress"
|
||||
#define SER_REMOTEADDR "remoteaddress"
|
||||
#define SER_MANUALADDR "manualaddress"
|
||||
#define SER_APPLIST "apps"
|
||||
|
||||
#define SER_APPNAME "name"
|
||||
#define SER_APPID "id"
|
||||
#define SER_APPHDR "hdr"
|
||||
|
||||
NvComputer::NvComputer(QSettings& settings)
|
||||
{
|
||||
this->name = settings.value(SER_NAME).toString();
|
||||
this->uuid = settings.value(SER_UUID).toString();
|
||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
||||
this->serverCodecModeSupport = settings.value(SER_CODECSUPP).toInt();
|
||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
NvApp app;
|
||||
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
app.name = settings.value(SER_APPNAME).toString();
|
||||
app.id = settings.value(SER_APPID).toInt();
|
||||
app.hdrSupported = settings.value(SER_APPHDR).toBool();
|
||||
|
||||
this->appList.append(app);
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
this->activeAddress = nullptr;
|
||||
this->currentGameId = 0;
|
||||
this->pairState = PS_UNKNOWN;
|
||||
this->state = CS_UNKNOWN;
|
||||
}
|
||||
|
||||
void
|
||||
NvComputer::serialize(QSettings& settings)
|
||||
{
|
||||
QReadLocker lock(&this->lock);
|
||||
|
||||
settings.setValue(SER_NAME, name);
|
||||
settings.setValue(SER_UUID, uuid);
|
||||
settings.setValue(SER_MAC, macAddress);
|
||||
settings.setValue(SER_CODECSUPP, serverCodecModeSupport);
|
||||
settings.setValue(SER_LOCALADDR, localAddress);
|
||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty()) {
|
||||
settings.remove(SER_APPLIST);
|
||||
settings.beginWriteArray(SER_APPLIST);
|
||||
for (int i = 0; i < appList.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
settings.setValue(SER_APPNAME, appList[i].name);
|
||||
settings.setValue(SER_APPID, appList[i].id);
|
||||
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
}
|
||||
|
||||
NvComputer::NvComputer(QString address, QString serverInfo)
|
||||
{
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
if (this->name.isNull()) {
|
||||
this->name = "UNKNOWN";
|
||||
}
|
||||
|
||||
this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid");
|
||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||
if (newMacString != "00:00:00:00:00:00") {
|
||||
QStringList macOctets = newMacString.split(':');
|
||||
for (QString macOctet : macOctets) {
|
||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
|
||||
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
|
||||
if (!codecSupport.isNull()) {
|
||||
this->serverCodecModeSupport = codecSupport.toInt();
|
||||
}
|
||||
else {
|
||||
this->serverCodecModeSupport = 0;
|
||||
}
|
||||
|
||||
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
||||
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||
PS_PAIRED : PS_NOT_PAIRED;
|
||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||
this->activeAddress = address;
|
||||
this->state = NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
bool NvComputer::update(NvComputer& that)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
// Lock us for write and them for read
|
||||
QWriteLocker thisLock(&this->lock);
|
||||
QReadLocker thatLock(&that.lock);
|
||||
|
||||
// UUID may not change or we're talking to a new PC
|
||||
Q_ASSERT(this->uuid == that.uuid);
|
||||
|
||||
#define ASSIGN_IF_CHANGED(field) \
|
||||
if (this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \
|
||||
if (!that.field.isEmpty() && \
|
||||
this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
||||
ASSIGN_IF_CHANGED(pairState);
|
||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||
ASSIGN_IF_CHANGED(currentGameId);
|
||||
ASSIGN_IF_CHANGED(activeAddress);
|
||||
ASSIGN_IF_CHANGED(state);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||
return changed;
|
||||
}
|
||||
|
||||
ComputerManager::ComputerManager(QObject *parent)
|
||||
: QObject(parent),
|
||||
m_Polling(false)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
// Inflate our hosts from QSettings
|
||||
int hosts = settings.beginReadArray(SER_HOSTS);
|
||||
for (int i = 0; i < hosts; i++) {
|
||||
settings.setArrayIndex(i);
|
||||
NvComputer* computer = new NvComputer(settings);
|
||||
m_KnownHosts[computer->uuid] = computer;
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
void ComputerManager::saveHosts()
|
||||
{
|
||||
QSettings settings;
|
||||
QReadLocker lock(&m_Lock);
|
||||
|
||||
settings.remove(SER_HOSTS);
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
m_KnownHosts[m_KnownHosts.keys()[i]]->serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
void ComputerManager::startPolling()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
m_Polling = true;
|
||||
|
||||
QMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
startPollingComputer(i.value());
|
||||
}
|
||||
}
|
||||
|
||||
QVector<NvComputer*> ComputerManager::getComputers()
|
||||
{
|
||||
QReadLocker lock(&m_Lock);
|
||||
|
||||
return QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
||||
}
|
||||
|
||||
void ComputerManager::deleteHost(NvComputer* computer)
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
QThread* pollingThread = m_PollThreads[computer->uuid];
|
||||
if (pollingThread != nullptr) {
|
||||
pollingThread->requestInterruption();
|
||||
|
||||
// We must wait here because we're going to delete computer
|
||||
// and we can't do that out from underneath the poller.
|
||||
pollingThread->wait();
|
||||
}
|
||||
|
||||
m_PollThreads.remove(computer->uuid);
|
||||
m_KnownHosts.remove(computer->uuid);
|
||||
|
||||
delete computer;
|
||||
}
|
||||
|
||||
void ComputerManager::stopPollingAsync()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
m_Polling = false;
|
||||
|
||||
// Interrupt all threads, but don't wait for them to terminate
|
||||
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
|
||||
// The threads will delete themselves when they terminate
|
||||
i.value()->requestInterruption();
|
||||
}
|
||||
}
|
||||
|
||||
bool ComputerManager::addNewHost(QString address, bool mdns)
|
||||
{
|
||||
NvHTTP http(address);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo();
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NvComputer* newComputer = new NvComputer(address, serverInfo);
|
||||
|
||||
// Update addresses depending on the context
|
||||
if (mdns) {
|
||||
newComputer->localAddress = address;
|
||||
}
|
||||
else {
|
||||
newComputer->manualAddress = address;
|
||||
}
|
||||
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_Lock);
|
||||
NvComputer* existingComputer = m_KnownHosts[newComputer->uuid];
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
handleComputerStateChanged(existingComputer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Store this in our active sets
|
||||
m_KnownHosts[newComputer->uuid] = newComputer;
|
||||
|
||||
// Start polling if enabled (write lock required)
|
||||
startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// Tell our client about this new PC
|
||||
handleComputerStateChanged(newComputer);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
ComputerManager::handlePollThreadTermination(NvComputer* computer)
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
QThread* me = m_PollThreads[computer->uuid];
|
||||
Q_ASSERT(me != nullptr);
|
||||
|
||||
m_PollThreads.remove(computer->uuid);
|
||||
me->deleteLater();
|
||||
}
|
||||
|
||||
void
|
||||
ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
||||
{
|
||||
emit computerStateChanged(computer);
|
||||
|
||||
// Save updated hosts to QSettings
|
||||
saveHosts();
|
||||
}
|
||||
|
||||
// Must hold m_Lock for write
|
||||
void
|
||||
ComputerManager::startPollingComputer(NvComputer* computer)
|
||||
{
|
||||
if (!m_Polling) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_PollThreads.contains(computer->uuid)) {
|
||||
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
|
||||
return;
|
||||
}
|
||||
|
||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||
connect(thread, SIGNAL(terminating(NvComputer*)),
|
||||
this, SLOT(handlePollThreadTermination(NvComputer*)));
|
||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||
m_PollThreads[computer->uuid] = thread;
|
||||
thread->start();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
#pragma once
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QReadWriteLock>
|
||||
#include <QSettings>
|
||||
|
||||
class NvComputer
|
||||
{
|
||||
public:
|
||||
explicit NvComputer(QString address, QString serverInfo);
|
||||
|
||||
explicit NvComputer(QSettings& settings);
|
||||
|
||||
bool
|
||||
update(NvComputer& that);
|
||||
|
||||
void
|
||||
serialize(QSettings& settings);
|
||||
|
||||
enum PairState
|
||||
{
|
||||
PS_UNKNOWN,
|
||||
PS_PAIRED,
|
||||
PS_NOT_PAIRED
|
||||
};
|
||||
|
||||
enum ComputerState
|
||||
{
|
||||
CS_UNKNOWN,
|
||||
CS_ONLINE,
|
||||
CS_OFFLINE
|
||||
};
|
||||
|
||||
// Ephemeral traits
|
||||
ComputerState state;
|
||||
PairState pairState;
|
||||
QString activeAddress;
|
||||
int currentGameId;
|
||||
|
||||
// Persisted traits
|
||||
QString localAddress;
|
||||
QString remoteAddress;
|
||||
QString manualAddress;
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
QString uuid;
|
||||
int serverCodecModeSupport;
|
||||
QVector<NvApp> appList;
|
||||
|
||||
// Synchronization
|
||||
QReadWriteLock lock;
|
||||
};
|
||||
|
||||
// FIXME: MOC isn't finding Q_OBJECT properly when this is confined
|
||||
// to computermanager.cpp as it should be.
|
||||
class PcMonitorThread : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
#define TRIES_BEFORE_OFFLINING 2
|
||||
#define POLLS_PER_APPLIST_FETCH 10
|
||||
|
||||
public:
|
||||
PcMonitorThread(NvComputer* computer)
|
||||
: m_Computer(computer)
|
||||
{
|
||||
setObjectName("Polling thread for " + computer->name);
|
||||
}
|
||||
|
||||
private:
|
||||
bool tryPollComputer(QString address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo();
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NvComputer newState(address, serverInfo);
|
||||
|
||||
// Ensure the machine that responded is the one we intended to contact
|
||||
if (m_Computer->uuid != newState.uuid) {
|
||||
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
|
||||
return false;
|
||||
}
|
||||
|
||||
changed = m_Computer->update(newState);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool updateAppList(bool& changed)
|
||||
{
|
||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
||||
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
|
||||
QVector<NvApp> appList;
|
||||
|
||||
try {
|
||||
appList = http.getAppList();
|
||||
if (appList.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
if (m_Computer->appList != appList) {
|
||||
m_Computer->appList = appList;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
// Always fetch the applist the first time
|
||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||
while (!isInterruptionRequested()) {
|
||||
QVector<QString> uniqueAddressList;
|
||||
|
||||
// Start with addresses correctly ordered
|
||||
uniqueAddressList.append(m_Computer->activeAddress);
|
||||
uniqueAddressList.append(m_Computer->localAddress);
|
||||
uniqueAddressList.append(m_Computer->remoteAddress);
|
||||
uniqueAddressList.append(m_Computer->manualAddress);
|
||||
|
||||
// Prune duplicates (always giving precedence to the first)
|
||||
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
||||
if (uniqueAddressList[i].isEmpty() || uniqueAddressList[i].isNull()) {
|
||||
uniqueAddressList.remove(i);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
for (int j = i + 1; j < uniqueAddressList.count(); j++) {
|
||||
if (uniqueAddressList[i] == uniqueAddressList[j]) {
|
||||
// Always remove the later occurrence
|
||||
uniqueAddressList.remove(j);
|
||||
j--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We must have at least 1 address for this host
|
||||
Q_ASSERT(uniqueAddressList.count() != 0);
|
||||
|
||||
bool stateChanged = false;
|
||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING; i++) {
|
||||
for (auto& address : uniqueAddressList) {
|
||||
if (isInterruptionRequested()) {
|
||||
goto Terminate;
|
||||
}
|
||||
|
||||
if (tryPollComputer(address, stateChanged)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No need to continue retrying if we're online
|
||||
if (m_Computer->state == NvComputer::CS_ONLINE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we failed after all retry attempts
|
||||
// Note: we don't need to acquire the read lock here,
|
||||
// because we're on the writing thread.
|
||||
if (m_Computer->state != NvComputer::CS_ONLINE) {
|
||||
if (m_Computer->state != NvComputer::CS_OFFLINE) {
|
||||
m_Computer->state = NvComputer::CS_OFFLINE;
|
||||
stateChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
||||
pollsSinceLastAppListFetch++;
|
||||
if (m_Computer->state == NvComputer::CS_ONLINE &&
|
||||
m_Computer->pairState == NvComputer::PS_PAIRED &&
|
||||
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
|
||||
if (updateAppList(stateChanged)) {
|
||||
pollsSinceLastAppListFetch = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (stateChanged) {
|
||||
// Tell anyone listening that we've changed state
|
||||
emit computerStateChanged(m_Computer);
|
||||
}
|
||||
|
||||
// Wait a bit to poll again
|
||||
QThread::sleep(3);
|
||||
}
|
||||
|
||||
Terminate:
|
||||
emit terminating(m_Computer);
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
void terminating(NvComputer* computer);
|
||||
|
||||
private:
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
class ComputerManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ComputerManager(QObject *parent = nullptr);
|
||||
|
||||
void startPolling();
|
||||
|
||||
void stopPollingAsync();
|
||||
|
||||
bool addNewHost(QString address, bool mdns);
|
||||
|
||||
QVector<NvComputer*> getComputers();
|
||||
|
||||
// computer is deleted inside this call
|
||||
void deleteHost(NvComputer* computer);
|
||||
|
||||
signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private slots:
|
||||
void handleComputerStateChanged(NvComputer* computer);
|
||||
|
||||
void handlePollThreadTermination(NvComputer* computer);
|
||||
|
||||
private:
|
||||
void saveHosts();
|
||||
|
||||
void startPollingComputer(NvComputer* computer);
|
||||
|
||||
bool m_Polling;
|
||||
QReadWriteLock m_Lock;
|
||||
QMap<QString, NvComputer*> m_KnownHosts;
|
||||
QMap<QString, QThread*> m_PollThreads;
|
||||
};
|
||||
@@ -0,0 +1,215 @@
|
||||
#include "identitymanager.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QRandomGenerator64>
|
||||
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/x509.h>
|
||||
|
||||
#define SER_UNIQUEID "uniqueid"
|
||||
#define SER_CERT "certificate"
|
||||
#define SER_KEY "key"
|
||||
|
||||
IdentityManager* IdentityManager::s_Im = nullptr;
|
||||
|
||||
IdentityManager*
|
||||
IdentityManager::get()
|
||||
{
|
||||
// This will always be called first on the main thread,
|
||||
// so it's safe to initialize without locks.
|
||||
if (s_Im == nullptr) {
|
||||
s_Im = new IdentityManager();
|
||||
}
|
||||
|
||||
return s_Im;
|
||||
}
|
||||
|
||||
void IdentityManager::createCredentials(QSettings& settings)
|
||||
{
|
||||
X509* cert = X509_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cert);
|
||||
|
||||
EVP_PKEY* pk = EVP_PKEY_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(pk);
|
||||
|
||||
BIGNUM* bne = BN_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(bne);
|
||||
|
||||
RSA* rsa = RSA_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(rsa);
|
||||
|
||||
BN_set_word(bne, RSA_F4);
|
||||
RSA_generate_key_ex(rsa, 2048, bne, nullptr);
|
||||
|
||||
EVP_PKEY_assign_RSA(pk, rsa);
|
||||
|
||||
X509_set_version(cert, 2);
|
||||
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
X509_gmtime_adj(X509_get_notBefore(cert), 0);
|
||||
X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs
|
||||
#else
|
||||
ASN1_TIME* before = ASN1_STRING_dup(X509_get0_notBefore(cert));
|
||||
THROW_BAD_ALLOC_IF_NULL(before);
|
||||
ASN1_TIME* after = ASN1_STRING_dup(X509_get0_notAfter(cert));
|
||||
THROW_BAD_ALLOC_IF_NULL(after);
|
||||
|
||||
X509_gmtime_adj(before, 0);
|
||||
X509_gmtime_adj(after, 60 * 60 * 24 * 365 * 20); // 20 yrs
|
||||
|
||||
X509_set1_notBefore(cert, before);
|
||||
X509_set1_notAfter(cert, after);
|
||||
|
||||
ASN1_STRING_free(before);
|
||||
ASN1_STRING_free(after);
|
||||
#endif
|
||||
|
||||
X509_set_pubkey(cert, pk);
|
||||
|
||||
X509_NAME* name = X509_get_subject_name(cert);
|
||||
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
||||
reinterpret_cast<unsigned char *>(const_cast<char*>("NVIDIA GameStream Client")),
|
||||
-1, -1, 0);
|
||||
X509_set_issuer_name(cert, name);
|
||||
|
||||
X509_sign(cert, pk, EVP_sha1());
|
||||
|
||||
BIO* biokey = BIO_new(BIO_s_mem());
|
||||
THROW_BAD_ALLOC_IF_NULL(biokey);
|
||||
PEM_write_bio_PrivateKey(biokey, pk, NULL, NULL, 0, NULL, NULL);
|
||||
|
||||
BIO* biocert = BIO_new(BIO_s_mem());
|
||||
THROW_BAD_ALLOC_IF_NULL(biocert);
|
||||
PEM_write_bio_X509(biocert, cert);
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(biokey, &mem);
|
||||
m_CachedPrivateKey = QByteArray(mem->data, (int)mem->length);
|
||||
|
||||
BIO_get_mem_ptr(biocert, &mem);
|
||||
m_CachedPemCert = QByteArray(mem->data, (int)mem->length);
|
||||
|
||||
X509_free(cert);
|
||||
EVP_PKEY_free(pk);
|
||||
BN_free(bne);
|
||||
BIO_free(biokey);
|
||||
BIO_free(biocert);
|
||||
|
||||
settings.setValue(SER_CERT, m_CachedPemCert);
|
||||
settings.setValue(SER_KEY, m_CachedPrivateKey);
|
||||
|
||||
qDebug() << "Wrote new identity credentials to settings";
|
||||
}
|
||||
|
||||
IdentityManager::IdentityManager()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
m_CachedPemCert = settings.value(SER_CERT).toByteArray();
|
||||
m_CachedPrivateKey = settings.value(SER_KEY).toByteArray();
|
||||
|
||||
if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) {
|
||||
qDebug() << "No existing credentials found";
|
||||
createCredentials(settings);
|
||||
}
|
||||
else if (getSslCertificate().isNull()) {
|
||||
qWarning() << "Certificate is unreadable";
|
||||
createCredentials(settings);
|
||||
}
|
||||
else if (getSslKey().isNull()) {
|
||||
qWarning() << "Private key is unreadable";
|
||||
createCredentials(settings);
|
||||
}
|
||||
|
||||
// We should have valid credentials now. If not, we're screwed
|
||||
if (getSslCertificate().isNull()) {
|
||||
qFatal("Newly generated certificate is unreadable");
|
||||
}
|
||||
if (getSslKey().isNull()) {
|
||||
qFatal("Newly generated private key is unreadable");
|
||||
}
|
||||
}
|
||||
|
||||
QSslCertificate
|
||||
IdentityManager::getSslCertificate()
|
||||
{
|
||||
if (m_CachedSslCert.isNull()) {
|
||||
m_CachedSslCert = QSslCertificate(m_CachedPemCert);
|
||||
}
|
||||
return m_CachedSslCert;
|
||||
}
|
||||
|
||||
QSslKey
|
||||
IdentityManager::getSslKey()
|
||||
{
|
||||
if (m_CachedSslKey.isNull()) {
|
||||
BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free(bio);
|
||||
|
||||
bio = BIO_new(BIO_s_mem());
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
// We must write out our PEM in the old PKCS1 format for SecureTransport
|
||||
// on macOS/iOS to be able to read it.
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(bio, &mem);
|
||||
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
|
||||
|
||||
m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa);
|
||||
|
||||
BIO_free(bio);
|
||||
EVP_PKEY_free(pk);
|
||||
}
|
||||
return m_CachedSslKey;
|
||||
}
|
||||
|
||||
QSslConfiguration
|
||||
IdentityManager::getSslConfig()
|
||||
{
|
||||
QSslConfiguration sslConfig(QSslConfiguration::defaultConfiguration());
|
||||
sslConfig.setLocalCertificate(getSslCertificate());
|
||||
sslConfig.setPrivateKey(getSslKey());
|
||||
return sslConfig;
|
||||
}
|
||||
|
||||
QString
|
||||
IdentityManager::getUniqueId()
|
||||
{
|
||||
if (m_CachedUniqueId.isNull()) {
|
||||
QSettings settings;
|
||||
|
||||
// Load the unique ID from settings
|
||||
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
|
||||
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
|
||||
qDebug() << "Loaded unique ID from settings: " << m_CachedUniqueId;
|
||||
}
|
||||
else {
|
||||
// Generate a new unique ID in base 16
|
||||
m_CachedUniqueId = QString::number(
|
||||
QRandomGenerator64::securelySeeded().generate64(), 16);
|
||||
|
||||
qDebug() << "Generated new unique ID: " << m_CachedUniqueId;
|
||||
|
||||
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
|
||||
}
|
||||
}
|
||||
return m_CachedUniqueId;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
IdentityManager::getCertificate()
|
||||
{
|
||||
return m_CachedPemCert;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
IdentityManager::getPrivateKey()
|
||||
{
|
||||
return m_CachedPrivateKey;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSslConfiguration>
|
||||
#include <QSslCertificate>
|
||||
#include <QSslKey>
|
||||
#include <QSettings>
|
||||
|
||||
class IdentityManager
|
||||
{
|
||||
public:
|
||||
QString
|
||||
getUniqueId();
|
||||
|
||||
QByteArray
|
||||
getCertificate();
|
||||
|
||||
QByteArray
|
||||
getPrivateKey();
|
||||
|
||||
QSslConfiguration
|
||||
getSslConfig();
|
||||
|
||||
static
|
||||
IdentityManager*
|
||||
get();
|
||||
|
||||
private:
|
||||
IdentityManager();
|
||||
|
||||
QSslCertificate
|
||||
getSslCertificate();
|
||||
|
||||
QSslKey
|
||||
getSslKey();
|
||||
|
||||
void
|
||||
createCredentials(QSettings& settings);
|
||||
|
||||
// Initialized in constructor
|
||||
QByteArray m_CachedPrivateKey;
|
||||
QByteArray m_CachedPemCert;
|
||||
|
||||
// Lazy initialized
|
||||
QString m_CachedUniqueId;
|
||||
QSslCertificate m_CachedSslCert;
|
||||
QSslKey m_CachedSslKey;
|
||||
|
||||
static IdentityManager* s_Im;
|
||||
};
|
||||
@@ -0,0 +1,366 @@
|
||||
#include "nvhttp.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QSslKey>
|
||||
#include <QImageReader>
|
||||
|
||||
#define REQUEST_TIMEOUT_MS 5000
|
||||
|
||||
NvHTTP::NvHTTP(QString address) :
|
||||
m_Address(address)
|
||||
{
|
||||
m_BaseUrlHttp.setScheme("http");
|
||||
m_BaseUrlHttps.setScheme("https");
|
||||
m_BaseUrlHttp.setHost(address);
|
||||
m_BaseUrlHttps.setHost(address);
|
||||
m_BaseUrlHttp.setPort(47989);
|
||||
m_BaseUrlHttps.setPort(47984);
|
||||
}
|
||||
|
||||
QVector<int>
|
||||
NvHTTP::getServerVersionQuad(QString serverInfo)
|
||||
{
|
||||
QString quad = getXmlString(serverInfo, "appversion");
|
||||
QStringList parts = quad.split(".");
|
||||
QVector<int> ret;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
ret.append(parts.at(i).toInt());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
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 (const GfeHttpResponseException& e)
|
||||
{
|
||||
if (e.getStatusCode() == 401)
|
||||
{
|
||||
// Certificate validation error, fallback to HTTP
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rethrow real errors
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
static QString
|
||||
getSurroundAudioInfoString(int audioConfig)
|
||||
{
|
||||
int channelMask;
|
||||
int channelCount;
|
||||
|
||||
switch (audioConfig)
|
||||
{
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
channelCount = 2;
|
||||
channelMask = 0x3;
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
channelCount = 6;
|
||||
channelMask = 0xFC;
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return QString::number(channelMask << 16 | channelCount);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask)
|
||||
{
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"launch",
|
||||
"appid="+QString::number(appId)+
|
||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||
QString::number(streamConfig->height)+"x"+
|
||||
QString::number(streamConfig->fps)+
|
||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||
"&rikeyid="+QString::number(*(int*)streamConfig->remoteInputAesIv)+
|
||||
(streamConfig->enableHdr ?
|
||||
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
||||
"")+
|
||||
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||
"&gcmap="+QString::number(gamepadMask),
|
||||
false);
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||
{
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"resume",
|
||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||
"&rikeyid="+QString::number(*(int*)streamConfig->remoteInputAesIv)+
|
||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||
false);
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::quitApp()
|
||||
{
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"cancel",
|
||||
nullptr,
|
||||
false);
|
||||
|
||||
// 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.
|
||||
if (getCurrentGame(getServerInfo()) != 0) {
|
||||
// Generate a synthetic GfeResponseException letting the caller know
|
||||
// that they can't kill someone else's stream.
|
||||
throw GfeHttpResponseException(599, "");
|
||||
}
|
||||
}
|
||||
|
||||
QVector<NvApp>
|
||||
NvHTTP::getAppList()
|
||||
{
|
||||
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
||||
"applist",
|
||||
nullptr,
|
||||
true);
|
||||
verifyResponseStatus(appxml);
|
||||
|
||||
QXmlStreamReader xmlReader(appxml);
|
||||
QVector<NvApp> apps;
|
||||
while (!xmlReader.atEnd()) {
|
||||
while (xmlReader.readNextStartElement()) {
|
||||
QStringRef name = xmlReader.name();
|
||||
if (xmlReader.name() == "App") {
|
||||
// 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 (xmlReader.name() == "AppTitle") {
|
||||
apps.last().name = xmlReader.readElementText();
|
||||
}
|
||||
else if (xmlReader.name() == "ID") {
|
||||
apps.last().id = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (xmlReader.name() == "IsHdrSupported") {
|
||||
apps.last().hdrSupported = xmlReader.readElementText() == "1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QImage
|
||||
NvHTTP::getBoxArt(int appId)
|
||||
{
|
||||
QNetworkReply* reply = openConnection(m_BaseUrlHttps,
|
||||
"appasset",
|
||||
"appid="+QString::number(appId)+
|
||||
"&AssetType=2&AssetIdx=0",
|
||||
true);
|
||||
QImage image = QImageReader(reply).read();
|
||||
delete reply;
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvHTTP::getXmlStringFromHex(QString xml,
|
||||
QString tagName)
|
||||
{
|
||||
QString str = getXmlString(xml, tagName);
|
||||
if (str == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return QByteArray::fromHex(str.toLatin1());
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::getXmlString(QString xml,
|
||||
QString tagName)
|
||||
{
|
||||
QXmlStreamReader xmlReader(xml);
|
||||
|
||||
while (!xmlReader.atEnd())
|
||||
{
|
||||
if (xmlReader.readNext() != QXmlStreamReader::StartElement)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (xmlReader.name() == tagName)
|
||||
{
|
||||
return xmlReader.readElementText();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
url.setQuery("uniqueid=" + IdentityManager::get()->getUniqueId() +
|
||||
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() +
|
||||
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
|
||||
// Add our client certificate
|
||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
|
||||
// Ignore self-signed certificate errors (since GFE uses them)
|
||||
reply->ignoreSslErrors();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// We must clear out cached authentication and connections or
|
||||
// GFE will puke next time
|
||||
m_Nam.clearAccessCache();
|
||||
|
||||
// Handle error
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qDebug() << command << " request failed with error " << reply->error();
|
||||
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include "identitymanager.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <QUrl>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
|
||||
class NvApp
|
||||
{
|
||||
public:
|
||||
bool operator==(const NvApp& other) const
|
||||
{
|
||||
return id == other.id;
|
||||
}
|
||||
|
||||
bool isInitialized()
|
||||
{
|
||||
return id != 0 && !name.isNull();
|
||||
}
|
||||
|
||||
int id;
|
||||
QString name;
|
||||
bool hdrSupported;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(NvApp)
|
||||
|
||||
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.toLatin1();
|
||||
}
|
||||
|
||||
const char* getStatusMessage() const
|
||||
{
|
||||
return m_StatusMessage.toLatin1();
|
||||
}
|
||||
|
||||
int getStatusCode() const
|
||||
{
|
||||
return m_StatusCode;
|
||||
}
|
||||
|
||||
private:
|
||||
int m_StatusCode;
|
||||
QString m_StatusMessage;
|
||||
};
|
||||
|
||||
class NvHTTP
|
||||
{
|
||||
public:
|
||||
explicit NvHTTP(QString address);
|
||||
|
||||
static
|
||||
int
|
||||
getCurrentGame(QString serverInfo);
|
||||
|
||||
QString
|
||||
getServerInfo();
|
||||
|
||||
static
|
||||
void
|
||||
verifyResponseStatus(QString xml);
|
||||
|
||||
static
|
||||
QString
|
||||
getXmlString(QString xml,
|
||||
QString tagName);
|
||||
|
||||
static
|
||||
QByteArray
|
||||
getXmlStringFromHex(QString xml,
|
||||
QString tagName);
|
||||
|
||||
QString
|
||||
openConnectionToString(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout);
|
||||
|
||||
static
|
||||
QVector<int>
|
||||
getServerVersionQuad(QString serverInfo);
|
||||
|
||||
void
|
||||
quitApp();
|
||||
|
||||
void
|
||||
resumeApp(PSTREAM_CONFIGURATION streamConfig);
|
||||
|
||||
void
|
||||
launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask);
|
||||
|
||||
QVector<NvApp>
|
||||
getAppList();
|
||||
|
||||
QImage
|
||||
getBoxArt(int appId);
|
||||
|
||||
QUrl m_BaseUrlHttp;
|
||||
QUrl m_BaseUrlHttps;
|
||||
private:
|
||||
QNetworkReply*
|
||||
openConnection(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout);
|
||||
|
||||
QString m_Address;
|
||||
QNetworkAccessManager m_Nam;
|
||||
};
|
||||
@@ -0,0 +1,317 @@
|
||||
#include "nvpairingmanager.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
NvPairingManager::NvPairingManager(QString address) :
|
||||
m_Http(address)
|
||||
{
|
||||
QByteArray cert = IdentityManager::get()->getCertificate();
|
||||
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
m_Cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free_all(bio);
|
||||
if (m_Cert == nullptr)
|
||||
{
|
||||
throw new std::runtime_error("Unable to load certificate");
|
||||
}
|
||||
|
||||
QByteArray pk = IdentityManager::get()->getPrivateKey();
|
||||
bio = BIO_new_mem_buf(pk.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
m_PrivateKey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free_all(bio);
|
||||
if (m_PrivateKey == nullptr)
|
||||
{
|
||||
throw new std::runtime_error("Unable to load private key");
|
||||
}
|
||||
}
|
||||
|
||||
NvPairingManager::~NvPairingManager()
|
||||
{
|
||||
X509_free(m_Cert);
|
||||
EVP_PKEY_free(m_PrivateKey);
|
||||
}
|
||||
|
||||
QString
|
||||
NvPairingManager::generatePinString()
|
||||
{
|
||||
return QString::asprintf("%04d", QRandomGenerator::global()->bounded(10000));
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::generateRandomBytes(int length)
|
||||
{
|
||||
char* data = static_cast<char*>(alloca(length));
|
||||
RAND_bytes(reinterpret_cast<unsigned char*>(data), length);
|
||||
return QByteArray(data, length);
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
|
||||
{
|
||||
QByteArray ciphertext(plaintext.size(), 0);
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
key);
|
||||
}
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
|
||||
{
|
||||
QByteArray plaintext(ciphertext.size(), 0);
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
key);
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
||||
{
|
||||
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free_all(bio);
|
||||
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
ASN1_BIT_STRING *asnSignature;
|
||||
#else
|
||||
const ASN1_BIT_STRING *asnSignature;
|
||||
#endif
|
||||
|
||||
X509_get0_signature(&asnSignature, NULL, cert);
|
||||
|
||||
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||
|
||||
X509_free(cert);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
bool
|
||||
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
|
||||
{
|
||||
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free_all(bio);
|
||||
|
||||
EVP_PKEY* pubKey = X509_get_pubkey(cert);
|
||||
THROW_BAD_ALLOC_IF_NULL(pubKey);
|
||||
|
||||
EVP_MD_CTX* mdctx = EVP_MD_CTX_create();
|
||||
THROW_BAD_ALLOC_IF_NULL(mdctx);
|
||||
|
||||
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
|
||||
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
|
||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
|
||||
|
||||
EVP_PKEY_free(pubKey);
|
||||
EVP_MD_CTX_destroy(mdctx);
|
||||
X509_free(cert);
|
||||
|
||||
return result > 0;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::signMessage(QByteArray message)
|
||||
{
|
||||
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
|
||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||
|
||||
const EVP_MD *md = EVP_get_digestbyname("SHA256");
|
||||
THROW_BAD_ALLOC_IF_NULL(md);
|
||||
|
||||
EVP_DigestInit_ex(ctx, md, NULL);
|
||||
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
|
||||
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
|
||||
|
||||
size_t signatureLength = 0;
|
||||
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
|
||||
|
||||
QByteArray signature((int)signatureLength, 0);
|
||||
EVP_DigestSignFinal(ctx, reinterpret_cast<unsigned char*>(signature.data()), &signatureLength);
|
||||
|
||||
EVP_MD_CTX_destroy(ctx);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::saltPin(QByteArray salt, QString pin)
|
||||
{
|
||||
return QByteArray().append(salt).append(pin.toLatin1());
|
||||
}
|
||||
|
||||
NvPairingManager::PairState
|
||||
NvPairingManager::pair(QString serverInfo, QString pin)
|
||||
{
|
||||
int serverMajorVersion = NvHTTP::getServerVersionQuad(serverInfo).at(0);
|
||||
qDebug() << "Pairing with server generation: " << serverMajorVersion;
|
||||
|
||||
QCryptographicHash::Algorithm hashAlgo;
|
||||
int hashLength;
|
||||
if (serverMajorVersion >= 7)
|
||||
{
|
||||
// Gen 7+ uses SHA-256 hashing
|
||||
hashAlgo = QCryptographicHash::Sha256;
|
||||
hashLength = 32;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prior to Gen 7 uses SHA-1 hashing
|
||||
hashAlgo = QCryptographicHash::Sha1;
|
||||
hashLength = 20;
|
||||
}
|
||||
|
||||
QByteArray salt = generateRandomBytes(16);
|
||||
QByteArray saltedPin = saltPin(salt, pin);
|
||||
|
||||
AES_KEY encKey, decKey;
|
||||
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
|
||||
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
|
||||
|
||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
||||
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
||||
false);
|
||||
NvHTTP::verifyResponseStatus(getCert);
|
||||
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
||||
{
|
||||
qDebug() << "Failed pairing at stage #1";
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||
if (serverCert == nullptr)
|
||||
{
|
||||
qDebug() << "Server likely already pairing";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
QByteArray randomChallenge = generateRandomBytes(16);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
||||
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&clientchallenge=" +
|
||||
encryptedChallenge.toHex(),
|
||||
true);
|
||||
NvHTTP::verifyResponseStatus(challengeXml);
|
||||
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
||||
{
|
||||
qDebug() << "Failed pairing at stage #2";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
|
||||
QByteArray clientSecretData = generateRandomBytes(16);
|
||||
QByteArray challengeResponse;
|
||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
ASN1_BIT_STRING *asnSignature;
|
||||
#else
|
||||
const ASN1_BIT_STRING *asnSignature;
|
||||
#endif
|
||||
|
||||
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
||||
|
||||
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
|
||||
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||
challengeResponse.append(clientSecretData);
|
||||
|
||||
QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
|
||||
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&serverchallengeresp=" +
|
||||
encryptedChallengeResponseHash.toHex(),
|
||||
true);
|
||||
NvHTTP::verifyResponseStatus(respXml);
|
||||
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
||||
{
|
||||
qDebug() << "Failed pairing at stage #3";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray pairingSecret = NvHTTP::getXmlStringFromHex(respXml, "pairingsecret");
|
||||
QByteArray serverSecret = QByteArray(pairingSecret.data(), 16);
|
||||
QByteArray serverSignature = QByteArray(&pairingSecret.data()[16], 256);
|
||||
|
||||
if (!verifySignature(serverSecret,
|
||||
serverSignature,
|
||||
serverCert))
|
||||
{
|
||||
qDebug() << "MITM detected";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray expectedResponseData;
|
||||
expectedResponseData.append(randomChallenge);
|
||||
expectedResponseData.append(getSignatureFromPemCert(serverCert));
|
||||
expectedResponseData.append(serverSecret);
|
||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||
{
|
||||
qDebug() << "Incorrect PIN";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::PIN_WRONG;
|
||||
}
|
||||
|
||||
QByteArray clientPairingSecret;
|
||||
clientPairingSecret.append(clientSecretData);
|
||||
clientPairingSecret.append(signMessage(clientSecretData));
|
||||
|
||||
QString secretRespXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&clientpairingsecret=" +
|
||||
clientPairingSecret.toHex(),
|
||||
true);
|
||||
NvHTTP::verifyResponseStatus(secretRespXml);
|
||||
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
||||
{
|
||||
qDebug() << "Failed pairing at stage #4";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&phrase=pairchallenge",
|
||||
true);
|
||||
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
||||
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
||||
{
|
||||
qDebug() << "Failed pairing at stage #5";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
return PairState::PAIRED;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "identitymanager.h"
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
class NvPairingManager
|
||||
{
|
||||
public:
|
||||
enum PairState
|
||||
{
|
||||
NOT_PAIRED,
|
||||
PAIRED,
|
||||
PIN_WRONG,
|
||||
FAILED,
|
||||
ALREADY_IN_PROGRESS
|
||||
};
|
||||
|
||||
explicit NvPairingManager(QString address);
|
||||
|
||||
~NvPairingManager();
|
||||
|
||||
QString
|
||||
generatePinString();
|
||||
|
||||
PairState
|
||||
pair(QString serverInfo, QString pin);
|
||||
|
||||
private:
|
||||
QByteArray
|
||||
generateRandomBytes(int length);
|
||||
|
||||
QByteArray
|
||||
saltPin(QByteArray salt, QString pin);
|
||||
|
||||
QByteArray
|
||||
encrypt(QByteArray plaintext, AES_KEY* key);
|
||||
|
||||
QByteArray
|
||||
decrypt(QByteArray ciphertext, AES_KEY* key);
|
||||
|
||||
QByteArray
|
||||
getSignatureFromPemCert(QByteArray certificate);
|
||||
|
||||
bool
|
||||
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
|
||||
|
||||
QByteArray
|
||||
signMessage(QByteArray message);
|
||||
|
||||
NvHTTP m_Http;
|
||||
X509* m_Cert;
|
||||
EVP_PKEY* m_PrivateKey;
|
||||
};
|
||||
Reference in New Issue
Block a user