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
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
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue