Milestone 5: deliver embedded RDP sessions and lifecycle hardening

This commit is contained in:
Keith Smith
2026-03-03 18:59:26 -07:00
parent 230a401386
commit 36006bd4aa
2941 changed files with 724359 additions and 77 deletions

View File

@@ -32,11 +32,14 @@ ProfileDialog::ProfileDialog(QWidget* parent)
m_hostInput(new QLineEdit(this)),
m_portInput(new QSpinBox(this)),
m_usernameInput(new QLineEdit(this)),
m_domainInput(new QLineEdit(this)),
m_protocolInput(new QComboBox(this)),
m_authModeInput(new QComboBox(this)),
m_privateKeyPathInput(new QLineEdit(this)),
m_browsePrivateKeyButton(new QPushButton(QStringLiteral("Browse"), this)),
m_knownHostsPolicyInput(new QComboBox(this))
m_knownHostsPolicyInput(new QComboBox(this)),
m_rdpSecurityModeInput(new QComboBox(this)),
m_rdpPerformanceProfileInput(new QComboBox(this))
{
resize(520, 340);
@@ -48,11 +51,18 @@ ProfileDialog::ProfileDialog(QWidget* parent)
m_portInput->setRange(1, 65535);
m_portInput->setValue(22);
m_usernameInput->setPlaceholderText(QStringLiteral("deploy"));
m_domainInput->setPlaceholderText(QStringLiteral("CONTOSO"));
m_protocolInput->addItems({QStringLiteral("SSH"), QStringLiteral("RDP"), QStringLiteral("VNC")});
m_authModeInput->addItems({QStringLiteral("Password"), QStringLiteral("Private Key")});
m_knownHostsPolicyInput->addItems(
{QStringLiteral("Ask"), QStringLiteral("Strict"), QStringLiteral("Accept New"), QStringLiteral("Ignore")});
m_rdpSecurityModeInput->addItems(
{QStringLiteral("Negotiate"), QStringLiteral("NLA"), QStringLiteral("TLS"), QStringLiteral("RDP")});
m_rdpPerformanceProfileInput->addItems({QStringLiteral("Balanced"),
QStringLiteral("Best Quality"),
QStringLiteral("Best Performance"),
QStringLiteral("Auto Detect")});
m_privateKeyPathInput->setPlaceholderText(QStringLiteral("/home/user/.ssh/id_ed25519"));
@@ -80,6 +90,10 @@ ProfileDialog::ProfileDialog(QWidget* parent)
this,
[this](const QString& protocol) {
m_portInput->setValue(standardPortForProtocol(protocol));
if (protocol != QStringLiteral("SSH")) {
const QSignalBlocker blocker(m_authModeInput);
m_authModeInput->setCurrentText(QStringLiteral("Password"));
}
refreshAuthFields();
});
@@ -92,10 +106,13 @@ ProfileDialog::ProfileDialog(QWidget* parent)
form->addRow(QStringLiteral("Host"), m_hostInput);
form->addRow(QStringLiteral("Port"), m_portInput);
form->addRow(QStringLiteral("Username"), m_usernameInput);
form->addRow(QStringLiteral("Domain"), m_domainInput);
form->addRow(QStringLiteral("Protocol"), m_protocolInput);
form->addRow(QStringLiteral("Auth Mode"), m_authModeInput);
form->addRow(QStringLiteral("Private Key"), privateKeyRow);
form->addRow(QStringLiteral("Known Hosts"), m_knownHostsPolicyInput);
form->addRow(QStringLiteral("RDP Security"), m_rdpSecurityModeInput);
form->addRow(QStringLiteral("RDP Performance"), m_rdpPerformanceProfileInput);
auto* note = new QLabel(
QStringLiteral("Passwords are requested at connect time and are not stored."),
@@ -124,6 +141,7 @@ void ProfileDialog::setProfile(const Profile& profile)
m_hostInput->setText(profile.host);
m_portInput->setValue(profile.port > 0 ? profile.port : 22);
m_usernameInput->setText(profile.username);
m_domainInput->setText(profile.domain);
m_privateKeyPathInput->setText(profile.privateKeyPath);
const int protocolIndex = m_protocolInput->findText(profile.protocol);
@@ -138,6 +156,12 @@ void ProfileDialog::setProfile(const Profile& profile)
const int knownHostsIndex = m_knownHostsPolicyInput->findText(profile.knownHostsPolicy);
m_knownHostsPolicyInput->setCurrentIndex(knownHostsIndex >= 0 ? knownHostsIndex : 0);
const int securityModeIndex = m_rdpSecurityModeInput->findText(profile.rdpSecurityMode);
m_rdpSecurityModeInput->setCurrentIndex(securityModeIndex >= 0 ? securityModeIndex : 0);
const int performanceProfileIndex =
m_rdpPerformanceProfileInput->findText(profile.rdpPerformanceProfile);
m_rdpPerformanceProfileInput->setCurrentIndex(performanceProfileIndex >= 0 ? performanceProfileIndex
: 0);
refreshAuthFields();
}
@@ -150,10 +174,13 @@ Profile ProfileDialog::profile() const
profile.host = m_hostInput->text().trimmed();
profile.port = m_portInput->value();
profile.username = m_usernameInput->text().trimmed();
profile.domain = m_domainInput->text().trimmed();
profile.protocol = m_protocolInput->currentText();
profile.authMode = m_authModeInput->currentText();
profile.privateKeyPath = m_privateKeyPathInput->text().trimmed();
profile.knownHostsPolicy = m_knownHostsPolicyInput->currentText();
profile.rdpSecurityMode = m_rdpSecurityModeInput->currentText();
profile.rdpPerformanceProfile = m_rdpPerformanceProfileInput->currentText();
return profile;
}
@@ -173,11 +200,12 @@ void ProfileDialog::accept()
return;
}
if (m_protocolInput->currentText() == QStringLiteral("SSH")
const QString protocol = m_protocolInput->currentText();
if ((protocol == QStringLiteral("SSH") || protocol == QStringLiteral("RDP"))
&& m_usernameInput->text().trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Validation Error"),
QStringLiteral("Username is required for SSH profiles."));
QStringLiteral("Username is required for %1 profiles.").arg(protocol));
return;
}
@@ -187,10 +215,14 @@ void ProfileDialog::accept()
void ProfileDialog::refreshAuthFields()
{
const bool isSsh = m_protocolInput->currentText() == QStringLiteral("SSH");
const bool isRdp = m_protocolInput->currentText() == QStringLiteral("RDP");
const bool isPrivateKey = m_authModeInput->currentText() == QStringLiteral("Private Key");
m_authModeInput->setEnabled(isSsh);
m_privateKeyPathInput->setEnabled(isSsh && isPrivateKey);
m_browsePrivateKeyButton->setEnabled(isSsh && isPrivateKey);
m_knownHostsPolicyInput->setEnabled(isSsh);
m_domainInput->setEnabled(isRdp);
m_rdpSecurityModeInput->setEnabled(isRdp);
m_rdpPerformanceProfileInput->setEnabled(isRdp);
}

