Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions app/app.pro
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ win32 {
}

INCLUDEPATH += $$PWD/../libs/windows/include
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib advapi32.lib shell32.lib
}
macx:!disable-prebuilts {
!exists($$PWD/../libs/mac) {
Expand Down Expand Up @@ -206,6 +206,9 @@ SOURCES += \
gui/sdlgamepadkeynavigation.cpp \
streaming/video/overlaymanager.cpp \
backend/systemproperties.cpp \
backend/usbforwardingtunnel.cpp \
backend/usbforwardingenvironment.cpp \
backend/usbforwardingbackend.cpp \
wm.cpp

HEADERS += \
Expand Down Expand Up @@ -246,7 +249,12 @@ HEADERS += \
settings/mappingmanager.h \
gui/sdlgamepadkeynavigation.h \
streaming/video/overlaymanager.h \
backend/systemproperties.h
backend/systemproperties.h \
backend/usbforwardingtunnel.h \
backend/usbforwardingcapability.h \
backend/pairedcertificate.h \
backend/usbforwardingenvironment.h \
backend/usbforwardingbackend.h

# Platform-specific renderers and decoders
ffmpeg {
Expand Down
88 changes: 87 additions & 1 deletion app/backend/nvhttp.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "nvcomputer.h"
#include "pairedcertificate.h"
#include <Limelight.h>

#include <QDebug>
Expand All @@ -11,6 +12,7 @@
#include <QImageReader>
#include <QtEndian>
#include <QNetworkProxy>
#include <memory>

#define FAST_FAIL_TIMEOUT_MS 2000
#define REQUEST_TIMEOUT_MS 5000
Expand Down Expand Up @@ -454,6 +456,35 @@ void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& error
}
}

UsbForwarding::Capability NvHTTP::getUsbForwardingCapability()
{
// Derived from qiin2333/moonlight-qt (GPL-3.0).
if (m_ServerCert.isNull() || m_BaseUrlHttps.port(0) == 0) {
throw GfeHttpResponseException(401, "USB forwarding requires a paired host");
}
// The bounded openConnection() pins the exact paired certificate, refuses
// redirects and caps the response size. The token in the reply must never
// reach a host that is not the paired one.
std::unique_ptr<QNetworkReply> reply(openConnection(m_BaseUrlHttps,
"api/v1/usb-forwarding", nullptr, REQUEST_TIMEOUT_MS, NvLogLevel::NVLL_NONE,
UsbForwarding::kMaxHandshakeBytes));
// A normally trusted certificate may not emit sslErrors at all. Require
// the paired leaf certificate even on that path before accepting credentials.
if (!PairedCertificate::matches(m_ServerCert, reply->sslConfiguration().peerCertificate())) {
throw QtNetworkReplyException(QNetworkReply::SslHandshakeFailedError,
"USB forwarding host certificate mismatch");
}
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
throw GfeHttpResponseException(400, "USB capability request failed");
}
// Never log the body: it carries the tunnel token.
const auto capability = UsbForwarding::Capability::parse(reply->readAll());
if (!capability) {
throw GfeHttpResponseException(400, "Invalid USB capability response");
}
return *capability;
}

