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
10 changes: 10 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# One place to define the Windows callback IPC port
set(IMAGER_CALLBACK_PORT "49629" CACHE STRING "TCP port for rpi-imager callback relay on Windows")

# Apply optimization flags globally to all targets (including bundled dependencies)
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
# Use -Os instead of -O3 for better code size vs performance balance
Expand Down Expand Up @@ -371,6 +374,13 @@ endif()
if (WIN32)
# Adding WIN32 prevents a console window being opened on Windows
add_executable(${PROJECT_NAME} WIN32 ${SOURCES} ${DEPENDENCIES})

# Inject the callback port macro for Windows builds
target_compile_definitions(${PROJECT_NAME}
PRIVATE RPI_IMAGER_CALLBACK_PORT=${IMAGER_CALLBACK_PORT})

# Ensure the relay builds with the app
add_dependencies(${PROJECT_NAME} rpi-imager-callback-relay)
else()
add_executable(${PROJECT_NAME} ${SOURCES} ${DEPENDENCIES})
endif()
Expand Down
78 changes: 76 additions & 2 deletions src/imagewriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@
#include <QQmlContext>
#include <QWindow>
#include <QGuiApplication>
#include <QClipboard>
#endif
#include <QUrl>
#include <QUrlQuery>
#include <QString>
#include <QStringList>
#include <QHostAddress>
#include <QNetworkAccessManager>
#include <QNetworkReply>
Expand Down Expand Up @@ -2547,18 +2552,87 @@ void ImageWriter::openUrl(const QUrl &url)
}
}

bool ImageWriter::verifyAuthKey(const QString &s, bool strict) const
{
// Base58 (no 0 O I l)
static const QRegularExpression base58OnlyRe(QStringLiteral("^[1-9A-HJ-NP-Za-km-z]+$"));

// Required prefix
bool hasPrefix = s.startsWith(QStringLiteral("rpuak_")) || s.startsWith(QStringLiteral("rpoak_"));
if (!hasPrefix)
return false;

const QString payload = s.mid(6);
bool base58Match = base58OnlyRe.match(payload).hasMatch();

if (payload.isEmpty() || !base58Match)
return false;

if (strict) {
// Exactly 24 Base58 chars today → total length 30
return payload.size() == 24;
} else {
// Future-proof: accept >=24 Base58 chars
return payload.size() >= 24;
}
}

QString ImageWriter::parseTokenFromUrl(const QUrl &url, bool strictAuthKey) const {
// Handle QUrl or string, accept auth_key
if (!url.isValid())
return {};

QUrlQuery q(url);
const QString val = q.queryItemValue(QStringLiteral("auth_key"), QUrl::FullyDecoded);
if (!val.isEmpty()) {
if (verifyAuthKey(val, strictAuthKey)) {
return val;
}

qWarning() << "Ignoring auth_key with invalid format/length:" << val;
}

return {};
}

void ImageWriter::handleIncomingUrl(const QUrl &url)
{
qDebug() << "Incoming URL:" << url;
emit connectCallbackReceived(QVariant::fromValue(url));

auto token = parseTokenFromUrl(url);
if (!token.isEmpty()) {
if (!_piConnectToken.isEmpty()) {
if (_piConnectToken != token) {
// Let QML decide whether to overwrite
emit connectTokenConflictDetected(token);
}

return;
}

overwriteConnectToken(token);
}
}

void ImageWriter::setRuntimeConnectToken(const QString &token)
void ImageWriter::overwriteConnectToken(const QString &token)
{
// Ephemeral session-only Connect token (never persisted)
_piConnectToken = token;
emit connectTokenReceived(token);
}

QString ImageWriter::getRuntimeConnectToken() const
{
return _piConnectToken;
}

QString ImageWriter::getClipboardText() const
{
#ifndef CLI_ONLY_BUILD
QClipboard *clipboard = QGuiApplication::clipboard();
if (clipboard) {
return clipboard->text();
}
#endif
return QString();
}
10 changes: 7 additions & 3 deletions src/imagewriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,10 @@ class ImageWriter : public QObject
Q_INVOKABLE void reboot();
Q_INVOKABLE void openUrl(const QUrl &url);
Q_INVOKABLE void handleIncomingUrl(const QUrl &url);
// Ephemeral session-only Connect token (never persisted)
Q_INVOKABLE void setRuntimeConnectToken(const QString &token);
Q_INVOKABLE void overwriteConnectToken(const QString &token);
Q_INVOKABLE QString getRuntimeConnectToken() const;
Q_INVOKABLE QString getClipboardText() const;
Q_INVOKABLE bool verifyAuthKey(const QString &token, bool strict = false) const;