View File

@@ -29,11 +29,14 @@ private:
QLineEdit* m_hostInput;
QSpinBox* m_portInput;
QLineEdit* m_usernameInput;
QLineEdit* m_domainInput;
QComboBox* m_protocolInput;
QComboBox* m_authModeInput;
QLineEdit* m_privateKeyPathInput;
QPushButton* m_browsePrivateKeyButton;
QComboBox* m_knownHostsPolicyInput;
QComboBox* m_rdpSecurityModeInput;
QComboBox* m_rdpPerformanceProfileInput;
void refreshAuthFields();
};

View File

@@ -22,18 +22,51 @@ QString buildDatabasePath()
return dataDir.filePath(QStringLiteral("orbithub_profiles.sqlite"));
}
QString normalizedRdpSecurityMode(const QString& value)
{
const QString mode = value.trimmed();
if (mode.compare(QStringLiteral("NLA"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("NLA");
}
if (mode.compare(QStringLiteral("TLS"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("TLS");
}
if (mode.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("RDP");
}
return QStringLiteral("Negotiate");
}
QString normalizedRdpPerformanceProfile(const QString& value)
{
const QString profile = value.trimmed();
if (profile.compare(QStringLiteral("Best Quality"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Quality");
}
if (profile.compare(QStringLiteral("Best Performance"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Performance");
}
if (profile.compare(QStringLiteral("Auto Detect"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Auto Detect");
}
return QStringLiteral("Balanced");
}
void bindProfileFields(QSqlQuery& query, const Profile& profile)
{
query.addBindValue(profile.name.trimmed());
query.addBindValue(profile.host.trimmed());
query.addBindValue(profile.port);
query.addBindValue(profile.username.trimmed());
query.addBindValue(profile.domain.trimmed());
query.addBindValue(profile.protocol.trimmed());
query.addBindValue(profile.authMode.trimmed());
query.addBindValue(profile.privateKeyPath.trimmed());
query.addBindValue(profile.knownHostsPolicy.trimmed().isEmpty()
? QStringLiteral("Ask")
: profile.knownHostsPolicy.trimmed());
query.addBindValue(normalizedRdpSecurityMode(profile.rdpSecurityMode));
query.addBindValue(normalizedRdpPerformanceProfile(profile.rdpPerformanceProfile));
}
Profile profileFromQuery(const QSqlQuery& query)
@@ -44,13 +77,16 @@ Profile profileFromQuery(const QSqlQuery& query)
profile.host = query.value(2).toString();
profile.port = query.value(3).toInt();
profile.username = query.value(4).toString();
profile.protocol = query.value(5).toString();
profile.authMode = query.value(6).toString();
profile.privateKeyPath = query.value(7).toString();
profile.knownHostsPolicy = query.value(8).toString();
profile.domain = query.value(5).toString();
profile.protocol = query.value(6).toString();
profile.authMode = query.value(7).toString();
profile.privateKeyPath = query.value(8).toString();
profile.knownHostsPolicy = query.value(9).toString();
if (profile.knownHostsPolicy.isEmpty()) {
profile.knownHostsPolicy = QStringLiteral("Ask");
}
profile.rdpSecurityMode = normalizedRdpSecurityMode(query.value(10).toString());
profile.rdpPerformanceProfile = normalizedRdpPerformanceProfile(query.value(11).toString());
return profile;
}
@@ -102,12 +138,12 @@ std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery)
QSqlQuery query(QSqlDatabase::database(m_connectionName));
if (searchQuery.trimmed().isEmpty()) {
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
"SELECT id, name, host, port, username, domain, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile "
"FROM profiles "
"ORDER BY lower(name) ASC, id ASC"));
} else {
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
"SELECT id, name, host, port, username, domain, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile "
"FROM profiles "
"WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) "
"ORDER BY lower(name) ASC, id ASC"));
@@ -138,7 +174,7 @@ std::optional<Profile> ProfileRepository::getProfile(qint64 id) const
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
"SELECT id, name, host, port, username, domain, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile "
"FROM profiles WHERE id = ?"));
query.addBindValue(id);
@@ -169,8 +205,8 @@ std::optional<Profile> ProfileRepository::createProfile(const Profile& profile)
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"INSERT INTO profiles(name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)"));
"INSERT INTO profiles(name, host, port, username, domain, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
bindProfileFields(query, profile);
if (!query.exec()) {
@@ -199,7 +235,7 @@ bool ProfileRepository::updateProfile(const Profile& profile) const
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"UPDATE profiles "
"SET name = ?, host = ?, port = ?, username = ?, protocol = ?, auth_mode = ?, private_key_path = ?, known_hosts_policy = ? "
"SET name = ?, host = ?, port = ?, username = ?, domain = ?, protocol = ?, auth_mode = ?, private_key_path = ?, known_hosts_policy = ?, rdp_security_mode = ?, rdp_performance_profile = ? "
"WHERE id = ?"));
bindProfileFields(query, profile);
query.addBindValue(profile.id);
@@ -250,10 +286,13 @@ bool ProfileRepository::initializeDatabase()
"host TEXT NOT NULL DEFAULT '',"
"port INTEGER NOT NULL DEFAULT 22,"
"username TEXT NOT NULL DEFAULT '',"
"domain TEXT NOT NULL DEFAULT '',"
"protocol TEXT NOT NULL DEFAULT 'SSH',"
"auth_mode TEXT NOT NULL DEFAULT 'Password',"
"private_key_path TEXT NOT NULL DEFAULT '',"
"known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'"
"known_hosts_policy TEXT NOT NULL DEFAULT 'Ask',"
"rdp_security_mode TEXT NOT NULL DEFAULT 'Negotiate',"
"rdp_performance_profile TEXT NOT NULL DEFAULT 'Balanced'"
")"));
if (!created) {
@@ -296,10 +335,13 @@ bool ProfileRepository::ensureProfileSchema() const
{QStringLiteral("host"), QStringLiteral("ALTER TABLE profiles ADD COLUMN host TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("port"), QStringLiteral("ALTER TABLE profiles ADD COLUMN port INTEGER NOT NULL DEFAULT 22")},
{QStringLiteral("username"), QStringLiteral("ALTER TABLE profiles ADD COLUMN username TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("domain"), QStringLiteral("ALTER TABLE profiles ADD COLUMN domain TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("protocol"), QStringLiteral("ALTER TABLE profiles ADD COLUMN protocol TEXT NOT NULL DEFAULT 'SSH'")},
{QStringLiteral("auth_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN auth_mode TEXT NOT NULL DEFAULT 'Password'")},
{QStringLiteral("private_key_path"), QStringLiteral("ALTER TABLE profiles ADD COLUMN private_key_path TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'")}};
{QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'")},
{QStringLiteral("rdp_security_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN rdp_security_mode TEXT NOT NULL DEFAULT 'Negotiate'")},
{QStringLiteral("rdp_performance_profile"), QStringLiteral("ALTER TABLE profiles ADD COLUMN rdp_performance_profile TEXT NOT NULL DEFAULT 'Balanced'")}};
for (const ColumnDef& column : required) {
if (columns.contains(column.name)) {

View File

@@ -14,10 +14,13 @@ struct Profile
QString host;
int port = 22;
QString username;
QString domain;
QString protocol = QStringLiteral("SSH");
QString authMode = QStringLiteral("Password");
QString privateKeyPath;
QString knownHostsPolicy = QStringLiteral("Ask");
QString rdpSecurityMode = QStringLiteral("Negotiate");
QString rdpPerformanceProfile = QStringLiteral("Balanced");
};
class ProfileRepository

View File

@@ -4,8 +4,6 @@
#include "profile_repository.h"
#include "session_window.h"
#include <algorithm>
#include <QAbstractItemView>
#include <QHBoxLayout>
#include <QLabel>
@@ -119,13 +117,29 @@ void ProfilesWindow::loadProfiles(const QString& query)
for (const Profile& profile : profiles) {
auto* item = new QListWidgetItem(formatProfileListItem(profile), m_profilesList);
item->setData(Qt::UserRole, QVariant::fromValue(profile.id));
item->setToolTip(QStringLiteral("%1://%2@%3:%4\nAuth: %5\nKnown Hosts: %6")
.arg(profile.protocol,
profile.username.isEmpty() ? QStringLiteral("<none>") : profile.username,
profile.host,
QString::number(profile.port),
profile.authMode,
profile.knownHostsPolicy));
const QString identity = [&profile]() {
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0
&& !profile.domain.trimmed().isEmpty()) {
return QStringLiteral("%1\\%2").arg(profile.domain.trimmed(),
profile.username.trimmed().isEmpty()
? QStringLiteral("<none>")
: profile.username.trimmed());
}
return profile.username.isEmpty() ? QStringLiteral("<none>") : profile.username;
}();
QString tooltip = QStringLiteral("%1://%2@%3:%4\nAuth: %5")
.arg(profile.protocol,
identity,
profile.host,
QString::number(profile.port),
profile.authMode);
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
tooltip += QStringLiteral("\nKnown Hosts: %1").arg(profile.knownHostsPolicy);
} else if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
tooltip += QStringLiteral("\nRDP Security: %1\nRDP Performance: %2")
.arg(profile.rdpSecurityMode, profile.rdpPerformanceProfile);
}
item->setToolTip(tooltip);
m_profileCache.insert_or_assign(profile.id, profile);
}
}
@@ -264,22 +278,16 @@ void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
return;
}
auto* session = new SessionWindow(profile.value());
session->setAttribute(Qt::WA_DeleteOnClose);
if (m_sessionWindow.isNull()) {
m_sessionWindow = new SessionWindow(profile.value());
m_sessionWindow->setAttribute(Qt::WA_DeleteOnClose);
connect(m_sessionWindow, &QObject::destroyed, this, [this]() { m_sessionWindow = nullptr; });
} else {
m_sessionWindow->openProfile(profile.value());
}
m_sessionWindows.emplace_back(session);
connect(session,
&QObject::destroyed,
this,
[this](QObject* object) {
m_sessionWindows.erase(
std::remove_if(m_sessionWindows.begin(),
m_sessionWindows.end(),
[object](const QPointer<SessionWindow>& candidate) {
return candidate.isNull() || candidate.data() == object;
}),
m_sessionWindows.end());
});
session->show();
m_sessionWindow->setWindowState(m_sessionWindow->windowState() & ~Qt::WindowMinimized);
m_sessionWindow->show();
m_sessionWindow->raise();
m_sessionWindow->activateWindow();
}

