Connection string support for XRay protocol (#777)
* Connection string support for XRay protocol
This commit is contained in:
parent
d8020878d5
commit
e6ee9085a2
39 changed files with 20709 additions and 11 deletions
|
@ -691,6 +691,30 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential
|
|||
for (auto &port : fixedPorts) {
|
||||
script = script.append("|:%1").arg(port);
|
||||
}
|
||||
|
||||
if (transportProto == "tcpandudp") {
|
||||
QString tcpProtoScript = script;
|
||||
QString udpProtoScript = script;
|
||||
tcpProtoScript.append("' | grep -i tcp");
|
||||
udpProtoScript.append("' | grep -i udp");
|
||||
tcpProtoScript.append(" | grep LISTEN");
|
||||
|
||||
ErrorCode errorCode = runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
errorCode = runScript(credentials, replaceVars(udpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (!stdOut.isEmpty()) {
|
||||
return ErrorCode::ServerPortAlreadyAllocatedError;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
script = script.append("' | grep -i %1").arg(transportProto);
|
||||
|
||||
if (transportProto == "tcp") {
|
||||
|
|
|
@ -24,6 +24,7 @@ QScopedPointer<ConfiguratorBase> VpnConfigurationsController::createConfigurator
|
|||
case Proto::Awg: return QScopedPointer<ConfiguratorBase>(new AwgConfigurator(m_settings, m_serverController));
|
||||
case Proto::Ikev2: return QScopedPointer<ConfiguratorBase>(new Ikev2Configurator(m_settings, m_serverController));
|
||||
case Proto::Xray: return QScopedPointer<ConfiguratorBase>(new XrayConfigurator(m_settings, m_serverController));
|
||||
case Proto::SSXray: return QScopedPointer<ConfiguratorBase>(new XrayConfigurator(m_settings, m_serverController));
|
||||
default: return QScopedPointer<ConfiguratorBase>();
|
||||
}
|
||||
}
|
||||
|
|
38
client/core/serialization/inbound.cpp
Normal file
38
client/core/serialization/inbound.cpp
Normal file
|
@ -0,0 +1,38 @@
|
|||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#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
|
||||
|
122
client/core/serialization/outbound.cpp
Normal file
122
client/core/serialization/outbound.cpp
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#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<ShadowSocksServerObject> &_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<TrojanObject> &_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
|
||||
|
66
client/core/serialization/serialization.h
Normal file
66
client/core/serialization/serialization.h
Normal file
|
@ -0,0 +1,66 @@
|
|||
#ifndef SERIALIZATION_H
|
||||
#define SERIALIZATION_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#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<std::pair<QString, QJsonObject>> 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<ShadowSocksServerObject> &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<TrojanObject> &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
|
142
client/core/serialization/ss.cpp
Normal file
142
client/core/serialization/ss.cpp
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#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
|
||||
|
244
client/core/serialization/ssd.cpp
Normal file
244
client/core/serialization/ssd.cpp
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A Naive SSD Decoder for Qv2ray
|
||||
*
|
||||
* @author DuckSoft <realducksoft@gmail.com>
|
||||
* @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<std::pair<QString, QJsonObject>> 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<std::pair<QString, QJsonObject>> 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
|
||||
|
313
client/core/serialization/transfer.h
Normal file
313
client/core/serialization/transfer.h
Normal file
|
@ -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<UserObject> 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<QString> path = { "/" };
|
||||
QMap<QString, QList<QString>> 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<QString, QList<QString>> 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<QString, QString> 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<QString> host;
|
||||
QString path = "/";
|
||||
QString method = "PUT";
|
||||
QMap<QString, QList<QString>> 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<QString> certificate;
|
||||
QList<QString> 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<QString> alpn;
|
||||
QList<QString> pinnedPeerCertificateChainSha256;
|
||||
QList<CertificateObject> 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<QString> alpn;
|
||||
QList<CertificateObject> 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
|
271
client/core/serialization/trojan.cpp
Normal file
271
client/core/serialization/trojan.cpp
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include <QUrlQuery>
|
||||
#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
|
||||
|
256
client/core/serialization/vless.cpp
Normal file
256
client/core/serialization/vless.cpp
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include <QUrlQuery>
|
||||
#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
|
||||
|
344
client/core/serialization/vmess.cpp
Normal file
344
client/core/serialization/vmess.cpp
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonStruct.hpp"
|
||||
#include <QJsonDocument>
|
||||
#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
|
||||
|
172
client/core/serialization/vmess_new.cpp
Normal file
172
client/core/serialization/vmess_new.cpp
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include "3rd/QJsonStruct/QJsonStruct.hpp"
|
||||
#include "transfer.h"
|
||||
#include "serialization.h"
|
||||
|
||||
#include <QUrlQuery>
|
||||
|
||||
#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 static 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
|
Loading…
Add table
Add a link
Reference in a new issue