diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f889a0..5c7e8e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -227,6 +227,13 @@ set(PROJECT_SOURCES include/configs/common/multiplex.h include/configs/common/transport.h include/configs/common/Outbound.h + + # common impl + src/configs/common/DialFields.cpp + src/configs/common/TLS.cpp + src/configs/common/multiplex.cpp + src/configs/common/transport.cpp + src/configs/common/Outbound.cpp include/configs/outbounds/socks.h include/configs/outbounds/http.h include/configs/outbounds/shadowsocks.h @@ -238,6 +245,24 @@ set(PROJECT_SOURCES include/configs/outbounds/hysteria2.h include/configs/outbounds/anyTLS.h include/configs/outbounds/ssh.h + + # outbounds impl + src/configs/outbounds/anyTLS.cpp + src/configs/outbounds/http.cpp + src/configs/outbounds/hysteria.cpp + src/configs/outbounds/hysteria2.cpp + src/configs/outbounds/shadowsocks.cpp + src/configs/outbounds/socks.cpp + src/configs/outbounds/ssh.cpp + src/configs/outbounds/tailscale.cpp + src/configs/outbounds/trojan.cpp + src/configs/outbounds/tuic.cpp + src/configs/outbounds/vless.cpp + src/configs/outbounds/vmess.cpp + src/configs/outbounds/wireguard.cpp + include/configs/generate.h + include/configs/common/utils.h + src/configs/common/utils.cpp ) if (NOT APPLE AND Qt6_VERSION VERSION_GREATER_EQUAL 6.9.0) diff --git a/include/configs/baseConfig.h b/include/configs/baseConfig.h index f153341..a36ca5e 100644 --- a/include/configs/baseConfig.h +++ b/include/configs/baseConfig.h @@ -1,7 +1,10 @@ #pragma once -#include #include +#include + +#include "generate.h" +#include "common/Outbound.h" #include "include/global/ConfigItem.hpp" namespace Configs @@ -9,30 +12,44 @@ namespace Configs class baseConfig : public JsonStore { public: - virtual bool ParseFromLink(QString link); + virtual bool ParseFromLink(const QString& link); - virtual bool ParseFromJson(QJsonObject object); + virtual bool ParseFromJson(const QJsonObject& object); virtual QString ExportToLink(); virtual QJsonObject ExportToJson(); - virtual QJsonObject Build(); + virtual BuildResult Build(); }; - class outboundMeta + class outbound : public baseConfig { public: + std::shared_ptr commons = std::make_shared(); + void ResolveDomainToIP(const std::function &onFinished); - virtual QString DisplayAddress(); + virtual QString DisplayAddress() + { + return ::DisplayAddress(commons->server, commons->server_port); + } - virtual QString DisplayName(); + QString DisplayName() + { + if (commons->name.isEmpty()) { + return DisplayAddress(); + } + return commons->name; + } virtual QString DisplayType() { return {}; }; - virtual QString DisplayTypeAndName(); + QString DisplayTypeAndName() + { + return QString("[%1] %2").arg(DisplayType(), DisplayName()); + } - virtual bool IsEndpoint() { return false; }; + static bool IsEndpoint() { return false; }; }; } diff --git a/include/configs/common/DialFields.h b/include/configs/common/DialFields.h index b58d987..5cc47f1 100644 --- a/include/configs/common/DialFields.h +++ b/include/configs/common/DialFields.h @@ -20,5 +20,12 @@ namespace Configs _add(new configItem("tcp_multi_path", &tcp_multi_path, itemType::boolean)); _add(new configItem("udp_fragment", &udp_fragment, itemType::boolean)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; }; } diff --git a/include/configs/common/Outbound.h b/include/configs/common/Outbound.h index 2f2e588..126b846 100644 --- a/include/configs/common/Outbound.h +++ b/include/configs/common/Outbound.h @@ -1,4 +1,5 @@ #pragma once +#include "DialFields.h" #include "include/configs/baseConfig.h" namespace Configs @@ -6,21 +7,25 @@ namespace Configs class OutboundCommons : public baseConfig { public: - QString type; + virtual ~OutboundCommons() = default; QString name; - int id = -1; QString server; int server_port = 0; std::shared_ptr dialFields = std::make_shared(); OutboundCommons() { - _add(new configItem("type", &type, string)); _add(new configItem("name", &name, string)); - _add(new configItem("id", &id, integer)); _add(new configItem("server", &server, string)); _add(new configItem("server_port", &server_port, integer)); _add(new configItem("dial_fields", dynamic_cast(dialFields.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; }; } diff --git a/include/configs/common/TLS.h b/include/configs/common/TLS.h index bcdd5e0..3e41b69 100644 --- a/include/configs/common/TLS.h +++ b/include/configs/common/TLS.h @@ -17,6 +17,13 @@ namespace Configs _add(new configItem("enabled", &enabled, boolean)); _add(new configItem("fingerprint", &fingerPrint, string)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; }; class ECH : public baseConfig @@ -32,6 +39,13 @@ namespace Configs _add(new configItem("config", &config, stringList)); _add(new configItem("config_path", &config_path, string)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; }; class Reality : public baseConfig @@ -47,6 +61,13 @@ namespace Configs _add(new configItem("public_key", &public_key, string)); _add(new configItem("short_id", &short_id, string)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; }; class TLS : public baseConfig @@ -100,5 +121,12 @@ namespace Configs _add(new configItem("utls", dynamic_cast(utls.get()), jsonStore)); _add(new configItem("reality", dynamic_cast(reality.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; }; } diff --git a/include/configs/common/multiplex.h b/include/configs/common/multiplex.h index 9179081..f949bc5 100644 --- a/include/configs/common/multiplex.h +++ b/include/configs/common/multiplex.h @@ -3,7 +3,7 @@ namespace Configs { - inline QStringLists muxProtocols = {"smux", "yamux", "h2mux"}; + inline QStringList muxProtocols = {"smux", "yamux", "h2mux"}; class TcpBrutal : public baseConfig { @@ -18,12 +18,20 @@ namespace Configs _add(new configItem("up_mbps", &up_mbps, integer)); _add(new configItem("down_mbps", &down_mbps, integer)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; }; class Multiplex : public baseConfig { public: bool enabled = false; + bool unspecified = false; QString protocol; int max_connections = 0; int min_streams = 0; @@ -34,6 +42,7 @@ namespace Configs Multiplex() { _add(new configItem("enabled", &enabled, boolean)); + _add(new configItem("unspecified", &unspecified, boolean)); _add(new configItem("protocol", &protocol, string)); _add(new configItem("max_connections", &max_connections, integer)); _add(new configItem("min_streams", &min_streams, integer)); @@ -41,5 +50,12 @@ namespace Configs _add(new configItem("padding", &padding, boolean)); _add(new configItem("brutal", dynamic_cast(brutal.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; }; } diff --git a/include/configs/common/transport.h b/include/configs/common/transport.h index b26b676..3f95e9c 100644 --- a/include/configs/common/transport.h +++ b/include/configs/common/transport.h @@ -36,5 +36,12 @@ namespace Configs _add(new configItem("early_data_header_name", &early_data_header_name, string)); _add(new configItem("service_name", &service_name, string)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; }; } diff --git a/include/configs/common/utils.h b/include/configs/common/utils.h new file mode 100644 index 0000000..dd6607c --- /dev/null +++ b/include/configs/common/utils.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +namespace Configs +{ + void mergeUrlQuery(QUrlQuery& baseQuery, const QString& strQuery); + + void mergeJsonObjects(QJsonObject& baseObject, const QJsonObject& obj); + + QStringList jsonObjectToQStringList(const QJsonObject& obj); + + QJsonObject qStringListToJsonObject(const QStringList& list); +} diff --git a/include/configs/generate.h b/include/configs/generate.h new file mode 100644 index 0000000..8c921c4 --- /dev/null +++ b/include/configs/generate.h @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace Configs +{ + struct BuildResult { + QJsonObject object; + QString error; + }; +} diff --git a/include/configs/outbounds/anyTLS.h b/include/configs/outbounds/anyTLS.h index 14d8a55..ad5c2aa 100644 --- a/include/configs/outbounds/anyTLS.h +++ b/include/configs/outbounds/anyTLS.h @@ -5,10 +5,9 @@ namespace Configs { - class anyTLS : public baseConfig, public outboundMeta + class anyTLS : public outbound { public: - std::shared_ptr commons = std::make_shared(); QString password; QString idle_session_check_interval = "30s"; QString idle_session_timeout = "30s"; @@ -24,5 +23,14 @@ namespace Configs _add(new configItem("min_idle_session", &min_idle_session, integer)); _add(new configItem("tls", dynamic_cast(tls.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/http.h b/include/configs/outbounds/http.h index 0ab7601..634c3b4 100644 --- a/include/configs/outbounds/http.h +++ b/include/configs/outbounds/http.h @@ -6,10 +6,9 @@ namespace Configs { - class http : public baseConfig, public outboundMeta + class http : public outbound { public: - std::shared_ptr commons = std::make_shared(); QString username; QString password; QString path; @@ -25,5 +24,14 @@ namespace Configs _add(new configItem("headers", &headers, stringList)); _add(new configItem("tls", dynamic_cast(tls.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/hysteria.h b/include/configs/outbounds/hysteria.h index f51d5e5..6bf7fc2 100644 --- a/include/configs/outbounds/hysteria.h +++ b/include/configs/outbounds/hysteria.h @@ -5,10 +5,9 @@ namespace Configs { - class hysteria : public baseConfig, public outboundMeta + class hysteria : public outbound { public: - std::shared_ptr commons = std::make_shared(); QStringList server_ports; QString hop_interval; int up_mbps = 0; @@ -36,5 +35,14 @@ namespace Configs _add(new configItem("disable_mtu_discovery", &disable_mtu_discovery, boolean)); _add(new configItem("tls", dynamic_cast(tls.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/hysteria2.h b/include/configs/outbounds/hysteria2.h index ef42313..afbc6a9 100644 --- a/include/configs/outbounds/hysteria2.h +++ b/include/configs/outbounds/hysteria2.h @@ -5,10 +5,9 @@ namespace Configs { - class hysteria2 : public baseConfig, public outboundMeta + class hysteria2 : public outbound { public: - std::shared_ptr commons = std::make_shared(); QStringList server_ports; QString hop_interval; int up_mbps = 0; @@ -30,5 +29,14 @@ namespace Configs _add(new configItem("password", &password, string)); _add(new configItem("tls", dynamic_cast(tls.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/shadowsocks.h b/include/configs/outbounds/shadowsocks.h index a0a0073..36b93dc 100644 --- a/include/configs/outbounds/shadowsocks.h +++ b/include/configs/outbounds/shadowsocks.h @@ -11,16 +11,15 @@ namespace Configs inline QStringList shadowsocksMethods = {"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305", "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "rc4-md5", "chacha20-ietf", "xchacha20"}; - class shadowsocks : public baseConfig, public outboundMeta + class shadowsocks : public outbound { public: - std::shared_ptr commons = std::make_shared(); QString method; QString password; QString plugin; QString plugin_opts; bool uot = false; - std::shared_ptr mux = std::make_shared(); + std::shared_ptr multiplex = std::make_shared(); shadowsocks() { @@ -30,7 +29,16 @@ namespace Configs _add(new configItem("plugin", &plugin, string)); _add(new configItem("plugin_opts", &plugin_opts, string)); _add(new configItem("uot", &uot, itemType::boolean)); - _add(new configItem("mux", dynamic_cast(mux.get()), jsonStore)); + _add(new configItem("multiplex", dynamic_cast(multiplex.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/socks.h b/include/configs/outbounds/socks.h index 1991400..9ac48c2 100644 --- a/include/configs/outbounds/socks.h +++ b/include/configs/outbounds/socks.h @@ -5,12 +5,12 @@ namespace Configs { - class socks : public baseConfig, public outboundMeta + class socks : public outbound { public: - std::shared_ptr commons = std::make_shared(); QString username; QString password; + int version = 5; bool uot = false; socks() @@ -18,7 +18,17 @@ namespace Configs _add(new configItem("commons", dynamic_cast(commons.get()), jsonStore)); _add(new configItem("username", &username, string)); _add(new configItem("password", &password, string)); + _add(new configItem("version", &version, integer)); _add(new configItem("uot", &uot, boolean)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/ssh.h b/include/configs/outbounds/ssh.h index b20f8d9..8f4346d 100644 --- a/include/configs/outbounds/ssh.h +++ b/include/configs/outbounds/ssh.h @@ -4,10 +4,9 @@ namespace Configs { - class ssh : public baseConfig, public outboundMeta + class ssh : public outbound { public: - std::shared_ptr commons = std::make_shared(); QString user; QString password; QString private_key; @@ -29,5 +28,14 @@ namespace Configs _add(new configItem("host_key_algorithms", &host_key_algorithms, stringList)); _add(new configItem("client_version", &client_version, string)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/tailscale.h b/include/configs/outbounds/tailscale.h new file mode 100644 index 0000000..7d68bc0 --- /dev/null +++ b/include/configs/outbounds/tailscale.h @@ -0,0 +1,51 @@ +#pragma once +#include "include/configs/baseConfig.h" +#include "include/configs/common/Outbound.h" + +namespace Configs +{ + class tailscale : public outbound + { + public: + QString state_directory = "$HOME/.tailscale"; + QString auth_key; + QString control_url = "https://controlplane.tailscale.com"; + bool ephemeral = false; + QString hostname; + bool accept_routes = false; + QString exit_node; + bool exit_node_allow_lan_access = false; + QStringList advertise_routes; + bool advertise_exit_node = false; + bool globalDNS = false; + + tailscale() + { + _add(new configItem("commons", dynamic_cast(commons.get()), jsonStore)); + _add(new configItem("state_directory", &state_directory, itemType::string)); + _add(new configItem("auth_key", &auth_key, itemType::string)); + _add(new configItem("control_url", &control_url, itemType::string)); + _add(new configItem("ephemeral", &ephemeral, itemType::boolean)); + _add(new configItem("hostname", &hostname, itemType::string)); + _add(new configItem("accept_routes", &accept_routes, itemType::boolean)); + _add(new configItem("exit_node", &exit_node, itemType::string)); + _add(new configItem("exit_node_allow_lan_access", &exit_node_allow_lan_access, itemType::boolean)); + _add(new configItem("advertise_routes", &advertise_routes, itemType::stringList)); + _add(new configItem("advertise_exit_node", &advertise_exit_node, itemType::boolean)); + _add(new configItem("globalDNS", &globalDNS, itemType::boolean)); + } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayAddress() override; + QString DisplayType() override; + static bool IsEndpoint(); + }; +} + + diff --git a/include/configs/outbounds/trojan.h b/include/configs/outbounds/trojan.h index 2e7c2f9..3bcf3aa 100644 --- a/include/configs/outbounds/trojan.h +++ b/include/configs/outbounds/trojan.h @@ -8,13 +8,12 @@ namespace Configs { - class Trojan : public baseConfig, public outboundMeta + class Trojan : public outbound { public: - std::shared_ptr commons = std::make_shared(); QString password; std::shared_ptr tls = std::make_shared(); - std::shared_ptr mux = std::make_shared(); + std::shared_ptr multiplex = std::make_shared(); std::shared_ptr transport = std::make_shared(); Trojan() @@ -22,8 +21,17 @@ namespace Configs _add(new configItem("commons", dynamic_cast(commons.get()), jsonStore)); _add(new configItem("password", &password, string)); _add(new configItem("tls", dynamic_cast(tls.get()), jsonStore)); - _add(new configItem("mux", dynamic_cast(mux.get()), jsonStore)); + _add(new configItem("multiplex", dynamic_cast(multiplex.get()), jsonStore)); _add(new configItem("transport", dynamic_cast(transport.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/tuic.h b/include/configs/outbounds/tuic.h index 4658285..f9b18f1 100644 --- a/include/configs/outbounds/tuic.h +++ b/include/configs/outbounds/tuic.h @@ -9,10 +9,9 @@ namespace Configs inline QStringList ccAlgorithms = {"cubic", "new_reno", "bbr"}; inline QStringList udpRelayModes = {"", "native", "quic"}; - class tuic : public baseConfig, public outboundMeta + class tuic : public outbound { public: - std::shared_ptr commons = std::make_shared(); QString uuid; QString password; QString congestion_control; @@ -34,5 +33,14 @@ namespace Configs _add(new configItem("heartbeat", &heartbeat, string)); _add(new configItem("tls", dynamic_cast(tls.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/vless.h b/include/configs/outbounds/vless.h index 3c87ff6..9b4d254 100644 --- a/include/configs/outbounds/vless.h +++ b/include/configs/outbounds/vless.h @@ -9,15 +9,14 @@ namespace Configs { inline QStringList vlessFlows = {"xtls-rprx-vision"}; - class vless : public baseConfig, public outboundMeta + class vless : public outbound { public: - std::shared_ptr commons = std::make_shared(); QString uuid; QString flow; std::shared_ptr tls = std::make_shared(); QString packet_encoding; - std::shared_ptr mux = std::make_shared(); + std::shared_ptr multiplex = std::make_shared(); std::shared_ptr transport = std::make_shared(); vless() @@ -27,8 +26,17 @@ namespace Configs _add(new configItem("flow", &flow, string)); _add(new configItem("tls", dynamic_cast(tls.get()), jsonStore)); _add(new configItem("packet_encoding", &packet_encoding, string)); - _add(new configItem("mux", dynamic_cast(mux.get()), jsonStore)); + _add(new configItem("multiplex", dynamic_cast(multiplex.get()), jsonStore)); _add(new configItem("transport", dynamic_cast(transport.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/vmess.h b/include/configs/outbounds/vmess.h index b42dc0a..3182779 100644 --- a/include/configs/outbounds/vmess.h +++ b/include/configs/outbounds/vmess.h @@ -12,10 +12,9 @@ namespace Configs inline QStringList vmessSecurity = {"auto", "none", "zero", "aes-128-gcm", "chacha20-poly1305"}; inline QStringList vPacketEncoding = {"", "packetaddr", "xudp"}; - class vmess : public baseConfig, public outboundMeta + class vmess : public outbound { public: - std::shared_ptr commons = std::make_shared(); QString uuid; QString security = "auto"; int alter_id = 0; @@ -24,7 +23,7 @@ namespace Configs std::shared_ptr tls = std::make_shared(); QString packet_encoding; std::shared_ptr transport = std::make_shared(); - std::shared_ptr mux = std::make_shared(); + std::shared_ptr multiplex = std::make_shared(); vmess() { @@ -37,7 +36,16 @@ namespace Configs _add(new configItem("tls", dynamic_cast(tls.get()), jsonStore)); _add(new configItem("packet_encoding", &packet_encoding, string)); _add(new configItem("transport", dynamic_cast(transport.get()), jsonStore)); - _add(new configItem("mux", dynamic_cast(mux.get()), jsonStore)); + _add(new configItem("multiplex", dynamic_cast(multiplex.get()), jsonStore)); } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayType() override; }; } diff --git a/include/configs/outbounds/wireguard.h b/include/configs/outbounds/wireguard.h new file mode 100644 index 0000000..4eae597 --- /dev/null +++ b/include/configs/outbounds/wireguard.h @@ -0,0 +1,95 @@ +#pragma once +#include "include/configs/baseConfig.h" +#include "include/configs/common/Outbound.h" + +namespace Configs +{ + class Peer : public baseConfig + { + public: + QString address; + int port = 0; + QString public_key; + QString pre_shared_key; + QList reserved; + int persistent_keepalive = 0; + + Peer() + { + _add(new configItem("address", &address, string)); + _add(new configItem("port", &port, integer)); + _add(new configItem("public_key", &public_key, itemType::string)); + _add(new configItem("pre_shared_key", &pre_shared_key, itemType::string)); + _add(new configItem("reserved", &reserved, itemType::integerList)); + _add(new configItem("persistent_keepalive", &persistent_keepalive, itemType::integer)); + } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + }; + + class wireguard : public outbound + { + public: + QString private_key; + std::shared_ptr peer = std::make_shared(); + QStringList address; + int mtu = 1420; + bool system = false; + int worker_count = 0; + QString udp_timeout; + + // Amnezia options + bool enable_amnezia = false; + int junk_packet_count = 0; + int junk_packet_min_size = 0; + int junk_packet_max_size = 0; + int init_packet_junk_size = 0; + int response_packet_junk_size = 0; + int init_packet_magic_header = 0; + int response_packet_magic_header = 0; + int underload_packet_magic_header = 0; + int transport_packet_magic_header = 0; + + wireguard() + { + _add(new configItem("commons", dynamic_cast(commons.get()), jsonStore)); + + _add(new configItem("private_key", &private_key, itemType::string)); + _add(new configItem("peer", dynamic_cast(peer.get()), jsonStore)); + _add(new configItem("address", &address, itemType::stringList)); + _add(new configItem("mtu", &mtu, itemType::integer)); + _add(new configItem("system", &system, itemType::boolean)); + _add(new configItem("worker_count", &worker_count, itemType::integer)); + _add(new configItem("udp_timeout", &udp_timeout, itemType::string)); + + _add(new configItem("enable_amnezia", &enable_amnezia, itemType::boolean)); + _add(new configItem("junk_packet_count", &junk_packet_count, itemType::integer)); + _add(new configItem("junk_packet_min_size", &junk_packet_min_size, itemType::integer)); + _add(new configItem("junk_packet_max_size", &junk_packet_max_size, itemType::integer)); + _add(new configItem("init_packet_junk_size", &init_packet_junk_size, itemType::integer)); + _add(new configItem("response_packet_junk_size", &response_packet_junk_size, itemType::integer)); + _add(new configItem("init_packet_magic_header", &init_packet_magic_header, itemType::integer)); + _add(new configItem("response_packet_magic_header", &response_packet_magic_header, itemType::integer)); + _add(new configItem("underload_packet_magic_header", &underload_packet_magic_header, itemType::integer)); + _add(new configItem("transport_packet_magic_header", &transport_packet_magic_header, itemType::integer)); + } + + // baseConfig overrides + bool ParseFromLink(const QString& link) override; + bool ParseFromJson(const QJsonObject& object) override; + QString ExportToLink() override; + QJsonObject ExportToJson() override; + BuildResult Build() override; + + QString DisplayAddress() override; + QString DisplayType() override; + static bool IsEndpoint(); + }; +} + + diff --git a/src/configs/common/DialFields.cpp b/src/configs/common/DialFields.cpp new file mode 100644 index 0000000..7fadff8 --- /dev/null +++ b/src/configs/common/DialFields.cpp @@ -0,0 +1,56 @@ +#include "include/configs/common/DialFields.h" + +#include + +namespace Configs { + bool DialFields::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + if (query.hasQueryItem("reuse_addr")) reuse_addr = query.queryItemValue("reuse_addr") == "true"; + if (query.hasQueryItem("connect_timeout")) connect_timeout = query.queryItemValue("connect_timeout"); + if (query.hasQueryItem("tcp_fast_open")) tcp_fast_open = query.queryItemValue("tcp_fast_open") == "true"; + if (query.hasQueryItem("tcp_multi_path")) tcp_multi_path = query.queryItemValue("tcp_multi_path") == "true"; + if (query.hasQueryItem("udp_fragment")) udp_fragment = query.queryItemValue("udp_fragment") == "true"; + return true; + } + bool DialFields::ParseFromJson(const QJsonObject& object) + { + if (object == nullptr) return false; + + if (object.contains("reuse_addr")) reuse_addr = object["reuse_addr"].toBool(); + if (object.contains("connect_timeout")) connect_timeout = object["connect_timeout"].toString(); + if (object.contains("tcp_fast_open")) tcp_fast_open = object["tcp_fast_open"].toBool(); + if (object.contains("tcp_multi_path")) tcp_multi_path = object["tcp_multi_path"].toBool(); + if (object.contains("udp_fragment")) udp_fragment = object["udp_fragment"].toBool(); + return true; + } + QString DialFields::ExportToLink() + { + QUrlQuery query; + if (reuse_addr) query.addQueryItem("reuse_addr", "true"); + if (!connect_timeout.isEmpty()) query.addQueryItem("connect_timeout", connect_timeout); + if (tcp_fast_open) query.addQueryItem("tcp_fast_open", "true"); + if (tcp_multi_path) query.addQueryItem("tcp_multi_path", "true"); + if (udp_fragment) query.addQueryItem("udp_fragment", "true"); + return query.toString(); + } + QJsonObject DialFields::ExportToJson() + { + QJsonObject object; + if (reuse_addr) object["reuse_addr"] = reuse_addr; + if (!connect_timeout.isEmpty()) object["connect_timeout"] = connect_timeout; + if (tcp_fast_open) object["tcp_fast_open"] = tcp_fast_open; + if (tcp_multi_path) object["tcp_multi_path"] = tcp_multi_path; + if (udp_fragment) object["udp_fragment"] = udp_fragment; + return object; + } + BuildResult DialFields::Build() + { + return {ExportToJson(), ""}; + } +} + + diff --git a/src/configs/common/Outbound.cpp b/src/configs/common/Outbound.cpp new file mode 100644 index 0000000..ff3a67c --- /dev/null +++ b/src/configs/common/Outbound.cpp @@ -0,0 +1,47 @@ +#include "include/configs/common/Outbound.h" +#include "include/configs/common/utils.h" + +namespace Configs { + bool OutboundCommons::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + + if (url.hasFragment()) name = url.fragment(QUrl::FullyDecoded); + server = url.host(); + server_port = url.port(); + dialFields->ParseFromLink(link); + return true; + } + bool OutboundCommons::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty()) return false; + if (object.contains("tag")) name = object["tag"].toString(); + if (object.contains("server")) server = object["server"].toString(); + if (object.contains("server_port")) server_port = object["server_port"].toInt(); + dialFields->ParseFromJson(object); + return true; + } + QString OutboundCommons::ExportToLink() + { + QUrlQuery query; + mergeUrlQuery(query, dialFields->ExportToLink()); + return query.toString(); + } + QJsonObject OutboundCommons::ExportToJson() + { + QJsonObject object; + if (!name.isEmpty()) object["tag"] = name; + if (!server.isEmpty()) object["server"] = server; + if (server_port > 0) object["server_port"] = server_port; + auto dialFieldsObj = dialFields->ExportToJson(); + mergeJsonObjects(object, dialFieldsObj); + return object; + } + BuildResult OutboundCommons::Build() + { + return {ExportToJson(), ""}; + } +} + + diff --git a/src/configs/common/TLS.cpp b/src/configs/common/TLS.cpp new file mode 100644 index 0000000..1e2228a --- /dev/null +++ b/src/configs/common/TLS.cpp @@ -0,0 +1,282 @@ +#include "include/configs/common/TLS.h" + +#include +#include +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool uTLS::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + // handle the common format + fingerPrint = dataStore->utlsFingerprint; + if (query.hasQueryItem("fp")) fingerPrint = query.queryItemValue("fp"); + if (!fingerPrint.isEmpty()) enabled = true; + return true; + } + bool uTLS::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty()) return false; + if (object.contains("enabled")) enabled = object["enabled"].toBool(); + if (object.contains("fingerprint")) fingerPrint = object["fingerprint"].toString(); + return true; + } + QString uTLS::ExportToLink() + { + QUrlQuery query; + if (!enabled) return ""; + if (!fingerPrint.isEmpty()) query.addQueryItem("fp", fingerPrint); + return query.toString(); + } + QJsonObject uTLS::ExportToJson() + { + QJsonObject object; + if (!enabled) return object; + object["enabled"] = enabled; + if (!fingerPrint.isEmpty()) object["fingerprint"] = fingerPrint; + return object; + } + BuildResult uTLS::Build() + { + return {ExportToJson(), ""}; + } + + bool ECH::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + if (query.hasQueryItem("ech_enabled")) enabled = query.queryItemValue("ech_enabled") == "true"; + if (query.hasQueryItem("ech_config")) config = query.queryItemValue("ech_config").split(","); + if (query.hasQueryItem("ech_config_path")) config_path = query.queryItemValue("ech_config_path"); + return true; + } + bool ECH::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty()) return false; + if (object.contains("enabled")) enabled = object["enabled"].toBool(); + if (object.contains("config")) { + config = QJsonArray2QListString(object["config"].toArray()); + } + if (object.contains("config_path")) config_path = object["config_path"].toString(); + return true; + } + QString ECH::ExportToLink() + { + QUrlQuery query; + if (!enabled) return ""; + query.addQueryItem("ech_enabled", "true"); + if (!config.isEmpty()) query.addQueryItem("ech_config", config.join(",")); + if (!config_path.isEmpty()) query.addQueryItem("ech_config_path", config_path); + return query.toString(); + } + QJsonObject ECH::ExportToJson() + { + QJsonObject object; + if (!enabled) return object; + object["enabled"] = enabled; + if (!config.isEmpty()) { + object["config"] = QListStr2QJsonArray(config); + } + if (!config_path.isEmpty()) object["config_path"] = config_path; + return object; + } + BuildResult ECH::Build() + { + return {ExportToJson(), ""}; + } + + bool Reality::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + // handle the common format + if (query.hasQueryItem("pbk")) + { + enabled = true; + public_key = query.queryItemValue("pbk"); + short_id = query.queryItemValue("sid"); + } + return true; + } + bool Reality::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty()) return false; + if (object.contains("enabled")) enabled = object["enabled"].toBool(); + if (object.contains("public_key")) public_key = object["public_key"].toString(); + if (object.contains("short_id")) short_id = object["short_id"].toString(); + return true; + } + QString Reality::ExportToLink() + { + QUrlQuery query; + if (!enabled) return ""; + query.addQueryItem("pbk", public_key); + query.addQueryItem("sid", short_id); + return query.toString(); + } + QJsonObject Reality::ExportToJson() + { + QJsonObject object; + if (!enabled) return object; + object["enabled"] = enabled; + if (!public_key.isEmpty()) object["public_key"] = public_key; + if (!short_id.isEmpty()) object["short_id"] = short_id; + return object; + } + BuildResult Reality::Build() + { + return {ExportToJson(), ""}; + } + + bool TLS::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + if (query.hasQueryItem("security")) enabled = query.queryItemValue("security").replace("reality", "tls").replace("none", "") == "tls"; + if (query.hasQueryItem("disable_sni")) disable_sni = query.queryItemValue("disable_sni").replace("1", "true") == "true"; + if (query.hasQueryItem("sni")) server_name = query.queryItemValue("sni"); + if (query.hasQueryItem("peer")) server_name = query.queryItemValue("peer"); + if (query.hasQueryItem("allowInsecure")) insecure = query.queryItemValue("allowInsecure").replace("1", "true") == "true"; + if (query.hasQueryItem("allow_insecure")) insecure = query.queryItemValue("allow_insecure").replace("1", "true") == "true"; + if (query.hasQueryItem("alpn")) alpn = query.queryItemValue("alpn").split(","); + if (query.hasQueryItem("tls_min_version")) min_version = query.queryItemValue("tls_min_version"); + if (query.hasQueryItem("tls_max_version")) max_version = query.queryItemValue("tls_max_version"); + if (query.hasQueryItem("tls_cipher_suites")) cipher_suites = query.queryItemValue("tls_cipher_suites").split(","); + if (query.hasQueryItem("tls_curve_preferences")) curve_preferences = query.queryItemValue("tls_curve_preferences").split(","); + if (query.hasQueryItem("tls_certificate")) certificate = query.queryItemValue("tls_certificate"); + if (query.hasQueryItem("tls_certificate_path")) certificate_path = query.queryItemValue("tls_certificate_path"); + if (query.hasQueryItem("tls_certificate_public_key_sha256")) certificate_public_key_sha256 = query.queryItemValue("tls_certificate_public_key_sha256").split(","); + if (query.hasQueryItem("tls_client_certificate")) client_certificate = query.queryItemValue("tls_client_certificate"); + if (query.hasQueryItem("tls_client_certificate_path")) client_certificate_path = query.queryItemValue("tls_client_certificate_path"); + if (query.hasQueryItem("tls_client_key")) client_key = query.queryItemValue("tls_client_key").split(","); + if (query.hasQueryItem("tls_client_key_path")) client_key_path = query.queryItemValue("tls_client_key_path"); + if (query.hasQueryItem("tls_fragment")) fragment = query.queryItemValue("tls_fragment") == "true"; + if (query.hasQueryItem("tls_fragment_fallback_delay")) fragment_fallback_delay = query.queryItemValue("tls_fragment_fallback_delay"); + if (query.hasQueryItem("tls_record_fragment")) record_fragment = query.queryItemValue("tls_record_fragment") == "true"; + ech->ParseFromLink(link); + utls->ParseFromLink(link); + reality->ParseFromLink(link); + return true; + } + bool TLS::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty()) return false; + if (object.contains("enabled")) enabled = object["enabled"].toBool(); + if (object.contains("disable_sni")) disable_sni = object["disable_sni"].toBool(); + if (object.contains("server_name")) server_name = object["server_name"].toString(); + if (object.contains("insecure")) insecure = object["insecure"].toBool(); + if (object.contains("alpn")) { + alpn = QJsonArray2QListString(object["alpn"].toArray()); + } + if (object.contains("min_version")) min_version = object["min_version"].toString(); + if (object.contains("max_version")) max_version = object["max_version"].toString(); + if (object.contains("cipher_suites")) { + cipher_suites = QJsonArray2QListString(object["cipher_suites"].toArray()); + } + if (object.contains("curve_preferences")) { + curve_preferences = QJsonArray2QListString(object["curve_preferences"].toArray()); + } + if (object.contains("certificate")) certificate = object["certificate"].toString(); + if (object.contains("certificate_path")) certificate_path = object["certificate_path"].toString(); + if (object.contains("certificate_public_key_sha256")) { + certificate_public_key_sha256 = QJsonArray2QListString(object["certificate_public_key_sha256"].toArray()); + } + if (object.contains("client_certificate")) client_certificate = object["client_certificate"].toString(); + if (object.contains("client_certificate_path")) client_certificate_path = object["client_certificate_path"].toString(); + if (object.contains("client_key")) { + client_key = QJsonArray2QListString(object["client_key"].toArray()); + } + if (object.contains("client_key_path")) client_key_path = object["client_key_path"].toString(); + if (object.contains("fragment")) fragment = object["fragment"].toBool(); + if (object.contains("fragment_fallback_delay")) fragment_fallback_delay = object["fragment_fallback_delay"].toString(); + if (object.contains("record_fragment")) record_fragment = object["record_fragment"].toBool(); + if (object.contains("ech")) ech->ParseFromJson(object["ech"].toObject()); + if (object.contains("utls")) utls->ParseFromJson(object["utls"].toObject()); + if (object.contains("reality")) reality->ParseFromJson(object["reality"].toObject()); + return true; + } + QString TLS::ExportToLink() + { + QUrlQuery query; + if (!enabled) return ""; + query.addQueryItem("security", "tls"); + if (disable_sni) query.addQueryItem("disable_sni", "true"); + if (!server_name.isEmpty()) query.addQueryItem("sni", server_name); + if (insecure) query.addQueryItem("allowInsecure", "true"); + if (!alpn.isEmpty()) query.addQueryItem("alpn", alpn.join(",")); + if (!min_version.isEmpty()) query.addQueryItem("tls_min_version", min_version); + if (!max_version.isEmpty()) query.addQueryItem("tls_max_version", max_version); + if (!cipher_suites.isEmpty()) query.addQueryItem("tls_cipher_suites", cipher_suites.join(",")); + if (!curve_preferences.isEmpty()) query.addQueryItem("tls_curve_preferences", curve_preferences.join(",")); + if (!certificate.isEmpty()) query.addQueryItem("tls_certificate", certificate); + if (!certificate_path.isEmpty()) query.addQueryItem("tls_certificate_path", certificate_path); + if (!certificate_public_key_sha256.isEmpty()) query.addQueryItem("tls_certificate_public_key_sha256", certificate_public_key_sha256.join(",")); + if (!client_certificate.isEmpty()) query.addQueryItem("tls_client_certificate", client_certificate); + if (!client_certificate_path.isEmpty()) query.addQueryItem("tls_client_certificate_path", client_certificate_path); + if (!client_key.isEmpty()) query.addQueryItem("tls_client_key", client_key.join(",")); + if (!client_key_path.isEmpty()) query.addQueryItem("tls_client_key_path", client_key_path); + if (fragment) query.addQueryItem("tls_fragment", "true"); + if (!fragment_fallback_delay.isEmpty()) query.addQueryItem("tls_fragment_fallback_delay", fragment_fallback_delay); + if (record_fragment) query.addQueryItem("tls_record_fragment", "true"); + mergeUrlQuery(query, ech->ExportToLink()); + mergeUrlQuery(query, utls->ExportToLink()); + mergeUrlQuery(query, reality->ExportToLink()); + return query.toString(); + } + QJsonObject TLS::ExportToJson() + { + QJsonObject object; + if (!enabled) return object; + object["enabled"] = enabled; + if (disable_sni) object["disable_sni"] = disable_sni; + if (!server_name.isEmpty()) object["server_name"] = server_name; + if (insecure) object["insecure"] = insecure; + if (!alpn.isEmpty()) { + object["alpn"] = QListStr2QJsonArray(alpn); + } + if (!min_version.isEmpty()) object["min_version"] = min_version; + if (!max_version.isEmpty()) object["max_version"] = max_version; + if (!cipher_suites.isEmpty()) { + object["cipher_suites"] = QListStr2QJsonArray(cipher_suites); + } + if (!curve_preferences.isEmpty()) { + object["curve_preferences"] = QListStr2QJsonArray(curve_preferences); + } + if (!certificate.isEmpty()) object["certificate"] = certificate; + if (!certificate_path.isEmpty()) object["certificate_path"] = certificate_path; + if (!certificate_public_key_sha256.isEmpty()) { + object["certificate_public_key_sha256"] = QListStr2QJsonArray(certificate_public_key_sha256); + } + if (!client_certificate.isEmpty()) object["client_certificate"] = client_certificate; + if (!client_certificate_path.isEmpty()) object["client_certificate_path"] = client_certificate_path; + if (!client_key.isEmpty()) { + object["client_key"] = QListStr2QJsonArray(client_key); + } + if (!client_key_path.isEmpty()) object["client_key_path"] = client_key_path; + if (fragment) object["fragment"] = fragment; + if (!fragment_fallback_delay.isEmpty()) object["fragment_fallback_delay"] = fragment_fallback_delay; + if (record_fragment) object["record_fragment"] = record_fragment; + if (ech->enabled) object["ech"] = ech->ExportToJson(); + if (utls->enabled) object["utls"] = utls->ExportToJson(); + if (reality->enabled) object["reality"] = reality->ExportToJson(); + return object; + } + BuildResult TLS::Build() + { + return {ExportToJson(), ""}; + } +} + + diff --git a/src/configs/common/multiplex.cpp b/src/configs/common/multiplex.cpp new file mode 100644 index 0000000..0367216 --- /dev/null +++ b/src/configs/common/multiplex.cpp @@ -0,0 +1,121 @@ +#include "include/configs/common/multiplex.h" + +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool TcpBrutal::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + if (query.hasQueryItem("brutal_enabled")) enabled = query.queryItemValue("brutal_enabled") == "true"; + if (query.hasQueryItem("brutal_up_mbps")) up_mbps = query.queryItemValue("brutal_up_mbps").toInt(); + if (query.hasQueryItem("brutal_down_mbps")) down_mbps = query.queryItemValue("brutal_down_mbps").toInt(); + return true; + } + bool TcpBrutal::ParseFromJson(const QJsonObject& object) + { + if (object == nullptr) return false; + if (object.contains("enabled")) enabled = object["enabled"].toBool(); + if (object.contains("up_mbps")) up_mbps = object["up_mbps"].toInt(); + if (object.contains("down_mbps")) down_mbps = object["down_mbps"].toInt(); + return true; + } + QString TcpBrutal::ExportToLink() + { + QUrlQuery query; + if (!enabled) return ""; + query.addQueryItem("brutal_enabled", "true"); + if (up_mbps > 0) query.addQueryItem("brutal_up_mbps", QString::number(up_mbps)); + if (down_mbps > 0) query.addQueryItem("brutal_down_mbps", QString::number(down_mbps)); + return QUrlQuery(query).toString(); + } + QJsonObject TcpBrutal::ExportToJson() + { + QJsonObject object; + if (!enabled) return object; + object["enabled"] = enabled; + if (up_mbps > 0) object["up_mbps"] = up_mbps; + if (down_mbps > 0) object["down_mbps"] = down_mbps; + return object; + } + BuildResult TcpBrutal::Build() + { + return {ExportToJson(), ""}; + } + + bool Multiplex::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + if (query.hasQueryItem("mux")) enabled = query.queryItemValue("mux") == "true"; + else unspecified = true; + protocol = query.hasQueryItem("mux_protocol") ? query.queryItemValue("mux_protocol") : "smux"; + if (query.hasQueryItem("mux_max_connections")) max_connections = query.queryItemValue("mux_max_connections").toInt(); + if (query.hasQueryItem("mux_min_streams")) min_streams = query.queryItemValue("mux_min_streams").toInt(); + if (query.hasQueryItem("mux_max_streams")) max_streams = query.queryItemValue("mux_max_streams").toInt(); + if (query.hasQueryItem("mux_padding")) padding = query.queryItemValue("mux_padding") == "true"; + brutal->ParseFromLink(link); + return true; + } + bool Multiplex::ParseFromJson(const QJsonObject& object) + { + if (object == nullptr) + { + unspecified = true; + return false; + } + if (object.contains("enabled")) enabled = object["enabled"].toBool(); + else unspecified = true; + if (object.contains("protocol")) protocol = object["protocol"].toString(); + if (object.contains("max_connections")) max_connections = object["max_connections"].toInt(); + if (object.contains("min_streams")) min_streams = object["min_streams"].toInt(); + if (object.contains("max_streams")) max_streams = object["max_streams"].toInt(); + if (object.contains("padding")) padding = object["padding"].toBool(); + if (object.contains("brutal")) brutal->ParseFromJson(object["brutal"].toObject()); + return true; + } + QString Multiplex::ExportToLink() + { + QUrlQuery query; + if (!enabled) return ""; + query.addQueryItem("mux", "true"); + if (!protocol.isEmpty()) query.addQueryItem("mux_protocol", protocol); + if (max_connections > 0) query.addQueryItem("mux_max_connections", QString::number(max_connections)); + if (min_streams > 0) query.addQueryItem("mux_min_streams", QString::number(min_streams)); + if (max_streams > 0) query.addQueryItem("mux_max_streams", QString::number(max_streams)); + if (padding) query.addQueryItem("mux_padding", "true"); + mergeUrlQuery(query, brutal->ExportToLink()); + return query.toString(); + } + QJsonObject Multiplex::ExportToJson() + { + QJsonObject object; + if (!enabled) return object; + object["enabled"] = enabled; + if (!protocol.isEmpty()) object["protocol"] = protocol; + if (max_connections > 0) object["max_connections"] = max_connections; + if (min_streams > 0) object["min_streams"] = min_streams; + if (max_streams > 0) object["max_streams"] = max_streams; + if (padding) object["padding"] = padding; + if (brutal->enabled) object["brutal"] = brutal->ExportToJson(); + return object; + } + BuildResult Multiplex::Build() + { + auto obj = ExportToJson(); + if (unspecified && dataStore->mux_default_on) obj["enabled"] = true; + if (protocol.isEmpty()) obj["protocol"] = dataStore->mux_protocol; + if (max_streams == 0 && max_connections == 0 && min_streams == 0) obj["max_streams"] = dataStore->mux_concurrency; + if (dataStore->mux_padding) obj["padding"] = true; + return {obj, ""}; + } +} + + diff --git a/src/configs/common/transport.cpp b/src/configs/common/transport.cpp new file mode 100644 index 0000000..652b7cf --- /dev/null +++ b/src/configs/common/transport.cpp @@ -0,0 +1,87 @@ +#include "include/configs/common/transport.h" + +#include +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool Transport::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + if (query.hasQueryItem("type")) + { + type = query.queryItemValue("type"); + if ((type == "tcp" && query.queryItemValue("headerType") == "http") || type == "h2") type = "http"; + } + if (query.hasQueryItem("host")) host = query.queryItemValue("host"); + if (query.hasQueryItem("path")) path = query.queryItemValue("path"); + if (query.hasQueryItem("method")) method = query.queryItemValue("method"); + if (query.hasQueryItem("headers")) headers = query.queryItemValue("headers").split(","); + if (query.hasQueryItem("idle_timeout")) idle_timeout = query.queryItemValue("idle_timeout"); + if (query.hasQueryItem("ping_timeout")) ping_timeout = query.queryItemValue("ping_timeout"); + if (query.hasQueryItem("max_early_data")) max_early_data = query.queryItemValue("max_early_data").toInt(); + if (query.hasQueryItem("early_data_header_name")) early_data_header_name = query.queryItemValue("early_data_header_name"); + if (query.hasQueryItem("serviceName")) service_name = query.queryItemValue("serviceName"); + return true; + } + bool Transport::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty()) return false; + if (object.contains("type")) type = object["type"].toString(); + if (object.contains("host")) host = object["host"].toString(); + if (object.contains("path")) path = object["path"].toString(); + if (object.contains("method")) method = object["method"].toString(); + if (object.contains("headers") && object["headers"].isObject()) { + headers = jsonObjectToQStringList(object["headers"].toObject()); + } + if (object.contains("idle_timeout")) idle_timeout = object["idle_timeout"].toString(); + if (object.contains("ping_timeout")) ping_timeout = object["ping_timeout"].toString(); + if (object.contains("max_early_data")) max_early_data = object["max_early_data"].toInt(); + if (object.contains("early_data_header_name")) early_data_header_name = object["early_data_header_name"].toString(); + if (object.contains("service_name")) service_name = object["service_name"].toString(); + return true; + } + QString Transport::ExportToLink() + { + QUrlQuery query; + if (!type.isEmpty()) query.addQueryItem("type", type); + if (!host.isEmpty()) query.addQueryItem("host", host); + if (!path.isEmpty()) query.addQueryItem("path", path); + if (!method.isEmpty()) query.addQueryItem("method", method); + if (!headers.isEmpty()) query.addQueryItem("headers", headers.join(",")); + if (!idle_timeout.isEmpty()) query.addQueryItem("idle_timeout", idle_timeout); + if (!ping_timeout.isEmpty()) query.addQueryItem("ping_timeout", ping_timeout); + if (max_early_data > 0) query.addQueryItem("max_early_data", QString::number(max_early_data)); + if (!early_data_header_name.isEmpty()) query.addQueryItem("early_data_header_name", early_data_header_name); + if (!service_name.isEmpty()) query.addQueryItem("serviceName", service_name); + return query.toString(); + } + QJsonObject Transport::ExportToJson() + { + QJsonObject object; + if (!type.isEmpty()) object["type"] = type; + if (!host.isEmpty()) object["host"] = host; + if (!path.isEmpty()) object["path"] = path; + if (!method.isEmpty()) object["method"] = method; + if (!headers.isEmpty()) { + object["headers"] = qStringListToJsonObject(headers); + } + if (!idle_timeout.isEmpty()) object["idle_timeout"] = idle_timeout; + if (!ping_timeout.isEmpty()) object["ping_timeout"] = ping_timeout; + if (max_early_data > 0) object["max_early_data"] = max_early_data; + if (!early_data_header_name.isEmpty()) object["early_data_header_name"] = early_data_header_name; + if (!service_name.isEmpty()) object["service_name"] = service_name; + return object; + } + BuildResult Transport::Build() + { + return {ExportToJson(), ""}; + } +} + + diff --git a/src/configs/common/utils.cpp b/src/configs/common/utils.cpp new file mode 100644 index 0000000..2071f32 --- /dev/null +++ b/src/configs/common/utils.cpp @@ -0,0 +1,47 @@ +#include "include/configs/common/utils.h" + +namespace Configs +{ + void mergeUrlQuery(QUrlQuery& baseQuery, const QString& strQuery) + { + QUrlQuery query = QUrlQuery(strQuery); + for (const auto& item : query.queryItems()) + { + baseQuery.addQueryItem(item.first, item.second); + } + } + + void mergeJsonObjects(QJsonObject& baseObject, const QJsonObject& obj) + { + for (const auto& key : obj.keys()) + { + baseObject[key] = obj[key]; + } + } + + QStringList jsonObjectToQStringList(const QJsonObject& obj) + { + auto result = QStringList(); + for (const auto& key : obj.keys()) + { + result << key << obj[key].toString(); + } + return result; + } + + QJsonObject qStringListToJsonObject(const QStringList& list) + { + auto result = QJsonObject(); + if (list.count() %2 != 0) + { + qDebug() << "QStringList of odd length in qStringListToJsonObject:" << list; + return result; + } + for (int i=0;i +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool anyTLS::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + password = url.userName(); + + if (commons->server_port == 0) commons->server_port = 443; + + if (query.hasQueryItem("idle_session_check_interval")) idle_session_check_interval = query.queryItemValue("idle_session_check_interval"); + if (query.hasQueryItem("idle_session_timeout")) idle_session_timeout = query.queryItemValue("idle_session_timeout"); + if (query.hasQueryItem("min_idle_session")) min_idle_session = query.queryItemValue("min_idle_session").toInt(); + + tls->ParseFromLink(link); + tls->enabled = true; // anyTLS always uses tls + + return true; + } + + bool anyTLS::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "anytls") return false; + commons->ParseFromJson(object); + if (object.contains("password")) password = object["password"].toString(); + if (object.contains("idle_session_check_interval")) idle_session_check_interval = object["idle_session_check_interval"].toString(); + if (object.contains("idle_session_timeout")) idle_session_timeout = object["idle_session_timeout"].toString(); + if (object.contains("min_idle_session")) min_idle_session = object["min_idle_session"].toInt(); + if (object.contains("tls")) tls->ParseFromJson(object["tls"].toObject()); + return true; + } + + QString anyTLS::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("anytls"); + url.setUserName(password); + url.setHost(commons->server); + url.setPort(commons->server_port); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (!idle_session_check_interval.isEmpty()) query.addQueryItem("idle_session_check_interval", idle_session_check_interval); + if (!idle_session_timeout.isEmpty()) query.addQueryItem("idle_session_timeout", idle_session_timeout); + if (min_idle_session > 0) query.addQueryItem("min_idle_session", QString::number(min_idle_session)); + + mergeUrlQuery(query, tls->ExportToLink()); + mergeUrlQuery(query, commons->ExportToLink()); + + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + + QJsonObject anyTLS::ExportToJson() + { + QJsonObject object; + object["type"] = "anytls"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!password.isEmpty()) object["password"] = password; + if (!idle_session_check_interval.isEmpty()) object["idle_session_check_interval"] = idle_session_check_interval; + if (!idle_session_timeout.isEmpty()) object["idle_session_timeout"] = idle_session_timeout; + if (min_idle_session > 0) object["min_idle_session"] = min_idle_session; + object["tls"] = tls->ExportToJson(); + return object; + } + + BuildResult anyTLS::Build() + { + return {ExportToJson(), ""}; + } + + QString anyTLS::DisplayType() + { + return "AnyTLS"; + } +} diff --git a/src/configs/outbounds/http.cpp b/src/configs/outbounds/http.cpp new file mode 100644 index 0000000..bf08e48 --- /dev/null +++ b/src/configs/outbounds/http.cpp @@ -0,0 +1,79 @@ +#include "include/configs/outbounds/http.h" + +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool http::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + username = url.userName(); + password = url.password(); + if (query.hasQueryItem("path")) path = query.queryItemValue("path"); + if (query.hasQueryItem("headers")) headers = query.queryItemValue("headers").split(","); + if (url.scheme() == "https" || query.queryItemValue("security") == "tls") + { + tls->ParseFromLink(link); + tls->enabled = true; // force enable in case scheme is https and no security queryValue is set + } + if (commons->server_port == 0) commons->server_port = 443; + return true; + } + bool http::ParseFromJson(const QJsonObject& object) + { + if (object.empty() || object["type"] != "http") return false; + commons->ParseFromJson(object); + if (object.contains("username")) username = object["username"].toString(); + if (object.contains("password")) password = object["password"].toString(); + if (object.contains("path")) path = object["path"].toString(); + if (object.contains("headers") && object["headers"].isObject()) headers = jsonObjectToQStringList(object["headers"].toObject()); + if (object.contains("tls")) tls->ParseFromJson(object["tls"].toObject()); + return true; + } + QString http::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme(tls->enabled ? "https" : "http"); + url.setHost(commons->server); + url.setPort(commons->server_port); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + if (!username.isEmpty()) url.setUserName(username); + if (!password.isEmpty()) url.setPassword(password); + if (!path.isEmpty()) query.addQueryItem("path", path); + if (!headers.empty()) query.addQueryItem("headers", headers.join(",")); + mergeUrlQuery(query, tls->ExportToLink()); + mergeUrlQuery(query, commons->ExportToLink()); + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + QJsonObject http::ExportToJson() + { + QJsonObject object; + object["type"] = "http"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!username.isEmpty()) object["username"] = username; + if (!password.isEmpty()) object["password"] = password; + if (!path.isEmpty()) object["path"] = path; + if (auto headerObj = qStringListToJsonObject(headers); !headerObj.isEmpty()) object["headers"] = headerObj; + if (auto tlsObj = tls->ExportToJson(); !tlsObj.isEmpty()) object["tls"] = tlsObj; + return object; + } + BuildResult http::Build() + { + return {ExportToJson(), ""}; + } + + QString http::DisplayType() + { + return "HTTP"; + } +} + + diff --git a/src/configs/outbounds/hysteria.cpp b/src/configs/outbounds/hysteria.cpp new file mode 100644 index 0000000..919ca8e --- /dev/null +++ b/src/configs/outbounds/hysteria.cpp @@ -0,0 +1,144 @@ +#include "include/configs/outbounds/hysteria.h" + +#include +#include +#include <3rdparty/URLParser/url_parser.h> +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool hysteria::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) + { + if(!url.errorString().startsWith("Invalid port")) + return false; + commons->server_port = 0; + server_ports = QString::fromStdString(URLParser::Parse((link.split("?")[0] + "/").toStdString()).port).split(","); + for (auto & serverPort : server_ports) { + serverPort.replace("-", ":"); + } + } + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + + if (query.hasQueryItem("upmbps")) up_mbps = query.queryItemValue("upmbps").toInt(); + if (query.hasQueryItem("downmbps")) down_mbps = query.queryItemValue("downmbps").toInt(); + if (query.hasQueryItem("obfsParam")) obfs = QUrl::fromPercentEncoding(query.queryItemValue("obfsParam").toUtf8()); + if (query.hasQueryItem("auth")) auth_str = query.queryItemValue("auth"); + if (query.hasQueryItem("recv_window_conn")) recv_window_conn = query.queryItemValue("recv_window_conn").toInt(); + if (query.hasQueryItem("recv_window")) recv_window = query.queryItemValue("recv_window").toInt(); + if (query.hasQueryItem("disable_mtu_discovery")) disable_mtu_discovery = query.queryItemValue("disable_mtu_discovery") == "true"; + if (query.hasQueryItem("hop_interval")) hop_interval = query.queryItemValue("hop_interval"); + if (query.hasQueryItem("mport")) { + server_ports = query.queryItemValue("mport").split(","); + for (auto & server_port : server_ports) { + server_port.replace("-", ":"); + } + } + + tls->ParseFromLink(link); + + if (commons->server_port == 0 && server_ports.isEmpty()) commons->server_port = 443; + + return true; + } + + bool hysteria::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "hysteria") return false; + commons->ParseFromJson(object); + if (object.contains("server_ports")) { + server_ports = QJsonArray2QListString(object["server_ports"].toArray()); + } + if (object.contains("hop_interval")) hop_interval = object["hop_interval"].toString(); + if (object.contains("up_mbps")) up_mbps = object["up_mbps"].toInt(); + if (object.contains("down_mbps")) down_mbps = object["down_mbps"].toInt(); + if (object.contains("obfs")) obfs = object["obfs"].toString(); + if (object.contains("auth")) auth = object["auth"].toString(); + if (object.contains("auth_str")) auth_str = object["auth_str"].toString(); + if (object.contains("recv_window_conn")) recv_window_conn = object["recv_window_conn"].toInt(); + if (object.contains("recv_window")) recv_window = object["recv_window"].toInt(); + if (object.contains("disable_mtu_discovery")) disable_mtu_discovery = object["disable_mtu_discovery"].toBool(); + if (object.contains("tls")) tls->ParseFromJson(object["tls"].toObject()); + return true; + } + + QString hysteria::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("hysteria"); + url.setHost(commons->server); + url.setPort(0); + + if (!server_ports.isEmpty()) { + QStringList portList; + for (const auto& port : server_ports) { + QString modified = port; + modified.replace(":", "-"); + portList.append(modified); + } + url.setPort(0); + query.addQueryItem("mport", portList.join(",")); + } else { + url.setPort(commons->server_port); + } + + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (up_mbps > 0) query.addQueryItem("upmbps", QString::number(up_mbps)); + if (down_mbps > 0) query.addQueryItem("downmbps", QString::number(down_mbps)); + if (!obfs.isEmpty()) { + query.addQueryItem("obfsParam", QUrl::toPercentEncoding(obfs)); + } + if (!auth_str.isEmpty()) query.addQueryItem("auth", auth_str); + if (recv_window_conn > 0) query.addQueryItem("recv_window_conn", QString::number(recv_window_conn)); + if (recv_window > 0) query.addQueryItem("recv_window", QString::number(recv_window)); + if (disable_mtu_discovery) query.addQueryItem("disable_mtu_discovery", "true"); + if (!hop_interval.isEmpty()) query.addQueryItem("hop_interval", hop_interval); + + mergeUrlQuery(query, tls->ExportToLink()); + mergeUrlQuery(query, commons->ExportToLink()); + + if (!query.isEmpty()) url.setQuery(query); + + QString result = url.toString(); + if (!server_ports.isEmpty()) { + result = result.replace(":0?", ":" + query.queryItemValue("mport") + "?"); + } + return result; + } + + QJsonObject hysteria::ExportToJson() + { + QJsonObject object; + object["type"] = "hysteria"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!server_ports.isEmpty()) object["server_ports"] = QListStr2QJsonArray(server_ports); + if (!hop_interval.isEmpty()) object["hop_interval"] = hop_interval; + if (up_mbps > 0) object["up_mbps"] = up_mbps; + if (down_mbps > 0) object["down_mbps"] = down_mbps; + if (!obfs.isEmpty()) object["obfs"] = obfs; + if (!auth.isEmpty()) object["auth"] = auth; + if (!auth_str.isEmpty()) object["auth_str"] = auth_str; + if (recv_window_conn > 0) object["recv_window_conn"] = recv_window_conn; + if (recv_window > 0) object["recv_window"] = recv_window; + if (disable_mtu_discovery) object["disable_mtu_discovery"] = disable_mtu_discovery; + if (tls->enabled) object["tls"] = tls->ExportToJson(); + return object; + } + + BuildResult hysteria::Build() + { + return {ExportToJson(), ""}; + } + + QString hysteria::DisplayType() + { + return "Hysteria"; + } +} diff --git a/src/configs/outbounds/hysteria2.cpp b/src/configs/outbounds/hysteria2.cpp new file mode 100644 index 0000000..6615318 --- /dev/null +++ b/src/configs/outbounds/hysteria2.cpp @@ -0,0 +1,146 @@ +#include "include/configs/outbounds/hysteria2.h" + +#include +#include +#include <3rdparty/URLParser/url_parser.h> +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool hysteria2::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) + { + if(!url.errorString().startsWith("Invalid port")) + return false; + commons->server_port = 0; + server_ports = QString::fromStdString(URLParser::Parse((link.split("?")[0] + "/").toStdString()).port).split(","); + for (auto & serverPort : server_ports) { + serverPort.replace("-", ":"); + } + } + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + + if (url.password().isEmpty()) { + password = url.userName(); + } else { + password = url.userName() + ":" + url.password(); + } + + if (query.hasQueryItem("obfs-password")) { + obfsPassword = QUrl::fromPercentEncoding(query.queryItemValue("obfs-password").toUtf8()); + } + if (query.hasQueryItem("upmbps")) up_mbps = query.queryItemValue("upmbps").toInt(); + if (query.hasQueryItem("downmbps")) down_mbps = query.queryItemValue("downmbps").toInt(); + + if (query.hasQueryItem("mport")) { + QStringList ports = query.queryItemValue("mport").split(","); + for (auto& port : ports) { + port.replace("-", ":"); + server_ports.append(port); + } + } + if (query.hasQueryItem("hop_interval")) hop_interval = query.queryItemValue("hop_interval"); + + tls->ParseFromLink(link); + + if (commons->server_port == 0 && server_ports.isEmpty()) commons->server_port = 443; + + return true; + } + + bool hysteria2::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "hysteria2") return false; + commons->ParseFromJson(object); + if (object.contains("server_ports")) { + server_ports = QJsonArray2QListString(object["server_ports"].toArray()); + } + if (object.contains("hop_interval")) hop_interval = object["hop_interval"].toString(); + if (object.contains("up_mbps")) up_mbps = object["up_mbps"].toInt(); + if (object.contains("down_mbps")) down_mbps = object["down_mbps"].toInt(); + if (object.contains("obfsType")) obfsType = object["obfsType"].toString(); + if (object.contains("obfsPassword")) obfsPassword = object["obfsPassword"].toString(); + if (object.contains("password")) password = object["password"].toString(); + if (object.contains("tls")) tls->ParseFromJson(object["tls"].toObject()); + return true; + } + + QString hysteria2::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("hy2"); + url.setHost(commons->server); + + if (password.contains(":")) { + url.setUserName(SubStrBefore(password, ":")); + url.setPassword(SubStrAfter(password, ":")); + } else { + url.setUserName(password); + } + + if (!server_ports.isEmpty()) { + QStringList portList; + for (const auto& port : server_ports) { + QString modified = port; + modified.replace(":", "-"); + portList.append(modified); + } + url.setPort(0); + query.addQueryItem("mport", portList.join(",")); + } else { + url.setPort(commons->server_port); + } + + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (!obfsPassword.isEmpty()) { + query.addQueryItem("obfs-password", QUrl::toPercentEncoding(obfsPassword)); + } + if (up_mbps > 0) query.addQueryItem("upmbps", QString::number(up_mbps)); + if (down_mbps > 0) query.addQueryItem("downmbps", QString::number(down_mbps)); + if (!hop_interval.isEmpty()) query.addQueryItem("hop_interval", hop_interval); + + mergeUrlQuery(query, tls->ExportToLink()); + mergeUrlQuery(query, commons->ExportToLink()); + + if (!query.isEmpty()) url.setQuery(query); + + QString result = url.toString(); + if (!server_ports.isEmpty()) { + result = result.replace(":0?", ":" + query.queryItemValue("mport") + "?"); + } + return result; + } + + QJsonObject hysteria2::ExportToJson() + { + QJsonObject object; + object["type"] = "hysteria2"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!server_ports.isEmpty()) object["server_ports"] = QListStr2QJsonArray(server_ports); + if (!hop_interval.isEmpty()) object["hop_interval"] = hop_interval; + if (up_mbps > 0) object["up_mbps"] = up_mbps; + if (down_mbps > 0) object["down_mbps"] = down_mbps; + if (!obfsType.isEmpty()) object["obfsType"] = obfsType; + if (!obfsPassword.isEmpty()) object["obfsPassword"] = obfsPassword; + if (!password.isEmpty()) object["password"] = password; + if (tls->enabled) object["tls"] = tls->ExportToJson(); + return object; + } + + BuildResult hysteria2::Build() + { + return {ExportToJson(), ""}; + } + + QString hysteria2::DisplayType() + { + return "Hysteria2"; + } +} diff --git a/src/configs/outbounds/shadowsocks.cpp b/src/configs/outbounds/shadowsocks.cpp new file mode 100644 index 0000000..2da81a7 --- /dev/null +++ b/src/configs/outbounds/shadowsocks.cpp @@ -0,0 +1,122 @@ +#include "include/configs/outbounds/shadowsocks.h" + +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool shadowsocks::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + + if (SubStrBefore(link, "#").contains("@")) { + // Traditional SS format + if (url.password().isEmpty()) { + // Traditional format: method:password base64 encoded in username + auto method_password = DecodeB64IfValid(url.userName(), QByteArray::Base64Option::Base64UrlEncoding); + if (method_password.isEmpty()) return false; + method = SubStrBefore(method_password, ":"); + password = SubStrAfter(method_password, ":"); + } else { + // 2022 format: method in username, password in password + method = url.userName(); + password = url.password(); + } + } else { + // v2rayN format: base64 encoded full URL + QString linkN = DecodeB64IfValid(SubStrBefore(SubStrAfter(link, "://"), "#"), QByteArray::Base64Option::Base64UrlEncoding); + if (linkN.isEmpty()) return false; + if (link.contains("#")) linkN += "#" + SubStrAfter(link, "#"); + url = QUrl("https://" + linkN); + if (!url.isValid()) return false; + query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + commons->ParseFromLink(url.toString()); + method = url.userName(); + password = url.password(); + } + + plugin = query.queryItemValue("plugin").replace("simple-obfs;", "obfs-local;"); + if (query.hasQueryItem("plugin-opts")) plugin_opts = query.queryItemValue("plugin-opts"); + if (query.hasQueryItem("uot")) uot = query.queryItemValue("uot") == "true" || query.queryItemValue("uot").toInt() > 0; + multiplex->ParseFromLink(link); + + return !(commons->server.isEmpty() || method.isEmpty() || password.isEmpty()); + } + + bool shadowsocks::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "shadowsocks") return false; + commons->ParseFromJson(object); + if (object.contains("method")) method = object["method"].toString(); + if (object.contains("password")) password = object["password"].toString(); + if (object.contains("plugin")) plugin = object["plugin"].toString(); + if (object.contains("plugin_opts")) plugin_opts = object["plugin_opts"].toString(); + if (object.contains("uot")) + { + if (object["uot"].isBool()) uot = object["uot"].toBool(); + if (object["uot"].isObject()) uot = object["uot"].toObject()["enabled"].toBool(); + } + if (object.contains("multiplex")) multiplex->ParseFromJson(object["multiplex"].toObject()); + return true; + } + + QString shadowsocks::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("ss"); + + if (method.startsWith("2022-")) { + // 2022 format: method:password directly + url.setUserName(method); + url.setPassword(password); + } else { + // Traditional format: base64 encode method:password + auto method_password = method + ":" + password; + url.setUserName(method_password.toUtf8().toBase64(QByteArray::Base64Option::Base64UrlEncoding)); + } + + url.setHost(commons->server); + url.setPort(commons->server_port); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (!plugin.isEmpty()) query.addQueryItem("plugin", plugin); + if (!plugin_opts.isEmpty()) query.addQueryItem("plugin-opts", plugin_opts); + if (uot) query.addQueryItem("uot", "true"); + + mergeUrlQuery(query, multiplex->ExportToLink()); + mergeUrlQuery(query, commons->ExportToLink()); + + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + + QJsonObject shadowsocks::ExportToJson() + { + QJsonObject object; + object["type"] = "shadowsocks"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!method.isEmpty()) object["method"] = method; + if (!password.isEmpty()) object["password"] = password; + if (!plugin.isEmpty()) object["plugin"] = plugin; + if (!plugin_opts.isEmpty()) object["plugin_opts"] = plugin_opts; + if (uot) object["uot"] = uot; + if (multiplex->enabled) object["multiplex"] = multiplex->ExportToJson(); + return object; + } + + BuildResult shadowsocks::Build() + { + return {ExportToJson(), ""}; + } + + QString shadowsocks::DisplayType() + { + return "Shadowsocks"; + } +} diff --git a/src/configs/outbounds/socks.cpp b/src/configs/outbounds/socks.cpp new file mode 100644 index 0000000..1acac98 --- /dev/null +++ b/src/configs/outbounds/socks.cpp @@ -0,0 +1,105 @@ +#include "include/configs/outbounds/socks.h" + +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool socks::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + if (query.hasQueryItem("version")) + { + version = query.queryItemValue("version").toInt(); + } else + { + if (url.scheme() == "socks4") version = 4; + if (url.scheme() == "socks5") version = 5; + } + + // Handle v2rayN format (base64 encoded username) + if (!url.password().isEmpty() || !url.userName().isEmpty()) { + username = url.userName(); + password = url.password(); + + // Check if username is base64 encoded (v2rayN format) + if (password.isEmpty() && !username.isEmpty()) { + QString decoded = DecodeB64IfValid(username); + if (!decoded.isEmpty()) { + username = SubStrBefore(decoded, ":"); + password = SubStrAfter(decoded, ":"); + } + } + } + + if (query.hasQueryItem("uot")) uot = query.queryItemValue("uot") == "true" || query.queryItemValue("uot").toInt() > 0; + + // Default port + if (commons->server_port == 0) commons->server_port = 1080; + + return !commons->server.isEmpty(); + } + + bool socks::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "socks") return false; + commons->ParseFromJson(object); + if (object.contains("username")) username = object["username"].toString(); + if (object.contains("password")) password = object["password"].toString(); + if (object.contains("version")) version = object["version"].toInt(); + if (object.contains("uot")) uot = object["uot"].toBool(); + return true; + } + + QString socks::ExportToLink() + { + QUrl url; + QUrlQuery query; + + // Determine scheme based on SOCKS version (default to socks5) + QString scheme = "socks5"; + if (version == 4) { + scheme = "socks4"; + } + url.setScheme(scheme); + + url.setHost(commons->server); + url.setPort(commons->server_port); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (!username.isEmpty()) url.setUserName(username); + if (!password.isEmpty()) url.setPassword(password); + if (uot) query.addQueryItem("uot", "1"); + + mergeUrlQuery(query, commons->ExportToLink()); + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + + QJsonObject socks::ExportToJson() + { + QJsonObject object; + object["type"] = "socks"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!username.isEmpty()) object["username"] = username; + if (!password.isEmpty()) object["password"] = password; + if (version == 4) object["version"] = "4"; + if (uot) object["uot"] = uot; + return object; + } + + BuildResult socks::Build() + { + return {ExportToJson(), ""}; + } + + QString socks::DisplayType() + { + return "Socks"; + } +} diff --git a/src/configs/outbounds/ssh.cpp b/src/configs/outbounds/ssh.cpp new file mode 100644 index 0000000..fd0521a --- /dev/null +++ b/src/configs/outbounds/ssh.cpp @@ -0,0 +1,134 @@ +#include "include/configs/outbounds/ssh.h" + +#include +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool ssh::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + + if (query.hasQueryItem("user")) user = query.queryItemValue("user"); + if (query.hasQueryItem("password")) password = query.queryItemValue("password"); + + QString privateKeyB64 = query.queryItemValue("private_key"); + if (!privateKeyB64.isEmpty()) { + private_key = QByteArray::fromBase64(privateKeyB64.toUtf8(), QByteArray::OmitTrailingEquals); + } + if (query.hasQueryItem("private_key_path")) private_key_path = query.queryItemValue("private_key_path"); + if (query.hasQueryItem("private_key_passphrase")) private_key_passphrase = query.queryItemValue("private_key_passphrase"); + + QString hostKeysRaw = query.queryItemValue("host_key"); + if (!hostKeysRaw.isEmpty()) { + for (const auto& item : hostKeysRaw.split("-")) { + auto b64hostKey = QByteArray::fromBase64(item.toUtf8(), QByteArray::OmitTrailingEquals); + if (!b64hostKey.isEmpty()) host_key.append(QString(b64hostKey)); + } + } + + QString hostKeyAlgsRaw = query.queryItemValue("host_key_algorithms"); + if (!hostKeyAlgsRaw.isEmpty()) { + for (const auto& item : hostKeyAlgsRaw.split("-")) { + auto b64hostKeyAlg = QByteArray::fromBase64(item.toUtf8(), QByteArray::OmitTrailingEquals); + if (!b64hostKeyAlg.isEmpty()) host_key_algorithms.append(QString(b64hostKeyAlg)); + } + } + + if (query.hasQueryItem("client_version")) client_version = query.queryItemValue("client_version"); + + return !commons->server.isEmpty(); + } + + bool ssh::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "ssh") return false; + commons->ParseFromJson(object); + if (object.contains("user")) user = object["user"].toString(); + if (object.contains("password")) password = object["password"].toString(); + if (object.contains("private_key")) private_key = object["private_key"].toString(); + if (object.contains("private_key_path")) private_key_path = object["private_key_path"].toString(); + if (object.contains("private_key_passphrase")) private_key_passphrase = object["private_key_passphrase"].toString(); + if (object.contains("host_key")) { + host_key = QJsonArray2QListString(object["host_key"].toArray()); + } + if (object.contains("host_key_algorithms")) { + host_key_algorithms = QJsonArray2QListString(object["host_key_algorithms"].toArray()); + } + if (object.contains("client_version")) client_version = object["client_version"].toString(); + return true; + } + + QString ssh::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("ssh"); + url.setHost(commons->server); + url.setPort(commons->server_port); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (!user.isEmpty()) query.addQueryItem("user", user); + if (!password.isEmpty()) query.addQueryItem("password", password); + if (!private_key.isEmpty()) { + query.addQueryItem("private_key", private_key.toUtf8().toBase64(QByteArray::OmitTrailingEquals)); + } + if (!private_key_path.isEmpty()) query.addQueryItem("private_key_path", private_key_path); + if (!private_key_passphrase.isEmpty()) query.addQueryItem("private_key_passphrase", private_key_passphrase); + + if (!host_key.isEmpty()) { + QStringList b64HostKeys; + for (const auto& item : host_key) { + b64HostKeys.append(item.toUtf8().toBase64(QByteArray::OmitTrailingEquals)); + } + query.addQueryItem("host_key", b64HostKeys.join("-")); + } + + if (!host_key_algorithms.isEmpty()) { + QStringList b64HostKeyAlgs; + for (const auto& item : host_key_algorithms) { + b64HostKeyAlgs.append(item.toUtf8().toBase64(QByteArray::OmitTrailingEquals)); + } + query.addQueryItem("host_key_algorithms", b64HostKeyAlgs.join("-")); + } + + if (!client_version.isEmpty()) query.addQueryItem("client_version", client_version); + + mergeUrlQuery(query, commons->ExportToLink()); + + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + + QJsonObject ssh::ExportToJson() + { + QJsonObject object; + object["type"] = "ssh"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!user.isEmpty()) object["user"] = user; + if (!password.isEmpty()) object["password"] = password; + if (!private_key.isEmpty()) object["private_key"] = private_key; + if (!private_key_path.isEmpty()) object["private_key_path"] = private_key_path; + if (!private_key_passphrase.isEmpty()) object["private_key_passphrase"] = private_key_passphrase; + if (!host_key.isEmpty()) object["host_key"] = QListStr2QJsonArray(host_key); + if (!host_key_algorithms.isEmpty()) object["host_key_algorithms"] = QListStr2QJsonArray(host_key_algorithms); + if (!client_version.isEmpty()) object["client_version"] = client_version; + return object; + } + + BuildResult ssh::Build() + { + return {ExportToJson(), ""}; + } + + QString ssh::DisplayType() + { + return "SSH"; + } +} diff --git a/src/configs/outbounds/tailscale.cpp b/src/configs/outbounds/tailscale.cpp new file mode 100644 index 0000000..cbdc7d2 --- /dev/null +++ b/src/configs/outbounds/tailscale.cpp @@ -0,0 +1,118 @@ +#include "include/configs/outbounds/tailscale.h" + +#include +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool tailscale::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + + if (query.hasQueryItem("state_directory")) state_directory = QUrl::fromPercentEncoding(query.queryItemValue("state_directory").toUtf8()); + if (query.hasQueryItem("auth_key")) auth_key = QUrl::fromPercentEncoding(query.queryItemValue("auth_key").toUtf8()); + if (query.hasQueryItem("control_url")) control_url = QUrl::fromPercentEncoding(query.queryItemValue("control_url").toUtf8()); + if (query.hasQueryItem("ephemeral")) ephemeral = query.queryItemValue("ephemeral") == "true"; + if (query.hasQueryItem("hostname")) hostname = QUrl::fromPercentEncoding(query.queryItemValue("hostname").toUtf8()); + if (query.hasQueryItem("accept_routes")) accept_routes = query.queryItemValue("accept_routes") == "true"; + if (query.hasQueryItem("exit_node")) exit_node = query.queryItemValue("exit_node"); + if (query.hasQueryItem("exit_node_allow_lan_access")) exit_node_allow_lan_access = query.queryItemValue("exit_node_allow_lan_access") == "true"; + if (query.hasQueryItem("advertise_routes")) { + advertise_routes = QUrl::fromPercentEncoding(query.queryItemValue("advertise_routes").toUtf8()).split(","); + } + if (query.hasQueryItem("advertise_exit_node")) advertise_exit_node = query.queryItemValue("advertise_exit_node") == "true"; + if (query.hasQueryItem("globalDNS")) globalDNS = query.queryItemValue("globalDNS") == "true"; + if (query.hasQueryItem("global_dns")) globalDNS = query.queryItemValue("global_dns") == "true"; + + return true; + } + + bool tailscale::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "tailscale") return false; + commons->ParseFromJson(object); + if (object.contains("state_directory")) state_directory = object["state_directory"].toString(); + if (object.contains("auth_key")) auth_key = object["auth_key"].toString(); + if (object.contains("control_url")) control_url = object["control_url"].toString(); + if (object.contains("ephemeral")) ephemeral = object["ephemeral"].toBool(); + if (object.contains("hostname")) hostname = object["hostname"].toString(); + if (object.contains("accept_routes")) accept_routes = object["accept_routes"].toBool(); + if (object.contains("exit_node")) exit_node = object["exit_node"].toString(); + if (object.contains("exit_node_allow_lan_access")) exit_node_allow_lan_access = object["exit_node_allow_lan_access"].toBool(); + if (object.contains("advertise_routes")) { + advertise_routes = QJsonArray2QListString(object["advertise_routes"].toArray()); + } + if (object.contains("advertise_exit_node")) advertise_exit_node = object["advertise_exit_node"].toBool(); + if (object.contains("globalDNS")) globalDNS = object["globalDNS"].toBool(); + return true; + } + + QString tailscale::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("ts"); + url.setHost("tailscale"); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (!state_directory.isEmpty()) query.addQueryItem("state_directory", QUrl::toPercentEncoding(state_directory)); + if (!auth_key.isEmpty()) query.addQueryItem("auth_key", QUrl::toPercentEncoding(auth_key)); + if (!control_url.isEmpty()) query.addQueryItem("control_url", QUrl::toPercentEncoding(control_url)); + if (ephemeral) query.addQueryItem("ephemeral", "true"); + if (!hostname.isEmpty()) query.addQueryItem("hostname", QUrl::toPercentEncoding(hostname)); + if (accept_routes) query.addQueryItem("accept_routes", "true"); + if (!exit_node.isEmpty()) query.addQueryItem("exit_node", exit_node); + if (exit_node_allow_lan_access) query.addQueryItem("exit_node_allow_lan_access", "true"); + if (!advertise_routes.isEmpty()) query.addQueryItem("advertise_routes", QUrl::toPercentEncoding(advertise_routes.join(","))); + if (advertise_exit_node) query.addQueryItem("advertise_exit_node", "true"); + if (globalDNS) query.addQueryItem("global_dns", "true"); + + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + + QJsonObject tailscale::ExportToJson() + { + QJsonObject object; + object["type"] = "tailscale"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!state_directory.isEmpty()) object["state_directory"] = state_directory; + if (!auth_key.isEmpty()) object["auth_key"] = auth_key; + if (!control_url.isEmpty()) object["control_url"] = control_url; + if (ephemeral) object["ephemeral"] = ephemeral; + if (!hostname.isEmpty()) object["hostname"] = hostname; + if (accept_routes) object["accept_routes"] = accept_routes; + if (!exit_node.isEmpty()) object["exit_node"] = exit_node; + if (exit_node_allow_lan_access) object["exit_node_allow_lan_access"] = exit_node_allow_lan_access; + if (!advertise_routes.isEmpty()) object["advertise_routes"] = QListStr2QJsonArray(advertise_routes); + if (advertise_exit_node) object["advertise_exit_node"] = advertise_exit_node; + if (globalDNS) object["globalDNS"] = globalDNS; + return object; + } + + BuildResult tailscale::Build() + { + return {ExportToJson(), ""}; + } + + QString tailscale::DisplayAddress() + { + return control_url; + } + + QString tailscale::DisplayType() + { + return "Tailscale"; + } + + bool tailscale::IsEndpoint() + { + return true; + } +} diff --git a/src/configs/outbounds/trojan.cpp b/src/configs/outbounds/trojan.cpp new file mode 100644 index 0000000..ce196bb --- /dev/null +++ b/src/configs/outbounds/trojan.cpp @@ -0,0 +1,68 @@ +#include "include/configs/outbounds/trojan.h" + +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool Trojan::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + + commons->ParseFromLink(link); + password = url.userName(); + tls->ParseFromLink(link); + transport->ParseFromLink(link); + multiplex->ParseFromLink(link); + return true; + } + bool Trojan::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "trojan") return false; + commons->ParseFromJson(object); + if (object.contains("password")) password = object["password"].toString(); + if (object.contains("tls")) tls->ParseFromJson(object["tls"].toObject()); + if (object.contains("transport")) transport->ParseFromJson(object["transport"].toObject()); + if (object.contains("multiplex")) multiplex->ParseFromJson(object["multiplex"].toObject()); + return true; + } + QString Trojan::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setHost(commons->server); + url.setPort(commons->server_port); + url.setScheme("trojan"); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + url.setUserName(password); + if (tls->enabled) mergeUrlQuery(query, tls->ExportToLink()); + if (!transport->type.isEmpty()) mergeUrlQuery(query, transport->ExportToLink()); + if (multiplex->enabled) mergeUrlQuery(query, multiplex->ExportToLink()); + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + QJsonObject Trojan::ExportToJson() + { + QJsonObject object; + object["type"] = "trojan"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!password.isEmpty()) object["password"] = password; + if (tls->enabled) object["tls"] = tls->ExportToJson(); + if (!transport->type.isEmpty()) object["transport"] = transport->ExportToJson(); + if (multiplex->enabled) object["multiplex"] = multiplex->ExportToJson(); + return object; + } + BuildResult Trojan::Build() + { + return {ExportToJson(), ""}; + } + + QString Trojan::DisplayType() + { + return "Trojan"; + } +} + + diff --git a/src/configs/outbounds/tuic.cpp b/src/configs/outbounds/tuic.cpp new file mode 100644 index 0000000..7c9aa87 --- /dev/null +++ b/src/configs/outbounds/tuic.cpp @@ -0,0 +1,96 @@ +#include "include/configs/outbounds/tuic.h" + +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool tuic::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + uuid = url.userName(); + password = url.password(); + + if (query.hasQueryItem("congestion_control")) congestion_control = query.queryItemValue("congestion_control"); + if (query.hasQueryItem("udp_relay_mode")) udp_relay_mode = query.queryItemValue("udp_relay_mode"); + if (query.hasQueryItem("udp_over_stream")) udp_over_stream = query.queryItemValue("udp_over_stream") == "true"; + if (query.hasQueryItem("zero_rtt_handshake")) zero_rtt_handshake = query.queryItemValue("zero_rtt_handshake") == "true"; + if (query.hasQueryItem("heartbeat")) heartbeat = query.queryItemValue("heartbeat"); + + tls->ParseFromLink(link); + + if (commons->server_port == 0) commons->server_port = 443; + + return !(uuid.isEmpty() || password.isEmpty() || commons->server.isEmpty()); + } + + bool tuic::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "tuic") return false; + commons->ParseFromJson(object); + if (object.contains("uuid")) uuid = object["uuid"].toString(); + if (object.contains("password")) password = object["password"].toString(); + if (object.contains("congestion_control")) congestion_control = object["congestion_control"].toString(); + if (object.contains("udp_relay_mode")) udp_relay_mode = object["udp_relay_mode"].toString(); + if (object.contains("udp_over_stream")) udp_over_stream = object["udp_over_stream"].toBool(); + if (object.contains("zero_rtt_handshake")) zero_rtt_handshake = object["zero_rtt_handshake"].toBool(); + if (object.contains("heartbeat")) heartbeat = object["heartbeat"].toString(); + if (object.contains("tls")) tls->ParseFromJson(object["tls"].toObject()); + return true; + } + + QString tuic::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("tuic"); + url.setUserName(uuid); + url.setPassword(password); + url.setHost(commons->server); + url.setPort(commons->server_port); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (!congestion_control.isEmpty()) query.addQueryItem("congestion_control", congestion_control); + if (!udp_relay_mode.isEmpty()) query.addQueryItem("udp_relay_mode", udp_relay_mode); + if (udp_over_stream) query.addQueryItem("udp_over_stream", "true"); + if (zero_rtt_handshake) query.addQueryItem("zero_rtt_handshake", "true"); + if (!heartbeat.isEmpty()) query.addQueryItem("heartbeat", heartbeat); + + mergeUrlQuery(query, tls->ExportToLink()); + mergeUrlQuery(query, commons->ExportToLink()); + + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + + QJsonObject tuic::ExportToJson() + { + QJsonObject object; + object["type"] = "tuic"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!uuid.isEmpty()) object["uuid"] = uuid; + if (!password.isEmpty()) object["password"] = password; + if (!congestion_control.isEmpty()) object["congestion_control"] = congestion_control; + if (!udp_relay_mode.isEmpty()) object["udp_relay_mode"] = udp_relay_mode; + if (udp_over_stream) object["udp_over_stream"] = udp_over_stream; + if (zero_rtt_handshake) object["zero_rtt_handshake"] = zero_rtt_handshake; + if (!heartbeat.isEmpty()) object["heartbeat"] = heartbeat; + if (tls->enabled) object["tls"] = tls->ExportToJson(); + return object; + } + + BuildResult tuic::Build() + { + return {ExportToJson(), ""}; + } + + QString tuic::DisplayType() + { + return "TUIC"; + } +} diff --git a/src/configs/outbounds/vless.cpp b/src/configs/outbounds/vless.cpp new file mode 100644 index 0000000..8e9e0cf --- /dev/null +++ b/src/configs/outbounds/vless.cpp @@ -0,0 +1,93 @@ +#include "include/configs/outbounds/vless.h" + +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool vless::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + uuid = url.userName(); + if (commons->server_port == 0) commons->server_port = 443; + + flow = GetQueryValue(query, "flow", ""); + + transport->ParseFromLink(link); + + tls->ParseFromLink(link); + if (!tls->server_name.isEmpty()) { + tls->enabled = true; + } + + packet_encoding = GetQueryValue(query, "packetEncoding", ""); + multiplex->ParseFromLink(link); + + return !(uuid.isEmpty() || commons->server.isEmpty()); + } + + bool vless::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "vless") return false; + commons->ParseFromJson(object); + if (object.contains("uuid")) uuid = object["uuid"].toString(); + if (object.contains("flow")) flow = object["flow"].toString(); + if (object.contains("packet_encoding")) packet_encoding = object["packet_encoding"].toString(); + if (object.contains("tls")) tls->ParseFromJson(object["tls"].toObject()); + if (object.contains("transport")) transport->ParseFromJson(object["transport"].toObject()); + if (object.contains("multiplex")) multiplex->ParseFromJson(object["multiplex"].toObject()); + return true; + } + + QString vless::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("vless"); + url.setUserName(uuid); + url.setHost(commons->server); + url.setPort(commons->server_port); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + query.addQueryItem("encryption", "none"); + if (!flow.isEmpty()) query.addQueryItem("flow", flow); + + mergeUrlQuery(query, tls->ExportToLink()); + mergeUrlQuery(query, transport->ExportToLink()); + mergeUrlQuery(query, multiplex->ExportToLink()); + + if (!packet_encoding.isEmpty()) query.addQueryItem("packetEncoding", packet_encoding); + + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + + QJsonObject vless::ExportToJson() + { + QJsonObject object; + object["type"] = "vless"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!uuid.isEmpty()) object["uuid"] = uuid; + if (!flow.isEmpty()) object["flow"] = flow; + if (!packet_encoding.isEmpty()) object["packet_encoding"] = packet_encoding; + if (tls->enabled) object["tls"] = tls->ExportToJson(); + if (!transport->type.isEmpty()) object["transport"] = transport->ExportToJson(); + if (multiplex->enabled) object["multiplex"] = multiplex->ExportToJson(); + return object; + } + + BuildResult vless::Build() + { + return {ExportToJson(), ""}; + } + + QString vless::DisplayType() + { + return "VLESS"; + } +} diff --git a/src/configs/outbounds/vmess.cpp b/src/configs/outbounds/vmess.cpp new file mode 100644 index 0000000..36c52a0 --- /dev/null +++ b/src/configs/outbounds/vmess.cpp @@ -0,0 +1,138 @@ +#include "include/configs/outbounds/vmess.h" + +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool vmess::ParseFromLink(const QString& link) + { + // Try V2RayN format first (base64 encoded JSON) + QString linkN = DecodeB64IfValid(SubStrAfter(link, "vmess://")); + if (!linkN.isEmpty()) { + auto objN = QString2QJsonObject(linkN); + if (!objN.isEmpty()) { + uuid = objN["id"].toString(); + commons->server = objN["add"].toString(); + commons->server_port = objN["port"].toVariant().toInt(); + commons->name = objN["ps"].toString(); + alter_id = objN["aid"].toVariant().toInt(); + + QString net = objN["net"].toString(); + if (net == "h2") net = "http"; + transport->type = net; + transport->host = objN["host"].toString(); + transport->path = objN["path"].toString(); + + QString scy = objN["scy"].toString(); + if (!scy.isEmpty()) security = scy; + + QString tlsStr = objN["tls"].toString(); + if (tlsStr == "tls") { + tls->enabled = true; + tls->server_name = objN["sni"].toString(); + } + + return !(uuid.isEmpty() || commons->server.isEmpty()); + } + } + + // Standard VMess URL format + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + uuid = url.userName(); + if (commons->server_port == 0) commons->server_port = 443; + + security = GetQueryValue(query, "encryption", "auto"); + + transport->ParseFromLink(link); + + tls->ParseFromLink(link); + if (!tls->server_name.isEmpty()) { + tls->enabled = true; + } + + multiplex->ParseFromLink(link); + + if (query.hasQueryItem("alterId")) alter_id = query.queryItemValue("alterId").toInt(); + if (query.hasQueryItem("globalPadding")) global_padding = query.queryItemValue("globalPadding") == "true"; + if (query.hasQueryItem("authenticatedLength")) authenticated_length = query.queryItemValue("authenticatedLength") == "true"; + if (query.hasQueryItem("packetEncoding")) packet_encoding = query.queryItemValue("packetEncoding"); + + return !(uuid.isEmpty() || commons->server.isEmpty()); + } + + bool vmess::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "vmess") return false; + commons->ParseFromJson(object); + if (object.contains("uuid")) uuid = object["uuid"].toString(); + if (object.contains("security")) security = object["security"].toString(); + if (object.contains("alter_id")) alter_id = object["alter_id"].toInt(); + if (object.contains("alter-id")) alter_id = object["alter-id"].toInt(); + if (object.contains("global_padding")) global_padding = object["global_padding"].toBool(); + if (object.contains("global-padding")) global_padding = object["global-padding"].toBool(); + if (object.contains("authenticated_length")) authenticated_length = object["authenticated_length"].toBool(); + if (object.contains("packet_encoding")) packet_encoding = object["packet_encoding"].toString(); + if (object.contains("tls")) tls->ParseFromJson(object["tls"].toObject()); + if (object.contains("transport")) transport->ParseFromJson(object["transport"].toObject()); + if (object.contains("multiplex")) multiplex->ParseFromJson(object["multiplex"].toObject()); + return true; + } + + QString vmess::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("vmess"); + url.setUserName(uuid); + url.setHost(commons->server); + url.setPort(commons->server_port); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (security != "auto") query.addQueryItem("encryption", security); + + mergeUrlQuery(query, tls->ExportToLink()); + mergeUrlQuery(query, transport->ExportToLink()); + mergeUrlQuery(query, multiplex->ExportToLink()); + + if (alter_id > 0) query.addQueryItem("alterId", QString::number(alter_id)); + if (global_padding) query.addQueryItem("globalPadding", "true"); + if (authenticated_length) query.addQueryItem("authenticatedLength", "true"); + if (!packet_encoding.isEmpty()) query.addQueryItem("packetEncoding", packet_encoding); + + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + + QJsonObject vmess::ExportToJson() + { + QJsonObject object; + object["type"] = "vmess"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!uuid.isEmpty()) object["uuid"] = uuid; + if (security != "auto") object["security"] = security; + if (alter_id > 0) object["alter_id"] = alter_id; + if (global_padding) object["global_padding"] = global_padding; + if (authenticated_length) object["authenticated_length"] = authenticated_length; + if (!packet_encoding.isEmpty()) object["packet_encoding"] = packet_encoding; + if (tls->enabled) object["tls"] = tls->ExportToJson(); + if (!transport->type.isEmpty()) object["transport"] = transport->ExportToJson(); + if (multiplex->enabled) object["multiplex"] = multiplex->ExportToJson(); + return object; + } + + BuildResult vmess::Build() + { + return {ExportToJson(), ""}; + } + + QString vmess::DisplayType() + { + return "VMess"; + } +} diff --git a/src/configs/outbounds/wireguard.cpp b/src/configs/outbounds/wireguard.cpp new file mode 100644 index 0000000..a0d3fff --- /dev/null +++ b/src/configs/outbounds/wireguard.cpp @@ -0,0 +1,254 @@ +#include "include/configs/outbounds/wireguard.h" + +#include +#include +#include + +#include "include/configs/common/utils.h" + +namespace Configs { + bool Peer::ParseFromLink(const QString& link) + { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + if (query.hasQueryItem("address")) address = query.queryItemValue("address"); + if (query.hasQueryItem("port")) port = query.queryItemValue("port").toInt(); + if (query.hasQueryItem("public_key")) public_key = query.queryItemValue("public_key"); + if (query.hasQueryItem("peer_public_key")) public_key = query.queryItemValue("peer_public_key"); + if (query.hasQueryItem("pre_shared_key")) pre_shared_key = query.queryItemValue("pre_shared_key"); + if (query.hasQueryItem("reserved")) { + QString rawReserved = query.queryItemValue("reserved"); + if (!rawReserved.isEmpty()) { + for (const auto& item : rawReserved.split("-")) { + int val = item.toInt(); + if (val > 0) reserved.append(val); + } + } + } + if (query.hasQueryItem("persistent_keepalive")) persistent_keepalive = query.queryItemValue("persistent_keepalive").toInt(); + + return true; + } + + bool Peer::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty()) return false; + if (object.contains("address")) address = object["address"].toString(); + if (object.contains("port")) port = object["port"].toInt(); + if (object.contains("public_key")) public_key = object["public_key"].toString(); + if (object.contains("pre_shared_key")) pre_shared_key = object["pre_shared_key"].toString(); + if (object.contains("reserved")) { + reserved = QJsonArray2QListInt(object["reserved"].toArray()); + } + if (object.contains("persistent_keepalive")) persistent_keepalive = object["persistent_keepalive"].toInt(); + return true; + } + + QString Peer::ExportToLink() + { + QUrlQuery query; + if (!address.isEmpty()) query.addQueryItem("address", address); + if (port > 0) query.addQueryItem("port", QString::number(port)); + if (!public_key.isEmpty()) query.addQueryItem("public_key", public_key); + if (!pre_shared_key.isEmpty()) query.addQueryItem("pre_shared_key", pre_shared_key); + if (!reserved.isEmpty()) { + QStringList reservedStr; + for (auto val : reserved) { + reservedStr.append(QString::number(val)); + } + query.addQueryItem("reserved", reservedStr.join("-")); + } + if (persistent_keepalive > 0) query.addQueryItem("persistent_keepalive", QString::number(persistent_keepalive)); + return query.toString(); + } + + QJsonObject Peer::ExportToJson() + { + QJsonObject object; + if (!address.isEmpty()) object["address"] = address; + if (port > 0) object["port"] = port; + if (!public_key.isEmpty()) object["public_key"] = public_key; + if (!pre_shared_key.isEmpty()) object["pre_shared_key"] = pre_shared_key; + if (!reserved.isEmpty()) object["reserved"] = QListInt2QJsonArray(reserved); + if (persistent_keepalive > 0) object["persistent_keepalive"] = persistent_keepalive; + return object; + } + + BuildResult Peer::Build() + { + return {ExportToJson(), ""}; + } + + bool wireguard::ParseFromLink(const QString& link) + { + // Try WireGuard config file format first + if (link.contains("[Interface]") && link.contains("[Peer]")) { + auto lines = link.split("\n"); + for (const auto& line : lines) { + QString trimmed = line.trimmed(); + if (trimmed.isEmpty()) continue; + if (trimmed == "[Peer]" || trimmed == "[Interface]") { + continue; + } + if (!trimmed.contains("=")) continue; + auto eqIdx = trimmed.indexOf("="); + QString key = trimmed.left(eqIdx).trimmed(); + QString value = trimmed.mid(eqIdx + 1).trimmed(); + + if (key == "PrivateKey") private_key = value; + if (key == "Address") address = value.split(","); + if (key == "MTU") mtu = value.toInt(); + if (key == "PublicKey") peer->public_key = value; + if (key == "PresharedKey") peer->pre_shared_key = value; + if (key == "PersistentKeepalive") peer->persistent_keepalive = value.toInt(); + if (key == "Endpoint") { + QStringList parts = value.split(":"); + if (parts.size() >= 2) { + peer->address = parts[0].trimmed(); + peer->port = parts.last().trimmed().toInt(); + commons->server = peer->address; + commons->server_port = peer->port; + } + } + if (key == "S1") enable_amnezia = true, init_packet_junk_size = value.toInt(); + if (key == "S2") enable_amnezia = true, response_packet_junk_size = value.toInt(); + if (key == "Jc") enable_amnezia = true, junk_packet_count = value.toInt(); + if (key == "Jmin") enable_amnezia = true, junk_packet_min_size = value.toInt(); + if (key == "Jmax") enable_amnezia = true, junk_packet_max_size = value.toInt(); + if (key == "H1") enable_amnezia = true, init_packet_magic_header = value.toInt(); + if (key == "H2") enable_amnezia = true, response_packet_magic_header = value.toInt(); + if (key == "H3") enable_amnezia = true, underload_packet_magic_header = value.toInt(); + if (key == "H4") enable_amnezia = true, transport_packet_magic_header = value.toInt(); + } + return !private_key.isEmpty() && !peer->public_key.isEmpty(); + } + + // Standard wg:// URL format + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = QUrlQuery(url.query(QUrl::ComponentFormattingOption::FullyDecoded)); + + commons->ParseFromLink(link); + + if (query.hasQueryItem("private_key")) private_key = query.queryItemValue("private_key"); + peer->ParseFromLink(link); + + QString rawLocalAddr = query.queryItemValue("local_address"); + if (!rawLocalAddr.isEmpty()) { + address = rawLocalAddr.split("-"); + } + + if (query.hasQueryItem("mtu")) mtu = query.queryItemValue("mtu").toInt(); + if (query.hasQueryItem("use_system_interface")) system = query.queryItemValue("use_system_interface") == "true"; + if (query.hasQueryItem("workers")) worker_count = query.queryItemValue("workers").toInt(); + if (query.hasQueryItem("udp_timeout")) udp_timeout = query.queryItemValue("udp_timeout"); + + enable_amnezia = query.queryItemValue("enable_amnezia") == "true"; + if (enable_amnezia) { + if (query.hasQueryItem("junk_packet_count")) junk_packet_count = query.queryItemValue("junk_packet_count").toInt(); + if (query.hasQueryItem("junk_packet_min_size")) junk_packet_min_size = query.queryItemValue("junk_packet_min_size").toInt(); + if (query.hasQueryItem("junk_packet_max_size")) junk_packet_max_size = query.queryItemValue("junk_packet_max_size").toInt(); + if (query.hasQueryItem("init_packet_junk_size")) init_packet_junk_size = query.queryItemValue("init_packet_junk_size").toInt(); + if (query.hasQueryItem("response_packet_junk_size")) response_packet_junk_size = query.queryItemValue("response_packet_junk_size").toInt(); + if (query.hasQueryItem("init_packet_magic_header")) init_packet_magic_header = query.queryItemValue("init_packet_magic_header").toInt(); + if (query.hasQueryItem("response_packet_magic_header")) response_packet_magic_header = query.queryItemValue("response_packet_magic_header").toInt(); + if (query.hasQueryItem("underload_packet_magic_header")) underload_packet_magic_header = query.queryItemValue("underload_packet_magic_header").toInt(); + if (query.hasQueryItem("transport_packet_magic_header")) transport_packet_magic_header = query.queryItemValue("transport_packet_magic_header").toInt(); + } + + return !(private_key.isEmpty() || peer->public_key.isEmpty() || commons->server.isEmpty()); + } + + bool wireguard::ParseFromJson(const QJsonObject& object) + { + if (object.isEmpty() || object["type"].toString() != "wireguard") return false; + commons->ParseFromJson(object); + if (object.contains("private_key")) private_key = object["private_key"].toString(); + if (object.contains("peer") && object["peer"].isObject()) peer->ParseFromJson(object["peer"].toObject()); + if (object.contains("address")) address = QJsonArray2QListString(object["address"].toArray()); + if (object.contains("mtu")) mtu = object["mtu"].toInt(); + if (object.contains("system")) system = object["system"].toBool(); + if (object.contains("worker_count")) worker_count = object["worker_count"].toInt(); + if (object.contains("udp_timeout")) udp_timeout = object["udp_timeout"].toString(); + return true; + } + + QString wireguard::ExportToLink() + { + QUrl url; + QUrlQuery query; + url.setScheme("wg"); + url.setHost(peer->address); + url.setPort(peer->port); + if (!commons->name.isEmpty()) url.setFragment(commons->name); + + if (!private_key.isEmpty()) query.addQueryItem("private_key", private_key); + + if (!address.isEmpty()) query.addQueryItem("local_address", address.join("-")); + if (mtu > 0 && mtu != 1420) query.addQueryItem("mtu", QString::number(mtu)); + if (system) query.addQueryItem("use_system_interface", "true"); + if (worker_count > 0) query.addQueryItem("workers", QString::number(worker_count)); + if (!udp_timeout.isEmpty()) query.addQueryItem("udp_timeout", udp_timeout); + + if (enable_amnezia) { + query.addQueryItem("enable_amnezia", "true"); + if (junk_packet_count > 0) query.addQueryItem("junk_packet_count", QString::number(junk_packet_count)); + if (junk_packet_min_size > 0) query.addQueryItem("junk_packet_min_size", QString::number(junk_packet_min_size)); + if (junk_packet_max_size > 0) query.addQueryItem("junk_packet_max_size", QString::number(junk_packet_max_size)); + if (init_packet_junk_size > 0) query.addQueryItem("init_packet_junk_size", QString::number(init_packet_junk_size)); + if (response_packet_junk_size > 0) query.addQueryItem("response_packet_junk_size", QString::number(response_packet_junk_size)); + if (init_packet_magic_header > 0) query.addQueryItem("init_packet_magic_header", QString::number(init_packet_magic_header)); + if (response_packet_magic_header > 0) query.addQueryItem("response_packet_magic_header", QString::number(response_packet_magic_header)); + if (underload_packet_magic_header > 0) query.addQueryItem("underload_packet_magic_header", QString::number(underload_packet_magic_header)); + if (transport_packet_magic_header > 0) query.addQueryItem("transport_packet_magic_header", QString::number(transport_packet_magic_header)); + } + + mergeUrlQuery(query, commons->ExportToLink()); + mergeUrlQuery(query, peer->ExportToLink()); + + if (!query.isEmpty()) url.setQuery(query); + return url.toString(); + } + + QJsonObject wireguard::ExportToJson() + { + QJsonObject object; + object["type"] = "wireguard"; + mergeJsonObjects(object, commons->ExportToJson()); + if (!private_key.isEmpty()) object["private_key"] = private_key; + if (!address.isEmpty()) object["address"] = QListStr2QJsonArray(address); + if (mtu > 0) object["mtu"] = mtu; + if (system) object["system"] = system; + if (worker_count > 0) object["worker_count"] = worker_count; + if (!udp_timeout.isEmpty()) object["udp_timeout"] = udp_timeout; + + auto peerObj = peer->ExportToJson(); + if (!peerObj.isEmpty()) { + object["peer"] = QJsonArray({peerObj}); + } + + return object; + } + + BuildResult wireguard::Build() + { + return {ExportToJson(), ""}; + } + + QString wireguard::DisplayAddress() + { + return ::DisplayAddress(peer->address, peer->port > 0); + } + + QString wireguard::DisplayType() + { + return "WireGuard"; + } + + bool wireguard::IsEndpoint() + { + return true; + } +} diff --git a/src/configs/proxy/Link2Bean.cpp b/src/configs/proxy/Link2Bean.cpp index a922485..dbe1f02 100644 --- a/src/configs/proxy/Link2Bean.cpp +++ b/src/configs/proxy/Link2Bean.cpp @@ -308,8 +308,8 @@ namespace Configs { return false; serverPort = 0; serverPorts = QString::fromStdString(URLParser::Parse((link.split("?")[0] + "/").toStdString()).port).split(","); - for (int i=0; i < serverPorts.size(); i++) { - serverPorts[i].replace("-", ":"); + for (auto & serverPort : serverPorts) { + serverPort.replace("-", ":"); } } auto query = QUrlQuery(url.query());