feat: Add SSH Support

This commit is contained in:
Nova 2024-09-05 00:45:13 +03:30
parent bbed0ba8a9
commit 4fb6e69e23
No known key found for this signature in database
GPG Key ID: 389787EC83F5D73A
14 changed files with 332 additions and 1 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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();
};

View File

@ -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;

View File

@ -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

View File

@ -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

36
fmt/SSHBean.h Normal file
View File

@ -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;
};
}

View File

@ -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));

View File

@ -8,4 +8,5 @@
#include "NaiveBean.hpp"
#include "QUICBean.hpp"
#include "WireguardBean.h"
#include "SSHBean.h"
#include "CustomBean.hpp"

View File

@ -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

View File

@ -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;

52
ui/edit/edit_ssh.cpp Normal file
View File

@ -0,0 +1,52 @@
#include "edit_ssh.h"
#include "ui_edit_ssh.h"
#include <QFileDialog>
#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<NekoGui::ProxyEntity> _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;
}

26
ui/edit/edit_ssh.h Normal file
View File

@ -0,0 +1,26 @@
#pragma once
#include <QWidget>
#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<NekoGui::ProxyEntity> _ent) override;
bool onEnd() override;
private:
Ui::EditSSH *ui;
std::shared_ptr<NekoGui::ProxyEntity> ent;
};

114
ui/edit/edit_ssh.ui Normal file
View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditSSH</class>
<widget class="QWidget" name="EditSSH">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>409</width>
<height>375</height>
</rect>
</property>
<property name="windowTitle">
<string>EditSSH</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="user_l">
<property name="text">
<string>User</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="user"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="password_l">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="private_key_l">
<property name="text">
<string>Private Key</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QTextEdit" name="private_key"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="private_key_pass_l">
<property name="text">
<string>Private Key Password</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="private_key_pass"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="host_key_l">
<property name="text">
<string>Host Key</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="host_key"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="host_key_algs_l">
<property name="text">
<string>Host Key Algorithms</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="host_key_algs"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="client_version_l">
<property name="text">
<string>Client Version</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="client_version"/>
</item>
<item row="3" column="0" colspan="2">
<widget class="QWidget" name="widget" native="true">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="private_key_path_l">
<property name="text">
<string>Private Key Path</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="private_key_path"/>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="choose_pk">
<property name="text">
<string>Choose File</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>