/* Override OS list refresh schedule (in minutes); pass negative to clear override */
Q_INVOKABLE void setOsListRefreshOverride(int intervalMinutes, int jitterMinutes);
Expand Down Expand Up @@ -299,7 +300,8 @@ class ImageWriter : public QObject
void keychainPermissionRequested();
void keychainPermissionResponseReceived();
void writeStateChanged();
void connectCallbackReceived(QVariant url);
void connectTokenReceived(const QString &token);
void connectTokenConflictDetected(const QString &token);
void cacheStatusChanged();

protected slots:
Expand Down Expand Up @@ -339,6 +341,8 @@ protected slots:
bool _deviceFilterIsInclusive;
std::shared_ptr<DeviceInfo> _device_info;

QString parseTokenFromUrl(const QUrl &url, bool strictAuthKey = false) const;

protected:
QUrl _src, _repo;
QString _dst, _parentCategory, _osName, _osReleaseDate, _currentLang, _currentLangcode, _currentKeyboard;
Expand Down
67 changes: 23 additions & 44 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
#ifdef Q_OS_WIN
#include <windows.h>
#include <winnls.h>
#include <QLocalServer>
#include <QLocalSocket>
#include <QTcpServer>
#include <QTcpSocket>
#endif
#include "imageadvancedoptions.h"

Expand All @@ -55,6 +55,15 @@ static QTextStream cerr(stderr);
static void consoleMsgHandler(QtMsgType, const QMessageLogContext &, const QString &str) {
cerr << str << endl;
}

// If CMake didn't inject it for some reason, fall back to a sensible default.
#ifndef RPI_IMAGER_CALLBACK_PORT
#define RPI_IMAGER_CALLBACK_PORT 49629
#endif
static_assert(RPI_IMAGER_CALLBACK_PORT > 0 && RPI_IMAGER_CALLBACK_PORT <= 65535,
"RPI_IMAGER_CALLBACK_PORT must be a valid TCP port");
static constexpr quint16 kPort =
static_cast<quint16>(RPI_IMAGER_CALLBACK_PORT);
#endif


Expand Down Expand Up @@ -311,55 +320,25 @@ int main(int argc, char *argv[])
}

#ifdef Q_OS_WIN
// ---- single-instance + URL forwarding ----
// Build a stable per-user name by hashing the per-user AppData path.
// This is consistent across elevated/non-elevated tokens for the same user.
auto base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (base.isEmpty()) base = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
QByteArray hash = QCryptographicHash::hash(base.toUtf8(), QCryptographicHash::Sha1);
const QString serverName = QStringLiteral("rpi-imager-urlpipe-%1").arg(QString::fromLatin1(hash.toHex()));
qDebug() << "Using IPC Server name:" << serverName;

// First, try to connect to an existing instance (client-first).
{
QLocalSocket probe;
probe.connectToServer(serverName);
if (probe.waitForConnected(150)) {
// A primary instance exists; forward the URL (if any) and exit.
if (!callbackUrl.isEmpty()) {
const QByteArray msg = callbackUrl.toString(QUrl::FullyEncoded).toUtf8();
probe.write(msg);
probe.flush();
probe.waitForBytesWritten(200);
}
return 0; // secondary: do not open another window
}
}

// No instance reachable. Become the server.
// Remove stale endpoint left by a crash (safe even if it doesn’t exist).
QLocalServer::removeServer(serverName);

static QLocalServer server; // must outlive the lambda
if (!server.listen(serverName)) {
// As a last resort: if listen still fails, we’re better off continuing as a single instance.
qWarning() << "Failed to listen on" << serverName << ":" << server.errorString();
} else {
QObject::connect(&server, &QLocalServer::newConnection, &app, [&imageWriter]() {
while (QLocalSocket *s = server.nextPendingConnection()) {
s->waitForReadyRead(1000);
// callback server
QTcpServer server;
QObject::connect(&server, &QTcpServer::newConnection, &app, [&]() {
while (auto *s = server.nextPendingConnection()) {
QObject::connect(s, &QTcpSocket::readyRead, s, [s, &imageWriter]() {
const QByteArray payload = s->readAll();
s->close();
s->deleteLater();
s->disconnectFromHost();
QMetaObject::invokeMethod(
&imageWriter,
[payload, &imageWriter] {
imageWriter.handleIncomingUrl(QUrl(QString::fromUtf8(payload)));
},
Qt::QueuedConnection
);
}
});
);
});
}
});
if (!server.listen(QHostAddress::LocalHost, kPort)) {
qWarning() << "TCP listen failed:" << server.errorString();
}
#endif

Expand Down
Loading