mirror of
https://github.com/Mahdi-zarei/nekoray.git
synced 2025-12-19 13:42:51 +08:00
feat: Add SSH Support
This commit is contained in:
parent
bbed0ba8a9
commit
4fb6e69e23
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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
36
fmt/SSHBean.h
Normal 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;
|
||||
};
|
||||
}
|
||||
@ -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));
|
||||
|
||||
@ -8,4 +8,5 @@
|
||||
#include "NaiveBean.hpp"
|
||||
#include "QUICBean.hpp"
|
||||
#include "WireguardBean.h"
|
||||
#include "SSHBean.h"
|
||||
#include "CustomBean.hpp"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
52
ui/edit/edit_ssh.cpp
Normal 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
26
ui/edit/edit_ssh.h
Normal 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
114
ui/edit/edit_ssh.ui
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user