Files
iDescriptor/src/sshterminalwidget.cpp
T
uncor3 1023777642 Refactor network device management and SSH terminal integration
- Removed the old NetworkDeviceManager class and replaced it with a new NetworkDeviceProvider class to streamline network device handling.
- Updated various widgets (NetworkDevicesToConnectWidget, NetworkDevicesWidget, SSHTerminalTool) to utilize the new NetworkDeviceProvider for device management.
- Introduced a new SSHTerminalTool class to manage SSH connections, replacing the OpenSSHTerminalWidget.
- Enhanced device selection UI to support both wired and wireless devices, including manual IP connection options.
- Improved signal-slot connections for device addition and removal events.
- Cleaned up unnecessary includes and code related to the old SSH terminal widget.
- Added checks for device jailbreak status during SSH connection attempts.
2026-03-20 20:25:24 +03:00

560 lines
17 KiB
C++

/*
* iDescriptor: A free and open-source idevice management tool.
*
* Copyright (C) 2025 Uncore <https://github.com/uncor3>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "sshterminalwidget.h"
SSHTerminalWidget::SSHTerminalWidget(const ConnectionInfo &connectionInfo,
QWidget *parent)
: QWidget(parent), m_connectionInfo(connectionInfo), m_sshSession(nullptr),
m_sshChannel(nullptr), m_iproxyProcess(nullptr), m_sshConnected(false),
m_isInitialized(false), m_currentState(TerminalState::Loading)
{
setWindowTitle(QString("SSH Terminal / %1 - iDescriptor")
.arg(m_connectionInfo.deviceName));
setMinimumSize(800, 600);
setupUI();
#ifdef WIN32
setupWinWindow(this);
#endif
// Initialize SSH
ssh_init();
// Setup timer for checking SSH data
m_sshTimer = new QTimer(this);
connect(m_sshTimer, &QTimer::timeout, this,
&SSHTerminalWidget::checkSshData);
// Start connection process
initializeConnection();
}
SSHTerminalWidget::~SSHTerminalWidget() { cleanup(); }
void SSHTerminalWidget::setupUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
m_stackedWidget = new QStackedWidget(this);
mainLayout->addWidget(m_stackedWidget);
setupLoadingState();
setupErrorState();
setupActionState();
setState(TerminalState::Loading);
}
void SSHTerminalWidget::setupLoadingState()
{
m_loadingWidget = new QWidget();
QVBoxLayout *loadingLayout = new QVBoxLayout(m_loadingWidget);
loadingLayout->setAlignment(Qt::AlignCenter);
// Process indicator
m_loadingIndicator = new QProcessIndicator(m_loadingWidget);
m_loadingIndicator->setType(QProcessIndicator::line_rotate);
m_loadingIndicator->setFixedSize(64, 64);
// Loading label
m_loadingLabel = new QLabel("Connecting to SSH server...");
m_loadingLabel->setAlignment(Qt::AlignCenter);
m_loadingLabel->setStyleSheet(
"QLabel { font-size: 14px; color: #666; margin-top: 20px; }");
loadingLayout->addWidget(m_loadingIndicator, 0, Qt::AlignCenter);
loadingLayout->addWidget(m_loadingLabel);
m_stackedWidget->addWidget(m_loadingWidget);
}
void SSHTerminalWidget::setupErrorState()
{
m_errorWidget = new QWidget();
QVBoxLayout *errorLayout = new QVBoxLayout(m_errorWidget);
errorLayout->setAlignment(Qt::AlignCenter);
errorLayout->setSpacing(20);
// Error label
m_errorLabel = new QLabel();
m_errorLabel->setAlignment(Qt::AlignCenter);
m_errorLabel->setWordWrap(true);
m_errorLabel->setStyleSheet(
"QLabel { font-size: 14px; color: #d32f2f; padding: 20px; }");
// Retry button
m_retryButton = new QPushButton("Retry Connection");
m_retryButton->setStyleSheet(
"QPushButton { padding: 10px 20px; font-size: 14px; }");
connect(m_retryButton, &QPushButton::clicked, this,
&SSHTerminalWidget::onRetryClicked);
errorLayout->addWidget(m_errorLabel);
errorLayout->addWidget(m_retryButton, 0, Qt::AlignCenter);
m_stackedWidget->addWidget(m_errorWidget);
}
void SSHTerminalWidget::setupActionState()
{
m_actionWidget = new QWidget();
QVBoxLayout *actionLayout = new QVBoxLayout(m_actionWidget);
actionLayout->setContentsMargins(0, 0, 0, 0);
// Terminal widget
m_terminal = new QTermWidget(0, m_actionWidget);
m_terminal->setScrollBarPosition(QTermWidget::ScrollBarRight);
m_terminal->setColorScheme("Linux");
m_terminal->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_terminal, &QWidget::customContextMenuRequested, this,
[this](const QPoint &pos) {
QMenu menu(this);
QList<QAction *> actions = m_terminal->filterActions(pos);
if (!actions.isEmpty()) {
menu.addActions(actions);
menu.exec(m_terminal->mapToGlobal(pos));
}
});
#ifdef WIN32
m_terminal->startTerminalEmulation();
#else
m_terminal->startTerminalTeletype();
#endif
m_terminal->setStyleSheet("padding: 5px;");
actionLayout->addWidget(m_terminal);
m_stackedWidget->addWidget(m_actionWidget);
}
void SSHTerminalWidget::setState(TerminalState state)
{
m_currentState = state;
switch (state) {
case TerminalState::Loading:
m_stackedWidget->setCurrentWidget(m_loadingWidget);
m_loadingIndicator->start();
break;
case TerminalState::Error:
m_stackedWidget->setCurrentWidget(m_errorWidget);
m_loadingIndicator->stop();
break;
case TerminalState::Connected:
m_stackedWidget->setCurrentWidget(m_actionWidget);
m_loadingIndicator->stop();
m_terminal->setFocus();
break;
}
}
void SSHTerminalWidget::showError(const QString &errorMessage)
{
m_errorLabel->setText(errorMessage);
setState(TerminalState::Error);
}
void SSHTerminalWidget::onRetryClicked()
{
// Reset all state
cleanup();
m_sshConnected = false;
m_isInitialized = false;
// Reinitialize SSH
ssh_init();
// Setup timer again
m_sshTimer = new QTimer(this);
connect(m_sshTimer, &QTimer::timeout, this,
&SSHTerminalWidget::checkSshData);
// Update loading message and start connection
m_loadingLabel->setText("Connecting to SSH server...");
setState(TerminalState::Loading);
initializeConnection();
}
void SSHTerminalWidget::initializeConnection()
{
if (m_connectionInfo.type == ConnectionType::Wired) {
initWiredDevice();
} else {
initWirelessDevice();
}
}
void SSHTerminalWidget::initWiredDevice()
{
if (m_isInitialized)
return;
m_isInitialized = true;
m_loadingLabel->setText("Setting up SSH tunnel...");
// Start iproxy for wired devices
m_iproxyProcess = new QProcess(this);
m_iproxyProcess->setProcessChannelMode(QProcess::MergedChannels);
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("PATH", env.value("PATH") + ":/usr/local/bin:/opt/homebrew/bin");
m_iproxyProcess->setProcessEnvironment(env);
connect(m_iproxyProcess, &QProcess::errorOccurred, this,
[this](QProcess::ProcessError error) {
// showError("Error starting iproxy: " +
// m_iproxyProcess->errorString());
qDebug() << "iproxy error:" << error;
});
connect(m_iproxyProcess, &QProcess::finished, this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
qDebug() << "iproxy finished with exit code:" << exitCode;
if (!m_sshConnected) {
// showError("iproxy process terminated unexpectedly");
}
});
// Monitor iproxy output for readiness
connect(m_iproxyProcess, &QProcess::readyRead, this, [this]() {
QByteArray output = m_iproxyProcess->readAll();
qDebug() << "iproxy output:" << output;
if (output.contains("waiting for connection")) {
qDebug() << "iproxy is ready, starting SSH connection";
disconnect(m_iproxyProcess, &QProcess::readyRead, this, nullptr);
startSSH(QHostAddress(QHostAddress::LocalHost).toString(), 3333);
} else if (output.contains("ERROR") || output.contains("failed")) {
qDebug() << "iproxy error detected in output" << output;
// showError("iproxy failed: " + QString::fromUtf8(output));
}
});
// Add timeout timer as backup
QTimer *timeoutTimer = new QTimer(this);
timeoutTimer->setSingleShot(true);
connect(timeoutTimer, &QTimer::timeout, this, [this, timeoutTimer]() {
qDebug() << "iproxy timeout - assuming it's ready and attempting SSH "
"connection";
timeoutTimer->deleteLater();
startSSH(QHostAddress(QHostAddress::LocalHost).toString(), 3333);
});
QStringList args;
args << "-u" << m_connectionInfo.deviceUdid << "3333" << "22";
qDebug() << "Starting iproxy with args:" << args;
QString iproxyPath;
QString appDirPath = QCoreApplication::applicationDirPath();
QString bundledIproxyPath = appDirPath + "/iproxy";
/* MacOS bundled iproxy */
if (QFileInfo(bundledIproxyPath).isExecutable()) {
iproxyPath = bundledIproxyPath;
}
/* AppImage - this is set by the plugin script */
else if (qEnvironmentVariableIsSet("IPROXY_BIN_APPIMAGE")) {
iproxyPath = qgetenv("IPROXY_BIN_APPIMAGE");
if (iproxyPath.isEmpty()) {
showError("Error: Running in AppImage mode, but "
"IPROXY_BIN_APPIMAGE is not set.");
return;
}
if (!QFileInfo(iproxyPath).isExecutable()) {
showError(
"Error: Bundled iproxy not found or is not executable at " +
iproxyPath);
return;
}
} else {
iproxyPath = QStandardPaths::findExecutable("iproxy");
if (iproxyPath.isEmpty()) {
showError(
"Error: iproxy not found. Please install libimobiledevice.");
return;
}
}
qDebug() << "Using iproxy at:" << iproxyPath;
m_iproxyProcess->start(iproxyPath, args);
if (!m_iproxyProcess->waitForStarted(5000)) {
showError("Failed to start iproxy process");
timeoutTimer->deleteLater();
return;
}
qDebug() << "iproxy process started, waiting for readiness...";
timeoutTimer->start(5000);
}
void SSHTerminalWidget::initWirelessDevice()
{
if (m_isInitialized)
return;
m_isInitialized = true;
m_loadingLabel->setText("Connecting to network device...");
// For wireless devices, connect directly without iproxy
startSSH(m_connectionInfo.hostAddress, m_connectionInfo.port);
}
void SSHTerminalWidget::startSSH(const QString &host, uint16_t port)
{
qDebug() << "Starting SSH to" << host << "on port" << port;
QString defaultPassword =
SettingsManager::sharedInstance()->defaultJailbrokenRootPassword();
QByteArray passwordBytes = defaultPassword.toUtf8();
bool ok;
QString code =
QInputDialog::getText(nullptr, "SSH Root Password",
"Enter the root password: \n(leave empty if you "
"want to use the default)",
QLineEdit::Normal, QString(), &ok);
if (!ok) {
showError("Root password input canceled");
return;
}
if (m_sshConnected)
return;
m_loadingLabel->setText("Establishing SSH connection...");
qDebug() << "Starting SSH connection to" << host << ":" << port;
// Create SSH session
m_sshSession = ssh_new();
if (!m_sshSession) {
showError("Error: Failed to create SSH session");
return;
}
// Configure SSH session
QByteArray hostBytes = host.toUtf8();
ssh_options_set(m_sshSession, SSH_OPTIONS_HOST, hostBytes.constData());
int sshPort = static_cast<int>(port);
ssh_options_set(m_sshSession, SSH_OPTIONS_PORT, &sshPort);
ssh_options_set(m_sshSession, SSH_OPTIONS_USER, "root");
// Disable strict host key checking
int stricthostcheck = 0;
ssh_options_set(m_sshSession, SSH_OPTIONS_STRICTHOSTKEYCHECK,
&stricthostcheck);
// Set log level for debugging
int log_level = SSH_LOG_PROTOCOL;
ssh_options_set(m_sshSession, SSH_OPTIONS_LOG_VERBOSITY, &log_level);
qDebug() << "SSH session configured, attempting connection...";
// Connect to SSH server
int rc = ssh_connect(m_sshSession);
qDebug() << "SSH connect result:" << rc << "SSH_OK:" << SSH_OK;
if (rc != SSH_OK) {
QString errorMsg = QString("SSH connection failed: %1")
.arg(ssh_get_error(m_sshSession));
showError(errorMsg);
qDebug() << errorMsg;
ssh_free(m_sshSession);
m_sshSession = nullptr;
return;
}
qDebug() << "SSH connected successfully, attempting authentication...";
rc =
ssh_userauth_password(m_sshSession, nullptr, passwordBytes.constData());
if (rc != SSH_AUTH_SUCCESS) {
showError(QString("SSH authentication failed: %1")
.arg(ssh_get_error(m_sshSession)));
ssh_disconnect(m_sshSession);
ssh_free(m_sshSession);
m_sshSession = nullptr;
return;
}
// Create SSH channel
m_sshChannel = ssh_channel_new(m_sshSession);
if (!m_sshChannel) {
showError("Error: Failed to create SSH channel");
ssh_disconnect(m_sshSession);
ssh_free(m_sshSession);
m_sshSession = nullptr;
return;
}
// Open SSH channel
rc = ssh_channel_open_session(m_sshChannel);
if (rc != SSH_OK) {
showError(QString("Failed to open SSH channel: %1")
.arg(ssh_get_error(m_sshSession)));
ssh_channel_free(m_sshChannel);
m_sshChannel = nullptr;
ssh_disconnect(m_sshSession);
ssh_free(m_sshSession);
m_sshSession = nullptr;
return;
}
// Request a PTY
rc = ssh_channel_request_pty(m_sshChannel);
if (rc != SSH_OK) {
showError("Failed to request PTY");
ssh_channel_close(m_sshChannel);
ssh_channel_free(m_sshChannel);
m_sshChannel = nullptr;
ssh_disconnect(m_sshSession);
ssh_free(m_sshSession);
m_sshSession = nullptr;
return;
}
// Start shell
rc = ssh_channel_request_shell(m_sshChannel);
if (rc != SSH_OK) {
showError("Failed to start shell");
ssh_channel_close(m_sshChannel);
ssh_channel_free(m_sshChannel);
m_sshChannel = nullptr;
ssh_disconnect(m_sshSession);
ssh_free(m_sshSession);
m_sshSession = nullptr;
return;
}
// Connect terminal to SSH
connectLibsshToTerminal();
// Start timer to check for SSH data
m_sshTimer->start(50); // Check every 50ms
m_sshConnected = true;
setState(TerminalState::Connected);
qDebug() << "SSH terminal connected successfully";
}
void SSHTerminalWidget::connectLibsshToTerminal()
{
if (!m_terminal)
return;
// Connect terminal input to SSH channel
connect(m_terminal, &QTermWidget::sendData, this,
[this](const char *data, int size) {
if (m_sshChannel && ssh_channel_is_open(m_sshChannel)) {
ssh_channel_write(m_sshChannel, data, size);
}
});
}
void SSHTerminalWidget::checkSshData()
{
if (!m_sshChannel || !ssh_channel_is_open(m_sshChannel))
return;
// Check if SSH channel has data to read
if (ssh_channel_poll(m_sshChannel, 0) > 0) {
char buffer[4096];
int nbytes = ssh_channel_read_nonblocking(m_sshChannel, buffer,
sizeof(buffer), 0);
if (nbytes > 0) {
#ifdef WIN32
m_terminal->receiveData(buffer, nbytes);
#else
// Write data to terminal's PTY
write(m_terminal->getPtySlaveFd(), buffer, nbytes);
#endif
}
}
// Check for stderr data
if (ssh_channel_poll(m_sshChannel, 1) > 0) {
char buffer[4096];
int nbytes = ssh_channel_read_nonblocking(m_sshChannel, buffer,
sizeof(buffer), 1);
if (nbytes > 0) {
// Write stderr data to terminal's PTY
#ifdef WIN32
m_terminal->receiveData(buffer, nbytes);
#else
write(m_terminal->getPtySlaveFd(), buffer, nbytes);
#endif
}
}
// Check if channel is closed
if (ssh_channel_is_eof(m_sshChannel)) {
disconnectSSH();
}
}
void SSHTerminalWidget::disconnectSSH()
{
cleanup();
showError("SSH connection was closed by the remote host");
}
void SSHTerminalWidget::cleanup()
{
if (m_sshTimer) {
m_sshTimer->stop();
m_sshTimer->deleteLater();
m_sshTimer = nullptr;
}
if (m_sshChannel) {
ssh_channel_close(m_sshChannel);
ssh_channel_free(m_sshChannel);
m_sshChannel = nullptr;
}
if (m_sshSession) {
ssh_disconnect(m_sshSession);
ssh_free(m_sshSession);
m_sshSession = nullptr;
}
if (m_iproxyProcess) {
// Properly terminate iproxy process
if (m_iproxyProcess->state() != QProcess::NotRunning) {
m_iproxyProcess->terminate(); // Send SIGTERM first
if (!m_iproxyProcess->waitForFinished(3000)) {
qDebug() << "iproxy didn't terminate gracefully, killing...";
m_iproxyProcess->kill(); // Force kill if needed
m_iproxyProcess->waitForFinished(1000);
}
}
m_iproxyProcess->deleteLater();
m_iproxyProcess = nullptr;
}
m_sshConnected = false;
}