View File

@@ -11,7 +11,6 @@
#include <optional>
#include <QPointer>
#include <unordered_map>
#include <vector>
class QListWidget;
class QListWidgetItem;
@@ -33,7 +32,7 @@ private:
QPushButton* m_newButton;
QPushButton* m_editButton;
QPushButton* m_deleteButton;
std::vector<QPointer<SessionWindow>> m_sessionWindows;
QPointer<SessionWindow> m_sessionWindow;
std::unique_ptr<ProfileRepository> m_repository;
std::unordered_map<qint64, Profile> m_profileCache;

207
src/rdp_display_widget.cpp Normal file
View File

@@ -0,0 +1,207 @@
#include "rdp_display_widget.h"
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QResizeEvent>
#include <QTimer>
#include <QWheelEvent>
#include <QtGlobal>
namespace {
QSize sanitizeSize(const QSize& size)
{
return QSize(qMax(1, size.width()), qMax(1, size.height()));
}
}
RdpDisplayWidget::RdpDisplayWidget(QWidget* parent)
: QWidget(parent), m_remoteSize(1280, 720)
{
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
setAutoFillBackground(false);
setMinimumSize(320, 200);
QTimer::singleShot(0, this, [this]() {
const QSize size = sanitizeSize(this->size());
emit viewportSizeChanged(size.width(), size.height());
});
}
void RdpDisplayWidget::setFrame(const QImage& frame)
{
if (frame.isNull()) {
return;
}
m_frame = frame;
m_remoteSize = sanitizeSize(frame.size());
update();
}
void RdpDisplayWidget::setRemoteDesktopSize(int width, int height)
{
if (width < 1 || height < 1) {
return;
}
const QSize nextSize(width, height);
if (m_remoteSize == nextSize) {
return;
}
m_remoteSize = nextSize;
update();
}
void RdpDisplayWidget::clearFrame()
{
m_frame = QImage();
update();
}
void RdpDisplayWidget::paintEvent(QPaintEvent* event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.fillRect(rect(), QColor(QStringLiteral("#101214")));
const QRectF target = renderRect();
if (!m_frame.isNull()) {
painter.drawImage(target, m_frame);
} else {
painter.setPen(QColor(QStringLiteral("#b0bec5")));
painter.drawText(rect(),
Qt::AlignCenter,
QStringLiteral("Waiting for remote desktop frame..."));
}
}
void RdpDisplayWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
const QSize size = sanitizeSize(event->size());
emit viewportSizeChanged(size.width(), size.height());
}
void RdpDisplayWidget::keyPressEvent(QKeyEvent* event)
{
if (event == nullptr || event->isAutoRepeat()) {
return;
}
emit keyInput(event->key(),
event->nativeScanCode(),
event->text(),
true,
static_cast<int>(event->modifiers()));
event->accept();
}
void RdpDisplayWidget::keyReleaseEvent(QKeyEvent* event)
{
if (event == nullptr || event->isAutoRepeat()) {
return;
}
emit keyInput(event->key(),
event->nativeScanCode(),
event->text(),
false,
static_cast<int>(event->modifiers()));
event->accept();
}
void RdpDisplayWidget::mousePressEvent(QMouseEvent* event)
{
if (event == nullptr) {
return;
}
setFocus(Qt::MouseFocusReason);
const QPoint mapped = mapToRemote(event->position());
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), true);
event->accept();
}
void RdpDisplayWidget::mouseReleaseEvent(QMouseEvent* event)
{
if (event == nullptr) {
return;
}
const QPoint mapped = mapToRemote(event->position());
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), false);
event->accept();
}
void RdpDisplayWidget::mouseMoveEvent(QMouseEvent* event)
{
if (event == nullptr) {
return;
}
const QPoint mapped = mapToRemote(event->position());
emit mouseMoveInput(mapped.x(), mapped.y());
event->accept();
}
void RdpDisplayWidget::wheelEvent(QWheelEvent* event)
{
if (event == nullptr) {
return;
}
const QPoint mapped = mapToRemote(event->position());
const QPoint angle = event->angleDelta();
emit mouseWheelInput(mapped.x(), mapped.y(), angle.x(), angle.y());
event->accept();
}
QRectF RdpDisplayWidget::renderRect() const
{
const QSize remote = effectiveRemoteSize();
const QRectF area = rect();
if (area.isEmpty()) {
return QRectF();
}
const qreal scale = qMin(area.width() / remote.width(), area.height() / remote.height());
const qreal drawWidth = remote.width() * scale;
const qreal drawHeight = remote.height() * scale;
const qreal x = area.x() + ((area.width() - drawWidth) * 0.5);
const qreal y = area.y() + ((area.height() - drawHeight) * 0.5);
return QRectF(x, y, drawWidth, drawHeight);
}
QPoint RdpDisplayWidget::mapToRemote(const QPointF& pos) const
{
const QSize remote = effectiveRemoteSize();
const QRectF target = renderRect();
if (target.isEmpty()) {
return QPoint(0, 0);
}
const qreal clampedX = qBound(target.left(), pos.x(), target.right());
const qreal clampedY = qBound(target.top(), pos.y(), target.bottom());
const qreal normalizedX = (clampedX - target.left()) / qMax(1.0, target.width());
const qreal normalizedY = (clampedY - target.top()) / qMax(1.0, target.height());
const int remoteX = qBound(0, static_cast<int>(normalizedX * remote.width()), remote.width() - 1);
const int remoteY = qBound(0, static_cast<int>(normalizedY * remote.height()), remote.height() - 1);
return QPoint(remoteX, remoteY);
}
QSize RdpDisplayWidget::effectiveRemoteSize() const
{
if (m_remoteSize.width() > 0 && m_remoteSize.height() > 0) {
return m_remoteSize;
}
if (!m_frame.isNull()) {
return sanitizeSize(m_frame.size());
}
return QSize(1280, 720);
}

