diff --git a/inbound.cpp b/inbound.cpp new file mode 100644 index 0000000..35eeb53 --- /dev/null +++ b/inbound.cpp @@ -0,0 +1,38 @@ +#include +#include +#include +#include "3rd/QJsonStruct/QJsonIO.hpp" +#include "transfer.h" +#include "serialization.h" + +namespace amnezia::serialization::inbounds +{ + +//"inbounds": [ +// { +// "listen": "127.0.0.1", +// "port": 10808, +// "protocol": "socks", +// "settings": { +// "udp": true +// } +// } +//], + +const static QString listen = "127.0.0.1"; +const static int port = 10808; +const static QString protocol = "socks"; + +QJsonObject GenerateInboundEntry() +{ + QJsonObject root; + QJsonIO::SetValue(root, listen, "listen"); + QJsonIO::SetValue(root, port, "port"); + QJsonIO::SetValue(root, protocol, "protocol"); + QJsonIO::SetValue(root, true, "settings", "udp"); + return root; +} + + +} // namespace amnezia::serialization::inbounds + diff --git a/outbound.cpp b/outbound.cpp new file mode 100644 index 0000000..283f0b4 --- /dev/null +++ b/outbound.cpp @@ -0,0 +1,122 @@ +// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++. +// This file is part of the Qv2ray VPN client. +// +// Qv2ray, A Qt frontend for V2Ray. Written in C++ + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Qv2ray VPN client. +// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + +#include +#include +#include +#include "3rd/QJsonStruct/QJsonIO.hpp" +#include "transfer.h" +#include "serialization.h" + +namespace amnezia::serialization::outbounds +{ +QJsonObject GenerateFreedomOUT(const QString &domainStrategy, const QString &redirect) +{ + QJsonObject root; + JADD(domainStrategy, redirect) + return root; +} + +QJsonObject GenerateBlackHoleOUT(bool useHTTP) +{ + QJsonObject root; + QJsonObject resp; + resp.insert("type", useHTTP ? "http" : "none"); + root.insert("response", resp); + return root; +} + +QJsonObject GenerateShadowSocksServerOUT(const QString &address, int port, const QString &method, const QString &password) +{ + QJsonObject root; + JADD(address, port, method, password) + return root; +} + +QJsonObject GenerateShadowSocksOUT(const QList &_servers) +{ + QJsonObject root; + QJsonArray x; + + for (const auto &server : _servers) + { + x.append(GenerateShadowSocksServerOUT(server.address, server.port, server.method, server.password)); + } + + root.insert("servers", x); + return root; +} + +QJsonObject GenerateHTTPSOCKSOut(const QString &addr, int port, bool useAuth, const QString &username, const QString &password) +{ + QJsonObject root; + QJsonIO::SetValue(root, addr, "servers", 0, "address"); + QJsonIO::SetValue(root, port, "servers", 0, "port"); + if (useAuth) + { + QJsonIO::SetValue(root, username, "servers", 0, "users", 0, "user"); + QJsonIO::SetValue(root, password, "servers", 0, "users", 0, "pass"); + } + return root; +} + +QJsonObject GenerateOutboundEntry(const QString &tag, const QString &protocol, const QJsonObject &settings, const QJsonObject &streamSettings, + const QJsonObject &mux, const QString &sendThrough) +{ + QJsonObject root; + JADD(sendThrough, protocol, settings, tag, streamSettings, mux) + return root; +} + +QJsonObject GenerateTrojanOUT(const QList &_servers) +{ + QJsonObject root; + QJsonArray x; + + for (const auto &server : _servers) + { + x.append(GenerateTrojanServerOUT(server.address, server.port, server.password)); + } + + root.insert("servers", x); + return root; +} + +QJsonObject GenerateTrojanServerOUT(const QString &address, int port, const QString &password) +{ + QJsonObject root; + JADD(address, port, password) + return root; +} + +} // namespace amnezia::serialization::outbounds + diff --git a/serialization.h b/serialization.h new file mode 100644 index 0000000..4d3f700 --- /dev/null +++ b/serialization.h @@ -0,0 +1,66 @@ +#ifndef SERIALIZATION_H +#define SERIALIZATION_H + +#include +#include "transfer.h" + +namespace amnezia::serialization +{ + namespace vmess + { + QJsonObject Deserialize(const QString &vmess, QString *alias, QString *errMessage); + const QString Serialize(const StreamSettingsObject &transfer, const VMessServerObject &server, const QString &alias); + } // namespace vmess + + namespace vmess_new + { + QJsonObject Deserialize(const QString &vmess, QString *alias, QString *errMessage); + const QString Serialize(const StreamSettingsObject &transfer, const VMessServerObject &server, const QString &alias); + } // namespace vmess_new + + namespace vless + { + QJsonObject Deserialize(const QString &vless, QString *alias, QString *errMessage); + } // namespace vless + + namespace ss + { + QJsonObject Deserialize(const QString &ss, QString *alias, QString *errMessage); + const QString Serialize(const ShadowSocksServerObject &server, const QString &alias, bool isSip002); + } // namespace ss + + namespace ssd + { + QList> Deserialize(const QString &uri, QString *groupName, QStringList *logList); + } // namespace ssd + + namespace trojan + { + QJsonObject Deserialize(const QString &trojan, QString *alias, QString *errMessage); + const QString Serialize(const TrojanObject &server, const QString &alias); + } // namespace trojan + + namespace outbounds + { + QJsonObject GenerateFreedomOUT(const QString &domainStrategy, const QString &redirect); + QJsonObject GenerateBlackHoleOUT(bool useHTTP); + QJsonObject GenerateShadowSocksOUT(const QList &servers); + QJsonObject GenerateShadowSocksServerOUT(const QString &address, int port, const QString &method, const QString &password); + QJsonObject GenerateHTTPSOCKSOut(const QString &address, int port, bool useAuth, const QString &username, const QString &password); + QJsonObject GenerateTrojanOUT(const QList &servers); + QJsonObject GenerateTrojanServerOUT(const QString &address, int port, const QString &password); + QJsonObject GenerateOutboundEntry(const QString &tag, // + const QString &protocol, // + const QJsonObject &settings, // + const QJsonObject &streamSettings, // + const QJsonObject &mux = {}, // + const QString &sendThrough = "0.0.0.0"); + } // namespace outbounds + + namespace inbounds + { + QJsonObject GenerateInboundEntry(); + } +} + +#endif // SERIALIZATION_H diff --git a/ss.cpp b/ss.cpp new file mode 100644 index 0000000..84c9f3c --- /dev/null +++ b/ss.cpp @@ -0,0 +1,142 @@ +// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++. +// This file is part of the Qv2ray VPN client. +// +// Qv2ray, A Qt frontend for V2Ray. Written in C++ + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Qv2ray VPN client. +// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + +#include "3rd/QJsonStruct/QJsonIO.hpp" +#include "3rd/QJsonStruct/QJsonStruct.hpp" +#include "utilities.h" +#include "serialization.h" + +#define OUTBOUND_TAG_PROXY "PROXY" +#define JADD(...) FOR_EACH(JADDEx, __VA_ARGS__) + +namespace amnezia::serialization::ss +{ +QJsonObject Deserialize(const QString &ssUri, QString *alias, QString *errMessage) +{ + ShadowSocksServerObject server; + QString d_name; + + // auto ssUri = _ssUri.toStdString(); + if (ssUri.length() < 5) + { + *errMessage = QObject::tr("SS URI is too short"); + } + + auto uri = ssUri.mid(5); + auto hashPos = uri.lastIndexOf("#"); + + if (hashPos >= 0) + { + // Get the name/remark + d_name = uri.mid(uri.lastIndexOf("#") + 1); + uri.truncate(hashPos); + } + + auto atPos = uri.indexOf('@'); + + if (atPos < 0) + { + // Old URI scheme + QString decoded = QByteArray::fromBase64(uri.toUtf8(), QByteArray::Base64Option::OmitTrailingEquals); + auto colonPos = decoded.indexOf(':'); + + if (colonPos < 0) + { + *errMessage = QObject::tr("Can't find the colon separator between method and password"); + } + + server.method = decoded.left(colonPos); + decoded.remove(0, colonPos + 1); + atPos = decoded.lastIndexOf('@'); + + if (atPos < 0) + { + *errMessage = QObject::tr("Can't find the at separator between password and hostname"); + } + + server.password = decoded.mid(0, atPos); + decoded.remove(0, atPos + 1); + colonPos = decoded.lastIndexOf(':'); + + if (colonPos < 0) + { + *errMessage = QObject::tr("Can't find the colon separator between hostname and port"); + } + + server.address = decoded.mid(0, colonPos); + server.port = decoded.mid(colonPos + 1).toInt(); + } + else + { + // SIP002 URI scheme + auto x = QUrl::fromUserInput(uri); + server.address = x.host(); + server.port = x.port(); + const auto userInfo = Utils::SafeBase64Decode(x.userName()); + const auto userInfoSp = userInfo.indexOf(':'); + + if (userInfoSp < 0) + { + *errMessage = QObject::tr("Can't find the colon separator between method and password"); + return QJsonObject{}; + } + + const auto method = userInfo.mid(0, userInfoSp); + server.method = method; + server.password = userInfo.mid(userInfoSp + 1); + } + + d_name = QUrl::fromPercentEncoding(d_name.toUtf8()); + QJsonObject root; + QJsonArray outbounds; + outbounds.append(outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "shadowsocks", outbounds::GenerateShadowSocksOUT({ server }), {})); + JADD(outbounds) + QJsonObject inbound = inbounds::GenerateInboundEntry(); + root["inbounds"] = QJsonArray{ inbound }; + *alias = alias->isEmpty() ? d_name : *alias + "_" + d_name; + return root; +} + +const QString Serialize(const ShadowSocksServerObject &server, const QString &alias, bool) +{ + QUrl url; + const auto plainUserInfo = server.method + ":" + server.password; + const auto userinfo = plainUserInfo.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + url.setUserInfo(userinfo); + url.setScheme("ss"); + url.setHost(server.address); + url.setPort(server.port); + url.setFragment(alias); + return url.toString(QUrl::ComponentFormattingOption::FullyEncoded); +} +} // namespace amnezia::serialization::ss + diff --git a/ssd.cpp b/ssd.cpp new file mode 100644 index 0000000..2d8adcc --- /dev/null +++ b/ssd.cpp @@ -0,0 +1,244 @@ +// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++. +// This file is part of the Qv2ray VPN client. +// +// Qv2ray, A Qt frontend for V2Ray. Written in C++ + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Qv2ray VPN client. +// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + +/** + * A Naive SSD Decoder for Qv2ray + * + * @author DuckSoft + * @copyright Licensed under GPLv3. + */ + +#include "3rd/QJsonStruct/QJsonIO.hpp" +#include "3rd/QJsonStruct/QJsonStruct.hpp" +#include "utilities.h" +#include "serialization.h" + +const inline QString QV2RAY_SSD_DEFAULT_NAME_PATTERN = "%1 - %2 (rate %3)"; +#define OUTBOUND_TAG_PROXY "PROXY" + +namespace amnezia::serialization::ssd +{ + // These below are super strict checking schemes, but necessary. +#define MUST_EXIST(fieldName) \ +if (!obj.contains((fieldName)) || obj[(fieldName)].isUndefined() || obj[(fieldName)].isNull()) \ + { \ + *logList << QObject::tr("Invalid ssd link: json: field %1 must exist").arg(fieldName); \ + return {}; \ + } +#define MUST_PORT(fieldName) \ +MUST_EXIST(fieldName); \ +if (int value = obj[(fieldName)].toInt(-1); value < 0 || value > 65535) \ + { \ + *logList << QObject::tr("Invalid ssd link: json: field %1 must be valid port number"); \ + return {}; \ + } +#define MUST_STRING(fieldName) \ +MUST_EXIST(fieldName); \ +if (!obj[(fieldName)].isString()) \ + { \ + *logList << QObject::tr("Invalid ssd link: json: field %1 must be of type 'string'").arg(fieldName); \ + return {}; \ + } +#define MUST_ARRAY(fieldName) \ +MUST_EXIST(fieldName); \ +if (!obj[(fieldName)].isArray()) \ + { \ + *logList << QObject::tr("Invalid ssd link: json: field %1 must be an array").arg(fieldName); \ + return {}; \ + } + +#define SERVER_SHOULD_BE_OBJECT(server) \ +if (!server.isObject()) \ + { \ + *logList << QObject::tr("Skipping invalid ssd server: server must be an object"); \ + continue; \ + } +#define SHOULD_EXIST(fieldName) \ +if (serverObject[(fieldName)].isUndefined()) \ + { \ + *logList << QObject::tr("Skipping invalid ssd server: missing required field %1").arg(fieldName); \ + continue; \ + } +#define SHOULD_STRING(fieldName) \ +SHOULD_EXIST(fieldName); \ +if (!serverObject[(fieldName)].isString()) \ + { \ + *logList << QObject::tr("Skipping invalid ssd server: field %1 should be of type 'string'").arg(fieldName); \ + continue; \ + } + +QList> Deserialize(const QString &uri, QString *groupName, QStringList *logList) +{ + // ssd links should begin with "ssd://" + if (!uri.startsWith("ssd://")) + { + *logList << QObject::tr("Invalid ssd link: should begin with ssd://"); + return {}; + } + + // decode base64 + const auto ssdURIBody = uri.mid(6, uri.length() - 6); //(&uri, 6, uri.length() - 6); + const auto decodedJSON = Utils::SafeBase64Decode(ssdURIBody).toUtf8(); + + if (decodedJSON.length() == 0) + { + *logList << QObject::tr("Invalid ssd link: base64 parse failed"); + return {}; + } + + const auto decodeError = Utils::VerifyJsonString(decodedJSON); + if (!decodeError.isEmpty()) + { + *logList << QObject::tr("Invalid ssd link: json parse failed"); + return {}; + } + + // casting to object + const auto obj = Utils::JsonFromString(decodedJSON); + + // obj.airport + MUST_STRING("airport"); + *groupName = obj["airport"].toString(); + + // obj.port + MUST_PORT("port"); + const int port = obj["port"].toInt(); + + // obj.encryption + MUST_STRING("encryption"); + const auto encryption = obj["encryption"].toString(); + + // check: rc4-md5 is not supported by v2ray-core + // TODO: more checks, including all algorithms + if (encryption.toLower() == "rc4-md5") + { + *logList << QObject::tr("Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core"); + return {}; + } + + // obj.password + MUST_STRING("password"); + const auto password = obj["password"].toString(); + // obj.servers + MUST_ARRAY("servers"); + // + QList> serverList; + // + + // iterate through the servers + for (const auto &server : obj["servers"].toArray()) + { + SERVER_SHOULD_BE_OBJECT(server); + const auto serverObject = server.toObject(); + ShadowSocksServerObject ssObject; + + // encryption + ssObject.method = encryption; + + // password + ssObject.password = password; + + // address :-> "server" + SHOULD_STRING("server"); + const auto serverAddress = serverObject["server"].toString(); + ssObject.address = serverAddress; + + // port selection: + // normal: use global settings + // overriding: use current config + if (serverObject["port"].isUndefined()) + { + ssObject.port = port; + } + else if (auto currPort = serverObject["port"].toInt(-1); (currPort >= 0 && currPort <= 65535)) + { + ssObject.port = currPort; + } + else + { + ssObject.port = port; + } + + // name decision: + // untitled: using server:port as name + // entitled: using given name + QString nodeName; + if (serverObject["remarks"].isUndefined()) + { + nodeName = QString("%1:%2").arg(ssObject.address).arg(ssObject.port); + } + else if (serverObject["remarks"].isString()) + { + nodeName = serverObject["remarks"].toString(); + } + else + { + nodeName = QString("%1:%2").arg(ssObject.address).arg(ssObject.port); + } + + // ratio decision: + // unspecified: ratio = 1 + // specified: use given value + double ratio = 1.0; + if (auto currRatio = serverObject["ratio"].toDouble(-1.0); currRatio != -1.0) + { + ratio = currRatio; + } + // else if (!serverObject["ratio"].isUndefined()) + // { + // //*logList << QObject::tr("Invalid ratio encountered. using fallback value."); + // } + + // format the total name of the node. + const auto finalName = QV2RAY_SSD_DEFAULT_NAME_PATTERN.arg(*groupName, nodeName).arg(ratio); + // appending to the total list + QJsonObject root; + QJsonArray outbounds; + QJsonObject inbound = inbounds::GenerateInboundEntry(); + outbounds.append(outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "shadowsocks", outbounds::GenerateShadowSocksOUT({ ssObject }), {})); + root["outbounds"] = outbounds; + root["inbounds"] = QJsonArray{ inbound }; + serverList.append({ finalName, root }); + } + + // returns the current result + return serverList; +} +#undef MUST_EXIST +#undef MUST_PORT +#undef MUST_ARRAY +#undef MUST_STRING +#undef SERVER_SHOULD_BE_OBJECT +#undef SHOULD_EXIST +#undef SHOULD_STRING +} // namespace amnezia::serialization::ssd + diff --git a/transfer.h b/transfer.h new file mode 100644 index 0000000..84c085b --- /dev/null +++ b/transfer.h @@ -0,0 +1,313 @@ +#ifndef TRANSFER_H +#define TRANSFER_H + +#include "3rd/QJsonStruct/QJsonIO.hpp" +#include "3rd/QJsonStruct/QJsonStruct.hpp" + +#define JADDEx(field) root.insert(#field, field); +#define JADD(...) FOR_EACH(JADDEx, __VA_ARGS__) + +constexpr auto VMESS_USER_ALTERID_DEFAULT = 0; + +namespace amnezia::serialization { + +struct ShadowSocksServerObject +{ + QString address; + QString method; + QString password; + int port; + JSONSTRUCT_COMPARE(ShadowSocksServerObject, address, method, password) + JSONSTRUCT_REGISTER(ShadowSocksServerObject, F(address, port, method, password)) +}; + + +struct VMessServerObject +{ + struct UserObject + { + QString id; + int alterId = VMESS_USER_ALTERID_DEFAULT; + QString security = "auto"; + int level = 0; + JSONSTRUCT_COMPARE(UserObject, id, alterId, security, level) + JSONSTRUCT_REGISTER(UserObject, F(id, alterId, security, level)) + }; + + QString address; + int port; + QList users; + JSONSTRUCT_COMPARE(VMessServerObject, address, port, users) + JSONSTRUCT_REGISTER(VMessServerObject, F(address, port, users)) +}; + + +namespace transfer +{ + +struct HTTPRequestObject +{ + QString version = "1.1"; + QString method = "GET"; + QList path = { "/" }; + QMap> headers; + HTTPRequestObject() + { + headers = { + { "Host", { "www.baidu.com", "www.bing.com" } }, + { "User-Agent", + { "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46" } }, + { "Accept-Encoding", { "gzip, deflate" } }, + { "Connection", { "keep-alive" } }, + { "Pragma", { "no-cache" } } + }; + } + JSONSTRUCT_COMPARE(HTTPRequestObject, version, method, path, headers) + JSONSTRUCT_REGISTER(HTTPRequestObject, F(version, method, path, headers)) +}; +// +// +struct HTTPResponseObject +{ + QString version = "1.1"; + QString status = "200"; + QString reason = "OK"; + QMap> headers; + HTTPResponseObject() + { + headers = { { "Content-Type", { "application/octet-stream", "video/mpeg" } }, // + { "Transfer-Encoding", { "chunked" } }, // + { "Connection", { "keep-alive" } }, // + { "Pragma", { "no-cache" } } }; + } + JSONSTRUCT_COMPARE(HTTPResponseObject, version, status, reason, headers) + JSONSTRUCT_REGISTER(HTTPResponseObject, F(version, status, reason, headers)) +}; +// +// +struct TCPHeader_Internal +{ + QString type = "none"; + HTTPRequestObject request; + HTTPResponseObject response; + JSONSTRUCT_COMPARE(TCPHeader_Internal, type, request, response) + JSONSTRUCT_REGISTER(TCPHeader_Internal, A(type), F(request, response)) +}; +// +// +struct ObfsHeaderObject +{ + QString type = "none"; + JSONSTRUCT_COMPARE(ObfsHeaderObject, type) + JSONSTRUCT_REGISTER(ObfsHeaderObject, F(type)) +}; +// +// +struct TCPObject +{ + TCPHeader_Internal header; + JSONSTRUCT_COMPARE(TCPObject, header) + JSONSTRUCT_REGISTER(TCPObject, F(header)) +}; +// +// +struct KCPObject +{ + int mtu = 1350; + int tti = 50; + int uplinkCapacity = 5; + int downlinkCapacity = 20; + bool congestion = false; + int readBufferSize = 2; + int writeBufferSize = 2; + QString seed; + ObfsHeaderObject header; + KCPObject(){}; + JSONSTRUCT_COMPARE(KCPObject, mtu, tti, uplinkCapacity, downlinkCapacity, congestion, readBufferSize, writeBufferSize, seed, header) + JSONSTRUCT_REGISTER(KCPObject, F(mtu, tti, uplinkCapacity, downlinkCapacity, congestion, readBufferSize, writeBufferSize, header, seed)) +}; +// +// +struct WebSocketObject +{ + QString path = "/"; + QMap headers; + int maxEarlyData = 0; + bool useBrowserForwarding = false; + QString earlyDataHeaderName; + JSONSTRUCT_COMPARE(WebSocketObject, path, headers, maxEarlyData, useBrowserForwarding, earlyDataHeaderName) + JSONSTRUCT_REGISTER(WebSocketObject, F(path, headers, maxEarlyData, useBrowserForwarding, earlyDataHeaderName)) +}; +// +// +struct HttpObject +{ + QList host; + QString path = "/"; + QString method = "PUT"; + QMap> headers; + JSONSTRUCT_COMPARE(HttpObject, host, path, method, headers) + JSONSTRUCT_REGISTER(HttpObject, F(host, path, method, headers)) +}; +// +// +struct DomainSocketObject +{ + QString path = "/"; + JSONSTRUCT_COMPARE(DomainSocketObject, path) + JSONSTRUCT_REGISTER(DomainSocketObject, F(path)) +}; +// +// +struct QuicObject +{ + QString security = "none"; + QString key; + ObfsHeaderObject header; + JSONSTRUCT_COMPARE(QuicObject, security, key, header) + JSONSTRUCT_REGISTER(QuicObject, F(security, key, header)) +}; +// +// +struct gRPCObject +{ + QString serviceName; + bool multiMode = false; + JSONSTRUCT_COMPARE(gRPCObject, serviceName, multiMode) + JSONSTRUCT_REGISTER(gRPCObject, F(serviceName, multiMode)) +}; + +// +// +struct SockoptObject +{ + int mark = 0; + bool tcpFastOpen = false; + QString tproxy = "off"; + int tcpKeepAliveInterval = 0; + JSONSTRUCT_COMPARE(SockoptObject, mark, tcpFastOpen, tproxy, tcpKeepAliveInterval) + JSONSTRUCT_REGISTER(SockoptObject, F(mark, tcpFastOpen, tproxy, tcpKeepAliveInterval)) +}; +// +// +struct CertificateObject +{ + QString usage = "encipherment"; + QString certificateFile; + QString keyFile; + QList certificate; + QList key; + JSONSTRUCT_COMPARE(CertificateObject, usage, certificateFile, keyFile, certificate, key) + JSONSTRUCT_REGISTER(CertificateObject, F(usage, certificateFile, keyFile, certificate, key)) +}; +// +// +struct TLSObject +{ + QString serverName; + bool allowInsecure = false; + bool enableSessionResumption = false; + bool disableSystemRoot = false; + QList alpn; + QList pinnedPeerCertificateChainSha256; + QList certificates; + JSONSTRUCT_COMPARE(TLSObject, serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn, + pinnedPeerCertificateChainSha256, certificates) + JSONSTRUCT_REGISTER(TLSObject, F(serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn, + pinnedPeerCertificateChainSha256, certificates)) +}; +// +// +struct XTLSObject +{ + QString serverName; + bool allowInsecure = false; + bool enableSessionResumption = false; + bool disableSystemRoot = false; + QList alpn; + QList certificates; + JSONSTRUCT_COMPARE(XTLSObject, serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn, certificates) + JSONSTRUCT_REGISTER(XTLSObject, F(serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn, certificates)) +}; +} // namespace transfer + +// +// +struct TrojanObject +{ + quint16 port; + QString address; + QString password; + QString sni; + bool ignoreCertificate = false; + bool ignoreHostname = false; + bool reuseSession = false; + bool sessionTicket = false; + bool reusePort = false; + bool tcpFastOpen = false; + +#define _X(name) json[#name] = name + QJsonObject toJson() const + { + QJsonObject json; + _X(port); + _X(address); + _X(password); + _X(sni); + _X(ignoreCertificate); + _X(ignoreHostname); + _X(reuseSession); + _X(reusePort); + _X(sessionTicket); + _X(tcpFastOpen); + return json; + }; +#undef _X + +#define _X(name, type) name = root[#name].to##type() + void loadJson(const QJsonObject &root) + { + _X(port, Int); + _X(address, String); + _X(password, String); + _X(sni, String); + _X(ignoreHostname, Bool); + _X(ignoreCertificate, Bool); + _X(reuseSession, Bool); + _X(reusePort, Bool); + _X(sessionTicket, Bool); + _X(tcpFastOpen, Bool); + } +#undef _X + + [[nodiscard]] static TrojanObject fromJson(const QJsonObject &root) + { + TrojanObject o; + o.loadJson(root); + return o; + } +}; + +struct StreamSettingsObject +{ + QString network = "tcp"; + QString security = "none"; + transfer::SockoptObject sockopt; + transfer::TLSObject tlsSettings; + transfer::XTLSObject xtlsSettings; + transfer::TCPObject tcpSettings; + transfer::KCPObject kcpSettings; + transfer::WebSocketObject wsSettings; + transfer::HttpObject httpSettings; + transfer::DomainSocketObject dsSettings; + transfer::QuicObject quicSettings; + transfer::gRPCObject grpcSettings; + JSONSTRUCT_COMPARE(StreamSettingsObject, network, security, sockopt, // + tcpSettings, tlsSettings, xtlsSettings, kcpSettings, wsSettings, httpSettings, dsSettings, quicSettings, grpcSettings) + JSONSTRUCT_REGISTER(StreamSettingsObject, F(network, security, sockopt), + F(tcpSettings, tlsSettings, xtlsSettings, kcpSettings, wsSettings, httpSettings, dsSettings, quicSettings, grpcSettings)) +}; + +} +#endif //TRANSFER_H diff --git a/trojan.cpp b/trojan.cpp new file mode 100644 index 0000000..e25fc4a --- /dev/null +++ b/trojan.cpp @@ -0,0 +1,271 @@ +// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++. +// This file is part of the Qv2ray VPN client. +// +// Qv2ray, A Qt frontend for V2Ray. Written in C++ + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Qv2ray VPN client. +// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + + +#include "3rd/QJsonStruct/QJsonIO.hpp" +#include +#include "serialization.h" + +#define OUTBOUND_TAG_PROXY "PROXY" + +namespace amnezia::serialization::trojan +{ + +const QString Serialize(const TrojanObject &object, const QString &alias) +{ + + QUrlQuery query; + if (object.ignoreHostname) + query.addQueryItem("allowInsecureHostname", "1"); + if (object.ignoreCertificate) + query.addQueryItem("allowInsecureCertificate", "1"); + if (object.sessionTicket) + query.addQueryItem("sessionTicket", "1"); + if (object.ignoreCertificate || object.ignoreHostname) + query.addQueryItem("allowInsecure", "1"); + if (object.tcpFastOpen) + query.addQueryItem("tfo", "1"); + + if (!object.sni.isEmpty()) + query.addQueryItem("sni", object.sni); + + QUrl link; + if (!object.password.isEmpty()) + link.setUserName(object.password, QUrl::DecodedMode); + link.setPort(object.port); + link.setHost(object.address); + link.setFragment(alias); + link.setQuery(query); + link.setScheme("trojan"); + + return link.toString(QUrl::FullyEncoded); +} + +QJsonObject Deserialize(const QString &trojanUri, QString *alias, QString *errMessage) +{ + const QString prefix = "trojan://"; + if (!trojanUri.startsWith(prefix)) + { + *errMessage = ("Invalid Trojan URI"); + return {}; + } + // + const auto trueList = QStringList{ "true", "1", "yes", "y" }; + const QUrl trojanUrl(trojanUri.trimmed()); + const QUrlQuery query(trojanUrl.query()); + *alias = trojanUrl.fragment(QUrl::FullyDecoded); + + auto getQueryValue = [&](const QString &key) { + return query.queryItemValue(key, QUrl::FullyDecoded); + }; + // + TrojanObject result; + result.address = trojanUrl.host(); + result.password = QUrl::fromPercentEncoding(trojanUrl.userInfo().toUtf8()); + result.port = trojanUrl.port(); + // process sni (and also "peer") + if (query.hasQueryItem("sni")) + { + result.sni = getQueryValue("sni"); + } + else if (query.hasQueryItem("peer")) + { + // This is evil and may be removed in a future version. + qWarning() << "use of 'peer' in trojan url is deprecated"; + result.sni = getQueryValue("peer"); + } + else + { + // Use the hostname + result.sni = result.address; + } + + + // + result.tcpFastOpen = trueList.contains(getQueryValue("tfo").toLower()); + result.sessionTicket = trueList.contains(getQueryValue("sessionTicket").toLower()); + // + bool allowAllInsecure = trueList.contains(getQueryValue("allowInsecure").toLower()); + result.ignoreHostname = allowAllInsecure || trueList.contains(getQueryValue("allowInsecureHostname").toLower()); + result.ignoreCertificate = allowAllInsecure || trueList.contains(getQueryValue("allowInsecureCertificate").toLower()); + + QJsonObject stream; + // handle type + const auto hasType = query.hasQueryItem("type"); + const auto type = hasType ? query.queryItemValue("type") : "tcp"; + if (type != "tcp") + QJsonIO::SetValue(stream, type, "network"); + + + // type-wise settings + if (type == "kcp") + { + const auto hasSeed = query.hasQueryItem("seed"); + if (hasSeed) + QJsonIO::SetValue(stream, query.queryItemValue("seed"), { "kcpSettings", "seed" }); + + const auto hasHeaderType = query.hasQueryItem("headerType"); + const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none"; + if (headerType != "none") + QJsonIO::SetValue(stream, headerType, { "kcpSettings", "header", "type" }); + } + else if (type == "http") + { + const auto hasPath = query.hasQueryItem("path"); + const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/"; + if (path != "/") + QJsonIO::SetValue(stream, path, { "httpSettings", "path" }); + + const auto hasHost = query.hasQueryItem("host"); + if (hasHost) + { + const auto hosts = QJsonArray::fromStringList(query.queryItemValue("host").split(",")); + QJsonIO::SetValue(stream, hosts, { "httpSettings", "host" }); + } + } + else if (type == "ws") + { + const auto hasPath = query.hasQueryItem("path"); + const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/"; + if (path != "/") + QJsonIO::SetValue(stream, path, { "wsSettings", "path" }); + + const auto hasHost = query.hasQueryItem("host"); + if (hasHost) + { + QJsonIO::SetValue(stream, query.queryItemValue("host"), { "wsSettings", "headers", "Host" }); + } + } + else if (type == "quic") + { + const auto hasQuicSecurity = query.hasQueryItem("quicSecurity"); + if (hasQuicSecurity) + { + const auto quicSecurity = query.queryItemValue("quicSecurity"); + QJsonIO::SetValue(stream, quicSecurity, { "quicSettings", "security" }); + + if (quicSecurity != "none") + { + const auto key = query.queryItemValue("key"); + QJsonIO::SetValue(stream, key, { "quicSettings", "key" }); + } + + const auto hasHeaderType = query.hasQueryItem("headerType"); + const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none"; + if (headerType != "none") + QJsonIO::SetValue(stream, headerType, { "quicSettings", "header", "type" }); + } + } + else if (type == "grpc") + { + const auto hasServiceName = query.hasQueryItem("serviceName"); + if (hasServiceName) + { + const auto serviceName = QUrl::fromPercentEncoding(query.queryItemValue("serviceName").toUtf8()); + QJsonIO::SetValue(stream, serviceName, { "grpcSettings", "serviceName" }); + } + + const auto hasMode = query.hasQueryItem("mode"); + if (hasMode) + { + const auto multiMode = QUrl::fromPercentEncoding(query.queryItemValue("mode").toUtf8()) == "multi"; + QJsonIO::SetValue(stream, multiMode, { "grpcSettings", "multiMode" }); + } + } + + // tls-wise settings + const auto hasSecurity = query.hasQueryItem("security"); + const auto security = hasSecurity ? query.queryItemValue("security") : "none"; + const auto tlsKey = security == "xtls" ? "xtlsSettings" : ( security == "tls" ? "tlsSettings" : "realitySettings" ); + if (security != "none") + { + QJsonIO::SetValue(stream, security, "security"); + } + // sni + const auto hasSNI = query.hasQueryItem("sni"); + if (hasSNI) + { + const auto sni = query.queryItemValue("sni"); + QJsonIO::SetValue(stream, sni, { tlsKey, "serverName" }); + } + // alpn + const auto hasALPN = query.hasQueryItem("alpn"); + if (hasALPN) + { + const auto alpnRaw = QUrl::fromPercentEncoding(query.queryItemValue("alpn").toUtf8()); + QStringList aplnElems = alpnRaw.split(","); + // h2 protocol is not supported by xray + aplnElems.removeAll("h2"); + if (!aplnElems.isEmpty()) { + const auto alpnArray = QJsonArray::fromStringList(aplnElems); + QJsonIO::SetValue(stream, alpnArray, { tlsKey, "alpn" }); + } + } + + if (security == "reality") + { + if (query.hasQueryItem("fp")) + { + const auto fp = QUrl::fromPercentEncoding(query.queryItemValue("fp").toUtf8()); + QJsonIO::SetValue(stream, fp, { "realitySettings", "fingerprint" }); + } + if (query.hasQueryItem("pbk")) + { + const auto pbk = QUrl::fromPercentEncoding(query.queryItemValue("pbk").toUtf8()); + QJsonIO::SetValue(stream, pbk, { "realitySettings", "publicKey" }); + } + if (query.hasQueryItem("spiderX")) + { + const auto spiderX = QUrl::fromPercentEncoding(query.queryItemValue("spiderX").toUtf8()); + QJsonIO::SetValue(stream, spiderX, { "realitySettings", "spiderX" }); + } + if (query.hasQueryItem("sid")) + { + const auto sid = QUrl::fromPercentEncoding(query.queryItemValue("sid").toUtf8()); + QJsonIO::SetValue(stream, sid, { "realitySettings", "shortId" }); + } + } + + QJsonObject root; + QJsonArray outbounds; + QJsonObject outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "trojan", outbounds::GenerateTrojanOUT({ result }), {}); + outbound["streamSettings"] = stream; + outbounds.append(outbound); + JADD(outbounds) + QJsonObject inbound = inbounds::GenerateInboundEntry(); + root["inbounds"] = QJsonArray { inbound }; + + return root; +} + +} // namespace amnezia::serialization::trojan + diff --git a/vless.cpp b/vless.cpp new file mode 100644 index 0000000..e809009 --- /dev/null +++ b/vless.cpp @@ -0,0 +1,256 @@ +// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++. +// This file is part of the Qv2ray VPN client. +// +// Qv2ray, A Qt frontend for V2Ray. Written in C++ + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Qv2ray VPN client. +// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + + +#include "3rd/QJsonStruct/QJsonIO.hpp" +#include +#include "serialization.h" + +namespace amnezia::serialization::vless +{ +QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage) +{ + // must start with vless:// + if (!str.startsWith("vless://")) + { + *errMessage = QObject::tr("VLESS link should start with vless://"); + return QJsonObject(); + } + + // parse url + QUrl url(str); + if (!url.isValid()) + { + *errMessage = QObject::tr("link parse failed: %1").arg(url.errorString()); + return QJsonObject(); + } + + // fetch host + const auto hostRaw = url.host(); + if (hostRaw.isEmpty()) + { + *errMessage = QObject::tr("empty host"); + return QJsonObject(); + } + const auto host = (hostRaw.startsWith('[') && hostRaw.endsWith(']')) ? hostRaw.mid(1, hostRaw.length() - 2) : hostRaw; + + // fetch port + const auto port = url.port(); + if (port == -1) + { + *errMessage = QObject::tr("missing port"); + return QJsonObject(); + } + + // fetch remarks + const auto remarks = url.fragment(); + if (!remarks.isEmpty()) + { + *alias = remarks; + } + + // fetch uuid + const auto uuid = url.userInfo(); + if (uuid.isEmpty()) + { + *errMessage = QObject::tr("missing uuid"); + return QJsonObject(); + } + + // initialize QJsonObject with basic info + QJsonObject outbound; + QJsonObject stream; + + QJsonIO::SetValue(outbound, "vless", "protocol"); + QJsonIO::SetValue(outbound, host, { "settings", "vnext", 0, "address" }); + QJsonIO::SetValue(outbound, port, { "settings", "vnext", 0, "port" }); + QJsonIO::SetValue(outbound, uuid, { "settings", "vnext", 0, "users", 0, "id" }); + + // parse query + QUrlQuery query(url.query()); + + // handle type + const auto hasType = query.hasQueryItem("type"); + const auto type = hasType ? query.queryItemValue("type") : "tcp"; + if (type != "tcp") + QJsonIO::SetValue(stream, type, "network"); + + // handle encryption + const auto hasEncryption = query.hasQueryItem("encryption"); + const auto encryption = hasEncryption ? query.queryItemValue("encryption") : "none"; + QJsonIO::SetValue(outbound, encryption, { "settings", "vnext", 0, "users", 0, "encryption" }); + + // type-wise settings + if (type == "kcp") + { + const auto hasSeed = query.hasQueryItem("seed"); + if (hasSeed) + QJsonIO::SetValue(stream, query.queryItemValue("seed"), { "kcpSettings", "seed" }); + + const auto hasHeaderType = query.hasQueryItem("headerType"); + const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none"; + if (headerType != "none") + QJsonIO::SetValue(stream, headerType, { "kcpSettings", "header", "type" }); + } + else if (type == "http") + { + const auto hasPath = query.hasQueryItem("path"); + const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/"; + if (path != "/") + QJsonIO::SetValue(stream, path, { "httpSettings", "path" }); + + const auto hasHost = query.hasQueryItem("host"); + if (hasHost) + { + const auto hosts = QJsonArray::fromStringList(query.queryItemValue("host").split(",")); + QJsonIO::SetValue(stream, hosts, { "httpSettings", "host" }); + } + } + else if (type == "ws") + { + const auto hasPath = query.hasQueryItem("path"); + const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/"; + if (path != "/") + QJsonIO::SetValue(stream, path, { "wsSettings", "path" }); + + const auto hasHost = query.hasQueryItem("host"); + if (hasHost) + { + QJsonIO::SetValue(stream, query.queryItemValue("host"), { "wsSettings", "headers", "Host" }); + } + } + else if (type == "quic") + { + const auto hasQuicSecurity = query.hasQueryItem("quicSecurity"); + if (hasQuicSecurity) + { + const auto quicSecurity = query.queryItemValue("quicSecurity"); + QJsonIO::SetValue(stream, quicSecurity, { "quicSettings", "security" }); + + if (quicSecurity != "none") + { + const auto key = query.queryItemValue("key"); + QJsonIO::SetValue(stream, key, { "quicSettings", "key" }); + } + + const auto hasHeaderType = query.hasQueryItem("headerType"); + const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none"; + if (headerType != "none") + QJsonIO::SetValue(stream, headerType, { "quicSettings", "header", "type" }); + } + } + else if (type == "grpc") + { + const auto hasServiceName = query.hasQueryItem("serviceName"); + if (hasServiceName) + { + const auto serviceName = QUrl::fromPercentEncoding(query.queryItemValue("serviceName").toUtf8()); + QJsonIO::SetValue(stream, serviceName, { "grpcSettings", "serviceName" }); + } + + const auto hasMode = query.hasQueryItem("mode"); + if (hasMode) + { + const auto multiMode = QUrl::fromPercentEncoding(query.queryItemValue("mode").toUtf8()) == "multi"; + QJsonIO::SetValue(stream, multiMode, { "grpcSettings", "multiMode" }); + } + } + + // tls-wise settings + const auto hasSecurity = query.hasQueryItem("security"); + const auto security = hasSecurity ? query.queryItemValue("security") : "none"; + const auto tlsKey = security == "xtls" ? "xtlsSettings" : ( security == "tls" ? "tlsSettings" : "realitySettings" ); + if (security != "none") + { + QJsonIO::SetValue(stream, security, "security"); + } + // sni + const auto hasSNI = query.hasQueryItem("sni"); + if (hasSNI) + { + const auto sni = query.queryItemValue("sni"); + QJsonIO::SetValue(stream, sni, { tlsKey, "serverName" }); + } + // alpn + const auto hasALPN = query.hasQueryItem("alpn"); + if (hasALPN) + { + const auto alpnRaw = QUrl::fromPercentEncoding(query.queryItemValue("alpn").toUtf8()); + QStringList aplnElems = alpnRaw.split(","); + // h2 protocol is not supported by xray + aplnElems.removeAll("h2"); + if (!aplnElems.isEmpty()) { + const auto alpnArray = QJsonArray::fromStringList(aplnElems); + QJsonIO::SetValue(stream, alpnArray, { tlsKey, "alpn" }); + } + } + // xtls-specific + if (security == "xtls" || security == "reality") + { + const auto flow = query.queryItemValue("flow"); + QJsonIO::SetValue(outbound, flow, { "settings", "vnext", 0, "users", 0, "flow" }); + } + + if (security == "reality") + { + if (query.hasQueryItem("fp")) + { + const auto fp = QUrl::fromPercentEncoding(query.queryItemValue("fp").toUtf8()); + QJsonIO::SetValue(stream, fp, { "realitySettings", "fingerprint" }); + } + if (query.hasQueryItem("pbk")) + { + const auto pbk = QUrl::fromPercentEncoding(query.queryItemValue("pbk").toUtf8()); + QJsonIO::SetValue(stream, pbk, { "realitySettings", "publicKey" }); + } + if (query.hasQueryItem("spiderX")) + { + const auto spiderX = QUrl::fromPercentEncoding(query.queryItemValue("spiderX").toUtf8()); + QJsonIO::SetValue(stream, spiderX, { "realitySettings", "spiderX" }); + } + if (query.hasQueryItem("sid")) + { + const auto sid = QUrl::fromPercentEncoding(query.queryItemValue("sid").toUtf8()); + QJsonIO::SetValue(stream, sid, { "realitySettings", "shortId" }); + } + } + + // assembling config + QJsonObject root; + outbound["streamSettings"] = stream; + QJsonObject inbound = inbounds::GenerateInboundEntry(); + root["outbounds"] = QJsonArray{ outbound }; + root["inbounds"] = QJsonArray { inbound }; + return root; +} +} // namespace amnezia::serialization::vless + diff --git a/vmess.cpp b/vmess.cpp new file mode 100644 index 0000000..c5258c8 --- /dev/null +++ b/vmess.cpp @@ -0,0 +1,344 @@ +// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++. +// This file is part of the Qv2ray VPN client. +// +// Qv2ray, A Qt frontend for V2Ray. Written in C++ + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Qv2ray VPN client. +// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + +#include "3rd/QJsonStruct/QJsonStruct.hpp" +#include +#include "transfer.h" +#include "utilities.h" +#include "serialization.h" + +#define nothing + +#define OUTBOUND_TAG_PROXY "PROXY" + +namespace amnezia::serialization::vmess +{ +// From https://github.com/2dust/v2rayN/wiki/分享链接格式说明(ver-2) +const QString Serialize(const StreamSettingsObject &transfer, const VMessServerObject &server, const QString &alias) +{ + QJsonObject vmessUriRoot; + // Constant + vmessUriRoot["v"] = 2; + vmessUriRoot["ps"] = alias; + vmessUriRoot["add"] = server.address; + vmessUriRoot["port"] = server.port; + vmessUriRoot["id"] = server.users.front().id; + vmessUriRoot["aid"] = server.users.front().alterId; + const auto scy = server.users.front().security; + vmessUriRoot["scy"] = (scy == "aes-128-gcm" || scy == "chacha20-poly1305" || scy == "none" || scy == "zero") ? scy : "auto"; + vmessUriRoot["net"] = transfer.network == "http" ? "h2" : transfer.network; + vmessUriRoot["tls"] = (transfer.security == "tls" || transfer.security == "xtls") ? "tls" : "none"; + if (transfer.security == "tls") + { + vmessUriRoot["sni"] = transfer.tlsSettings.serverName; + } + else if (transfer.security == "xtls") + { + vmessUriRoot["sni"] = transfer.xtlsSettings.serverName; + } + + if (transfer.network == "tcp") + { + vmessUriRoot["type"] = transfer.tcpSettings.header.type; + } + else if (transfer.network == "kcp") + { + vmessUriRoot["type"] = transfer.kcpSettings.header.type; + } + else if (transfer.network == "quic") + { + vmessUriRoot["type"] = transfer.quicSettings.header.type; + vmessUriRoot["host"] = transfer.quicSettings.security; + vmessUriRoot["path"] = transfer.quicSettings.key; + } + else if (transfer.network == "ws") + { + auto x = transfer.wsSettings.headers; + auto host = x.contains("host"); + auto CapHost = x.contains("Host"); + auto realHost = host ? x["host"] : (CapHost ? x["Host"] : ""); + // + vmessUriRoot["host"] = realHost; + vmessUriRoot["path"] = transfer.wsSettings.path; + } + else if (transfer.network == "h2" || transfer.network == "http") + { + vmessUriRoot["host"] = transfer.httpSettings.host.join(","); + vmessUriRoot["path"] = transfer.httpSettings.path; + } + else if (transfer.network == "grpc") + { + vmessUriRoot["path"] = transfer.grpcSettings.serviceName; + } + + if (!vmessUriRoot.contains("type") || vmessUriRoot["type"].toString().isEmpty()) + { + vmessUriRoot["type"] = "none"; + } + + // + QString jString = Utils::JsonToString(vmessUriRoot, QJsonDocument::JsonFormat::Compact); + auto vmessPart = jString.toUtf8().toBase64(); + return "vmess://" + vmessPart; +} + +// This generates global config containing only one outbound.... +QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMessage) +{ +#define default QJsonObject() + QString vmess = vmessStr; + + if (vmess.trimmed() != vmess) + { + vmess = vmessStr.trimmed(); + } + + // Reset errMessage + *errMessage = ""; + + if (!vmess.toLower().startsWith("vmess://")) + { + *errMessage = QObject::tr("VMess string should start with 'vmess://'"); + return default; + } + + const auto b64Str = vmess.mid(8, vmess.length() - 8); + if (b64Str.isEmpty()) + { + *errMessage = QObject::tr("VMess string should be a valid base64 string"); + return default; + } + + auto vmessString = Utils::SafeBase64Decode(b64Str); + auto jsonErr = Utils::VerifyJsonString(vmessString); + + if (!jsonErr.isEmpty()) + { + *errMessage = jsonErr; + return default; + } + + auto vmessConf = Utils::JsonFromString(vmessString); + + if (vmessConf.isEmpty()) + { + *errMessage = QObject::tr("JSON should not be empty"); + return default; + } + + // -------------------------------------------------------------------------------------- + QJsonObject root; + QString ps, add, id, net, type, host, path, tls, scy, sni; + int port, aid; + // + // __vmess_checker__func(key, values) + // + // - Key = Key in JSON and the variable name. + // - Values = Candidate variable list, if not match, the first one is used as default. + // + // - [[val.size() <= 1]] is used when only the default value exists. + // + // - It can be empty, if so, if the key is not in the JSON, or the value is empty, report an error. + // - Else if it contains one thing. if the key is not in the JSON, or the value is empty, use that one. + // - Else if it contains many things, when the key IS in the JSON but not within the THINGS, use the first in the THINGS + // - Else -------------------------------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> use the JSON value + // +#define __vmess_checker__func(key, values) \ + { \ + auto val = QStringList() values; \ + if (vmessConf.contains(#key) && !vmessConf[#key].toVariant().toString().trimmed().isEmpty() && \ + (val.size() <= 1 || val.contains(vmessConf[#key].toVariant().toString()))) \ + { \ + key = vmessConf[#key].toVariant().toString(); \ + } \ + else if (!val.isEmpty()) \ + { \ + key = val.first(); \ + } \ + else \ + { \ + *errMessage = QObject::tr(#key " does not exist."); \ + } \ + } + + // vmess v1 upgrader + if (!vmessConf.contains("v")) + { + qDebug() << "Detected deprecated vmess v1. Trying to upgrade..."; + if (const auto network = vmessConf["net"].toString(); network == "ws" || network == "h2") + { + const QStringList hostComponents = vmessConf["host"].toString().replace(" ", "").split(";"); + if (const auto nParts = hostComponents.length(); nParts == 1) + vmessConf["path"] = hostComponents[0], vmessConf["host"] = ""; + else if (nParts == 2) + vmessConf["path"] = hostComponents[0], vmessConf["host"] = hostComponents[1]; + else + vmessConf["path"] = "/", vmessConf["host"] = ""; + } + } + + // Strict check of VMess protocol, to check if the specified value + // is in the correct range. + // + // Get Alias (AKA ps) from address and port. + { + // Some idiot vmess:// links are using alterId... + aid = vmessConf.contains("aid") ? vmessConf.value("aid").toInt(VMESS_USER_ALTERID_DEFAULT) : + vmessConf.value("alterId").toInt(VMESS_USER_ALTERID_DEFAULT); + // + // + __vmess_checker__func(ps, << vmessConf["add"].toVariant().toString() + ":" + vmessConf["port"].toVariant().toString()); // + __vmess_checker__func(add, nothing); // + __vmess_checker__func(id, nothing); // + __vmess_checker__func(scy, << "aes-128-gcm" // + << "chacha20-poly1305" // + << "auto" // + << "none" // + << "zero"); // + // + __vmess_checker__func(type, << "none" // + << "http" // + << "srtp" // + << "utp" // + << "wechat-video"); // + // + __vmess_checker__func(net, << "tcp" // + << "http" // + << "h2" // + << "ws" // + << "kcp" // + << "quic" // + << "grpc"); // + // + __vmess_checker__func(tls, << "none" // + << "tls"); // + // + path = vmessConf.contains("path") ? vmessConf["path"].toVariant().toString() : (net == "quic" ? "" : "/"); + host = vmessConf.contains("host") ? vmessConf["host"].toVariant().toString() : (net == "quic" ? "none" : ""); + } + + // Respect connection type rather than obfs type + if (QStringList{ "srtp", "utp", "wechat-video" }.contains(type)) // + { // + if (net != "quic" && net != "kcp") // + { // + type = "none"; // + } // + } + + port = vmessConf["port"].toVariant().toInt(); + aid = vmessConf["aid"].toVariant().toInt(); + // + // Apply the settings. + // User + VMessServerObject::UserObject user; + user.id = id; + user.alterId = aid; + user.security = scy; + // + // Server + VMessServerObject serv; + serv.port = port; + serv.address = add; + serv.users.push_back(user); + // + // + // Stream Settings + StreamSettingsObject streaming; + + if (net == "tcp") + { + streaming.tcpSettings.header.type = type; + } + else if (net == "http" || net == "h2") + { + // Fill hosts for HTTP + for (const auto &_host : host.split(',')) + { + if (!_host.isEmpty()) + { + streaming.httpSettings.host << _host.trimmed(); + } + } + + streaming.httpSettings.path = path; + } + else if (net == "ws") + { + if (!host.isEmpty()) + streaming.wsSettings.headers["Host"] = host; + streaming.wsSettings.path = path; + } + else if (net == "kcp") + { + streaming.kcpSettings.header.type = type; + } + else if (net == "quic") + { + streaming.quicSettings.security = host; + streaming.quicSettings.header.type = type; + streaming.quicSettings.key = path; + } + else if (net == "grpc") + { + streaming.grpcSettings.serviceName = path; + } + + streaming.security = tls; + if (tls == "tls") + { + if (sni.isEmpty() && !host.isEmpty()) + sni = host; + streaming.tlsSettings.serverName = sni; + streaming.tlsSettings.allowInsecure = false; + } + // + // Network type + // NOTE(DuckSoft): Damn vmess:// just don't write 'http' properly + if (net == "h2") + net = "http"; + streaming.network = net; + // + // VMess root config + QJsonObject vConf; + vConf["vnext"] = QJsonArray{ serv.toJson() }; + const auto outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "vmess", vConf, streaming.toJson()); + QJsonObject inbound = inbounds::GenerateInboundEntry(); + root["outbounds"] = QJsonArray{ outbound }; + root["inbounds"] = QJsonArray{ inbound }; + // If previous alias is empty, just the PS is needed, else, append a "_" + *alias = alias->trimmed().isEmpty() ? ps : *alias + "_" + ps; + return root; +#undef default +} +} // namespace amnezia::serialization::vmess + diff --git a/vmess_new.cpp b/vmess_new.cpp new file mode 100644 index 0000000..68d3220 --- /dev/null +++ b/vmess_new.cpp @@ -0,0 +1,172 @@ +// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++. +// This file is part of the Qv2ray VPN client. +// +// Qv2ray, A Qt frontend for V2Ray. Written in C++ + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Qv2ray VPN client. +// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + +#include "3rd/QJsonStruct/QJsonIO.hpp" +#include "3rd/QJsonStruct/QJsonStruct.hpp" +#include "transfer.h" +#include "serialization.h" + +#include + +#define OUTBOUND_TAG_PROXY "PROXY" + +namespace amnezia::serialization::vmess_new +{ +const static QStringList NetworkType{ "tcp", "http", "ws", "kcp", "quic", "grpc" }; +const static QStringList QuicSecurityTypes{ "none", "aes-128-gcm", "chacha20-poly1305" }; +const static QStringList QuicKcpHeaderTypes{ "none", "srtp", "utp", "wechat-video", "dtls", "wireguard" }; +const static QStringList FalseTypes{ "false", "False", "No", "Off", "0" }; + +QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMessage) +{ + QUrl url{ vmessStr }; + QUrlQuery query{ url }; + // +#define default QJsonObject() + if (!url.isValid()) + { + *errMessage = QObject::tr("vmess:// url is invalid"); + return default; + } + + // If previous alias is empty, just the PS is needed, else, append a "_" + const auto name = url.fragment(QUrl::FullyDecoded).trimmed(); + *alias = alias->isEmpty() ? name : (*alias + "_" + name); + + VMessServerObject server; + server.users << VMessServerObject::UserObject{}; + + StreamSettingsObject stream; + QString net; + bool tls = false; + // Check streamSettings + { + for (const auto &_protocol : url.userName().split("+")) + { + if (_protocol == "tls") + tls = true; + else + net = _protocol; + } + if (!NetworkType.contains(net)) + { + *errMessage = QObject::tr("Invalid streamSettings protocol: ") + net; + return default; + } + stream.network = net; + stream.security = tls ? "tls" : ""; + } + // Host Port UUID AlterID + { + const auto host = url.host(); + int port = url.port(); + QString uuid; + int aid; + { + const auto pswd = url.password(); + const auto index = pswd.lastIndexOf("-"); + uuid = pswd.mid(0, index); + aid = pswd.right(pswd.length() - index - 1).toInt(); + } + server.address = host; + server.port = port; + server.users.first().id = uuid; + server.users.first().alterId = aid; + server.users.first().security = "auto"; + } + + const auto getQueryValue = [&query](const QString &key, const QString &defaultValue) { + if (query.hasQueryItem(key)) + return query.queryItemValue(key, QUrl::FullyDecoded); + else + return defaultValue; + }; + + // + // Begin transport settings parser + { + if (net == "tcp") + { + stream.tcpSettings.header.type = getQueryValue("type", "none"); + } + else if (net == "http") + { + stream.httpSettings.host.append(getQueryValue("host", "")); + stream.httpSettings.path = getQueryValue("path", "/"); + } + else if (net == "ws") + { + stream.wsSettings.headers["Host"] = getQueryValue("host", ""); + stream.wsSettings.path = getQueryValue("path", "/"); + } + else if (net == "kcp") + { + stream.kcpSettings.seed = getQueryValue("seed", ""); + stream.kcpSettings.header.type = getQueryValue("type", "none"); + } + else if (net == "quic") + { + stream.quicSettings.security = getQueryValue("security", "none"); + stream.quicSettings.key = getQueryValue("key", ""); + stream.quicSettings.header.type = getQueryValue("type", "none"); + } + else if (net == "grpc") + { + stream.grpcSettings.serviceName = getQueryValue("serviceName", ""); + } + else + { + *errMessage = QObject::tr("Unknown transport method: ") + net; + return default; + } + } +#undef default + if (tls) + { + stream.tlsSettings.allowInsecure = !FalseTypes.contains(getQueryValue("allowInsecure", "false")); + stream.tlsSettings.serverName = getQueryValue("tlsServerName", ""); + } + QJsonObject root; + QJsonObject vConf; + QJsonArray vnextArray; + vnextArray.append(server.toJson()); + vConf["vnext"] = vnextArray; + auto outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "vmess", vConf, stream.toJson()); + QJsonObject inbound = inbounds::GenerateInboundEntry(); + + // + root["outbounds"] = QJsonArray{ outbound }; + root["inbound"] = QJsonArray{ inbound }; + return root; +} + +} // namespace amnezia::serialization::vmess_new