/* * iDescriptor: A free and open-source idevice management tool. * * Copyright (C) 2025 Uncore * * 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 . */ #include "sshterminaltool.h" #include "appcontext.h" #include "responsiveqlabel.h" #include "sshterminalwidget.h" SSHTerminalTool::SSHTerminalTool(QWidget *parent) : Tool{parent}, m_selectedUniq(QString(""), false) { setWindowTitle("SSH Terminal - iDescriptor"); QHBoxLayout *mainLayout = new QHBoxLayout(this); mainLayout->setContentsMargins(2, 2, 2, 2); mainLayout->setSpacing(2); // Create responsive image label ResponsiveQLabel *deviceImageLabel = new ResponsiveQLabel(this); deviceImageLabel->setPixmap(QPixmap(":/resources/iphone.png")); deviceImageLabel->setMinimumWidth(200); deviceImageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); deviceImageLabel->setStyleSheet("background: transparent; border: none;"); mainLayout->addWidget(deviceImageLabel, 1); connect(AppContext::sharedInstance(), &AppContext::deviceAdded, this, &SSHTerminalTool::On_iDeviceAdded); connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, &SSHTerminalTool::On_iDeviceRemoved); connect(NetworkDeviceProvider::sharedInstance(), &NetworkDeviceProvider::deviceAdded, this, &SSHTerminalTool::onWirelessDeviceAdded); connect(NetworkDeviceProvider::sharedInstance(), &NetworkDeviceProvider::deviceRemoved, this, &SSHTerminalTool::onWirelessDeviceRemoved); QWidget *rightContainer = new QWidget(); rightContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); rightContainer->setMinimumWidth(400); QVBoxLayout *rightLayout = new QVBoxLayout(rightContainer); rightLayout->setContentsMargins(15, 15, 15, 15); rightLayout->setSpacing(10); setupDeviceSelectionUI(rightLayout); mainLayout->addWidget(rightContainer, 3); updateDeviceList(); } void SSHTerminalTool::setupDeviceSelectionUI(QVBoxLayout *layout) { // Create scroll area for device selection QScrollArea *scrollArea = new QScrollArea(); scrollArea->setWidgetResizable(true); scrollArea->setMinimumHeight(200); scrollArea->setMaximumHeight(300); scrollArea->setObjectName("devicescrollArea"); // scrollArea->setStyleSheet("QWidget#devicescrollArea {border: none;}"); QWidget *scrollContent = new QWidget(); m_deviceLayout = new QVBoxLayout(scrollContent); m_deviceLayout->setContentsMargins(5, 5, 5, 5); m_deviceLayout->setSpacing(10); // Button group for device selection m_deviceButtonGroup = new QButtonGroup(this); connect(m_deviceButtonGroup, QOverload::of(&QButtonGroup::buttonClicked), this, &SSHTerminalTool::onDeviceSelected); // Wired devices group m_connectedDevicesGroup = new QGroupBox("Connected Devices"); m_connectedDevicesLayout = new QVBoxLayout(m_connectedDevicesGroup); m_deviceLayout->addWidget(m_connectedDevicesGroup); // Wireless devices group m_networkDevicesGroup = new QGroupBox("Network (All) Devices"); m_networkDevicesLayout = new QVBoxLayout(m_networkDevicesGroup); m_deviceLayout->addWidget(m_networkDevicesGroup); scrollArea->setWidget(scrollContent); layout->addWidget(scrollArea); // Info and connect button m_infoLabel = new QLabel("Select a device to connect"); layout->addWidget(m_infoLabel); // support m_connectButton = new QPushButton("Choose a device"); m_connectButton->setMaximumWidth(m_connectButton->sizeHint().width()); m_connectButton->setEnabled(false); connect(m_connectButton, &QPushButton::clicked, this, &SSHTerminalTool::onOpenSSHTerminal); layout->addWidget(m_connectButton, 0, Qt::AlignCenter); // Manual IP connection group m_manualConnectionGroup = new QGroupBox("Manual IP connection"); QVBoxLayout *manualLayout = new QVBoxLayout(m_manualConnectionGroup); QHBoxLayout *ipLayout = new QHBoxLayout(); QLabel *ipLabel = new QLabel("Device IP:"); m_manualIpEdit = new QLineEdit(); m_manualIpEdit->setPlaceholderText("e.g. 192.168.1.10"); ipLayout->addWidget(ipLabel); ipLayout->addWidget(m_manualIpEdit); m_manualConnectButton = new QPushButton("Connect via IP"); connect(m_manualConnectButton, &QPushButton::clicked, this, &SSHTerminalTool::onManualConnectClicked); manualLayout->addLayout(ipLayout); manualLayout->addWidget(m_manualConnectButton, 0, Qt::AlignLeft); layout->addWidget(m_manualConnectionGroup); } void SSHTerminalTool::updateDeviceList() { // Clear existing devices clearDeviceButtons(); // Add wired devices QList> wiredDevices = AppContext::sharedInstance()->getAllDevices(); for (const std::shared_ptr &device : wiredDevices) { addDevice(device); } // Add wireless devices QMap wirelessDevices = NetworkDeviceProvider::sharedInstance()->getNetworkDevices(); for (const NetworkDevice &device : wirelessDevices) { addWirelessDevice(device); } } void SSHTerminalTool::clearDeviceButtons() { // Remove all buttons from button group and layouts for (QAbstractButton *button : m_deviceButtonGroup->buttons()) { m_deviceButtonGroup->removeButton(button); // button->deleteLater(); } // // Clear layouts // QLayoutItem *item; // while ((item = m_deviceLayout->takeAt(0)) != nullptr) { // delete item->widget(); // delete item; // } // while ((item = m_networkDevicesLayout->takeAt(0)) != nullptr) { // delete item->widget(); // delete item; // } auto clearLayout = [](QLayout *layout) { QLayoutItem *item; while ((item = layout->takeAt(0)) != nullptr) { if (QWidget *w = item->widget()) { w->deleteLater(); } delete item; } }; // Only clear the inner layouts, keep the group boxes and m_deviceLayout clearLayout(m_connectedDevicesLayout); clearLayout(m_networkDevicesLayout); } // FIXME: should not add already exiting devices void SSHTerminalTool::addDevice(const std::shared_ptr device) { QString deviceName = QString::fromStdString(device->deviceInfo.deviceName); QString udid = device->udid; QString displayText = QString("%1\n%2").arg(deviceName, udid); QRadioButton *radioButton = new QRadioButton(displayText); if (device->deviceInfo.isWireless) { radioButton->setProperty( "deviceName", QString::fromStdString(device->deviceInfo.deviceName)); radioButton->setProperty( "uniq", QString::fromStdString(device->deviceInfo.wifiMacAddress)); radioButton->setProperty( "ip", QString::fromStdString(device->deviceInfo.ipAddress)); // workaround radioButton->setProperty("udid", device->udid); radioButton->setProperty("deviceType", "wireless"); } else { radioButton->setProperty("uniq", udid); radioButton->setProperty("deviceType", "wired"); } m_deviceButtonGroup->addButton(radioButton); m_connectedDevicesLayout->addWidget(radioButton); } void SSHTerminalTool::addWirelessDevice(const NetworkDevice &device) { QString displayText = QString("%1\n%2").arg(device.name, device.address); QRadioButton *radioButton = new QRadioButton(displayText); radioButton->setProperty("deviceName", device.name); radioButton->setProperty("uniq", device.macAddress); radioButton->setProperty("ip", device.address); radioButton->setProperty("deviceType", "wireless"); m_deviceButtonGroup->addButton(radioButton); m_networkDevicesLayout->addWidget(radioButton); } // m_selectedUniq void SSHTerminalTool::On_iDeviceAdded( const std::shared_ptr device) { addDevice(device); } void SSHTerminalTool::On_iDeviceRemoved(const QString &udid) { // Find and remove the corresponding radio button for (QAbstractButton *button : m_deviceButtonGroup->buttons()) { if (button->property("udid").toString() == udid) { m_deviceButtonGroup->removeButton(button); button->deleteLater(); break; } } // Reset selection if this device was selected // if (m_selectedUniq == qudid) { // resetSelection(); // } } void SSHTerminalTool::onWirelessDeviceAdded(const NetworkDevice &device) { addWirelessDevice(device); } void SSHTerminalTool::onWirelessDeviceRemoved(const QString &deviceName) { // Find and remove the corresponding radio button for (QAbstractButton *button : m_deviceButtonGroup->buttons()) { if (button->property("deviceType").toString() == "wireless" && button->property("deviceName").toString() == deviceName) { m_deviceButtonGroup->removeButton(button); button->deleteLater(); break; } } // // Reset selection if this device was selected // if (m_selectedDeviceType == DeviceType::Wireless && // m_selectedNetworkDevice.name == deviceName) { // resetSelection(); // } } void SSHTerminalTool::onDeviceSelected(QAbstractButton *button) { QString deviceType = button->property("deviceType").toString(); QString ip = button->property("ip").toString(); bool isWireless = (deviceType == "wireless"); m_selectedUniq = iDescriptor::Uniq(button->property("uniq").toString(), isWireless); // required for wireless devices if (!ip.isEmpty()) { m_selectedUniq.setIP(ip); } auto selectedDevice = AppContext::sharedInstance()->getDevice(m_selectedUniq); if (!selectedDevice) { QString udid = button->property("udid").toString(); if (!udid.isEmpty()) { auto selectedDevice = AppContext::sharedInstance()->getDevice(udid); if (selectedDevice) { if (selectedDevice->deviceInfo.jailbroken) { m_infoLabel->setText( "Device selected (detected as jailbroken)"); } else { m_infoLabel->setText( "Device selected (detected as non-jailbroken)"); } } } else { // device may get removed just as user clicks, // so we'll treat it as unknown m_infoLabel->setText( "Network device selected (jailbreak status unknown)"); } } else { if (selectedDevice->deviceInfo.jailbroken) { m_infoLabel->setText("Device selected (detected as jailbroken)"); } else { m_infoLabel->setText( "Device selected (detected as non-jailbroken)"); } } m_connectButton->setEnabled(true); m_connectButton->setText("Connect"); } void SSHTerminalTool::onManualConnectClicked() { QString ip = m_manualIpEdit->text().trimmed(); if (ip.isEmpty()) { m_infoLabel->setText("Please enter an IP address for manual connect"); QMessageBox::warning(this, "Missing IP Address", "Please enter a device IP address."); return; } // Treat manual connection as wireless with unknown jailbreak status m_selectedUniq.set(ip, true); // mark as "wireless"-style m_selectedUniq.setIP(ip); m_infoLabel->setText( QString("Manual IP %1 selected (jailbreak status unknown)").arg(ip)); // Reuse normal connect logic onOpenSSHTerminal(); } void SSHTerminalTool::resetSelection() { // m_selectedDeviceType = DeviceType::None; // m_selectedWiredDevice = nullptr; // m_selectedNetworkDevice = NetworkDevice{}; m_connectButton->setEnabled(false); m_infoLabel->setText("Select a device to connect"); // Uncheck all radio buttons if (m_deviceButtonGroup->checkedButton()) { m_deviceButtonGroup->setExclusive(false); m_deviceButtonGroup->checkedButton()->setChecked(false); m_deviceButtonGroup->setExclusive(true); } } void SSHTerminalTool::onOpenSSHTerminal() { if (m_selectedUniq.get().isEmpty()) { m_infoLabel->setText("Please select a device first"); QMessageBox::warning( this, "No Device Selected", "Please select a device before trying to connect."); return; } // Warn if known device is not jailbroken auto selectedDevice = AppContext::sharedInstance()->getDevice(m_selectedUniq); if (selectedDevice && !selectedDevice->deviceInfo.jailbroken) { auto reply = QMessageBox::question( this, "Device Not Jailbroken", "The selected device is not detected as jailbroken.\n" "SSH access may not be available.\n\n" "Do you want to continue anyway?", QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (reply == QMessageBox::No) { m_infoLabel->setText( "Connection cancelled (device not jailbroken)"); return; } } bool isWireless = m_selectedUniq.isMac(); // there must be an IP for wireless devices, check just in case if (isWireless && m_selectedUniq.getIP().isEmpty()) { m_infoLabel->setText( "Selected network device is missing IP address. Please try again."); QMessageBox::warning(this, "Missing IP Address", "The selected network device is missing an IP " "address. Please try again."); return; } // Prepare connection info ConnectionInfo connectionInfo; if (!isWireless) { connectionInfo.type = ConnectionType::Wired; connectionInfo.deviceName = "FIXME"; connectionInfo.deviceUdid = m_selectedUniq.get(); connectionInfo.hostAddress = "127.0.0.1"; connectionInfo.port = 22; } else { connectionInfo.type = ConnectionType::Wireless; connectionInfo.deviceName = m_selectedUniq.get(); connectionInfo.deviceUdid = ""; connectionInfo.hostAddress = m_selectedUniq.getIP(); connectionInfo.port = 22; } // 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(); } SSHTerminalTool::~SSHTerminalTool() {}