50
src/rdp_display_widget.h Normal file
View File

@@ -0,0 +1,50 @@
#ifndef ORBITHUB_RDP_DISPLAY_WIDGET_H
#define ORBITHUB_RDP_DISPLAY_WIDGET_H
#include <QImage>
#include <QWidget>
class QKeyEvent;
class QMouseEvent;
class QPaintEvent;
class QResizeEvent;
class QWheelEvent;
class RdpDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit RdpDisplayWidget(QWidget* parent = nullptr);
void setFrame(const QImage& frame);
void setRemoteDesktopSize(int width, int height);
void clearFrame();
signals:
void keyInput(int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers);
void mouseMoveInput(int x, int y);
void mouseButtonInput(int x, int y, int button, bool pressed);
void mouseWheelInput(int x, int y, int deltaX, int deltaY);
void viewportSizeChanged(int width, int height);
protected:
void paintEvent(QPaintEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void keyPressEvent(QKeyEvent* event) override;
void keyReleaseEvent(QKeyEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
private:
QImage m_frame;
QSize m_remoteSize;
QRectF renderRect() const;
QPoint mapToRemote(const QPointF& pos) const;
QSize effectiveRemoteSize() const;
};
#endif

1809
src/rdp_session_backend.cpp Normal file

File diff suppressed because it is too large Load Diff

108
src/rdp_session_backend.h Normal file
View File

@@ -0,0 +1,108 @@
#ifndef ORBITHUB_RDP_SESSION_BACKEND_H
#define ORBITHUB_RDP_SESSION_BACKEND_H
#include "session_backend.h"
#include <atomic>
#include <cstdint>
#include <deque>
#include <mutex>
#include <thread>
struct rdp_freerdp;
class RdpSessionBackend : public SessionBackend
{
Q_OBJECT
public:
explicit RdpSessionBackend(const Profile& profile, QObject* parent = nullptr);
~RdpSessionBackend() override;
public slots:
void connectSession(const SessionConnectOptions& options) override;
void disconnectSession() override;
void reconnectSession(const SessionConnectOptions& options) override;
void sendInput(const QString& input) override;
void confirmHostKey(bool trustHost) override;
void updateTerminalSize(int columns, int rows) override;
void sendKeyEvent(int key,
quint32 nativeScanCode,
const QString& text,
bool pressed,
int modifiers) override;
void sendMouseMoveEvent(int x, int y) override;
void sendMouseButtonEvent(int x, int y, int button, bool pressed) override;
void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY) override;
private:
enum class InputEventType {
Key,
MouseMove,
MouseButton,
MouseWheel,
Resize,
};
struct InputEvent {
InputEventType type = InputEventType::MouseMove;
int key = 0;
quint32 nativeScanCode = 0;
QString text;
bool pressed = false;
int modifiers = 0;
int x = 0;
int y = 0;
int button = 0;
int deltaX = 0;
int deltaY = 0;
int width = 0;
int height = 0;
};
SessionState m_state;
SessionConnectOptions m_activeOptions;
std::atomic_bool m_userInitiatedDisconnect;
std::atomic_int m_requestedDesktopWidth;
std::atomic_int m_requestedDesktopHeight;
std::thread m_worker;
std::atomic_bool m_workerRunning;
std::atomic_bool m_stopRequested;
std::mutex m_instanceMutex;
rdp_freerdp* m_instance;
std::mutex m_inputMutex;
std::deque<InputEvent> m_inputEvents;
std::mutex m_displayControlMutex;
void* m_displayControlContext;
bool m_displayControlReady;
bool m_resizeFailureLogged;
int m_lastResizeWidth;
int m_lastResizeHeight;
void setState(SessionState state, const QString& message);
bool validateProfile(QString& message) const;
void startWorker();
void stopWorker(bool userInitiated);
void workerMain();
void enqueueInputEvent(const InputEvent& event);
void processInputEvents(rdp_freerdp* instance);
bool sendDisplayResize(rdp_freerdp* instance, int width, int height);
public:
void onChannelConnectedEvent(const char* name, void* channelInterface);
void onChannelDisconnectedEvent(const char* name, void* channelInterface);
void onDisplayControlCaps(uint32_t maxNumMonitors,
uint32_t maxMonitorAreaFactorA,
uint32_t maxMonitorAreaFactorB);
private:
void emitStateAsync(SessionState state, const QString& message);
void emitConnectionFailureAsync(const QString& displayMessage, const QString& rawMessage);
int sanitizeDesktopWidth(int width) const;
int sanitizeDesktopHeight(int height) const;
};
#endif

