diff --git a/CMakeLists.txt b/CMakeLists.txt index e5e67b2..ac807c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -213,6 +213,10 @@ set(PROJECT_SOURCES ui/edit/edit_naive.cpp ui/edit/edit_naive.ui + ui/edit/edit_hysteria.h + ui/edit/edit_hysteria.cpp + ui/edit/edit_hysteria.ui + ui/edit/edit_custom.h ui/edit/edit_custom.cpp ui/edit/edit_custom.ui diff --git a/db/ConfigBuilder.cpp b/db/ConfigBuilder.cpp index 4969f6d..57941a0 100644 --- a/db/ConfigBuilder.cpp +++ b/db/ConfigBuilder.cpp @@ -9,6 +9,7 @@ #include #define BOX_UNDERLYING_DNS NekoRay::dataStore->core_box_underlying_dns.isEmpty() ? "underlying://0.0.0.0" : NekoRay::dataStore->core_box_underlying_dns +#define BOX_UNDERLYING_DNS_EXPORT NekoRay::dataStore->core_box_underlying_dns.isEmpty() ? "local" : NekoRay::dataStore->core_box_underlying_dns namespace NekoRay { @@ -769,7 +770,7 @@ namespace NekoRay { // Direct auto directDNSAddress = dataStore->direct_dns; - if (directDNSAddress == "localhost") directDNSAddress = BOX_UNDERLYING_DNS; + if (directDNSAddress == "localhost") directDNSAddress = BOX_UNDERLYING_DNS_EXPORT; if (!status->forTest) dnsServers += QJsonObject{ {"tag", "dns-direct"}, @@ -782,7 +783,7 @@ namespace NekoRay { // Underlying 100% Working DNS dnsServers += QJsonObject{ {"tag", "dns-local"}, - {"address", BOX_UNDERLYING_DNS}, + {"address", BOX_UNDERLYING_DNS_EXPORT}, {"detour", "direct"}, }; diff --git a/db/Database.cpp b/db/Database.cpp index 5203309..fa9cac2 100644 --- a/db/Database.cpp +++ b/db/Database.cpp @@ -85,6 +85,8 @@ namespace NekoRay { bean = new fmt::TrojanVLESSBean(fmt::TrojanVLESSBean::proxy_VLESS); } else if (type == "naive") { bean = new fmt::NaiveBean(); + } else if (type == "hysteria") { + bean = new fmt::HysteriaBean(); } else if (type == "custom") { bean = new fmt::CustomBean(); } else { diff --git a/db/ProxyEntity.hpp b/db/ProxyEntity.hpp index ae28431..6d05a58 100644 --- a/db/ProxyEntity.hpp +++ b/db/ProxyEntity.hpp @@ -16,6 +16,8 @@ namespace NekoRay { class NaiveBean; + class HysteriaBean; + class CustomBean; class ChainBean; @@ -63,6 +65,10 @@ namespace NekoRay { return (fmt::NaiveBean *) bean.get(); }; + [[nodiscard]] fmt::HysteriaBean *HysteriaBean() const { + return (fmt::HysteriaBean *) bean.get(); + }; + [[nodiscard]] fmt::CustomBean *CustomBean() const { return (fmt::CustomBean *) bean.get(); }; diff --git a/fmt/Bean2CoreObj_box.cpp b/fmt/Bean2CoreObj_box.cpp index 2934c05..b3e4300 100644 --- a/fmt/Bean2CoreObj_box.cpp +++ b/fmt/Bean2CoreObj_box.cpp @@ -135,6 +135,34 @@ namespace NekoRay::fmt { return result; } + CoreObjOutboundBuildResult HysteriaBean::BuildCoreObjSingBox() { + CoreObjOutboundBuildResult result; + + QJsonObject coreTlsObj{ + {"enabled", true}, + {"insecure", allowInsecure}, + }; + if (!alpn.trimmed().isEmpty()) coreTlsObj["alpn"] = QJsonArray{alpn}; + + QJsonObject coreHysteriaObj{ + {"type", "hysteria"}, + {"server", serverAddress}, + {"server_port", serverPort}, + {"disable_mtu_discovery", disableMtuDiscovery}, + {"recv_window", streamReceiveWindow}, + {"recv_window_conn", connectionReceiveWindow}, + {"up_mbps", uploadMbps}, + {"down_mbps", downloadMbps}, + {"tls", coreTlsObj}, + }; + + if (authPayloadType == hysteria_auth_base64) coreHysteriaObj["auth"] = authPayload; + if (authPayloadType == hysteria_auth_string) coreHysteriaObj["auth_str"] = authPayload; + + result.outbound = coreHysteriaObj; + return result; + } + CoreObjOutboundBuildResult CustomBean::BuildCoreObjSingBox() { CoreObjOutboundBuildResult result; diff --git a/fmt/Bean2External.cpp b/fmt/Bean2External.cpp index d8a52ae..d7d194b 100644 --- a/fmt/Bean2External.cpp +++ b/fmt/Bean2External.cpp @@ -31,13 +31,28 @@ namespace NekoRay::fmt { return 1; } - int CustomBean::NeedExternal(bool isFirstProfile, bool isVPN) { - if (core == "internal" || core == "internal-full") return 0; - if (core == "hysteria") { + int HysteriaBean::NeedExternal(bool isFirstProfile, bool isVPN) { + if (IS_NEKO_BOX) { + if (protocol == hysteria_protocol_udp && hopPort.trimmed().isEmpty()) { + // sing-box support + return 0; + } else { + // hysteria core support + if (isFirstProfile && !isVPN) { + return 2; + } + return 1; + } + } else { if (isFirstProfile && !isVPN) { return 2; } + return 1; } + } + + int CustomBean::NeedExternal(bool isFirstProfile, bool isVPN) { + if (core == "internal" || core == "internal-full") return 0; return 1; } @@ -64,8 +79,8 @@ namespace NekoRay::fmt { if (domain_address != connect_address) result.arguments += "--host-resolver-rules=MAP " + domain_address + " " + connect_address; if (insecure_concurrency > 0) result.arguments += "--insecure-concurrency=" + Int2String(insecure_concurrency); - if (!extra_headers.isEmpty()) result.arguments += "--extra-headers=" + extra_headers; - if (!certificate.isEmpty()) { + if (!extra_headers.trimmed().isEmpty()) result.arguments += "--extra-headers=" + extra_headers; + if (!certificate.trimmed().isEmpty()) { WriteTempFile("naive_" + GetRandomString(10) + ".crt", certificate.toUtf8()); result.env += "SSL_CERT_FILE=" + TempFile; } @@ -77,6 +92,67 @@ namespace NekoRay::fmt { return result; } + ExternalBuildResult HysteriaBean::BuildExternal(int mapping_port, int socks_port, int external_stat) { + ExternalBuildResult result{dataStore->extraCore->Get("hysteria")}; + + QJsonObject config; + + // determine server format + auto is_direct = external_stat == 2; + auto sni2 = sni; + if (sni.isEmpty() && is_direct) sni2 = serverAddress; + + auto server = serverAddress; + if (!hopPort.trimmed().isEmpty()) { + server = WrapIPV6Host(server) + ":" + hopPort; + } else { + server = WrapIPV6Host(server) + ":" + Int2String(serverPort); + } + config["server"] = is_direct ? server : "127.0.0.1:" + Int2String(mapping_port); + + // listen + config["socks5"] = QJsonObject{ + {"listen", "127.0.0.1:" + Int2String(socks_port)}, + }; + + // misc + + config["retry"] = 5; + config["fast_open"] = true; + config["lazy_start"] = true; + config["obfs"] = obfsPassword; + config["up_mbps"] = uploadMbps; + config["down_mbps"] = downloadMbps; + + if (authPayloadType == hysteria_auth_base64) config["auth"] = authPayload; + if (authPayloadType == hysteria_auth_string) config["auth_str"] = authPayload; + + if (protocol == hysteria_protocol_facktcp) config["protocol"] = "faketcp"; + if (protocol == hysteria_protocol_wechat_video) config["protocol"] = "wechat-video"; + + if (!sni2.isEmpty()) config["server_name"] = sni2; + if (!alpn.isEmpty()) config["alpn"] = alpn; + + if (!caText.trimmed().isEmpty()) { + WriteTempFile("hysteria_" + GetRandomString(10) + ".crt", caText.toUtf8()); + config["ca"] = TempFile; + } + + if (allowInsecure) config["insecure"] = true; + if (streamReceiveWindow > 0) config["recv_window_conn"] = streamReceiveWindow; + if (connectionReceiveWindow > 0) config["recv_window"] = connectionReceiveWindow; + if (disableMtuDiscovery) config["disable_mtu_discovery"] = true; + config["hop_interval"] = hopInterval; + + // + + result.config_export = QJsonObject2QString(config, false); + WriteTempFile("hysteria_" + GetRandomString(10) + ".json", result.config_export.toUtf8()); + result.arguments = QStringList{"--no-check", "-c", TempFile}; + + return result; + } + ExternalBuildResult CustomBean::BuildExternal(int mapping_port, int socks_port, int external_stat) { ExternalBuildResult result{dataStore->extraCore->Get(core)}; @@ -107,14 +183,6 @@ namespace NekoRay::fmt { suffix = ".json"; } - // known core direct out - if (external_stat == 2) { - if (core == "hysteria") { - config = config.replace(QString("\"127.0.0.1:%1\"").arg(mapping_port), - "\"" + DisplayAddress() + "\""); - } - } - // write config WriteTempFile("custom_" + GetRandomString(10) + suffix, config.toUtf8()); for (int i = 0; i < result.arguments.count(); i++) { diff --git a/fmt/Bean2Link.cpp b/fmt/Bean2Link.cpp index ea12877..dd381e3 100644 --- a/fmt/Bean2Link.cpp +++ b/fmt/Bean2Link.cpp @@ -102,7 +102,7 @@ namespace NekoRay::fmt { {"tls", stream->security == "tls" ? "tls" : ""}, {"sni", stream->sni}, }; - return "vmess://" + QJsonObject2QString(N, false).toUtf8().toBase64(); + return "vmess://" + QJsonObject2QString(N, true).toUtf8().toBase64(); } QString NaiveBean::ToShareLink() { @@ -116,4 +116,30 @@ namespace NekoRay::fmt { return url.toString(QUrl::FullyEncoded); } + QString HysteriaBean::ToShareLink() { + QUrl url; + url.setScheme("hysteria"); + url.setHost(serverAddress); + url.setPort(serverPort); + QUrlQuery q; + q.addQueryItem("upmbps", Int2String(uploadMbps)); + q.addQueryItem("downmbps", Int2String(downloadMbps)); + if (!obfsPassword.isEmpty()) { + q.addQueryItem("obfs", "xplus"); + q.addQueryItem("obfsParam", obfsPassword); + } + if (authPayloadType == hysteria_auth_string) q.addQueryItem("auth", authPayload); + if (protocol == hysteria_protocol_facktcp) q.addQueryItem("protocol", "faketcp"); + if (protocol == hysteria_protocol_wechat_video) q.addQueryItem("protocol", "wechat-video"); + if (!hopPort.trimmed().isEmpty()) q.addQueryItem("mport", hopPort); + if (allowInsecure) q.addQueryItem("insecure", "1"); + if (!sni.isEmpty()) q.addQueryItem("peer", sni); + if (!alpn.isEmpty()) q.addQueryItem("alpn", alpn); + if (connectionReceiveWindow > 0) q.addQueryItem("recv_window", Int2String(connectionReceiveWindow)); + if (streamReceiveWindow > 0) q.addQueryItem("recv_window_conn", Int2String(streamReceiveWindow)); + if (!q.isEmpty()) url.setQuery(q); + if (!name.isEmpty()) url.setFragment(name); + return url.toString(QUrl::FullyEncoded); + } + } // namespace NekoRay::fmt \ No newline at end of file diff --git a/fmt/HysteriaBean.hpp b/fmt/HysteriaBean.hpp new file mode 100644 index 0000000..0aed776 --- /dev/null +++ b/fmt/HysteriaBean.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" + +namespace NekoRay::fmt { + class HysteriaBean : public AbstractBean { + public: + static constexpr int hysteria_protocol_udp = 0; + static constexpr int hysteria_protocol_facktcp = 1; + static constexpr int hysteria_protocol_wechat_video = 2; + + int protocol = 0; + + // + + static constexpr int hysteria_auth_none = 0; + static constexpr int hysteria_auth_string = 1; + static constexpr int hysteria_auth_base64 = 2; + + int authPayloadType = 0; + QString authPayload = ""; + QString obfsPassword = ""; + + // + + int uploadMbps = 100; + int downloadMbps = 100; + + qint64 streamReceiveWindow = 0; + qint64 connectionReceiveWindow = 0; + bool disableMtuDiscovery = false; + + bool allowInsecure = false; + QString sni = ""; + QString alpn = ""; // only 1 + QString caText = ""; + + // + + int hopInterval = 10; + QString hopPort = ""; + + HysteriaBean() : AbstractBean(0) { + _add(new configItem("protocol", &protocol, itemType::integer)); + _add(new configItem("authPayloadType", &authPayloadType, itemType::integer)); + _add(new configItem("authPayload", &authPayload, itemType::string)); + _add(new configItem("obfsPassword", &obfsPassword, itemType::string)); + _add(new configItem("uploadMbps", &uploadMbps, itemType::integer)); + _add(new configItem("downloadMbps", &downloadMbps, itemType::integer)); + _add(new configItem("streamReceiveWindow", &streamReceiveWindow, itemType::integer64)); + _add(new configItem("connectionReceiveWindow", &connectionReceiveWindow, itemType::integer64)); + _add(new configItem("disableMtuDiscovery", &disableMtuDiscovery, itemType::boolean)); + _add(new configItem("allowInsecure", &allowInsecure, itemType::boolean)); + _add(new configItem("sni", &sni, itemType::string)); + _add(new configItem("alpn", &alpn, itemType::string)); + _add(new configItem("caText", &caText, itemType::string)); + _add(new configItem("hopInterval", &hopInterval, itemType::integer)); + _add(new configItem("hopPort", &hopPort, itemType::string)); + }; + + QString DisplayAddress() override { + if (!hopPort.trimmed().isEmpty()) return WrapIPV6Host(serverAddress) + ":" + hopPort; + return ::DisplayAddress(serverAddress, serverPort); + } + + QString DisplayCoreType() override { return NeedExternal(false, false) == 0 ? software_core_name : "Hysteria"; }; + + QString DisplayType() override { return "Hysteria"; }; + + int NeedExternal(bool isFirstProfile, bool isVPN) override; + + ExternalBuildResult BuildExternal(int mapping_port, int socks_port, int external_stat) override; + + CoreObjOutboundBuildResult BuildCoreObjSingBox() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + }; +} // namespace NekoRay::fmt \ No newline at end of file diff --git a/fmt/Link2Bean.cpp b/fmt/Link2Bean.cpp index acc17a5..9b1d2e6 100644 --- a/fmt/Link2Bean.cpp +++ b/fmt/Link2Bean.cpp @@ -176,4 +176,41 @@ namespace NekoRay::fmt { return !(username.isEmpty() || password.isEmpty()); } + bool HysteriaBean::TryParseLink(const QString &link) { + // https://hysteria.network/docs/uri-scheme/ + auto url = QUrl(link); + auto query = QUrlQuery(url.query()); + if (url.host().isEmpty() || url.port() == -1 || !query.hasQueryItem("upmbps") || !query.hasQueryItem("downmbps")) return false; + + name = url.fragment(); + serverAddress = url.host(); + serverPort = url.port(); + serverAddress = url.host(); // default sni + hopPort = query.queryItemValue("mport"); + obfsPassword = query.queryItemValue("obfsParam"); + allowInsecure = query.queryItemValue("insecure") == "1"; + uploadMbps = query.queryItemValue("upmbps").toInt(); + downloadMbps = query.queryItemValue("downmbps").toInt(); + + auto protocolStr = (query.hasQueryItem("protocol") ? query.queryItemValue("protocol") : "udp").toLower(); + if (protocolStr == "faketcp") { + protocol = fmt::HysteriaBean::hysteria_protocol_facktcp; + } else if (protocolStr.startsWith("wechat")) { + protocol = fmt::HysteriaBean::hysteria_protocol_wechat_video; + } + + if (query.hasQueryItem("auth")) { + authPayload = query.queryItemValue("auth"); + authPayloadType = fmt::HysteriaBean::hysteria_auth_string; + } + + alpn = query.queryItemValue("alpn"); + sni = FIRST_OR_SECOND(query.queryItemValue("peer"), query.queryItemValue("sni")); + + connectionReceiveWindow = query.queryItemValue("recv_window").toInt(); + streamReceiveWindow = query.queryItemValue("recv_window_conn").toInt(); + + return true; + } + } // namespace NekoRay::fmt diff --git a/fmt/Preset.hpp b/fmt/Preset.hpp index ea47c2d..673d7a9 100644 --- a/fmt/Preset.hpp +++ b/fmt/Preset.hpp @@ -1,21 +1,6 @@ #pragma once namespace Preset { - namespace Hysteria { - inline const char *command = "--no-check -c %config%"; - inline const char *config = - "{\n" - " \"server\": \"127.0.0.1:%mapping_port%\",\n" - " \"server_name\": \"example.com\",\n" - " \"obfs\": \"fuck me till the daylight\",\n" - " \"up_mbps\": 10,\n" - " \"down_mbps\": 50,\n" - " \"socks5\": {\n" - " \"listen\": \"127.0.0.1:%socks_port%\"\n" - " }\n" - "}"; - } // namespace Hysteria - namespace V2Ray { inline QStringList UtlsFingerPrint = {"", "randomized", "randomizedalpn", "randomizednoalpn", "firefox_auto", "firefox_55", "firefox_56", "firefox_63", "firefox_65", "firefox_99", "firefox_102", "firefox_105", "chrome_auto", "chrome_58", "chrome_62", "chrome_70", "chrome_72", "chrome_83", "chrome_87", "chrome_96", "chrome_100", "chrome_102", "ios_auto", "ios_11_1", "ios_12_1", "ios_13", "ios_14", "android_11_okhttp", "edge_auto", "edge_85", "edge_106", "safari_auto", "safari_16_0", "360_auto", "360_7_5", "360_11_0", "qq_auto", "qq_11_1"}; } // namespace V2Ray diff --git a/fmt/includes.h b/fmt/includes.h index 426fe4b..b2f7ecc 100644 --- a/fmt/includes.h +++ b/fmt/includes.h @@ -6,4 +6,5 @@ #include "VMessBean.hpp" #include "TrojanVLESSBean.hpp" #include "NaiveBean.hpp" +#include "HysteriaBean.hpp" #include "CustomBean.hpp" diff --git a/main/GuiUtils.hpp b/main/GuiUtils.hpp index ae47472..ca69fab 100644 --- a/main/GuiUtils.hpp +++ b/main/GuiUtils.hpp @@ -17,16 +17,17 @@ // NekoRay Save&Load +#define P_C_LOAD_STRING(a) CACHE.a = bean->a; +#define P_C_SAVE_STRING(a) bean->a = CACHE.a; +#define D_C_LOAD_STRING(a) CACHE.a = NekoRay::dataStore->a; +#define D_C_SAVE_STRING(a) NekoRay::dataStore->a = CACHE.a; + #define P_LOAD_STRING(a) ui->a->setText(bean->a); #define P_SAVE_STRING(a) bean->a = ui->a->text(); #define P_SAVE_STRING_QTEXTEDIT(a) bean->a = ui->a->toPlainText(); #define D_LOAD_STRING(a) ui->a->setText(NekoRay::dataStore->a); #define D_SAVE_STRING(a) NekoRay::dataStore->a = ui->a->text(); #define D_SAVE_STRING_QTEXTEDIT(a) NekoRay::dataStore->a = ui->a->toPlainText(); -#define P_C_LOAD_STRING(a) CACHE.a = bean->a; -#define P_C_SAVE_STRING(a) bean->a = CACHE.a; -#define D_C_LOAD_STRING(a) CACHE.a = NekoRay::dataStore->a; -#define D_C_SAVE_STRING(a) NekoRay::dataStore->a = CACHE.a; #define P_LOAD_INT(a) \ ui->a->setText(Int2String(bean->a)); \ ui->a->setValidator(QRegExpValidator_Number); @@ -35,10 +36,14 @@ ui->a->setText(Int2String(NekoRay::dataStore->a)); \ ui->a->setValidator(QRegExpValidator_Number); #define D_SAVE_INT(a) NekoRay::dataStore->a = ui->a->text().toInt(); -#define P_LOAD_COMBO(a) ui->a->setCurrentText(bean->a); -#define P_SAVE_COMBO(a) bean->a = ui->a->currentText(); +#define P_LOAD_COMBO_STR(a) ui->a->setCurrentText(bean->a); +#define P_SAVE_COMBO_STR(a) bean->a = ui->a->currentText(); +#define P_LOAD_COMBO_INT(a) ui->a->setCurrentIndex(bean->a); +#define P_SAVE_COMBO_INT(a) bean->a = ui->a->currentIndex(); #define D_LOAD_BOOL(a) ui->a->setChecked(NekoRay::dataStore->a); #define D_SAVE_BOOL(a) NekoRay::dataStore->a = ui->a->isChecked(); +#define P_LOAD_BOOL(a) ui->a->setChecked(bean->a); +#define P_SAVE_BOOL(a) bean->a = ui->a->isChecked(); #define D_LOAD_INT_ENABLE(i, e) \ if (NekoRay::dataStore->i > 0) { \ diff --git a/main/NekoRay_Utils.hpp b/main/NekoRay_Utils.hpp index d6d012f..2007b4a 100644 --- a/main/NekoRay_Utils.hpp +++ b/main/NekoRay_Utils.hpp @@ -25,6 +25,8 @@ inline std::function MW_dialog_message; // String +#define FIRST_OR_SECOND(a, b) a.isEmpty() ? b : a + inline const QString UNICODE_LRO = QString::fromUtf8(QByteArray::fromHex("E280AD")); #define Int2String(num) QString::number(num) diff --git a/sub/GroupUpdater.cpp b/sub/GroupUpdater.cpp index 02a2e9e..e36ea5e 100644 --- a/sub/GroupUpdater.cpp +++ b/sub/GroupUpdater.cpp @@ -16,8 +16,6 @@ #endif -#define FIRST_OR_SECOND(a, b) a.isEmpty() ? b : a - namespace NekoRay::sub { GroupUpdater *groupUpdater = new GroupUpdater; @@ -130,28 +128,9 @@ namespace NekoRay::sub { // Hysteria if (str.startsWith("hysteria://")) { needFix = false; - // https://github.com/HyNetwork/hysteria/wiki/URI-Scheme - ent = ProfileManager::NewProxyEntity("custom"); - auto bean = ent->CustomBean(); - auto url = QUrl(str); - auto query = QUrlQuery(url.query()); - if (url.host().isEmpty() || url.port() == -1 || !query.hasQueryItem("upmbps")) return; - bean->name = url.fragment(); - bean->serverAddress = url.host(); - bean->serverPort = url.port(); - bean->core = "hysteria"; - bean->command = QString(Preset::Hysteria::command).split(" "); - auto result = QString2QJsonObject(Preset::Hysteria::config); - result["server_name"] = url.host(); // default sni - result["obfs"] = query.queryItemValue("obfsParam"); - result["insecure"] = query.queryItemValue("insecure") == "1"; - result["up_mbps"] = query.queryItemValue("upmbps").toInt(); - result["down_mbps"] = query.queryItemValue("downmbps").toInt(); - result["protocol"] = query.hasQueryItem("protocol") ? query.queryItemValue("protocol") : "udp"; - if (query.hasQueryItem("auth")) result["auth_str"] = query.queryItemValue("auth"); - if (query.hasQueryItem("alpn")) result["alpn"] = query.queryItemValue("alpn"); - if (query.hasQueryItem("peer")) result["server_name"] = query.queryItemValue("peer"); - bean->config_simple = QJsonObject2QString(result, false); + ent = ProfileManager::NewProxyEntity("hysteria"); + auto ok = ent->HysteriaBean()->TryParseLink(str); + if (!ok) return; } if (ent == nullptr) return; @@ -237,7 +216,6 @@ namespace NekoRay::sub { if (type == "ss" || type == "ssr") type = "shadowsocks"; if (type == "socks5") type = "socks"; - if (type == "hysteria") type = "custom"; auto ent = ProfileManager::NewProxyEntity(type); if (ent->bean->version == -114514) continue; @@ -379,45 +357,37 @@ namespace NekoRay::sub { } } } else if (type_clash == "hysteria") { - if (!IS_NEKO_BOX) { - MW_show_log("Found Clash Meta format hysteria. This is only supported in NekoBox, please switch the core."); - continue; + auto bean = ent->HysteriaBean(); + + bean->allowInsecure = Node2Bool(proxy["skip-cert-verify"]); + auto alpn = Node2QStringList(proxy["alpn"]); + if (!alpn.isEmpty()) bean->alpn = alpn[0]; + bean->sni = Node2QString(proxy["sni"]); + + bean->name = Node2QString(proxy["name"]); + bean->serverAddress = Node2QString(proxy["server"]); + bean->serverPort = Node2Int(proxy["port"]); + if (bean->serverPort == 0) bean->hopPort = Node2QString(proxy["port"]); + + auto auth_str = FIRST_OR_SECOND(Node2QString(proxy["auth_str"]), Node2QString(proxy["auth-str"])); + auto auth = Node2QString(proxy["auth"]); + if (!auth_str.isEmpty()) { + bean->authPayloadType = fmt::HysteriaBean::hysteria_auth_string; + bean->authPayload = auth_str; + } + if (!auth.isEmpty()) { + bean->authPayloadType = fmt::HysteriaBean::hysteria_auth_base64; + bean->authPayload = auth; } - auto bean = ent->CustomBean(); - bean->core = "internal"; + if (Node2Bool(proxy["disable_mtu_discovery"]) || Node2Bool(proxy["disable-mtu-discovery"])) bean->disableMtuDiscovery = true; + bean->streamReceiveWindow = Node2Int(proxy["recv-window"]); + bean->connectionReceiveWindow = Node2Int(proxy["recv-window-conn"]); - QJsonObject coreTlsObj{ - {"enabled", true}, - {"insecure", Node2Bool(proxy["skip-cert-verify"])}, - {"alpn", QList2QJsonArray(Node2QStringList(proxy["alpn"]))}, - {"server_name", Node2QString(proxy["sni"])}, - }; - - QJsonObject coreHysteriaObj{ - {"type", "hysteria"}, - {"tag", Node2QString(proxy["name"])}, - {"server", Node2QString(proxy["server"])}, - {"server_port", Node2Int(proxy["port"])}, - {"auth_str", FIRST_OR_SECOND(Node2QString(proxy["auth_str"]), Node2QString(proxy["auth-str"]))}, - {"disable_mtu_discovery", Node2Bool(proxy["disable_mtu_discovery"])}, - {"recv_window", Node2Int(proxy["recv-window"])}, - {"recv_window_conn", Node2Int(proxy["recv-window-conn"])}, - {"tls", coreTlsObj}, - }; - - if (!Node2QString(proxy["up"]).contains("bps")) { - coreHysteriaObj["up_mbps"] = Node2Int(proxy["up"]); - } else { - coreHysteriaObj["up"] = Node2QString(proxy["up"]); - } - if (!Node2QString(proxy["down"]).contains("bps")) { - coreHysteriaObj["down_mbps"] = Node2Int(proxy["down"]); - } else { - coreHysteriaObj["down"] = Node2QString(proxy["down"]); - } - - bean->config_simple = QJsonObject2QString(coreHysteriaObj, false); + auto upMbps = Node2QString(proxy["up"]).split(" ")[0].toInt(); + auto downMbps = Node2QString(proxy["down"]).split(" ")[0].toInt(); + if (upMbps > 0) bean->uploadMbps = upMbps; + if (downMbps > 0) bean->downloadMbps = downMbps; } else { continue; } diff --git a/translations/fa_IR.ts b/translations/fa_IR.ts index 410a111..5d3847b 100644 --- a/translations/fa_IR.ts +++ b/translations/fa_IR.ts @@ -719,10 +719,6 @@ https://matsuridayo.github.io/n-configuration/#vpn-tun Config Suffix - - Please read the documentation. If you don't understand, use a share link instead. - - Outbound JSON, please read the documentation. JSON خروجی، لطفاً مستندات را بخوانید. @@ -752,6 +748,57 @@ https://matsuridayo.github.io/n-configuration/#vpn-tun + + EditHysteria + + Certificate + گواهی + + + Auth Type + + + + Protocol + پروتکل + + + Download (Mbps) + + + + Disable MTU Discovery + + + + Hop Interval (s) + + + + Allow Insecure + + + + Hop Port + + + + Upload (Mbps) + + + + Obfs Password + + + + SNI + + + + Auth Payload + + + EditNaive diff --git a/translations/zh_CN.ts b/translations/zh_CN.ts index 0e29d79..5b9a742 100644 --- a/translations/zh_CN.ts +++ b/translations/zh_CN.ts @@ -716,10 +716,6 @@ https://matsuridayo.github.io/n-configuration/#vpn-tun Outbound JSON, please read the documentation. 填写出站 JSON 对象,详细请看文档。 - - Please read the documentation. If you don't understand, use a share link instead. - 格式请看文档。如果不懂,直接导入 hysteria:// 链接。 - Config Suffix 配置文件后缀 @@ -745,6 +741,57 @@ https://matsuridayo.github.io/n-configuration/#vpn-tun 请填写完整配置。 + + EditHysteria + + Certificate + 证书 + + + Auth Type + 认证类型 + + + Protocol + 协议 + + + Download (Mbps) + 下载速度 (Mbps) + + + Disable MTU Discovery + 禁用 MTU 探测 + + + Hop Interval (s) + 端口跳跃间隔 (秒) + + + Allow Insecure + 不检查服务器证书 + + + Hop Port + 跳跃端口 + + + Upload (Mbps) + 上传速度 (Mbps) + + + Obfs Password + 混淆密码 + + + SNI + SNI + + + Auth Payload + 认证有效载荷 + + EditNaive diff --git a/ui/edit/dialog_edit_profile.cpp b/ui/edit/dialog_edit_profile.cpp index 7fef6a9..a46b1e3 100644 --- a/ui/edit/dialog_edit_profile.cpp +++ b/ui/edit/dialog_edit_profile.cpp @@ -7,6 +7,7 @@ #include "ui/edit/edit_vmess.h" #include "ui/edit/edit_trojan_vless.h" #include "ui/edit/edit_naive.h" +#include "ui/edit/edit_hysteria.h" #include "ui/edit/edit_custom.h" #include "fmt/includes.h" @@ -123,7 +124,7 @@ DialogEditProfile::DialogEditProfile(const QString &_type, int profileOrGroupId, LOAD_TYPE("vmess"); LOAD_TYPE("vless"); LOAD_TYPE("naive"); - ui->type->addItem("Hysteria", "hysteria"); + LOAD_TYPE("hysteria"); 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"); @@ -177,7 +178,11 @@ void DialogEditProfile::typeSelected(const QString &newType) { auto _innerWidget = new EditNaive(this); innerWidget = _innerWidget; innerEditor = _innerWidget; - } else if (type == "custom" || type == "internal" || type == "internal-full" || type == "hysteria") { + } else if (type == "hysteria") { + auto _innerWidget = new EditHysteria(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "custom" || type == "internal" || type == "internal-full") { auto _innerWidget = new EditCustom(this); innerWidget = _innerWidget; innerEditor = _innerWidget; diff --git a/ui/edit/edit_custom.cpp b/ui/edit/edit_custom.cpp index 0613e4c..7968a5d 100644 --- a/ui/edit/edit_custom.cpp +++ b/ui/edit/edit_custom.cpp @@ -26,10 +26,10 @@ EditCustom::~EditCustom() { } #define SAVE_CUSTOM_BEAN \ - P_SAVE_COMBO(core) \ + P_SAVE_COMBO_STR(core) \ bean->command = ui->command->text().split(" "); \ P_SAVE_STRING_QTEXTEDIT(config_simple) \ - P_SAVE_COMBO(config_suffix) \ + P_SAVE_COMBO_STR(config_suffix) \ P_SAVE_INT(mapping_port) \ P_SAVE_INT(socks_port) @@ -43,13 +43,7 @@ void EditCustom::onStart(QSharedPointer _ent) { if (key == "naive" || key == "hysteria") continue; ui->core->addItem(key); } - if (preset_core == "hysteria") { - preset_command = Preset::Hysteria::command; - preset_config = Preset::Hysteria::config; - ui->config_simple->setPlaceholderText(""); - ui->core->hide(); - ui->core_l->setText(tr("Please read the documentation. If you don't understand, use a share link instead.")); - } else if (preset_core == "internal") { + if (preset_core == "internal") { preset_command = preset_config = ""; ui->config_simple->setPlaceholderText( "{\n" @@ -66,10 +60,10 @@ void EditCustom::onStart(QSharedPointer _ent) { } // load core ui - P_LOAD_COMBO(core) + P_LOAD_COMBO_STR(core) ui->command->setText(bean->command.join(" ")); P_LOAD_STRING(config_simple) - P_LOAD_COMBO(config_suffix) + P_LOAD_COMBO_STR(config_suffix) P_LOAD_INT(mapping_port) P_LOAD_INT(socks_port) diff --git a/ui/edit/edit_hysteria.cpp b/ui/edit/edit_hysteria.cpp new file mode 100644 index 0000000..5b85af3 --- /dev/null +++ b/ui/edit/edit_hysteria.cpp @@ -0,0 +1,72 @@ +#include "edit_hysteria.h" +#include "ui_edit_hysteria.h" + +#include "fmt/HysteriaBean.hpp" + +#include + +EditHysteria::EditHysteria(QWidget *parent) : QWidget(parent), ui(new Ui::EditHysteria) { + ui->setupUi(this); +} + +EditHysteria::~EditHysteria() { + delete ui; +} + +void EditHysteria::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->HysteriaBean(); + + P_LOAD_STRING(hopPort); + P_LOAD_INT(hopInterval); + P_LOAD_INT(uploadMbps); + P_LOAD_INT(downloadMbps); + P_LOAD_COMBO_INT(protocol); + P_LOAD_BOOL(disableMtuDiscovery) + P_LOAD_STRING(obfsPassword); + P_LOAD_COMBO_INT(authPayloadType); + P_LOAD_STRING(authPayload); + P_LOAD_STRING(sni); + P_LOAD_STRING(alpn); + P_LOAD_BOOL(allowInsecure) + P_C_LOAD_STRING(caText); + P_LOAD_INT(streamReceiveWindow); + P_LOAD_INT(connectionReceiveWindow); +} + +bool EditHysteria::onEnd() { + auto bean = this->ent->HysteriaBean(); + + P_SAVE_STRING(hopPort); + P_SAVE_INT(hopInterval); + P_SAVE_INT(uploadMbps); + P_SAVE_INT(downloadMbps); + P_SAVE_COMBO_INT(protocol); + P_SAVE_BOOL(disableMtuDiscovery) + P_SAVE_STRING(obfsPassword); + P_SAVE_COMBO_INT(authPayloadType); + P_SAVE_STRING(authPayload); + P_SAVE_STRING(sni); + P_SAVE_STRING(alpn); + P_SAVE_BOOL(allowInsecure) + P_C_SAVE_STRING(caText); + P_SAVE_INT(streamReceiveWindow); + P_SAVE_INT(connectionReceiveWindow); + + return true; +} + +QList> EditHysteria::get_editor_cached() { + return { + {ui->certificate, CACHE.caText}, + }; +} + +void EditHysteria::on_certificate_clicked() { + bool ok; + auto txt = QInputDialog::getMultiLineText(this, tr("Certificate"), "", CACHE.caText, &ok); + if (ok) { + CACHE.caText = txt; + editor_cache_updated(); + } +} diff --git a/ui/edit/edit_hysteria.h b/ui/edit/edit_hysteria.h new file mode 100644 index 0000000..396307e --- /dev/null +++ b/ui/edit/edit_hysteria.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include "profile_editor.h" + +QT_BEGIN_NAMESPACE +namespace Ui { + class EditHysteria; +} +QT_END_NAMESPACE + +class EditHysteria : public QWidget, public ProfileEditor { + Q_OBJECT + +public: + explicit EditHysteria(QWidget *parent = nullptr); + + ~EditHysteria() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + + QList> get_editor_cached() override; + +private: + Ui::EditHysteria *ui; + QSharedPointer ent; + + struct { + QString caText; + } CACHE; + +private slots: + + void on_certificate_clicked(); +}; diff --git a/ui/edit/edit_hysteria.ui b/ui/edit/edit_hysteria.ui new file mode 100644 index 0000000..104083c --- /dev/null +++ b/ui/edit/edit_hysteria.ui @@ -0,0 +1,231 @@ + + + EditHysteria + + + + 0 + 0 + 500 + 600 + + + + EditHysteria + + + + + + + + + Auth Type + + + + + + + Protocol + + + + + + + + + + + + + + + + Download (Mbps) + + + + + + + + + + QUIC + + + + + FakeTCP + + + + + wechat-video + + + + + + + + + 0 + 0 + + + + Disable MTU Discovery + + + + + + + + + Hop Interval (s) + + + + + + + + + + Certificate + + + + + + + + NONE + + + + + STRING + + + + + BASE64 + + + + + + + + + + + + + Allow Insecure + + + + + + + + + Hop Port + + + + + + + PushButton + + + + + + + Upload (Mbps) + + + + + + + Obfs Password + + + + + + + SNI + + + + + + + + + + Only 1 value + + + ALPN + + + + + + + + + + Auth Payload + + + + + + + + + + + + + recv_window + + + + + + + recv_window_conn + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + uploadMbps + downloadMbps + sni + certificate + + + +
diff --git a/ui/edit/edit_naive.cpp b/ui/edit/edit_naive.cpp index 9f3ff19..a0ba3e7 100644 --- a/ui/edit/edit_naive.cpp +++ b/ui/edit/edit_naive.cpp @@ -19,7 +19,7 @@ void EditNaive::onStart(QSharedPointer _ent) { P_LOAD_STRING(username); P_LOAD_STRING(password); - P_LOAD_COMBO(protocol); + P_LOAD_COMBO_STR(protocol); P_C_LOAD_STRING(extra_headers); P_LOAD_STRING(sni); P_C_LOAD_STRING(certificate); @@ -31,7 +31,7 @@ bool EditNaive::onEnd() { P_SAVE_STRING(username); P_SAVE_STRING(password); - P_SAVE_COMBO(protocol); + P_SAVE_COMBO_STR(protocol); P_C_SAVE_STRING(extra_headers); P_SAVE_STRING(sni); P_C_SAVE_STRING(certificate);