mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
refactor: open ssh terminal in a new window
This commit is contained in:
+35
-362
@@ -1,5 +1,6 @@
|
||||
#include "jailbrokenwidget.h"
|
||||
#include "appcontext.h"
|
||||
#include "sshterminalwidget.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#include "core/services/avahi/avahi_service.h"
|
||||
@@ -16,18 +17,11 @@
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QScrollArea>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libssh/libssh.h>
|
||||
#include <qtermwidget6/qtermwidget.h>
|
||||
#include <unistd.h>
|
||||
|
||||
JailbrokenWidget::JailbrokenWidget(QWidget *parent) : QWidget{parent}
|
||||
{
|
||||
@@ -54,7 +48,7 @@ JailbrokenWidget::JailbrokenWidget(QWidget *parent) : QWidget{parent}
|
||||
connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this,
|
||||
&JailbrokenWidget::onWiredDeviceRemoved);
|
||||
|
||||
#ifdef __linux__
|
||||
#ifdef __linux____
|
||||
m_wirelessProvider = new AvahiService(this);
|
||||
connect(m_wirelessProvider, &AvahiService::deviceAdded, this,
|
||||
&JailbrokenWidget::onWirelessDeviceAdded);
|
||||
@@ -78,21 +72,9 @@ JailbrokenWidget::JailbrokenWidget(QWidget *parent) : QWidget{parent}
|
||||
rightLayout->setSpacing(10);
|
||||
|
||||
setupDeviceSelectionUI(rightLayout);
|
||||
setupTerminal();
|
||||
rightLayout->addWidget(m_terminal, 1);
|
||||
|
||||
mainLayout->addWidget(rightContainer, 3);
|
||||
|
||||
// Initialize SSH
|
||||
ssh_init();
|
||||
m_sshSession = nullptr;
|
||||
m_sshChannel = nullptr;
|
||||
|
||||
// Setup timer for checking SSH data
|
||||
m_sshTimer = new QTimer(this);
|
||||
connect(m_sshTimer, &QTimer::timeout, this,
|
||||
&JailbrokenWidget::checkSshData);
|
||||
|
||||
// Start scanning for wireless devices
|
||||
m_wirelessProvider->startBrowsing();
|
||||
|
||||
@@ -100,27 +82,6 @@ JailbrokenWidget::JailbrokenWidget(QWidget *parent) : QWidget{parent}
|
||||
updateDeviceList();
|
||||
}
|
||||
|
||||
void JailbrokenWidget::setupTerminal()
|
||||
{
|
||||
m_terminal = new QTermWidget(0, this);
|
||||
m_terminal->setMinimumHeight(400);
|
||||
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));
|
||||
}
|
||||
});
|
||||
m_terminal->startTerminalTeletype();
|
||||
m_terminal->hide();
|
||||
m_terminal->setStyleSheet("padding : 10px;");
|
||||
}
|
||||
|
||||
void JailbrokenWidget::setupDeviceSelectionUI(QVBoxLayout *layout)
|
||||
{
|
||||
// Create scroll area for device selection
|
||||
@@ -157,10 +118,10 @@ void JailbrokenWidget::setupDeviceSelectionUI(QVBoxLayout *layout)
|
||||
m_infoLabel = new QLabel("Select a device to connect");
|
||||
layout->addWidget(m_infoLabel);
|
||||
|
||||
m_connectButton = new QPushButton("Connect SSH Terminal");
|
||||
m_connectButton = new QPushButton("Open SSH Terminal");
|
||||
m_connectButton->setEnabled(false);
|
||||
connect(m_connectButton, &QPushButton::clicked, this,
|
||||
&JailbrokenWidget::onConnectSSH);
|
||||
&JailbrokenWidget::onOpenSSHTerminal);
|
||||
layout->addWidget(m_connectButton);
|
||||
}
|
||||
|
||||
@@ -296,7 +257,8 @@ void JailbrokenWidget::onDeviceSelected(QAbstractButton *button)
|
||||
if (m_selectedWiredDevice->deviceInfo.jailbroken) {
|
||||
m_infoLabel->setText("Jailbroken device selected");
|
||||
} else {
|
||||
m_infoLabel->setText("Device selected (jailbreak status unknown)");
|
||||
m_infoLabel->setText(
|
||||
"Device selected (detected as non-jailbroken)");
|
||||
}
|
||||
} else if (deviceType == "wireless") {
|
||||
m_selectedDeviceType = DeviceType::Wireless;
|
||||
@@ -311,7 +273,7 @@ void JailbrokenWidget::onDeviceSelected(QAbstractButton *button)
|
||||
}
|
||||
|
||||
m_connectButton->setEnabled(true);
|
||||
m_connectButton->setText("Connect SSH Terminal");
|
||||
m_connectButton->setText("Open SSH Terminal");
|
||||
}
|
||||
|
||||
void JailbrokenWidget::resetSelection()
|
||||
@@ -330,333 +292,44 @@ void JailbrokenWidget::resetSelection()
|
||||
}
|
||||
}
|
||||
|
||||
void JailbrokenWidget::connectLibsshToTerminal()
|
||||
void JailbrokenWidget::onOpenSSHTerminal()
|
||||
{
|
||||
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 JailbrokenWidget::deviceConnected(iDescriptorDevice *device)
|
||||
{
|
||||
if (device->deviceInfo.jailbroken) {
|
||||
m_infoLabel->setText("Jailbroken device connected");
|
||||
} else {
|
||||
m_infoLabel->setText(
|
||||
"Connected device is not detected as jailbroken. Continue anyway?");
|
||||
}
|
||||
m_device = device;
|
||||
m_connectButton->setEnabled(true);
|
||||
m_connectButton->setText("Connect SSH Terminal");
|
||||
}
|
||||
|
||||
void JailbrokenWidget::onConnectSSH()
|
||||
{
|
||||
if (m_sshConnected) {
|
||||
disconnectSSH();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_selectedDeviceType == DeviceType::None) {
|
||||
m_infoLabel->setText("Please select a device first");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare connection info
|
||||
ConnectionInfo connectionInfo;
|
||||
|
||||
if (m_selectedDeviceType == DeviceType::Wired) {
|
||||
initWiredDevice();
|
||||
} else {
|
||||
initWirelessDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void JailbrokenWidget::initWiredDevice()
|
||||
{
|
||||
if (m_isInitialized)
|
||||
return;
|
||||
m_isInitialized = true;
|
||||
|
||||
if (!m_selectedWiredDevice) {
|
||||
m_infoLabel->setText("No wired device selected");
|
||||
return;
|
||||
}
|
||||
|
||||
m_connectButton->setEnabled(false);
|
||||
m_infoLabel->setText("Setting up SSH tunnel...");
|
||||
|
||||
// Start iproxy first for wired devices
|
||||
iproxyProcess = new QProcess(this);
|
||||
iproxyProcess->setProcessChannelMode(QProcess::MergedChannels);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
// Add common directories where iproxy might be installed
|
||||
env.insert("PATH", env.value("PATH") + ":/usr/local/bin:/opt/homebrew/bin");
|
||||
|
||||
iproxyProcess->setProcessEnvironment(env);
|
||||
|
||||
connect(iproxyProcess, &QProcess::errorOccurred, this,
|
||||
[this](QProcess::ProcessError error) {
|
||||
m_infoLabel->setText("Error: " + iproxyProcess->errorString());
|
||||
m_connectButton->setEnabled(true);
|
||||
qDebug() << "iproxy error:" << error;
|
||||
});
|
||||
|
||||
// Monitor iproxy output for readiness
|
||||
connect(iproxyProcess, &QProcess::readyRead, this, [this]() {
|
||||
QByteArray output = iproxyProcess->readAll();
|
||||
if (output.contains("waiting for connection")) {
|
||||
// iproxy is ready, disconnect the signal to avoid multiple calls
|
||||
disconnect(iproxyProcess, &QProcess::readyRead, this, nullptr);
|
||||
startSSH("127.0.0.1", 3333);
|
||||
if (!m_selectedWiredDevice) {
|
||||
m_infoLabel->setText("No wired device selected");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
QStringList args;
|
||||
args << "-u" << m_selectedWiredDevice->udid.c_str() << "3333" << "22";
|
||||
connectionInfo.type = ConnectionType::Wired;
|
||||
connectionInfo.deviceName = QString::fromStdString(
|
||||
m_selectedWiredDevice->deviceInfo.deviceName);
|
||||
connectionInfo.deviceUdid =
|
||||
QString::fromStdString(m_selectedWiredDevice->udid);
|
||||
connectionInfo.hostAddress = "127.0.0.1";
|
||||
connectionInfo.port = 22;
|
||||
|
||||
qDebug() << "Starting iproxy with args:" << args;
|
||||
|
||||
iproxyProcess->start("iproxy", args);
|
||||
|
||||
// Check if iproxy started successfully
|
||||
if (!iproxyProcess->waitForStarted(5000)) {
|
||||
m_infoLabel->setText("Failed to start iproxy");
|
||||
m_connectButton->setEnabled(true);
|
||||
return;
|
||||
} else if (m_selectedDeviceType == DeviceType::Wireless) {
|
||||
connectionInfo.type = ConnectionType::Wireless;
|
||||
connectionInfo.deviceName = m_selectedNetworkDevice.name;
|
||||
connectionInfo.deviceUdid = "";
|
||||
connectionInfo.hostAddress = m_selectedNetworkDevice.address;
|
||||
connectionInfo.port = m_selectedNetworkDevice.port;
|
||||
}
|
||||
|
||||
// Create and show SSH terminal widget in a new window
|
||||
SSHTerminalWidget *sshTerminal = new SSHTerminalWidget(connectionInfo);
|
||||
sshTerminal->setAttribute(Qt::WA_DeleteOnClose);
|
||||
sshTerminal->show();
|
||||
sshTerminal->raise();
|
||||
sshTerminal->activateWindow();
|
||||
}
|
||||
|
||||
void JailbrokenWidget::initWirelessDevice()
|
||||
{
|
||||
if (m_isInitialized)
|
||||
return;
|
||||
m_isInitialized = true;
|
||||
|
||||
m_connectButton->setEnabled(false);
|
||||
m_infoLabel->setText("Connecting to network device...");
|
||||
|
||||
// For wireless devices, connect directly without iproxy
|
||||
startSSH(m_selectedNetworkDevice.address, m_selectedNetworkDevice.port);
|
||||
}
|
||||
|
||||
void JailbrokenWidget::startSSH(const QString &host, uint16_t port)
|
||||
{
|
||||
if (m_sshConnected)
|
||||
return;
|
||||
|
||||
m_infoLabel->setText("Connecting to SSH server...");
|
||||
qDebug() << "Starting SSH connection to" << host << ":" << port;
|
||||
|
||||
// Create SSH session
|
||||
m_sshSession = ssh_new();
|
||||
if (!m_sshSession) {
|
||||
m_infoLabel->setText("Error: Failed to create SSH session");
|
||||
m_connectButton->setEnabled(true);
|
||||
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));
|
||||
m_infoLabel->setText(errorMsg);
|
||||
qDebug() << errorMsg;
|
||||
ssh_free(m_sshSession);
|
||||
m_sshSession = nullptr;
|
||||
m_connectButton->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "SSH connected successfully, attempting authentication...";
|
||||
|
||||
// Authenticate with password
|
||||
rc = ssh_userauth_password(m_sshSession, nullptr, "alpine");
|
||||
if (rc != SSH_AUTH_SUCCESS) {
|
||||
m_infoLabel->setText(QString("SSH authentication failed: %1")
|
||||
.arg(ssh_get_error(m_sshSession)));
|
||||
ssh_disconnect(m_sshSession);
|
||||
ssh_free(m_sshSession);
|
||||
m_sshSession = nullptr;
|
||||
m_connectButton->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create SSH channel
|
||||
m_sshChannel = ssh_channel_new(m_sshSession);
|
||||
if (!m_sshChannel) {
|
||||
m_infoLabel->setText("Error: Failed to create SSH channel");
|
||||
ssh_disconnect(m_sshSession);
|
||||
ssh_free(m_sshSession);
|
||||
m_sshSession = nullptr;
|
||||
m_connectButton->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open SSH channel
|
||||
rc = ssh_channel_open_session(m_sshChannel);
|
||||
if (rc != SSH_OK) {
|
||||
m_infoLabel->setText(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;
|
||||
m_connectButton->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Request a PTY
|
||||
rc = ssh_channel_request_pty(m_sshChannel);
|
||||
if (rc != SSH_OK) {
|
||||
m_infoLabel->setText("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;
|
||||
m_connectButton->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start shell
|
||||
rc = ssh_channel_request_shell(m_sshChannel);
|
||||
if (rc != SSH_OK) {
|
||||
m_infoLabel->setText("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;
|
||||
m_connectButton->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show terminal and connect to libssh
|
||||
m_terminal->show();
|
||||
connectLibsshToTerminal();
|
||||
|
||||
// Start timer to check for SSH data
|
||||
m_sshTimer->start(50); // Check every 50ms
|
||||
|
||||
m_sshConnected = true;
|
||||
m_connectButton->setEnabled(true);
|
||||
m_connectButton->setText("Disconnect SSH");
|
||||
m_infoLabel->setText("SSH terminal connected");
|
||||
|
||||
// Set focus to terminal
|
||||
m_terminal->setFocus();
|
||||
}
|
||||
|
||||
void JailbrokenWidget::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) {
|
||||
// Write data to terminal's PTY
|
||||
write(m_terminal->getPtySlaveFd(), buffer, nbytes);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
write(m_terminal->getPtySlaveFd(), buffer, nbytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if channel is closed
|
||||
if (ssh_channel_is_eof(m_sshChannel)) {
|
||||
disconnectSSH();
|
||||
}
|
||||
}
|
||||
|
||||
void JailbrokenWidget::disconnectSSH()
|
||||
{
|
||||
if (m_sshTimer) {
|
||||
m_sshTimer->stop();
|
||||
}
|
||||
|
||||
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 (iproxyProcess) {
|
||||
iproxyProcess->kill();
|
||||
delete iproxyProcess;
|
||||
iproxyProcess = nullptr;
|
||||
}
|
||||
|
||||
m_terminal->hide();
|
||||
m_infoLabel->setText("SSH disconnected");
|
||||
m_sshConnected = false;
|
||||
m_isInitialized = false;
|
||||
m_connectButton->setEnabled(false);
|
||||
}
|
||||
|
||||
// todo: crash at exit
|
||||
JailbrokenWidget::~JailbrokenWidget()
|
||||
{
|
||||
if (m_sshTimer) {
|
||||
m_sshTimer->stop();
|
||||
}
|
||||
|
||||
if (m_sshChannel) {
|
||||
ssh_channel_close(m_sshChannel);
|
||||
ssh_channel_free(m_sshChannel);
|
||||
}
|
||||
|
||||
if (m_sshSession) {
|
||||
ssh_disconnect(m_sshSession);
|
||||
ssh_free(m_sshSession);
|
||||
}
|
||||
|
||||
if (iproxyProcess) {
|
||||
iproxyProcess->kill();
|
||||
}
|
||||
}
|
||||
JailbrokenWidget::~JailbrokenWidget() {}
|
||||
|
||||
+3
-17
@@ -8,18 +8,14 @@
|
||||
#endif
|
||||
|
||||
#include "iDescriptor.h"
|
||||
#include "sshterminalwidget.h"
|
||||
#include <QAbstractButton>
|
||||
#include <QButtonGroup>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QProcess>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
class QTermWidget;
|
||||
|
||||
enum class DeviceType { None, Wired, Wireless };
|
||||
|
||||
@@ -32,8 +28,7 @@ public:
|
||||
~JailbrokenWidget();
|
||||
|
||||
private slots:
|
||||
void onConnectSSH();
|
||||
void checkSshData();
|
||||
void onOpenSSHTerminal();
|
||||
void onWiredDeviceAdded(iDescriptorDevice *device);
|
||||
void onWiredDeviceRemoved(const std::string &udid);
|
||||
void onWirelessDeviceAdded(const NetworkDevice &device);
|
||||
@@ -41,7 +36,6 @@ private slots:
|
||||
void onDeviceSelected(QAbstractButton *button);
|
||||
|
||||
private:
|
||||
void setupTerminal();
|
||||
void setupDeviceSelectionUI(QVBoxLayout *layout);
|
||||
void updateDeviceList();
|
||||
void clearDeviceButtons();
|
||||
@@ -49,14 +43,6 @@ private:
|
||||
void addWirelessDevice(const NetworkDevice &device);
|
||||
void resetSelection();
|
||||
|
||||
void initWiredDevice();
|
||||
void initWirelessDevice();
|
||||
void startSSH(const QString &host, uint16_t port);
|
||||
void disconnectSSH();
|
||||
void connectLibsshToTerminal();
|
||||
void deviceConnected(iDescriptorDevice *device);
|
||||
|
||||
QTermWidget *m_terminal;
|
||||
QLabel *m_infoLabel;
|
||||
QPushButton *m_connectButton;
|
||||
|
||||
@@ -68,7 +54,7 @@ private:
|
||||
QVBoxLayout *m_wirelessDevicesLayout;
|
||||
QButtonGroup *m_deviceButtonGroup;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#ifdef __linux__
|
||||
AvahiService *m_wirelessProvider = nullptr;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
#include "sshterminalwidget.h"
|
||||
#include "qprocessindicator.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QTimer>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QHostAddress>
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
#include <QMenu>
|
||||
#include <qtermwidget6/qtermwidget.h>
|
||||
#include <libssh/libssh.h>
|
||||
#include <unistd.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").arg(m_connectionInfo.deviceName));
|
||||
setMinimumSize(800, 600);
|
||||
|
||||
setupUI();
|
||||
|
||||
// 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));
|
||||
}
|
||||
});
|
||||
|
||||
m_terminal->startTerminalTeletype();
|
||||
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")) {
|
||||
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;
|
||||
QStringList possiblePaths = {"/usr/local/bin/iproxy",
|
||||
"/opt/homebrew/bin/iproxy", "/usr/bin/iproxy",
|
||||
"iproxy"};
|
||||
|
||||
for (const QString &path : possiblePaths) {
|
||||
if (QFile::exists(path) || path == "iproxy") {
|
||||
iproxyPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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...";
|
||||
|
||||
// Authenticate with password
|
||||
rc = ssh_userauth_password(m_sshSession, nullptr, "alpine");
|
||||
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) {
|
||||
// Write data to terminal's PTY
|
||||
write(m_terminal->getPtySlaveFd(), buffer, nbytes);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
write(m_terminal->getPtySlaveFd(), buffer, nbytes);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
m_iproxyProcess->kill();
|
||||
m_iproxyProcess->deleteLater();
|
||||
m_iproxyProcess = nullptr;
|
||||
}
|
||||
|
||||
m_sshConnected = false;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#ifndef SSHTERMINALWIDGET_H
|
||||
#define SSHTERMINALWIDGET_H
|
||||
|
||||
#include <QLabel>
|
||||
#include <QProcess>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
class QTermWidget;
|
||||
class QProcessIndicator;
|
||||
|
||||
enum class ConnectionType { Wired, Wireless };
|
||||
|
||||
struct ConnectionInfo {
|
||||
ConnectionType type;
|
||||
QString deviceName;
|
||||
QString deviceUdid; // For wired devices
|
||||
QString hostAddress; // For wireless devices
|
||||
uint16_t port;
|
||||
};
|
||||
|
||||
enum class TerminalState { Loading, Error, Connected };
|
||||
|
||||
class SSHTerminalWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SSHTerminalWidget(const ConnectionInfo &connectionInfo,
|
||||
QWidget *parent = nullptr);
|
||||
~SSHTerminalWidget();
|
||||
|
||||
private slots:
|
||||
void onRetryClicked();
|
||||
void checkSshData();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void setupLoadingState();
|
||||
void setupErrorState();
|
||||
void setupActionState();
|
||||
|
||||
void setState(TerminalState state);
|
||||
void showError(const QString &errorMessage);
|
||||
|
||||
void initializeConnection();
|
||||
void initWiredDevice();
|
||||
void initWirelessDevice();
|
||||
void startSSH(const QString &host, uint16_t port);
|
||||
void disconnectSSH();
|
||||
void connectLibsshToTerminal();
|
||||
void cleanup();
|
||||
|
||||
// UI Components
|
||||
QStackedWidget *m_stackedWidget;
|
||||
|
||||
// Loading state
|
||||
QWidget *m_loadingWidget;
|
||||
QProcessIndicator *m_loadingIndicator;
|
||||
QLabel *m_loadingLabel;
|
||||
|
||||
// Error state
|
||||
QWidget *m_errorWidget;
|
||||
QLabel *m_errorLabel;
|
||||
QPushButton *m_retryButton;
|
||||
|
||||
// Action state (connected terminal)
|
||||
QWidget *m_actionWidget;
|
||||
QTermWidget *m_terminal;
|
||||
|
||||
// Connection data
|
||||
ConnectionInfo m_connectionInfo;
|
||||
|
||||
// SSH components
|
||||
ssh_session m_sshSession;
|
||||
ssh_channel m_sshChannel;
|
||||
QTimer *m_sshTimer;
|
||||
QProcess *m_iproxyProcess;
|
||||
|
||||
// State tracking
|
||||
bool m_sshConnected;
|
||||
bool m_isInitialized;
|
||||
TerminalState m_currentState;
|
||||
};
|
||||
|
||||
#endif // SSHTERMINALWIDGET_H
|
||||
Reference in New Issue
Block a user