View File

@@ -3,8 +3,10 @@
#include "profile_repository.h"
#include <QImage>
#include <QObject>
#include <QString>
#include <QtGlobal>
class SessionConnectOptions
{
@@ -44,6 +46,37 @@ public slots:
virtual void sendInput(const QString& input) = 0;
virtual void confirmHostKey(bool trustHost) = 0;
virtual void updateTerminalSize(int columns, int rows) = 0;
virtual void sendKeyEvent(int key,
quint32 nativeScanCode,
const QString& text,
bool pressed,
int modifiers)
{
Q_UNUSED(key);
Q_UNUSED(nativeScanCode);
Q_UNUSED(text);
Q_UNUSED(pressed);
Q_UNUSED(modifiers);
}
virtual void sendMouseMoveEvent(int x, int y)
{
Q_UNUSED(x);
Q_UNUSED(y);
}
virtual void sendMouseButtonEvent(int x, int y, int button, bool pressed)
{
Q_UNUSED(x);
Q_UNUSED(y);
Q_UNUSED(button);
Q_UNUSED(pressed);
}
virtual void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY)
{
Q_UNUSED(x);
Q_UNUSED(y);
Q_UNUSED(deltaX);
Q_UNUSED(deltaY);
}
signals:
void stateChanged(SessionState state, const QString& message);
@@ -51,6 +84,8 @@ signals:
void connectionError(const QString& displayMessage, const QString& rawMessage);
void outputReceived(const QString& text);
void hostKeyConfirmationRequested(const QString& prompt);
void frameUpdated(const QImage& frame);
void remoteDesktopSizeChanged(int width, int height);
private:
Profile m_profile;

