From 9916b8119c1e744960aaf8ef652518b8d367f4a4 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Wed, 22 Jan 2025 17:47:58 +0400 Subject: [PATCH] Add xray config serialization --- inbound.cpp | 38 ++++++ outbound.cpp | 122 +++++++++++++++++ serialization.h | 66 ++++++++++ ss.cpp | 142 ++++++++++++++++++++ ssd.cpp | 244 ++++++++++++++++++++++++++++++++++ transfer.h | 313 +++++++++++++++++++++++++++++++++++++++++++ trojan.cpp | 271 ++++++++++++++++++++++++++++++++++++++ vless.cpp | 256 +++++++++++++++++++++++++++++++++++ vmess.cpp | 344 ++++++++++++++++++++++++++++++++++++++++++++++++ vmess_new.cpp | 172 ++++++++++++++++++++++++ 10 files changed, 1968 insertions(+) create mode 100644 inbound.cpp create mode 100644 outbound.cpp create mode 100644 serialization.h create mode 100644 ss.cpp create mode 100644 ssd.cpp create mode 100644 transfer.h create mode 100644 trojan.cpp create mode 100644 vless.cpp create mode 100644 vmess.cpp create mode 100644 vmess_new.cpp 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