mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
1023777642
- 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.
560 lines
17 KiB
C++
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;
|
|
}
|