View File

@@ -1,5 +1,6 @@
#include "session_backend_factory.h"
#include "rdp_session_backend.h"
#include "session_backend.h"
#include "ssh_session_backend.h"
#include "unsupported_session_backend.h"
@@ -9,6 +10,9 @@ std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile)
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
return std::make_unique<SshSessionBackend>(profile);
}
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
return std::make_unique<RdpSessionBackend>(profile);
}
return std::make_unique<UnsupportedSessionBackend>(profile);
}

View File

@@ -1,5 +1,6 @@
#include "session_tab.h"
#include "rdp_display_widget.h"
#include "session_backend_factory.h"
#include "terminal_view.h"
@@ -62,6 +63,7 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
m_state(SessionState::Disconnected),
m_terminalThemeName(QStringLiteral("Dark")),
m_sshTerminal(nullptr),
m_rdpDisplay(nullptr),
m_terminalOutput(nullptr),
m_eventLog(nullptr),
m_toggleEventsButton(nullptr),
@@ -148,6 +150,26 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
m_backend,
&SessionBackend::updateTerminalSize,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestKeyEvent,
m_backend,
&SessionBackend::sendKeyEvent,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestMouseMoveEvent,
m_backend,
&SessionBackend::sendMouseMoveEvent,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestMouseButtonEvent,
m_backend,
&SessionBackend::sendMouseButtonEvent,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestMouseWheelEvent,
m_backend,
&SessionBackend::sendMouseWheelEvent,
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::stateChanged,
@@ -174,6 +196,24 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
this,
&SessionTab::onBackendHostKeyConfirmationRequested,
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::frameUpdated,
this,
[this](const QImage& frame) {
if (m_rdpDisplay != nullptr) {
m_rdpDisplay->setFrame(frame);
}
},
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::remoteDesktopSizeChanged,
this,
[this](int width, int height) {
if (m_rdpDisplay != nullptr) {
m_rdpDisplay->setRemoteDesktopSize(width, height);
}
},
Qt::QueuedConnection);
m_backendThread->start();
}
@@ -284,6 +324,12 @@ void SessionTab::clearTerminal()
emit requestInput(QStringLiteral("\x0c"));
}
m_terminalOutput->setFocus();
return;
}
if (m_rdpDisplay != nullptr) {
m_rdpDisplay->clearFrame();
m_rdpDisplay->setFocus();
}
}
@@ -308,6 +354,16 @@ QString SessionTab::terminalThemeName() const
return m_terminalThemeName;
}
bool SessionTab::supportsThemeSelection() const
{
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
}
bool SessionTab::supportsClearAction() const
{
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
}
void SessionTab::onBackendStateChanged(SessionState state, const QString& message)
{
setState(state, message);
@@ -322,6 +378,9 @@ void SessionTab::onBackendConnectionError(const QString& displayMessage, const Q
{
m_lastError = rawMessage.isEmpty() ? displayMessage : rawMessage;
appendEvent(QStringLiteral("Error: %1").arg(displayMessage));
if (!rawMessage.trimmed().isEmpty() && rawMessage.trimmed() != displayMessage.trimmed()) {
appendEvent(QStringLiteral("Raw Error: %1").arg(rawMessage.trimmed()));
}
}
void SessionTab::onBackendOutputReceived(const QString& text)
@@ -363,12 +422,21 @@ void SessionTab::setupUi()
config.maxScrollback = 12000;
m_sshTerminal->setConfig(config);
rootLayout->addWidget(m_sshTerminal, 1);
} else if (m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
m_rdpDisplay = new RdpDisplayWidget(this);
rootLayout->addWidget(m_rdpDisplay, 1);
} else {
m_terminalOutput = new TerminalView(this);
m_terminalOutput->setFont(defaultTerminalFont());
m_terminalOutput->setMinimumHeight(260);
m_terminalOutput->setPlaceholderText(
QStringLiteral("Session is connecting. Type directly here once connected."));
m_terminalOutput->setReadOnly(true);
if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
m_terminalOutput->setPlaceholderText(
QStringLiteral("Embedded VNC session output appears here when the backend is available."));
} else {
m_terminalOutput->setPlaceholderText(
QStringLiteral("Session output appears here."));
}
rootLayout->addWidget(m_terminalOutput, 1);
}
@@ -415,6 +483,33 @@ void SessionTab::setupUi()
&TerminalView::terminalSizeChanged,
this,
[this](int columns, int rows) { emit requestTerminalSize(columns, rows); });
} else if (m_rdpDisplay != nullptr) {
connect(m_rdpDisplay,
&RdpDisplayWidget::viewportSizeChanged,
this,
[this](int width, int height) { emit requestTerminalSize(width, height); });
connect(m_rdpDisplay,
&RdpDisplayWidget::keyInput,
this,
[this](int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers) {
emit requestKeyEvent(key, nativeScanCode, text, pressed, modifiers);
});
connect(m_rdpDisplay,
&RdpDisplayWidget::mouseMoveInput,
this,
[this](int x, int y) { emit requestMouseMoveEvent(x, y); });
connect(m_rdpDisplay,
&RdpDisplayWidget::mouseButtonInput,
this,
[this](int x, int y, int button, bool pressed) {
emit requestMouseButtonEvent(x, y, button, pressed);
});
connect(m_rdpDisplay,
&RdpDisplayWidget::mouseWheelInput,
this,
[this](int x, int y, int deltaX, int deltaY) {
emit requestMouseWheelEvent(x, y, deltaX, deltaY);
});
}
}
@@ -423,7 +518,41 @@ std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
SessionConnectOptions options;
options.knownHostsPolicy = m_profile.knownHostsPolicy;
if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) {
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
if (!isSsh && !isRdp) {
return options;
}
if (isRdp) {
if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) != 0) {
return options;
}
bool accepted = false;
const QString password = QInputDialog::getText(
this,
QStringLiteral("RDP Password"),
QStringLiteral("Password for %1:")
.arg(m_profile.username.trimmed().isEmpty()
? m_profile.host
: QStringLiteral("%1@%2").arg(m_profile.username, m_profile.host)),
QLineEdit::Password,
QString(),
&accepted);
if (!accepted) {
return std::nullopt;
}
if (password.isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("Password is required for password authentication."));
return std::nullopt;
}
options.password = password;
return options;
}
@@ -481,28 +610,26 @@ std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
bool SessionTab::validateProfileForConnect()
{
if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) {
return true;
}
if (m_profile.host.trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("SSH host is required."));
return false;
}
if (m_profile.username.trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("SSH username is required."));
QStringLiteral("%1 host is required.").arg(m_profile.protocol));
return false;
}
if (m_profile.port < 1 || m_profile.port > 65535) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("SSH port must be between 1 and 65535."));
QStringLiteral("Port must be between 1 and 65535."));
return false;
}
if ((m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0
|| m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0)
&& m_profile.username.trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("%1 username is required.").arg(m_profile.protocol));
return false;
}
@@ -554,6 +681,14 @@ void SessionTab::refreshActionButtons()
if (m_terminalOutput != nullptr) {
m_terminalOutput->setEnabled(isConnected);
m_terminalOutput->setFocus();
return;
}
if (m_rdpDisplay != nullptr) {
m_rdpDisplay->setEnabled(isConnected);
if (isConnected) {
m_rdpDisplay->setFocus();
}
}
}

