diff --git a/CMakeLists.txt b/CMakeLists.txt index 43930c7..6473163 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,6 +190,10 @@ set(PROJECT_SOURCES db/RouteEntity.h db/RouteEntity.cpp res/darkstyle.qrc + ui/edit/edit_ssh.cpp + ui/edit/edit_ssh.h + ui/edit/edit_ssh.ui + fmt/SSHBean.h ) # Qt exe diff --git a/db/Database.cpp b/db/Database.cpp index 59cad5e..739bbd0 100644 --- a/db/Database.cpp +++ b/db/Database.cpp @@ -225,6 +225,8 @@ namespace NekoGui { bean = new NekoGui_fmt::QUICBean(NekoGui_fmt::QUICBean::proxy_TUIC); } else if (type == "wireguard") { bean = new NekoGui_fmt::WireguardBean(NekoGui_fmt::WireguardBean()); + } else if (type == "ssh") { + bean = new NekoGui_fmt::SSHBean(NekoGui_fmt::SSHBean()); } else if (type == "custom") { bean = new NekoGui_fmt::CustomBean(); } else { diff --git a/db/ProxyEntity.hpp b/db/ProxyEntity.hpp index bc5911e..e169e51 100644 --- a/db/ProxyEntity.hpp +++ b/db/ProxyEntity.hpp @@ -19,6 +19,8 @@ namespace NekoGui_fmt { class WireguardBean; + class SSHBean; + class CustomBean; class ChainBean; @@ -75,6 +77,10 @@ namespace NekoGui { return (NekoGui_fmt::WireguardBean *) bean.get(); }; + [[nodiscard]] NekoGui_fmt::SSHBean *SSHBean() const { + return (NekoGui_fmt::SSHBean *) bean.get(); + }; + [[nodiscard]] NekoGui_fmt::CustomBean *CustomBean() const { return (NekoGui_fmt::CustomBean *) bean.get(); }; diff --git a/fmt/Bean2CoreObj_box.cpp b/fmt/Bean2CoreObj_box.cpp index 1d27615..fc3e09b 100644 --- a/fmt/Bean2CoreObj_box.cpp +++ b/fmt/Bean2CoreObj_box.cpp @@ -295,6 +295,27 @@ namespace NekoGui_fmt { return result; } + CoreObjOutboundBuildResult SSHBean::BuildCoreObjSingBox(){ + CoreObjOutboundBuildResult result; + + QJsonObject outbound{ + {"type", "ssh"}, + {"server", serverAddress}, + {"server_port", serverPort}, + {"user", user}, + {"password", password}, + }; + if (!privateKey.isEmpty()) outbound["private_key"] = privateKey; + if (!privateKeyPath.isEmpty()) outbound["private_key_path"] = privateKeyPath; + if (!privateKeyPass.isEmpty()) outbound["private_key_passphrase"] = privateKeyPass; + if (!hostKey.isEmpty()) outbound["host_key"] = QListStr2QJsonArray(hostKey); + if (!hostKeyAlgs.isEmpty()) outbound["host_key_algorithms"] = QListStr2QJsonArray(hostKeyAlgs); + if (!clientVersion.isEmpty()) outbound["client_version"] = clientVersion; + + result.outbound = outbound; + return result; + } + CoreObjOutboundBuildResult CustomBean::BuildCoreObjSingBox() { CoreObjOutboundBuildResult result; diff --git a/fmt/Bean2Link.cpp b/fmt/Bean2Link.cpp index 52d8218..184014f 100644 --- a/fmt/Bean2Link.cpp +++ b/fmt/Bean2Link.cpp @@ -266,4 +266,31 @@ namespace NekoGui_fmt { return url.toString(QUrl::FullyEncoded); } + QString SSHBean::ToShareLink() { + QUrl url; + url.setScheme("ssh"); + url.setHost(serverAddress); + url.setPort(serverPort); + if (!name.isEmpty()) url.setFragment(name); + QUrlQuery q; + q.addQueryItem("user", user); + q.addQueryItem("password", password); + q.addQueryItem("private_key", privateKey.toUtf8().toBase64(QByteArray::OmitTrailingEquals)); + q.addQueryItem("private_key_path", privateKeyPath); + q.addQueryItem("private_key_passphrase", privateKeyPass); + QStringList b64HostKeys = {}; + for (const auto& item: hostKey) { + b64HostKeys << item.toUtf8().toBase64(QByteArray::OmitTrailingEquals); + } + q.addQueryItem("host_key", b64HostKeys.join("-")); + QStringList b64HostKeyAlgs = {}; + for (const auto& item: hostKeyAlgs) { + b64HostKeyAlgs << item.toUtf8().toBase64(QByteArray::OmitTrailingEquals); + } + q.addQueryItem("host_key_algorithms", b64HostKeyAlgs.join("-")); + q.addQueryItem("client_version", clientVersion); + url.setQuery(q); + return url.toString(QUrl::FullyEncoded); + } + } // namespace NekoGui_fmt \ No newline at end of file diff --git a/fmt/Link2Bean.cpp b/fmt/Link2Bean.cpp index dc87136..2f946c7 100644 --- a/fmt/Link2Bean.cpp +++ b/fmt/Link2Bean.cpp @@ -366,4 +366,32 @@ namespace NekoGui_fmt { return true; } + bool SSHBean::TryParseLink(const QString &link) { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = GetQuery(url); + + name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + user = query.queryItemValue("user"); + password = query.queryItemValue("password"); + privateKey = QByteArray::fromBase64(query.queryItemValue("private_key").toUtf8(), QByteArray::OmitTrailingEquals); + privateKeyPath = query.queryItemValue("private_key_path"); + privateKeyPass = query.queryItemValue("private_key_passphrase"); + auto hostKeysRaw = query.queryItemValue("host_key"); + for (const auto &item: hostKeysRaw.split("-")) { + auto b64hostKey = QByteArray::fromBase64(item.toUtf8(), QByteArray::OmitTrailingEquals); + if (!b64hostKey.isEmpty()) hostKey << QString(b64hostKey); + } + auto hostKeyAlgsRaw = query.queryItemValue("host_key_algorithms"); + for (const auto &item: hostKeyAlgsRaw.split("-")) { + auto b64hostKeyAlg = QByteArray::fromBase64(item.toUtf8(), QByteArray::OmitTrailingEquals); + if (!b64hostKeyAlg.isEmpty()) hostKeyAlgs << QString(b64hostKeyAlg); + } + clientVersion = query.queryItemValue("client_version"); + + return true; + } + } // namespace NekoGui_fmt \ No newline at end of file diff --git a/fmt/SSHBean.h b/fmt/SSHBean.h new file mode 100644 index 0000000..e76f80a --- /dev/null +++ b/fmt/SSHBean.h @@ -0,0 +1,36 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" + +namespace NekoGui_fmt { + class SSHBean : public AbstractBean { + public: + QString user = "root"; + QString password; + QString privateKey; + QString privateKeyPath; + QString privateKeyPass; + QStringList hostKey; + QStringList hostKeyAlgs; + QString clientVersion; + + SSHBean() : AbstractBean(0) { + _add(new configItem("user", &user, itemType::string)); + _add(new configItem("password", &password, itemType::string)); + _add(new configItem("privateKey", &privateKey, itemType::string)); + _add(new configItem("privateKeyPath", &privateKeyPath, itemType::string)); + _add(new configItem("privateKeyPass", &privateKeyPass, itemType::string)); + _add(new configItem("hostKey", &hostKey, itemType::stringList)); + _add(new configItem("hostKeyAlgs", &hostKeyAlgs, itemType::stringList)); + _add(new configItem("clientVersion", &clientVersion, itemType::string)); + }; + + QString DisplayType() override { return "SSH"; }; + + CoreObjOutboundBuildResult BuildCoreObjSingBox() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + }; +} diff --git a/fmt/WireguardBean.h b/fmt/WireguardBean.h index 5f5b52b..f8039e1 100644 --- a/fmt/WireguardBean.h +++ b/fmt/WireguardBean.h @@ -13,7 +13,7 @@ namespace NekoGui_fmt { int MTU = 1420; bool useSystemInterface = false; bool enableGSO = false; - int workerCount; + int workerCount = 0; WireguardBean() : AbstractBean(0) { _add(new configItem("private_key", &privateKey, itemType::string)); diff --git a/fmt/includes.h b/fmt/includes.h index 3da6c2e..9263454 100644 --- a/fmt/includes.h +++ b/fmt/includes.h @@ -8,4 +8,5 @@ #include "NaiveBean.hpp" #include "QUICBean.hpp" #include "WireguardBean.h" +#include "SSHBean.h" #include "CustomBean.hpp" diff --git a/sub/GroupUpdater.cpp b/sub/GroupUpdater.cpp index 93f7a16..dc8e5de 100644 --- a/sub/GroupUpdater.cpp +++ b/sub/GroupUpdater.cpp @@ -210,6 +210,14 @@ namespace NekoGui_sub { if (!ok) return; } + // SSH + if (str.startsWith("ssh://")) { + needFix = false; + ent = NekoGui::ProfileManager::NewProxyEntity("ssh"); + auto ok = ent->SSHBean()->TryParseLink(str); + if (!ok) return; + } + if (ent == nullptr) return; // Fix diff --git a/ui/edit/dialog_edit_profile.cpp b/ui/edit/dialog_edit_profile.cpp index 9977884..9efc31e 100644 --- a/ui/edit/dialog_edit_profile.cpp +++ b/ui/edit/dialog_edit_profile.cpp @@ -9,6 +9,7 @@ #include "ui/edit/edit_naive.h" #include "ui/edit/edit_quic.h" #include "ui/edit/edit_wireguard.h" +#include "ui/edit/edit_ssh.h" #include "ui/edit/edit_custom.h" #include "fmt/includes.h" @@ -161,6 +162,7 @@ DialogEditProfile::DialogEditProfile(const QString &_type, int profileOrGroupId, LOAD_TYPE("hysteria2") LOAD_TYPE("tuic") LOAD_TYPE("wireguard") + LOAD_TYPE("ssh") ui->type->addItem(tr("Custom (%1 outbound)").arg(software_core_name), "internal"); ui->type->addItem(tr("Custom (%1 config)").arg(software_core_name), "internal-full"); ui->type->addItem(tr("Custom (Extra Core)"), "custom"); @@ -224,6 +226,10 @@ void DialogEditProfile::typeSelected(const QString &newType) { auto _innerWidget = new EditWireguard(this); innerWidget = _innerWidget; innerEditor = _innerWidget; + } else if (type == "ssh") { + auto _innerWidget = new EditSSH(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; } else if (type == "custom" || type == "internal" || type == "internal-full") { auto _innerWidget = new EditCustom(this); innerWidget = _innerWidget; diff --git a/ui/edit/edit_ssh.cpp b/ui/edit/edit_ssh.cpp new file mode 100644 index 0000000..f919e84 --- /dev/null +++ b/ui/edit/edit_ssh.cpp @@ -0,0 +1,52 @@ +#include "edit_ssh.h" +#include "ui_edit_ssh.h" +#include + +#include "fmt/SSHBean.h" + +EditSSH::EditSSH(QWidget *parent) : QWidget(parent), ui(new Ui::EditSSH) { + ui->setupUi(this); +} + +EditSSH::~EditSSH() { + delete ui; +} + +void EditSSH::onStart(std::shared_ptr _ent) { + this->ent = _ent; + auto bean = this->ent->SSHBean(); + + ui->user->setText(bean->user); + ui->password->setText(bean->password); + ui->private_key->setText(bean->privateKey); + ui->private_key_path->setText(bean->privateKeyPath); + ui->private_key_pass->setText(bean->privateKeyPass); + ui->host_key->setText(bean->hostKey.join(",")); + ui->host_key_algs->setText(bean->hostKeyAlgs.join(",")); + ui->client_version->setText(bean->clientVersion); + + connect(ui->choose_pk, &QPushButton::clicked, this, [=] { + auto fn = QFileDialog::getOpenFileName(this, QObject::tr("Select"), QDir::currentPath(), + "", nullptr, QFileDialog::Option::ReadOnly); + if (!fn.isEmpty()) { + ui->private_key_path->setText(fn); + } + }); +} + +bool EditSSH::onEnd() { + auto bean = this->ent->SSHBean(); + + bean->user = ui->user->text(); + bean->password = ui->password->text(); + bean->privateKey = ui->private_key->toPlainText(); + bean->privateKeyPath = ui->private_key_path->text(); + bean->privateKeyPass = ui->private_key_pass->text(); + if (!ui->host_key->text().trimmed().isEmpty()) bean->hostKey = ui->host_key->text().split(","); + else bean->hostKey = {}; + if (!ui->host_key_algs->text().trimmed().isEmpty()) bean->hostKeyAlgs = ui->host_key_algs->text().split(","); + else bean->hostKeyAlgs = {}; + bean->clientVersion = ui->client_version->text(); + + return true; +} \ No newline at end of file diff --git a/ui/edit/edit_ssh.h b/ui/edit/edit_ssh.h new file mode 100644 index 0000000..779f76e --- /dev/null +++ b/ui/edit/edit_ssh.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include "profile_editor.h" + +QT_BEGIN_NAMESPACE +namespace Ui { + class EditSSH; +} +QT_END_NAMESPACE + +class EditSSH : public QWidget, public ProfileEditor { + Q_OBJECT + +public: + explicit EditSSH(QWidget *parent = nullptr); + ~EditSSH() override; + + void onStart(std::shared_ptr _ent) override; + + bool onEnd() override; + +private: + Ui::EditSSH *ui; + std::shared_ptr ent; +}; diff --git a/ui/edit/edit_ssh.ui b/ui/edit/edit_ssh.ui new file mode 100644 index 0000000..4275e99 --- /dev/null +++ b/ui/edit/edit_ssh.ui @@ -0,0 +1,114 @@ + + + EditSSH + + + + 0 + 0 + 409 + 375 + + + + EditSSH + + + + + + User + + + + + + + + + + Password + + + + + + + + + + Private Key + + + + + + + + + + Private Key Password + + + + + + + + + + Host Key + + + + + + + + + + Host Key Algorithms + + + + + + + + + + Client Version + + + + + + + + + + + + + Private Key Path + + + + + + + + + + Choose File + + + + + + + + + + +