QString
NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
Expand Down Expand Up @@ -483,11 +514,18 @@ NvHTTP::openConnection(QUrl baseUrl,
QString command,
QString arguments,
int timeoutMs,
NvLogLevel logLevel)
NvLogLevel logLevel,
qint64 maxResponseBytes)
{
// Port must be set
Q_ASSERT(baseUrl.port(0) != 0);

// A bounded request (maxResponseBytes > 0) carries credentials back to us,
// so it gets extra protection: no redirects, a response size cap and a
// check that the peer is exactly the paired certificate. Unbounded
// requests behave exactly as before.
const bool bounded = maxResponseBytes > 0;

// Build a URL for the request
QUrl url(baseUrl);
url.setPath("/" + command);
Expand All @@ -499,6 +537,12 @@ NvHTTP::openConnection(QUrl baseUrl,

QNetworkRequest request(url);

if (bounded) {
// Never forward the request to another URL.
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::ManualRedirectPolicy);
}

// Add our client certificate
request.setSslConfiguration(IdentityManager::get()->getSslConfig());

Expand All @@ -518,9 +562,26 @@ NvHTTP::openConnection(QUrl baseUrl,
#endif

auto sslErrorsConnection = connect(m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
QMetaObject::Connection pinnedConnection;
if (bounded) {
// A normally trusted peer may produce no sslErrors, so also check the
// pin before any HTTP data is sent.
pinnedConnection = PairedCertificate::enforce(m_Nam, this, m_ServerCert);
}
QNetworkReply* reply = m_Nam->get(request);
QEventLoop loop;

bool oversized = false;
if (bounded) {
reply->setReadBufferSize(maxResponseBytes + 1);
connect(reply, &QNetworkReply::readyRead, &loop, [&] {
if (reply->bytesAvailable() > maxResponseBytes) {
oversized = true;
reply->abort();
}
});
}

// Connect the quit signals
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
Expand Down Expand Up @@ -568,6 +629,31 @@ NvHTTP::openConnection(QUrl baseUrl,
#endif
disconnect(sslErrorsConnection);

if (bounded) {
disconnect(pinnedConnection);

// The pin check comes first: a pin mismatch aborts the reply, which
// would otherwise be reported as a timeout.
// A request that never reached a TLS peer (refused, timed out) keeps
// its own error below.
const QSslCertificate peerCertificate = reply->sslConfiguration().peerCertificate();
if ((reply->error() == QNetworkReply::NoError || !peerCertificate.isNull()) &&
!PairedCertificate::matches(m_ServerCert, peerCertificate)) {
delete reply;
throw QtNetworkReplyException(QNetworkReply::SslHandshakeFailedError,
"Server certificate mismatch");
}
if (oversized || reply->bytesAvailable() > maxResponseBytes) {
delete reply;
throw GfeHttpResponseException(400, "Host response too large");
}
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status >= 300 && status < 400) {
delete reply;
throw GfeHttpResponseException(status, "Host redirects are not allowed");
}
}

// Handle error
if (reply->error() != QNetworkReply::NoError)
{
Expand Down
12 changes: 11 additions & 1 deletion app/backend/nvhttp.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "identitymanager.h"
#include "nvapp.h"
#include "nvaddress.h"
#include "usbforwardingcapability.h"

#include <Limelight.h>

Expand Down Expand Up @@ -141,6 +142,14 @@ class NvHTTP : public QObject
int timeoutMs,
NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE);

// Asks the paired host whether USB forwarding is available and, if so,
// for the tunnel port and token (GET /api/v1/usb-forwarding). Pins the
// exact paired certificate, allows no redirects and no more than 4096
// response bytes. Throws GfeHttpResponseException or
// QtNetworkReplyException on any failure, including an unpaired host.
UsbForwarding::Capability
getUsbForwardingCapability();

void setServerCert(QSslCertificate serverCert);
void setAddress(NvAddress address);
void setHttpsPort(uint16_t port);
Expand Down Expand Up @@ -193,7 +202,8 @@ class NvHTTP : public QObject
QString command,
QString arguments,
int timeoutMs,
NvLogLevel logLevel);
NvLogLevel logLevel,
qint64 maxResponseBytes = 0);

NvAddress m_Address;
QNetworkAccessManager* m_Nam;
Expand Down
56 changes: 56 additions & 0 deletions app/backend/pairedcertificate.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

// Derived from qiin2333/moonlight-qt (GPL-3.0).

#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QSslCertificate>
#include <QSslConfiguration>
#include <QSslError>
#include <QVariant>

namespace PairedCertificate {

inline bool matches(const QSslCertificate& pinned, const QSslCertificate& peer)
{
return !pinned.isNull() && !peer.isNull() && pinned == peer;
}

inline bool canIgnoreErrors(const QSslCertificate& pinned, const QList<QSslError>& errors)
{
if (pinned.isNull() || errors.isEmpty()) {
return false;
}
for (const QSslError& error : errors) {
if (!matches(pinned, error.certificate())) {
return false;
}
}
return true;
}

// A normally trusted peer may produce no sslErrors. Check the pin before
// HTTP data is sent as well. The manager must be used exclusively under this
// policy, with automatic redirects disabled. Change identities only between
// requests, disconnecting the old enforcement connection first.
inline QMetaObject::Connection enforce(QNetworkAccessManager* manager, QObject* context,
const QSslCertificate& pinned)
{
// Reused connections do not emit encrypted(). Discard connections created
// before adopting this policy or under a previous paired identity.
const char* identityProperty = "_moonlightPairedCertificate";
const QByteArray identity = pinned.toDer();
const QVariant previous = manager->property(identityProperty);
if (!previous.isValid() || previous.toByteArray() != identity) {
manager->clearConnectionCache();
manager->setProperty(identityProperty, identity);
}
return QObject::connect(manager, &QNetworkAccessManager::encrypted, context,
[pinned](QNetworkReply* reply) {
if (!matches(pinned, reply->sslConfiguration().peerCertificate())) {
reply->abort();
}
});
}

} // namespace PairedCertificate
Loading
Loading