View File

@@ -5,6 +5,7 @@
#include "session_backend.h"
#include <QWidget>
#include <QtGlobal>
#include <optional>
@@ -12,6 +13,7 @@ class QPlainTextEdit;
class QThread;
class SessionBackend;
class TerminalView;
class RdpDisplayWidget;
class QToolButton;
class KodoTerm;
@@ -30,6 +32,8 @@ public:
void clearTerminal();
void setTerminalThemeName(const QString& themeName);
QString terminalThemeName() const;
bool supportsThemeSelection() const;
bool supportsClearAction() const;
signals:
void tabTitleChanged(const QString& title);
@@ -40,6 +44,14 @@ signals:
void requestInput(const QString& input);
void requestHostKeyConfirmation(bool trustHost);
void requestTerminalSize(int columns, int rows);
void requestKeyEvent(int key,
quint32 nativeScanCode,
const QString& text,
bool pressed,
int modifiers);
void requestMouseMoveEvent(int x, int y);
void requestMouseButtonEvent(int x, int y, int button, bool pressed);
void requestMouseWheelEvent(int x, int y, int deltaX, int deltaY);
private slots:
void onBackendStateChanged(SessionState state, const QString& message);
@@ -59,6 +71,7 @@ private:
QString m_terminalThemeName;
KodoTerm* m_sshTerminal;
RdpDisplayWidget* m_rdpDisplay;
TerminalView* m_terminalOutput;
QPlainTextEdit* m_eventLog;
QToolButton* m_toggleEventsButton;

