commit c51ea837e91fc1d73df20f58b2b8f97b340886cd Author: Local User Date: Mon Nov 17 10:06:35 2025 +0800 Initial commit: project setup and UI improvements (SSH creds above MAC, preset IPs, hide Windows console) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ce02a82 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,53 @@ +cmake_minimum_required(VERSION 3.16) +project(MacModifier LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +# Try Qt6 first, fall back to Qt5 for wider compatibility +set(QT_MIN_VERSION 5.15) +find_package(Qt6 6.2 COMPONENTS Widgets Core QUIET) +if (NOT Qt6_FOUND) + find_package(Qt5 ${QT_MIN_VERSION} COMPONENTS Widgets Core REQUIRED) +endif() + +if (WIN32) + add_executable(MacModifier WIN32 + src/main.cpp + src/MainWindow.cpp + src/MainWindow.h + src/NetworkManager.cpp + src/NetworkManager.h + src/SshClient.cpp + src/SshClient.h + src/Logger.cpp + src/Logger.h + ) +else() + add_executable(MacModifier + src/main.cpp + src/MainWindow.cpp + src/MainWindow.h + src/NetworkManager.cpp + src/NetworkManager.h + src/SshClient.cpp + src/SshClient.h + src/Logger.cpp + src/Logger.h + ) +endif() + +if (Qt6_FOUND) + target_link_libraries(MacModifier PRIVATE Qt6::Widgets Qt6::Core) +else() + target_link_libraries(MacModifier PRIVATE Qt5::Widgets Qt5::Core) +endif() + +if (WIN32) + # No extra linker flags needed; WIN32 executable suppresses console window +endif() + +install(TARGETS MacModifier RUNTIME DESTINATION .) \ No newline at end of file diff --git a/src/Logger.cpp b/src/Logger.cpp new file mode 100644 index 0000000..a886b97 --- /dev/null +++ b/src/Logger.cpp @@ -0,0 +1,22 @@ +#include "Logger.h" +#include + +Logger::Logger(QObject *parent) : QObject(parent), file("MacModifier.log") { + file.open(QIODevice::Append | QIODevice::Text); + stream = new QTextStream(&file); +} + +Logger::~Logger() { + if (stream) { + delete stream; + stream = nullptr; + } + if (file.isOpen()) file.close(); +} + +void Logger::append(const QString &msg) { + if (!file.isOpen()) return; + QString ts = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); + (*stream) << ts << " - " << msg << '\n'; + stream->flush(); +} \ No newline at end of file diff --git a/src/Logger.h b/src/Logger.h new file mode 100644 index 0000000..8b00f21 --- /dev/null +++ b/src/Logger.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include +#include +#include + +class Logger : public QObject { + Q_OBJECT +public: + explicit Logger(QObject *parent = nullptr); + ~Logger(); + void append(const QString &msg); +private: + QFile file; + QTextStream *stream{nullptr}; +}; \ No newline at end of file diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp new file mode 100644 index 0000000..c829c85 --- /dev/null +++ b/src/MainWindow.cpp @@ -0,0 +1,337 @@ +#include "MainWindow.h" +#include "NetworkManager.h" +#include "SshClient.h" +#include "Logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *kInterfacesPath = "/etc/network/interfaces"; +// 尝试在存在参考文件时使用其前缀以保持格式一致 + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent), + ipInput(new QLineEdit(this)), + macInput(new QLineEdit(this)), + currentMacLabel(new QLabel("当前MAC: 未读取", this)), + modifyButton(new QPushButton("修改MAC", this)), + backupButton(new QPushButton("备份文件", this)), + logView(new QTextEdit(this)), + useSshCheck(new QCheckBox("使用SSH远程执行", this)), + sshUserInput(new QLineEdit(this)), + sshPassInput(new QLineEdit(this)), + presetPassCombo(new QComboBox(this)), + connectButton(new QPushButton("连接", this)), + refreshButton(new QPushButton("刷新", this)), + netMgr(new NetworkManager(this)), + sshClient(new SshClient(this)), + logger(new Logger(this)) +{ + setWindowTitle("MAC修改工具"); + logView->setReadOnly(true); + + // 根据屏幕分辨率自适应调整日志框大小 + QScreen *screen = QGuiApplication::primaryScreen(); + if (screen) { + QRect screenGeometry = screen->availableGeometry(); + int screenWidth = screenGeometry.width(); + int screenHeight = screenGeometry.height(); + + // 日志框宽度为屏幕宽度的 25-30%,最小 390px + int logWidth = qMax(390, static_cast(screenWidth * 0.25)); + // 日志框高度为屏幕高度的 40-50%,最小 410px + int logHeight = qMax(410, static_cast(screenHeight * 0.4)); + + logView->setMinimumWidth(logWidth); + logView->setMinimumHeight(logHeight); + } else { + // 如果无法获取屏幕信息,使用默认值 + logView->setMinimumWidth(390); + logView->setMinimumHeight(410); + } + + sshPassInput->setEchoMode(QLineEdit::Password); + + auto *central = new QWidget(this); + auto *layout = new QVBoxLayout(central); + + auto *form = new QFormLayout(); + form->addRow("SSH主机/IP", ipInput); + presetIpCombo = new QComboBox(this); + presetIpCombo->addItem("自定义"); + presetIpCombo->addItem("圆通"); + presetIpCombo->addItem("拼多多"); + form->addRow("内置IP", presetIpCombo); + form->addRow("SSH用户", sshUserInput); + presetPassCombo->addItem("自定义"); + presetPassCombo->addItem("圆通"); + presetPassCombo->addItem("拼多多"); + presetPassCombo->addItem("兔喜"); + form->addRow("内置密码", presetPassCombo); + form->addRow("登录密码", sshPassInput); + form->addRow("修改MAC地址", macInput); + // 在“当前MAC”行右侧添加 连接/刷新 按钮 + QWidget *macButtonsWidget = new QWidget(this); + auto *macBtnsLayout = new QHBoxLayout(macButtonsWidget); + macBtnsLayout->setContentsMargins(0,0,0,0); + macBtnsLayout->addWidget(connectButton); + macBtnsLayout->addWidget(refreshButton); + form->addRow(currentMacLabel, macButtonsWidget); + layout->addLayout(form); + + auto *btns = new QHBoxLayout(); + btns->addWidget(backupButton); + btns->addWidget(modifyButton); + layout->addLayout(btns); + + layout->addWidget(useSshCheck); + useSshCheck->setChecked(true); + useSshCheck->hide(); + connect(useSshCheck, &QCheckBox::toggled, this, &MainWindow::onSshModeToggled); + + layout->addWidget(new QLabel("日志", this)); + layout->addWidget(logView); + setCentralWidget(central); + + connect(modifyButton, &QPushButton::clicked, this, &MainWindow::onModifyClicked); + connect(backupButton, &QPushButton::clicked, this, &MainWindow::onBackupClicked); + connect(connectButton, &QPushButton::clicked, this, &MainWindow::onConnectClicked); + connect(refreshButton, &QPushButton::clicked, this, &MainWindow::onRefreshClicked); + connect(presetPassCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &MainWindow::onPresetPassChanged); + connect(presetIpCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &MainWindow::onPresetIpChanged); + + // 默认使用SSH远程执行,提供占位提示 + useSshCheck->setChecked(true); + ipInput->setPlaceholderText("例: 10.10.12.12/hostname"); + macInput->setText("90:A9:F7:30:00:00"); + sshUserInput->setText("root"); + sshPassInput->setPlaceholderText("登录密码"); + + updateCurrentMac(); +} + +MainWindow::~MainWindow() {} + +void MainWindow::log(const QString &msg) { + QString ts = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); + QString line = ts + " - " + msg; + logger->append(msg); + logView->append(line); + logView->append(""); +} + +QString MainWindow::generateMacFromIp(const QString &ip) { + // 简单示例:固定前缀90:A9:F7:30,根据IP最后两段计算两个字节,末尾固定00 + QRegularExpression re("^(?:\\d{1,3}\\.){3}\\d{1,3}$"); + if (!re.match(ip).hasMatch()) return {}; + auto parts = ip.split('.'); + if (parts.size() != 4) return {}; + bool ok1=false, ok2=false; + int p2 = parts[2].toInt(&ok1); + int p3 = parts[3].toInt(&ok2); + if (!ok1 || !ok2) return {}; + auto toHexByte = [](int v){ v = qBound(0, v, 255); return QString("%1").arg(v,2,16,QChar('0')).toUpper(); }; + QString mac = QString("90:A9:F7:30:%1:%2").arg(toHexByte(p2)) + .arg(toHexByte(p3)); + return mac; +} + +bool MainWindow::isValidMac(const QString &mac) const { + QRegularExpression mre("^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$"); + return mre.match(mac).hasMatch(); +} + +void MainWindow::updateCurrentMac() { + if (useSshCheck->isChecked() && !sshConnected) { + setCurrentMacStatus("当前MAC: 未连接主机", false); + return; + } + auto mac = netMgr->readCurrentMac(kInterfacesPath); + if (mac.isEmpty()) { + setCurrentMacStatus("当前MAC: 未找到或读取失败", false); + } else { + setCurrentMacStatus("当前MAC: " + mac, true); + } +} + +void MainWindow::onBackupClicked() { + QString err; + + // 检查是否使用 SSH 远程模式 + if (useSshCheck->isChecked()) { + QString host = ipInput->text().trimmed(); + QString user = sshUserInput->text().trimmed(); + QString password = sshPassInput->text().trimmed(); + + if (host.isEmpty() || user.isEmpty()) { + QMessageBox::warning(this, "参数错误", "请先填写IP地址和SSH用户名"); + return; + } + + // 使用 SshClient 执行远程备份 + SshClient ssh; + if (!ssh.backupRemoteFile(host, user, password, kInterfacesPath, &err)) { + QMessageBox::critical(this, "备份失败", err); + log("备份失败: " + err); + return; + } + + QMessageBox::information(this, "备份完成", "已在远程服务器备份到 " + QString(kInterfacesPath) + ".bak"); + log("已备份远程 interfaces 文件"); + return; + } + + // 本地模式 + if (!netMgr->backupFile(kInterfacesPath, &err)) { + QMessageBox::critical(this, "备份失败", err); + log("备份失败: " + err); + return; + } + QMessageBox::information(this, "备份完成", "已备份到同目录 .bak 文件"); + log("已备份 interfaces 文件"); +} + +void MainWindow::onModifyClicked() { + QString ip = ipInput->text().trimmed(); + QString mac = macInput->text().trimmed(); + if (mac.isEmpty()) mac = generateMacFromIp(ip); + if (!isValidMac(mac)) { + QMessageBox::warning(this, "输入错误", "MAC地址格式错误,需形如AA:BB:CC:DD:EE:FF"); + return; + } + + if (useSshCheck->isChecked()) { + // 通过SSH远程执行修改 + QString host = ipInput->text().trimmed(); + QString user = sshUserInput->text().trimmed(); + QString auth = QString("密码"); + QString secret = sshPassInput->text(); + QString err; + QString refPath = QCoreApplication::applicationDirPath() + "/interfaces"; + QString useRef = QFile::exists(refPath) ? refPath : QString(); + bool ok = sshClient->applyMacRemote(host, user, auth, secret, mac, kInterfacesPath, useRef, &err); + if (!ok) { + QMessageBox::critical(this, "远程修改失败", err); + log("远程修改失败: " + err); + return; + } + QMessageBox::information(this, "远程修改完成", "已通过SSH修改MAC"); + log("远程修改完成"); + if (!err.isEmpty()) log(err); + onRefreshClicked(); + } else { + // 本地修改 + QString err; + if (!netMgr->ensureRootPermission(&err)) { + QMessageBox::critical(this, "权限不足", err); + log("权限不足: " + err); + return; + } + QString refPath = QCoreApplication::applicationDirPath() + "/interfaces"; + QString useRef = QFile::exists(refPath) ? refPath : QString(); + if (!netMgr->applyMacLocal(kInterfacesPath, useRef, mac, &err)) { + QMessageBox::critical(this, "修改失败", err); + log("修改失败: " + err); + return; + } + QMessageBox::information(this, "修改完成", "MAC地址已更新"); + log("本地修改完成,更新MAC为: " + mac); + updateCurrentMac(); + } +} + +void MainWindow::onConnectClicked() { + // 连接:尝试读取远程当前MAC + QString host = ipInput->text().trimmed(); + QString user = sshUserInput->text().trimmed(); + QString secret = sshPassInput->text(); + QString err; + // 远端读取:复用 applyMacRemote 的读取函数(将实现) + QString remoteMac; + if (sshClient->readRemoteCurrentMac(host, user, secret, kInterfacesPath, &remoteMac, &err)) { + setCurrentMacStatus("当前MAC: " + remoteMac, true); + log("已连接并读取远程MAC: " + remoteMac); + sshConnected = true; + } else { + QMessageBox::critical(this, "连接失败", err); + log("连接失败: " + err); + sshConnected = false; + setCurrentMacStatus("当前MAC: 未连接主机", false); + } +} + +void MainWindow::onRefreshClicked() { + // 刷新当前MAC(根据是否使用SSH选择本地或远程) + if (useSshCheck->isChecked()) { + QString host = ipInput->text().trimmed(); + QString user = sshUserInput->text().trimmed(); + QString secret = sshPassInput->text(); + QString err; + QString remoteMac; + if (sshClient->readRemoteCurrentMac(host, user, secret, kInterfacesPath, &remoteMac, &err)) { + setCurrentMacStatus("当前MAC: " + remoteMac, true); + log("已刷新远程MAC: " + remoteMac); + sshConnected = true; + } else { + QMessageBox::warning(this, "刷新失败", err); + log("刷新失败: " + err); + sshConnected = false; + setCurrentMacStatus("当前MAC: 未连接主机", false); + } + } else { + updateCurrentMac(); + log("已刷新本地MAC"); + } +} + +void MainWindow::onPresetPassChanged(int idx) { + switch (idx) { + case 1: + sshPassInput->setText("&Over#B0Ost!"); + break; + case 2: + sshPassInput->setText("PddloTSecPwdOnly!"); + break; + case 3: + sshPassInput->setText("TxApPwd#2025!"); + break; + default: + // 自定义:不覆盖用户输入,可选择清空 + // sshPassInput->clear(); + break; + } +} + +void MainWindow::onPresetIpChanged(int idx) { + switch (idx) { + case 1: + ipInput->setText("192.168.172.173"); + break; + case 2: + ipInput->setText("10.10.12.12"); + break; + default: + // 自定义:不强制覆盖,保留用户当前输入 + break; + } +} + +void MainWindow::onSshModeToggled(bool checked) { + sshConnected = false; + updateCurrentMac(); +} + +void MainWindow::setCurrentMacStatus(const QString &text, bool ok) { + currentMacLabel->setText(text); + QPalette pal = currentMacLabel->palette(); + pal.setColor(QPalette::WindowText, ok ? QColor(0, 128, 0) : QColor(200, 0, 0)); + currentMacLabel->setPalette(pal); +} diff --git a/src/MainWindow.h b/src/MainWindow.h new file mode 100644 index 0000000..436bdf5 --- /dev/null +++ b/src/MainWindow.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +class NetworkManager; +class SshClient; +class Logger; + +class MainWindow : public QMainWindow { + Q_OBJECT +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); +private: + // UI + QLineEdit *ipInput; // 用作SSH主机/IP输入 + QLineEdit *macInput; // 用户直接输入MAC + QLabel *currentMacLabel; + QPushButton *modifyButton; + QPushButton *backupButton; + QTextEdit *logView; + QCheckBox *useSshCheck; + QLineEdit *sshUserInput; + QLineEdit *sshPassInput; // 登录密码 + QComboBox *presetPassCombo; // 内置密码选择 + QComboBox *presetIpCombo; // 内置IP选择 + QPushButton *connectButton; + QPushButton *refreshButton; + bool sshConnected; + + // Logic + NetworkManager *netMgr; + SshClient *sshClient; + Logger *logger; + + // helpers + QString generateMacFromIp(const QString &ip); + bool isValidMac(const QString &mac) const; + void updateCurrentMac(); + void log(const QString &msg); + void setCurrentMacStatus(const QString &text, bool ok); +private slots: + void onModifyClicked(); + void onBackupClicked(); + void onConnectClicked(); + void onRefreshClicked(); + void onPresetPassChanged(int idx); + void onPresetIpChanged(int idx); + void onSshModeToggled(bool checked); +}; \ No newline at end of file diff --git a/src/NetworkManager.cpp b/src/NetworkManager.cpp new file mode 100644 index 0000000..4df1edb --- /dev/null +++ b/src/NetworkManager.cpp @@ -0,0 +1,125 @@ +#include "NetworkManager.h" +#include +#include +#include +#include +#include + +NetworkManager::NetworkManager(QObject *parent) : QObject(parent) {} + +QString NetworkManager::readCurrentMac(const QString &interfacesPath) { + QFile f(interfacesPath); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + QTextStream in(&f); + QString line; + QRegularExpression re(QString::fromLatin1(R"(^\s*hwaddress\s+ether\s+([0-9A-Fa-f:]{17})\s*$)")); + while (!in.atEnd()) { + line = in.readLine(); + auto m = re.match(line); + if (m.hasMatch()) { + return m.captured(1).toUpper(); + } + } + return {}; +} + +bool NetworkManager::backupFile(const QString &interfacesPath, QString *err) { + QFile src(interfacesPath); + if (!src.exists()) { + if (err) *err = "interfaces文件不存在"; + return false; + } + QString backupPath = interfacesPath + ".bak"; + if (!src.copy(backupPath)) { + if (err) *err = QString("备份失败: %1").arg(src.errorString()); + return false; + } + return true; +} + +bool NetworkManager::ensureRootPermission(QString *err) { + // 简易检查:尝试以写方式打开,若失败且权限错误则提示需要root + QFile f("/etc/network/interfaces"); + if (f.open(QIODevice::ReadWrite | QIODevice::Text)) { + f.close(); + return true; + } + if (err) *err = "需要root权限以写入 /etc/network/interfaces,请以sudo运行应用或输入管理员凭证。"; + return false; +} + +QString NetworkManager::readReferencePrefixFromFile(const QString &referencePath, QString *err) { + QFile ref(referencePath); + if (!ref.open(QIODevice::ReadOnly | QIODevice::Text)) { + if (err) *err = QString("读取参考文件失败: %1").arg(ref.errorString()); + return {}; + } + QTextStream in(&ref); + QString contents = in.readAll(); + // 取行中前缀部分 "hwaddress ether " + QRegularExpression re(QString::fromLatin1(R"(^((\s*)hwaddress\s+ether\s+))"), QRegularExpression::MultilineOption); + auto m = re.match(contents); + if (!m.hasMatch()) { + if (err) *err = "参考文件中未找到 hwaddress ether 行"; + return {}; + } + return m.captured(1); +} + +QString NetworkManager::defaultReferencePrefix() const { + return "hwaddress ether "; +} + +bool NetworkManager::applyMacLocal(const QString &interfacesPath, + const QString &referencePathOrEmpty, + const QString &newMac, + QString *err) { + // 读取参考前缀,确保格式一致;若未提供文件则使用默认前缀 + QString prefix; + if (!referencePathOrEmpty.isEmpty()) { + prefix = readReferencePrefixFromFile(referencePathOrEmpty, err); + if (prefix.isEmpty()) return false; + } else { + prefix = defaultReferencePrefix(); + } + + QFile f(interfacesPath); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + if (err) *err = QString("读取interfaces失败: %1").arg(f.errorString()); + return false; + } + QTextStream in(&f); + QStringList lines; + QRegularExpression re(QString::fromLatin1(R"(^(\n?\s*hwaddress\s+ether\s+)([0-9A-Fa-f:]{17})\s*$)")); + while (!in.atEnd()) { + QString line = in.readLine(); + auto m = re.match(line); + if (m.hasMatch()) { + // 使用参考前缀的空白和关键字形式保持一致 + // 获取捕获的前缀(可能包含空白) + QString capturedPrefix = m.captured(1); + // 如果提供了参考前缀,尽量用提供的前缀替换;否则保持原前缀 + QString usePrefix = referencePathOrEmpty.isEmpty() ? capturedPrefix : prefix; + line = usePrefix + newMac; + } + lines << line; + } + f.close(); + + // 备份 + if (!backupFile(interfacesPath, err)) return false; + + QFile out(interfacesPath); + if (!out.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + if (err) *err = QString("写入interfaces失败: %1").arg(out.errorString()); + return false; + } + QTextStream os(&out); + for (const auto &l : lines) { + os << l << '\n'; + } + out.close(); + return true; +} \ No newline at end of file diff --git a/src/NetworkManager.h b/src/NetworkManager.h new file mode 100644 index 0000000..62bd7e5 --- /dev/null +++ b/src/NetworkManager.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +class NetworkManager : public QObject { + Q_OBJECT +public: + explicit NetworkManager(QObject *parent = nullptr); + QString readCurrentMac(const QString &interfacesPath); + bool backupFile(const QString &interfacesPath, QString *err); + bool ensureRootPermission(QString *err); + bool applyMacLocal(const QString &interfacesPath, + const QString &referencePathOrEmpty, + const QString &newMac, + QString *err); +private: + QString readReferencePrefixFromFile(const QString &referencePath, QString *err); + QString defaultReferencePrefix() const; +}; \ No newline at end of file diff --git a/src/SshClient.cpp b/src/SshClient.cpp new file mode 100644 index 0000000..ac37c18 --- /dev/null +++ b/src/SshClient.cpp @@ -0,0 +1,495 @@ +#include "SshClient.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static QString findExecPreferApp(const QString &name){ +#ifdef Q_OS_WIN + QString appDir = QCoreApplication::applicationDirPath(); + QString candidate = QDir(appDir).filePath(name + ".exe"); + if (QFile::exists(candidate)) return candidate; +#endif + return QStandardPaths::findExecutable(name); +} + +static QString escapeSingleQuotes(const QString &s){ + QString r = s; + r.replace("'", "'\\''"); + return r; +} + +static bool removeKnownHost(const QString &host){ + // 自动删除 known_hosts 中的指定主机记录 + QString sshKeygen = findExecPreferApp("ssh-keygen"); + if (sshKeygen.isEmpty()) { + // 如果没有 ssh-keygen,尝试手动删除 + QString knownHostsPath = QDir::homePath() + "/.ssh/known_hosts"; + QFile file(knownHostsPath); + if (!file.exists()) return true; // 文件不存在,无需删除 + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false; + + QStringList lines; + QTextStream in(&file); + while (!in.atEnd()) { + QString line = in.readLine(); + // 跳过包含该主机的行 + if (!line.startsWith(host + " ") && !line.startsWith(host + ",") && + !line.contains("," + host + " ") && !line.contains("," + host + ",")) { + lines.append(line); + } + } + file.close(); + + // 重写文件 + if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) return false; + QTextStream out(&file); + for (const QString &line : lines) { + out << line << "\n"; + } + file.close(); + return true; + } + + // 使用 ssh-keygen -R 删除 + QProcess p; + p.start(sshKeygen, {"-R", host}); + p.waitForFinished(5000); + return p.exitCode() == 0; +} + +static QString makeModifyScript(const QString &interfacesPath, const QString &newMac){ + return QString( + "set -e\n" + "f=\"%1\"; nm=\"%2\"\n" + "if [ ! -f \"$f\" ]; then echo \"interfaces not found\" >&2; exit 1; fi\n" + "cp \"$f\" \"$f.bak\"\n" + "echo \"BEFORE:\"; sed -n '1,200p' \"$f\"\n" + "if ! grep -Eq '^[[:space:]]*hwaddress[[:space:]]+ether[[:space:]]+[0-9A-Fa-f:]{17}' \"$f\"; then echo \"no hwaddress line\" >&2; exit 2; fi\n" + "sed -i -E 's/^([[:space:]]*hwaddress[[:space:]]+ether[[:space:]]+)[0-9A-Fa-f:]{17}/\\1'\"$nm\"'/g' \"$f\"\n" + "echo \"AFTER:\"; sed -n '1,200p' \"$f\"\n" + "if ! grep -Eq '^[[:space:]]*hwaddress[[:space:]]+ether[[:space:]]+'\"$nm\" \"$f\"; then echo \"replace failed\" >&2; exit 3; fi\n" + "echo \"OK\"\n" + ).arg(interfacesPath, newMac); +} + +SshClient::SshClient(QObject *parent) : QObject(parent) {} + +bool SshClient::applyMacRemote(const QString &host, + const QString &user, + const QString &auth, + const QString &secret, + const QString &newMac, + const QString &interfacesPath, + const QString &referencePathOrEmpty, + QString *err) { + // 远程命令:备份并按参考格式替换 hwaddress 行 + // 使用 sed 精确匹配并替换MAC部分 + // 构造远程执行脚本:保持前缀,仅替换MAC + QString scriptText = QString( + "set -e\n" + "f=\"%1\"; nm=\"%2\"\n" + "pat='^[[:space:]]*hwaddress[[:space:]]+ether[[:space:]]+[0-9A-Fa-f:]{17}([[:space:]]+[#].*)?[[:space:]]*$'\n" + "if [ ! -f \"$f\" ]; then echo \"interfaces not found\" >&2; exit 1; fi\n" + "cp \"$f\" \"$f.bak\"\n" + "if ! grep -Eq \"$pat\" \"$f\"; then echo \"no hwaddress line\" >&2; exit 2; fi\n" + "awk -v nm=\"$nm\" 'BEGIN{changed=0} \\n+ $0 ~ /^[[:space:]]*hwaddress[[:space:]]+ether[[:space:]]+[0-9A-Fa-f:]{17}([[:space:]]+[#].*)?[[:space:]]*$/ { \\n+ sub(/[0-9A-Fa-f:]{17}/, nm); changed=1 \\n+ } \\n+ { print } \\n+ END { if (!changed) exit 3 }' \"$f\" > \"$f.tmp\"\n" + "mv \"$f.tmp\" \"$f\"\n" + "grep -Eq \"^[[:space:]]*hwaddress[[:space:]]+ether[[:space:]]+$nm([[:space:]]+[#].*)?[[:space:]]*$\" \"$f\"\n" + ).arg(interfacesPath, newMac); + scriptText = makeModifyScript(interfacesPath, newMac); + + // 统一走 bash -c 传内联脚本,避免 stdin 与 sshpass 冲突 + QString remoteCmd; + bool sendStdin = false; + { + QString quoted = escapeSingleQuotes(scriptText); + if (user == "root") { + remoteCmd = QString("bash -lc '%1'").arg(quoted); + } else if (auth == "密码" && !secret.isEmpty()) { + remoteCmd = QString("echo \"%1\" | sudo -S bash -lc '%2'").arg(secret, quoted); + } else { + remoteCmd = QString("sudo -n bash -lc '%1'").arg(quoted); + } + } + + QString program; + QStringList args; + QStringList baseOpts; +#ifdef Q_OS_WIN + baseOpts << "-o" << "StrictHostKeyChecking=no" +#else + baseOpts << "-o" << "StrictHostKeyChecking=accept-new" +#endif + << "-o" << QString("UserKnownHostsFile=%1").arg(QDir::homePath() + "/.ssh/known_hosts") + << "-o" << "ConnectTimeout=10" + << "-o" << "LogLevel=ERROR" + << "-T" + << "-o" << "NumberOfPasswordPrompts=1"; +#ifdef Q_OS_WIN + if (auth == "密钥") { + QString ssh = findExecPreferApp("ssh"); + QString plink = findExecPreferApp("plink"); + if (!ssh.isEmpty()) { + program = ssh; + if (!secret.isEmpty()) args << "-i" << secret; + args << "-o" << "PreferredAuthentications=publickey" + << "-o" << "BatchMode=yes"; + args << baseOpts << QString("%1@%2").arg(user, host) << remoteCmd; + } else if (!plink.isEmpty()) { + program = plink; + args << "-batch"; + if (!secret.isEmpty()) args << "-i" << secret; + args << QString("%1@%2").arg(user, host) << remoteCmd; + } else { + if (err) *err = "Windows 未找到 ssh 或 plink"; + return false; + } + } else { + QString sshpass = findExecPreferApp("sshpass"); + QString plink = findExecPreferApp("plink"); + QString sshPath = findExecPreferApp("ssh"); + if (!sshpass.isEmpty() && !secret.isEmpty() && !sshPath.isEmpty()) { + program = sshpass; + args = {"-p", secret, sshPath}; + args << "-o" << "PreferredAuthentications=password" + << "-o" << "PubkeyAuthentication=no" + << "-o" << "KbdInteractiveAuthentication=no" + << "-o" << "PasswordAuthentication=yes"; + args << baseOpts << QString("%1@%2").arg(user, host) << remoteCmd; + } else if (!plink.isEmpty()) { + program = plink; + args << "-batch"; + if (!secret.isEmpty()) args << "-pw" << secret; + args << QString("%1@%2").arg(user, host) << remoteCmd; + } else if (!sshPath.isEmpty()) { + // 在 Windows 上没有 sshpass/plink 时不进行密码模式的 ssh 调用,避免交互挂起 + if (err) *err = "Windows 未找到支持密码认证的工具(sshpass/plink),请改用密钥或安装 PuTTY"; + return false; + } else { + if (err) *err = "Windows 未找到 sshpass、plink 或 ssh"; + return false; + } + } +#else + program = "ssh"; + if (auth == "密钥" && !secret.isEmpty()) { + args << "-i" << secret; + args << "-o" << "PreferredAuthentications=publickey" + << "-o" << "BatchMode=yes"; + args << baseOpts << QString("%1@%2").arg(user, host) << remoteCmd; + } else if (auth == "密钥" && secret.isEmpty()) { + args << "-o" << "PreferredAuthentications=publickey" + << "-o" << "BatchMode=yes"; + args << baseOpts << QString("%1@%2").arg(user, host) << remoteCmd; + } else { + QString sshpass = QStandardPaths::findExecutable("sshpass"); + if (!sshpass.isEmpty() && !secret.isEmpty()) { + program = sshpass; + args = {"-p", secret, "ssh"}; + } + args << "-o" << "PreferredAuthentications=password" + << "-o" << "PubkeyAuthentication=no" + << "-o" << "KbdInteractiveAuthentication=no" + << "-o" << "PasswordAuthentication=yes"; + args << baseOpts << QString("%1@%2").arg(user, host) << remoteCmd; + } +#endif + // 注:不再在此处根据平台重复调整 sshpass/私钥参数,已在上方分支统一处理 + + QProcess p; + p.setProcessChannelMode(QProcess::MergedChannels); + p.start(program, args); + if (sendStdin) { + p.write(scriptText.toUtf8()); + p.closeWriteChannel(); + } + if (!p.waitForStarted(10000)) { + if (err) *err = "无法启动SSH进程"; + return false; + } + if (!p.waitForFinished(30000)) { + p.kill(); + if (err) *err = "SSH命令执行超时"; + return false; + } + QString mergedOut = QString::fromLocal8Bit(p.readAllStandardOutput()); + if (p.exitCode() != 0) { + QString outputText = mergedOut.trimmed(); + if (outputText.contains("REMOTE HOST IDENTIFICATION HAS CHANGED")) { + // 自动删除旧的主机密钥并重试 + if (removeKnownHost(host)) { + // 重试连接 + QProcess p2; + p2.setProcessChannelMode(QProcess::MergedChannels); + p2.start(program, args); + if (sendStdin) { + p2.write(scriptText.toUtf8()); + p2.closeWriteChannel(); + } + if (p2.waitForStarted(10000) && p2.waitForFinished(30000)) { + if (p2.exitCode() == 0) { + QString retryOut = QString::fromLocal8Bit(p2.readAllStandardOutput()); + if (err) *err = QString("OUTPUT:\n%1").arg(retryOut); + return true; + } + // 重试后仍然失败 + QString retryOut = QString::fromLocal8Bit(p2.readAllStandardOutput()).trimmed(); + if (err) *err = QString("已自动删除旧主机密钥并重试,但仍失败: %1").arg(retryOut.isEmpty() ? "未知错误" : retryOut); + return false; + } + } + if (err) *err = QString("主机密钥已变化,自动删除失败。\n请手动执行: ssh-keygen -R '%1'").arg(host); + } else if (outputText.contains("Permission denied")) { + if (err) *err = QString("认证失败: %1\n" + "请检查账号/密钥/密码,并确认服务器允许该方式。\n" + "例如: /etc/ssh/sshd_config 中设置 PermitRootLogin、PasswordAuthentication。") + .arg(outputText); + } else { + if (err) *err = QString("SSH错误: %1").arg(outputText.isEmpty() ? QString("未知错误,可能为认证失败/无sudo权限/网络不可达") : outputText); + } + return false; + } + if (err) { + *err = QString("OUTPUT:\n%1").arg(mergedOut); + } + return true; +} + +bool SshClient::readRemoteCurrentMac(const QString &host, + const QString &user, + const QString &password, + const QString &interfacesPath, + QString *mac, + QString *err) { + // 构造远端读取命令:打印匹配到的MAC + QString cmd = QString( + "if [ ! -f '%1' ]; then echo 'interfaces not found' >&2; exit 1; fi; " + "grep -E '^[[:space:]]*hwaddress[[:space:]]+ether[[:space:]]+[0-9A-Fa-f:]{17}[[:space:]]*$' '%1' | " + "sed -E 's/^.*([0-9A-Fa-f:]{17}).*$/\\1/'" + ).arg(interfacesPath); + + QString remoteCmd = QString("bash -c '%1'").arg(escapeSingleQuotes(cmd)); + + QString program; + QStringList args; + QStringList baseOpts; +#ifdef Q_OS_WIN + baseOpts << "-o" << "StrictHostKeyChecking=no" +#else + baseOpts << "-o" << "StrictHostKeyChecking=accept-new" +#endif + << "-o" << QString("UserKnownHostsFile=%1").arg(QDir::homePath() + "/.ssh/known_hosts") + << "-o" << "ConnectTimeout=10" + << "-o" << "LogLevel=ERROR" + << "-T" + << "-o" << "PreferredAuthentications=password" + << "-o" << "PubkeyAuthentication=no" + << "-o" << "KbdInteractiveAuthentication=no" + << "-o" << "PasswordAuthentication=yes" + << "-o" << "NumberOfPasswordPrompts=1"; +#ifdef Q_OS_WIN + QString sshpass = findExecPreferApp("sshpass"); + QString plink = findExecPreferApp("plink"); + if (!password.isEmpty() && !sshpass.isEmpty()) { + program = sshpass; + QString sshPath = findExecPreferApp("ssh"); + if (sshPath.isEmpty()) sshPath = "ssh"; + args = {"-p", password, sshPath}; + args << baseOpts << QString("%1@%2").arg(user, host) << remoteCmd; + } else if (!plink.isEmpty()) { + program = plink; + args << "-batch"; + if (!password.isEmpty()) args << "-pw" << password; + args << QString("%1@%2").arg(user, host) << remoteCmd; + } else { + if (err) *err = "Windows 未找到支持密码认证的工具(sshpass/plink),请改用密钥或安装 PuTTY"; + return false; + } +#else + if (!password.isEmpty()) { + QString sshpassUnix = QStandardPaths::findExecutable("sshpass"); + if (!sshpassUnix.isEmpty()) { + program = sshpassUnix; + args = {"-p", password, "ssh"}; + } else { + program = "ssh"; + } + } else { + program = "ssh"; + } + args << baseOpts << QString("%1@%2").arg(user, host) << remoteCmd; +#endif + + QProcess p; + p.start(program, args); + if (!p.waitForStarted(10000)) { + if (err) *err = "无法启动SSH进程"; + return false; + } + if (!p.waitForFinished(15000)) { + p.kill(); + if (err) *err = "SSH命令执行超时"; + return false; + } + if (p.exitCode() != 0) { + QString stderrText = QString::fromLocal8Bit(p.readAllStandardError()); + QString stdoutText = QString::fromLocal8Bit(p.readAllStandardOutput()); + QString outputText = stderrText.trimmed(); + if (outputText.isEmpty()) outputText = stdoutText.trimmed(); + + // 检查是否是主机密钥变化错误 + if (outputText.contains("REMOTE HOST IDENTIFICATION HAS CHANGED")) { + if (removeKnownHost(host)) { + // 重试连接 + QProcess p2; + p2.start(program, args); + if (p2.waitForStarted(10000) && p2.waitForFinished(15000)) { + if (p2.exitCode() == 0) { + QString out = QString::fromLocal8Bit(p2.readAllStandardOutput()).trimmed(); + // 过滤掉控制字符,只提取 MAC 地址 + QRegularExpression macPattern("([0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2})"); + QRegularExpressionMatch match = macPattern.match(out); + if (match.hasMatch()) { + if (mac) *mac = match.captured(1).toUpper(); + return true; + } + if (err) *err = "未读取到有效的MAC地址"; + return false; + } + } + } + if (err) *err = QString("主机密钥已变化,自动删除失败。\n请手动执行: ssh-keygen -R '%1'").arg(host); + return false; + } + + if (err) *err = QString("SSH错误: %1").arg(outputText.isEmpty() ? QString("未知错误,可能为认证失败/文件不存在/网络不可达") : outputText); + return false; + } + QString out = QString::fromLocal8Bit(p.readAllStandardOutput()).trimmed(); + + // 过滤掉 ANSI 控制字符和其他非打印字符 + // 只保留 MAC 地址格式的字符(数字、字母A-F、冒号) + QRegularExpression macPattern("([0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2})"); + QRegularExpressionMatch match = macPattern.match(out); + if (match.hasMatch()) { + if (mac) *mac = match.captured(1).toUpper(); + return true; + } + + if (err) *err = QString("未读取到有效的MAC地址,输出: %1").arg(out); + return false; +} + +bool SshClient::backupRemoteFile(const QString &host, + const QString &user, + const QString &password, + const QString &filePath, + QString *err) { + // 构造远程备份命令 + QString cmd = QString( + "if [ ! -f '%1' ]; then echo 'file not found' >&2; exit 1; fi; " + "cp '%1' '%1.bak' && echo 'OK'" + ).arg(filePath); + + QString remoteCmd = QString("bash -c '%1'").arg(escapeSingleQuotes(cmd)); + + QString program; + QStringList args; + QStringList baseOpts; +#ifdef Q_OS_WIN + baseOpts << "-o" << "StrictHostKeyChecking=no" +#else + baseOpts << "-o" << "StrictHostKeyChecking=accept-new" +#endif + << "-o" << QString("UserKnownHostsFile=%1").arg(QDir::homePath() + "/.ssh/known_hosts") + << "-o" << "ConnectTimeout=10" + << "-o" << "LogLevel=ERROR" + << "-T" + << "-o" << "PreferredAuthentications=password" + << "-o" << "PubkeyAuthentication=no" + << "-o" << "KbdInteractiveAuthentication=no" + << "-o" << "PasswordAuthentication=yes" + << "-o" << "NumberOfPasswordPrompts=1"; +#ifdef Q_OS_WIN + QString sshpass = findExecPreferApp("sshpass"); + QString plink = findExecPreferApp("plink"); + if (!password.isEmpty() && !sshpass.isEmpty()) { + program = sshpass; + QString sshPath = findExecPreferApp("ssh"); + if (sshPath.isEmpty()) sshPath = "ssh"; + args = {"-p", password, sshPath}; + args << baseOpts << QString("%1@%2").arg(user, host) << remoteCmd; + } else if (!plink.isEmpty()) { + program = plink; + args << "-batch"; + if (!password.isEmpty()) args << "-pw" << password; + args << QString("%1@%2").arg(user, host) << remoteCmd; + } else { + if (err) *err = "Windows 未找到支持密码认证的工具(sshpass/plink),请改用密钥或安装 PuTTY"; + return false; + } +#else + if (!password.isEmpty()) { + QString sshpassUnix = QStandardPaths::findExecutable("sshpass"); + if (!sshpassUnix.isEmpty()) { + program = sshpassUnix; + args = {"-p", password, "ssh"}; + } else { + program = "ssh"; + } + } else { + program = "ssh"; + } + args << baseOpts << QString("%1@%2").arg(user, host) << remoteCmd; +#endif + + QProcess p; + p.start(program, args); + if (!p.waitForStarted(10000)) { + if (err) *err = "无法启动SSH进程"; + return false; + } + if (!p.waitForFinished(15000)) { + p.kill(); + if (err) *err = "SSH命令执行超时"; + return false; + } + + if (p.exitCode() != 0) { + QString stderrText = QString::fromLocal8Bit(p.readAllStandardError()); + QString stdoutText = QString::fromLocal8Bit(p.readAllStandardOutput()); + QString outputText = stderrText.trimmed(); + if (outputText.isEmpty()) outputText = stdoutText.trimmed(); + + // 检查是否是主机密钥变化错误 + if (outputText.contains("REMOTE HOST IDENTIFICATION HAS CHANGED")) { + if (removeKnownHost(host)) { + // 重试连接 + QProcess p2; + p2.start(program, args); + if (p2.waitForStarted(10000) && p2.waitForFinished(15000)) { + if (p2.exitCode() == 0) { + return true; + } + } + } + if (err) *err = QString("主机密钥已变化,自动删除失败。\n请手动执行: ssh-keygen -R '%1'").arg(host); + return false; + } + + if (outputText.contains("file not found")) { + if (err) *err = "远程文件不存在"; + } else { + if (err) *err = QString("SSH错误: %1").arg(outputText.isEmpty() ? "未知错误" : outputText); + } + return false; + } + + return true; +} diff --git a/src/SshClient.h b/src/SshClient.h new file mode 100644 index 0000000..ee8663a --- /dev/null +++ b/src/SshClient.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include + +class SshClient : public QObject { + Q_OBJECT +public: + explicit SshClient(QObject *parent = nullptr); + // auth: "密码" 或 "密钥" + bool applyMacRemote(const QString &host, + const QString &user, + const QString &auth, + const QString &secret, + const QString &newMac, + const QString &interfacesPath, + const QString &referencePathOrEmpty, + QString *err); + // 仅密码认证:读取远端当前MAC + bool readRemoteCurrentMac(const QString &host, + const QString &user, + const QString &password, + const QString &interfacesPath, + QString *mac, + QString *err); + // 远程备份文件 + bool backupRemoteFile(const QString &host, + const QString &user, + const QString &password, + const QString &filePath, + QString *err); +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..3d3c7a8 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,9 @@ +#include +#include "MainWindow.h" + +int main(int argc, char *argv[]) { + QApplication app(argc, argv); + MainWindow w; + w.show(); + return app.exec(); +} \ No newline at end of file