nekoray_Mahdi-zarei/include/global/XhttpExtraConverter.hpp
2025-12-04 11:50:10 +08:00

281 lines
12 KiB
C++

#pragma once
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QString>
class XhttpExtraConverter
{
public:
static QJsonObject xrayToSingBox(const QString &xrayExtra)
{
if (xrayExtra.trimmed().isEmpty()) return {};
QJsonParseError err{};
const auto doc = QJsonDocument::fromJson(xrayExtra.toUtf8(), &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
return {};
}
const QJsonObject xray = doc.object();
if (isSingBoxFormat(xray)) return xray;
QJsonObject singBox;
convertField(xray, singBox, "xPaddingBytes", "x_padding_bytes");
convertField(xray, singBox, "scMaxEachPostBytes", "sc_max_each_post_bytes");
convertField(xray, singBox, "scMinPostsIntervalMs", "sc_min_posts_interval_ms");
convertField(xray, singBox, "noGRPCHeader", "no_grpc_header");
// xmux
if (xray.contains("xmux") && xray["xmux"].isObject()) {
singBox["xmux"] = convertXrayXmux(xray["xmux"].toObject());
}
// downloadSettings → download
if (xray.contains("downloadSettings") && xray["downloadSettings"].isObject()) {
const QJsonObject xDown = xray["downloadSettings"].toObject();
QJsonObject sDown;
// xhttpSettings (mode/host/path)
if (xDown.contains("xhttpSettings") && xDown["xhttpSettings"].isObject()) {
const QJsonObject xhttp = xDown["xhttpSettings"].toObject();
convertField(xhttp, sDown, "mode", "mode");
convertField(xhttp, sDown, "host", "host");
convertField(xhttp, sDown, "path", "path");
}
convertField(xDown, sDown, "address", "server");
convertField(xDown, sDown, "port", "server_port");
// TLS / Reality
if (xDown.contains("security") && xDown["security"].isString()) {
QJsonObject tls{ {"enabled", true} };
const QString security = xDown["security"].toString();
if (security == "tls") {
if (xDown.contains("tlsSettings") && xDown["tlsSettings"].isObject()) {
const QJsonObject tlsSet = xDown["tlsSettings"].toObject();
convertField(tlsSet, tls, "serverName", "server_name");
convertField(tlsSet, tls, "alpn", "alpn");
convertField(tlsSet, tls, "allowInsecure", "insecure");
if (tlsSet.contains("fingerprint") && !tlsSet["fingerprint"].toString().isEmpty()) {
QJsonObject utls{ {"enabled", true}, {"fingerprint", tlsSet["fingerprint"]} };
tls["utls"] = utls;
}
}
} else if (security == "reality") {
if (xDown.contains("realitySettings") && xDown["realitySettings"].isObject()) {
const QJsonObject realSet = xDown["realitySettings"].toObject();
convertField(realSet, tls, "serverName", "server_name");
QJsonObject reality{ {"enabled", true} };
convertField(realSet, reality, "publicKey", "public_key");
convertField(realSet, reality, "shortId", "short_id");
tls["reality"] = reality;
if (realSet.contains("fingerprint") && !realSet["fingerprint"].toString().isEmpty()) {
QJsonObject utls{ {"enabled", true}, {"fingerprint", realSet["fingerprint"]} };
tls["utls"] = utls;
}
}
}
sDown["tls"] = tls;
}
// downloadSettings.xhttpSettings.extra → download + xmux
if (xDown.contains("xhttpSettings") && xDown["xhttpSettings"].isObject()) {
const QJsonObject xhttp = xDown["xhttpSettings"].toObject();
if (xhttp.contains("extra") && xhttp["extra"].isObject()) {
const QJsonObject extra = xhttp["extra"].toObject();
convertField(extra, sDown, "xPaddingBytes", "x_padding_bytes");
convertField(extra, sDown, "scMaxEachPostBytes", "sc_max_each_post_bytes");
convertField(extra, sDown, "scMinPostsIntervalMs", "sc_min_posts_interval_ms");
convertField(extra, sDown, "noGRPCHeader", "no_grpc_header");
if (extra.contains("xmux") && extra["xmux"].isObject()) {
sDown["xmux"] = convertXrayXmux(extra["xmux"].toObject());
}
}
}
if (!sDown.isEmpty()) {
singBox["download"] = sDown;
}
}
return singBox;
}
static QString singBoxToXray(const QJsonObject &sing)
{
if (isXrayFormat(sing)) return QJsonDocument(sing).toJson(QJsonDocument::Compact).replace("\\/", "/");
QJsonObject xray;
convertField(sing, xray, "x_padding_bytes", "xPaddingBytes");
convertField(sing, xray, "sc_max_each_post_bytes", "scMaxEachPostBytes");
convertField(sing, xray, "sc_min_posts_interval_ms", "scMinPostsIntervalMs");
convertField(sing, xray, "no_grpc_header", "noGRPCHeader");
if (sing.contains("xmux") && sing["xmux"].isObject()) {
xray["xmux"] = convertSingBoxXmux(sing["xmux"].toObject());
}
// download → downloadSettings
if (sing.contains("download") && sing["download"].isObject()) {
const QJsonObject sDown = sing["download"].toObject();
QJsonObject xDown;
convertField(sDown, xDown, "server", "address");
convertField(sDown, xDown, "server_port", "port");
xDown["network"] = "xhttp";
// TLS / Reality
if (sDown.contains("tls") && sDown["tls"].isObject()) {
const QJsonObject tls = sDown["tls"].toObject();
if (tls.contains("reality") && tls["reality"].isObject() &&
tls["reality"].toObject().value("enabled").toBool(false)) {
xDown["security"] = "reality";
QJsonObject realitySettings;
convertField(tls, realitySettings, "server_name", "serverName");
const QJsonObject reality = tls["reality"].toObject();
convertField(reality, realitySettings, "public_key", "publicKey");
convertField(reality, realitySettings, "short_id", "shortId");
if (tls.contains("utls") && tls["utls"].isObject()) {
convertField(tls["utls"].toObject(), realitySettings, "fingerprint", "fingerprint");
}
xDown["realitySettings"] = realitySettings;
} else {
xDown["security"] = "tls";
QJsonObject tlsSettings;
convertField(tls, tlsSettings, "server_name", "serverName");
convertField(tls, tlsSettings, "alpn", "alpn");
convertField(tls, tlsSettings, "insecure", "allowInsecure");
if (tls.contains("utls") && tls["utls"].isObject()) {
convertField(tls["utls"].toObject(), tlsSettings, "fingerprint", "fingerprint");
}
xDown["tlsSettings"] = tlsSettings;
}
}
// mode/host/path + extra
QJsonObject xhttpSettings;
convertField(sDown, xhttpSettings, "mode", "mode");
convertField(sDown, xhttpSettings, "host", "host");
convertField(sDown, xhttpSettings, "path", "path");
QJsonObject xhttpExtra;
convertField(sDown, xhttpExtra, "x_padding_bytes", "xPaddingBytes");
convertField(sDown, xhttpExtra, "sc_max_each_post_bytes", "scMaxEachPostBytes");
convertField(sDown, xhttpExtra, "sc_min_posts_interval_ms", "scMinPostsIntervalMs");
convertField(sDown, xhttpExtra, "no_grpc_header", "noGRPCHeader");
if (sDown.contains("xmux") && sDown["xmux"].isObject()) {
xhttpExtra["xmux"] = convertSingBoxXmux(sDown["xmux"].toObject());
}
if (!xhttpExtra.isEmpty()) {
xhttpSettings["extra"] = xhttpExtra;
}
xDown["xhttpSettings"] = xhttpSettings;
if (!xDown.isEmpty()) {
xray["downloadSettings"] = xDown;
}
}
return QJsonDocument(xray).toJson(QJsonDocument::Compact).replace("\\/", "/");
}
private:
static bool isSingBoxFormat(const QJsonObject &json)
{
return json.contains("x_padding_bytes") ||
json.contains("sc_max_each_post_bytes") ||
json.contains("sc_min_posts_interval_ms") ||
json.contains("sc_stream_up_server_secs") ||
json.contains("download");
}
static bool isXrayFormat(const QJsonObject &json)
{
return json.contains("xPaddingBytes") ||
json.contains("scMaxEachPostBytes") ||
json.contains("scMinPostsIntervalMs") ||
json.contains("scStreamUpServerSecs") ||
json.contains("downloadSettings");
}
static QJsonValue normalizeRangeField(const QJsonValue &val)
{
if (val.isDouble()) {
int num = val.toInt();
QString range = QString("%1-%1").arg(num);
return QJsonValue(range);
} else if (val.isString()) {
int num = val.toString().trimmed().toInt();
if (QString::number(num) == val.toString().trimmed()) {
QString range = QString("%1-%1").arg(num);
return QJsonValue(range);
}
}
return val;
}
static void convertField(const QJsonObject &from, QJsonObject &to,
const QString &fromKey, const QString &toKey)
{
if (from.contains(fromKey)) {
QJsonValue v = from[fromKey];
if (fromKey == "xPaddingBytes" ||
fromKey == "scMaxEachPostBytes" ||
fromKey == "scMinPostsIntervalMs" ||
fromKey == "scStreamUpServerSecs" ||
fromKey == "maxConcurrency" ||
fromKey == "maxConnections" ||
fromKey == "cMaxReuseTimes" ||
fromKey == "hMaxRequestTimes" ||
fromKey == "hMaxReusableSecs") {
v = normalizeRangeField(v);
}
to[toKey] = v;
}
}
static QJsonObject convertXrayXmux(const QJsonObject &xrayXmux)
{
QJsonObject sing;
convertField(xrayXmux, sing, "maxConcurrency", "max_concurrency");
convertField(xrayXmux, sing, "maxConnections", "max_connections");
convertField(xrayXmux, sing, "cMaxReuseTimes", "c_max_reuse_times");
convertField(xrayXmux, sing, "hMaxRequestTimes", "h_max_request_times");
convertField(xrayXmux, sing, "hMaxReusableSecs", "h_max_reusable_secs");
convertField(xrayXmux, sing, "hKeepAlivePeriod", "h_keep_alive_period");
return sing;
}
static QJsonObject convertSingBoxXmux(const QJsonObject &singXmux)
{
QJsonObject xray;
convertField(singXmux, xray, "max_concurrency", "maxConcurrency");
convertField(singXmux, xray, "max_connections", "maxConnections");
convertField(singXmux, xray, "c_max_reuse_times", "cMaxReuseTimes");
convertField(singXmux, xray, "h_max_request_times","hMaxRequestTimes");
convertField(singXmux, xray, "h_max_reusable_secs","hMaxReusableSecs");
convertField(singXmux, xray, "h_keep_alive_period","hKeepAlivePeriod");
return xray;
}
};