View File

@@ -72,24 +72,32 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
QMenu menu(this);
QAction* disconnectAction = menu.addAction(QStringLiteral("Disconnect"));
QAction* reconnectAction = menu.addAction(QStringLiteral("Reconnect"));
menu.addSeparator();
QMenu* themeMenu = menu.addMenu(QStringLiteral("Theme"));
QList<QAction*> themeActions;
const QString currentTheme = tab->terminalThemeName();
for (const QString& themeName : terminalThemeNames()) {
QAction* themeAction = themeMenu->addAction(themeName);
themeAction->setCheckable(true);
themeAction->setChecked(
themeName.compare(currentTheme, Qt::CaseInsensitive) == 0);
themeActions.append(themeAction);
if (tab->supportsThemeSelection()) {
menu.addSeparator();
QMenu* themeMenu = menu.addMenu(QStringLiteral("Theme"));
const QString currentTheme = tab->terminalThemeName();
for (const QString& themeName : terminalThemeNames()) {
QAction* themeAction = themeMenu->addAction(themeName);
themeAction->setCheckable(true);
themeAction->setChecked(
themeName.compare(currentTheme, Qt::CaseInsensitive) == 0);
themeActions.append(themeAction);
}
}
QAction* clearAction = menu.addAction(QStringLiteral("Clear"));
QAction* clearAction = nullptr;
if (tab->supportsClearAction()) {
clearAction = menu.addAction(QStringLiteral("Clear"));
}
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
if (chosen == disconnectAction) {
tab->disconnectSession();
} else if (chosen == reconnectAction) {
tab->reconnectSession();
} else if (chosen == clearAction) {
} else if (clearAction != nullptr && chosen == clearAction) {
tab->clearTerminal();
} else {
for (QAction* themeAction : themeActions) {
@@ -105,11 +113,19 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
addSessionTab(profile);
}
void SessionWindow::openProfile(const Profile& profile)
{
addSessionTab(profile);
}
void SessionWindow::addSessionTab(const Profile& profile)
{
auto* tab = new SessionTab(profile, this);
const int index = m_tabs->addTab(tab, tab->tabTitle());
m_tabs->setCurrentIndex(index);
if (m_tabs->count() > 1) {
setWindowTitle(QStringLiteral("OrbitHub Sessions"));
}
m_tabs->tabBar()->setTabTextColor(
index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));

View File

@@ -14,6 +14,7 @@ class SessionWindow : public QMainWindow
public:
explicit SessionWindow(const Profile& profile, QWidget* parent = nullptr);
void openProfile(const Profile& profile);
private:
QTabWidget* m_tabs;