mirror of
http://180.163.74.83:13000/zhangzhenghao/write_mac_qt.git
synced 2025-12-12 13:44:29 +00:00
Initial commit: project setup and UI improvements (SSH creds above MAC, preset IPs, hide Windows console)
This commit is contained in:
commit
c51ea837e9
53
CMakeLists.txt
Normal file
53
CMakeLists.txt
Normal file
@ -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 .)
|
||||
22
src/Logger.cpp
Normal file
22
src/Logger.cpp
Normal file
@ -0,0 +1,22 @@
|
||||
#include "Logger.h"
|
||||
#include <QDateTime>
|
||||
|
||||
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();
|
||||
}
|
||||
16
src/Logger.h
Normal file
16
src/Logger.h
Normal file
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
|
||||
class Logger : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Logger(QObject *parent = nullptr);
|
||||
~Logger();
|
||||
void append(const QString &msg);
|
||||
private:
|
||||
QFile file;
|
||||
QTextStream *stream{nullptr};
|
||||
};
|
||||
337
src/MainWindow.cpp
Normal file
337
src/MainWindow.cpp
Normal file
@ -0,0 +1,337 @@
|
||||
#include "MainWindow.h"
|
||||
#include "NetworkManager.h"
|
||||
#include "SshClient.h"
|
||||
#include "Logger.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
#include <QDateTime>
|
||||
#include <QCoreApplication>
|
||||
#include <QScreen>
|
||||
#include <QGuiApplication>
|
||||
|
||||
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<int>(screenWidth * 0.25));
|
||||
// 日志框高度为屏幕高度的 40-50%,最小 410px
|
||||
int logHeight = qMax(410, static_cast<int>(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<int>::of(&QComboBox::currentIndexChanged), this, &MainWindow::onPresetPassChanged);
|
||||
connect(presetIpCombo, QOverload<int>::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);
|
||||
}
|
||||
56
src/MainWindow.h
Normal file
56
src/MainWindow.h
Normal file
@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
#include <QMainWindow>
|
||||
#include <QLineEdit>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QStatusBar>
|
||||
#include <QComboBox>
|
||||
|
||||
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);
|
||||
};
|
||||
125
src/NetworkManager.cpp
Normal file
125
src/NetworkManager.cpp
Normal file
@ -0,0 +1,125 @@
|
||||
#include "NetworkManager.h"
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QDir>
|
||||
#include <QRegularExpression>
|
||||
#include <QProcess>
|
||||
|
||||
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;
|
||||
}
|
||||
19
src/NetworkManager.h
Normal file
19
src/NetworkManager.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
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;
|
||||
};
|
||||
495
src/SshClient.cpp
Normal file
495
src/SshClient.cpp
Normal file
@ -0,0 +1,495 @@
|
||||
#include "SshClient.h"
|
||||
#include <QProcess>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <QCoreApplication>
|
||||
#include <QStringList>
|
||||
|
||||
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;
|
||||
}
|
||||
31
src/SshClient.h
Normal file
31
src/SshClient.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
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);
|
||||
};
|
||||
9
src/main.cpp
Normal file
9
src/main.cpp
Normal file
@ -0,0 +1,9 @@
|
||||
#include <QApplication>
|
||||
#include "MainWindow.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
MainWindow w;
|
||||
w.show();
|
||||
return app.exec();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user