Merge pull request #90 from sczheng189/feat/xhttp

feat(xhttp): add XHTTP proxy support and related parsing fixes
This commit is contained in:
asdlokj1qpi23 authored and GitHub committed 2026-06-08 14:36:19 +08:00
commit 948f049d1c
3 files changed
+152 -88

No files matched your search

+42 -1
View File
@@ -46,6 +46,36 @@ bool isNumeric(const std::string &str) {
return true;
}
bool isIntegerString(const std::string &str) {
if (str.empty())
return false;
size_t start = str[0] == '-' ? 1 : 0;
if (start == str.size())
return false;
for (size_t i = start; i < str.size(); i++) {
if (!std::isdigit(static_cast<unsigned char>(str[i]))) {
return false;
}
}
return true;
}
YAML::Node yamlScalarFromString(const std::string &value) {
YAML::Node node;
if (value == "true") {
node = true;
} else if (value == "false") {
node = false;
} else if (isIntegerString(value)) {
node = to_int(value);
} else {
node = value;
}
return node;
}
std::string
vmessLinkConstruct(const std::string &remarks, const std::string &add, const std::string &port, const std::string &type,
@@ -688,6 +718,17 @@ proxyToClash(std::vector<Proxy> &nodes, YAML::Node &yamlnode, const ProxyGroupCo
if (!x.Host.empty())
singleproxy["h2-opts"]["host"].push_back(x.Host);
break;
case "xhttp"_hash:
singleproxy["network"] = x.TransferProtocol;
singleproxy["xhttp-opts"]["path"] = x.Path;
if (!x.Host.empty())
singleproxy["xhttp-opts"]["host"] = x.Host;
for (const auto &option: x.XHTTPOptions) {
singleproxy["xhttp-opts"][option.first] = yamlScalarFromString(option.second);
}
if (!x.Edge.empty())
singleproxy["xhttp-opts"]["headers"]["Edge"] = x.Edge;
break;
case "grpc"_hash:
singleproxy["network"] = x.TransferProtocol;
singleproxy["grpc-opts"]["grpc-mode"] = x.GRPCMode;
@@ -1358,7 +1399,7 @@ std::string proxyToSingle(std::vector<Proxy> &nodes, int types, extra_settings &
proxyStr += "&packet-encoding=" + packet_encoding;
}
if (!alpns.empty()) {
proxyStr += "&alpn=" + alpns[0];
proxyStr += "&alpn=" + urlEncode(join(alpns, ","));
}
if (!sni.empty()) {
proxyStr += "&sni=" + sni;
+3
View File
@@ -1,6 +1,7 @@
#ifndef PROXY_H_INCLUDED
#define PROXY_H_INCLUDED
#include <map>
#include <string>
#include <vector>
@@ -8,6 +9,7 @@
using String = std::string;
using StringArray = std::vector<String>;
using StringMap = std::map<String, String>;
enum class ProxyType
{
@@ -147,6 +149,7 @@ struct Proxy {
String PacketEncoding;
String Multiplexing;
tribool V2rayHttpUpgrade;
StringMap XHTTPOptions;
};
#define SS_DEFAULT_GROUP "SSProvider"
+107 -87
View File
@@ -36,6 +36,30 @@ string_array ssr_ciphers = {
std::map<std::string, std::string> parsedMD5;
std::string modSSMD5 = "f7653207090ce3389115e9c88541afe0";
const string_array xhttp_option_keys = {
"mode",
"no-grpc-header",
"no-sse-header",
"x-padding-bytes",
"x-padding-obfs-mode",
"x-padding-key",
"x-padding-header",
"x-padding-placement",
"x-padding-method",
"uplink-http-method",
"session-placement",
"session-key",
"seq-placement",
"seq-key",
"uplink-data-placement",
"uplink-data-key",
"uplink-chunk-size",
"sc-max-each-post-bytes",
"sc-min-posts-interval-ms",
"sc-max-buffered-posts",
"sc-stream-up-server-secs"
};
//remake from speedtestutil
std::string removeBrackets(const std::string& input) {
std::string result = input;
@@ -49,6 +73,39 @@ std::string removeBrackets(const std::string& input) {
return result;
}
void extractRemark(std::string &link, std::string &remark) {
string_size pos = link.rfind("#");
if (pos != link.npos) {
remark = urlDecode(link.substr(pos + 1));
link.erase(pos);
}
pos = link.find("#");
if (pos != link.npos) {
link.erase(pos);
}
}
std::vector<std::string> getUrlAlpnList(const std::string &addition) {
std::vector<std::string> result;
std::string alpn = urlDecode(getUrlArg(addition, "alpn"));
if (alpn.empty())
return result;
auto alpns = split(alpn, ",");
for (auto &item: alpns) {
item = trim(item);
if (!item.empty())
result.emplace_back(item);
}
return result;
}
std::string getUrlAlpn(const std::string &addition) {
return join(getUrlAlpnList(addition), ",");
}
void commonConstruct(Proxy &node, ProxyType type, const std::string &group, const std::string &remarks,
const std::string &server, const std::string &port, const tribool &udp, const tribool &tfo,
const tribool &scv, const tribool &tls13, const std::string &underlying_proxy) {
@@ -77,6 +134,7 @@ void vmessConstruct(Proxy &node, const std::string &group, const std::string &re
node.TransferProtocol = net.empty() ? "tcp" : net;
node.Edge = edge;
node.ServerName = sni;
node.AlpnList = alpnList;
if (net == "quic") {
node.QUICSecure = host;
@@ -906,11 +964,8 @@ void explodeTrojan(std::string trojan, Proxy &node) {
if (startsWith(trojan, "trojan-go://")) {
trojan.erase(0, 12);
}
string_size pos = trojan.rfind('#');
if (pos != std::string::npos) {
remark = urlDecode(trojan.substr(pos + 1));
trojan.erase(pos);
}
extractRemark(trojan, remark);
string_size pos;
pos = trojan.find('?');
if (pos != std::string::npos) {
addition = trojan.substr(pos + 1);
@@ -956,11 +1011,7 @@ void explodeTrojan(std::string trojan, Proxy &node) {
remark = server + ":" + port;
if (group.empty())
group = TROJAN_DEFAULT_GROUP;
std::string alpn = getUrlArg(addition, "alpn");
std::vector<std::string> alpnList;
if (!alpn.empty()) {
alpnList.push_back(alpn);
}
std::vector<std::string> alpnList = getUrlAlpnList(addition);
trojanConstruct(node, group, remark, server, port, psk, network, host, path, fp, sni, alpnList, true, tribool(),
tfo, scv);
}
@@ -1462,6 +1513,23 @@ void explodeClash(Node yamlnode, std::vector<Proxy> &nodes) {
singleproxy["h2-opts"]["host"][0] >>= host;
edge.clear();
break;
case "xhttp"_hash:
path = singleproxy["xhttp-opts"]["path"].IsDefined()
? safe_as<std::string>(singleproxy["xhttp-opts"]["path"])
: "/";
singleproxy["xhttp-opts"]["host"] >>= host;
if (host.empty()) {
singleproxy["xhttp-opts"]["headers"]["Host"] >>= host;
}
singleproxy["xhttp-opts"]["headers"]["Edge"] >>= edge;
if (singleproxy["xhttp-opts"].IsMap()) {
for (const auto &key: xhttp_option_keys) {
if (singleproxy["xhttp-opts"][key].IsScalar()) {
node.XHTTPOptions[key] = safe_as<std::string>(singleproxy["xhttp-opts"][key]);
}
}
}
break;
case "grpc"_hash:
singleproxy["servername"] >>= host;
singleproxy["grpc-opts"]["grpc-service-name"] >>= path;
@@ -1618,11 +1686,7 @@ void explodeStdVMess(std::string vmess, Proxy &node) {
vmess = vmess.substr(8);
string_size pos;
pos = vmess.rfind('#');
if (pos != std::string::npos) {
remarks = urlDecode(vmess.substr(pos + 1));
vmess.erase(pos);
}
extractRemark(vmess, remarks);
const std::string stdvmess_matcher =
R"(^([a-z]+)(?:\+([a-z]+))?:([\da-f]{4}(?:[\da-f]{4}-){4}[\da-f]{12})-(\d+)@(.+):(\d+)(?:\/?\?(.*))?$)";
if (regGetMatch(vmess, stdvmess_matcher, 8, 0, &net, &tls, &id, &aid, &add, &port, &addition))
@@ -1649,11 +1713,7 @@ void explodeStdVMess(std::string vmess, Proxy &node) {
if (remarks.empty())
remarks = add + ":" + port;
std::string alpn = getUrlArg(addition, "alpn");
std::vector<std::string> alpnList;
if (!alpn.empty()) {
alpnList.push_back(alpn);
}
std::vector<std::string> alpnList = getUrlAlpnList(addition);
vmessConstruct(node, V2RAY_DEFAULT_GROUP, remarks, add, port, type, id, aid, net, "auto", path, host, "", tls, "",
alpnList);
}
@@ -1665,11 +1725,7 @@ void explodeStdHysteria(std::string hysteria, Proxy &node) {
hysteria = hysteria.substr(11);
string_size pos;
pos = hysteria.rfind("#");
if (pos != hysteria.npos) {
remarks = urlDecode(hysteria.substr(pos + 1));
hysteria.erase(pos);
}
extractRemark(hysteria, remarks);
const std::string stdhysteria_matcher = R"(^(.*)[:](\d+)[?](.*)$)";
if (regGetMatch(hysteria, stdhysteria_matcher, 4, 0, &add, &port, &addition))
return;
@@ -1680,16 +1736,12 @@ void explodeStdHysteria(std::string hysteria, Proxy &node) {
insecure = getUrlArg(addition, "insecure");
up = getUrlArg(addition, "upmbps");
down = getUrlArg(addition, "downmbps");
alpn = getUrlArg(addition, "alpn");
alpn = getUrlAlpn(addition);
obfsParam = getUrlArg(addition, "obfsParam");
sni = getUrlArg(addition, "peer");
if (remarks.empty())
remarks = add + ":" + port;
std::vector<std::string> alpnList;
if (!alpn.empty()) {
alpnList.push_back(alpn);
}
hysteriaConstruct(node, HYSTERIA_DEFAULT_GROUP, remarks, add, port, type, auth, auth_str, host, up, down, alpn,
obfsParam,
insecure, "", sni);
@@ -1705,11 +1757,7 @@ void explodeStdMieru(std::string mieru, Proxy &node) {
string_size pos;
// 提取 remarks
pos = mieru.rfind("#");
if (pos != mieru.npos) {
remarks = urlDecode(mieru.substr(pos + 1));
mieru.erase(pos);
}
extractRemark(mieru, remarks);
// 提取参数
pos = mieru.rfind("?");
@@ -1748,11 +1796,7 @@ void explodeStdHysteria2(std::string hysteria2, Proxy &node) {
hysteria2 = hysteria2.substr(12);
string_size pos;
pos = hysteria2.rfind("#");
if (pos != hysteria2.npos) {
remarks = urlDecode(hysteria2.substr(pos + 1));
hysteria2.erase(pos);
}
extractRemark(hysteria2, remarks);
pos = hysteria2.rfind("?");
if (pos != hysteria2.npos) {
@@ -1778,7 +1822,7 @@ void explodeStdHysteria2(std::string hysteria2, Proxy &node) {
scv = getUrlArg(addition, "insecure");
up = getUrlArg(addition, "up");
down = getUrlArg(addition, "down");
alpn = getUrlArg(addition, "alpn");
alpn = getUrlAlpn(addition);
obfsParam = getUrlArg(addition, "obfs");
obfsPassword = getUrlArg(addition, "obfs-password");
host = getUrlArg(addition, "sni");
@@ -1799,11 +1843,7 @@ void explodeStdVless(std::string vless, Proxy &node) {
vless = vless.substr(8);
string_size pos;
pos = vless.rfind("#");
if (pos != vless.npos) {
remarks = urlDecode(vless.substr(pos + 1));
vless.erase(pos);
}
extractRemark(vless, remarks);
const std::string stdvless_matcher =
R"(^([\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12})@\[?([\d\-a-zA-Z:.]+)\]?:(\d+)(?:\/?\?(.*))?$)";
if (regGetMatch(vless, stdvless_matcher, 5, 0, &id, &add, &port, &addition))
@@ -1817,11 +1857,7 @@ void explodeStdVless(std::string vless, Proxy &node) {
encryption = getUrlArg(addition, "encryption");
fp = getUrlArg(addition, "fp");
std::string packet_encoding = getUrlArg(addition, "packet-encoding");
std::string alpn = getUrlArg(addition, "alpn");
std::vector<std::string> alpnList;
if (!alpn.empty()) {
alpnList.push_back(alpn);
}
std::vector<std::string> alpnList = getUrlAlpnList(addition);
switch (hash_(net)) {
case "tcp"_hash:
case "ws"_hash:
@@ -1830,12 +1866,6 @@ void explodeStdVless(std::string vless, Proxy &node) {
host = getUrlArg(addition, strFind(addition, "sni") ? "sni" : "host");
path = getUrlArg(addition, "path");
break;
case "xhttp"_hash: // 新增对 type=xhttp 的支持
net = "h2"; // 视为 h2/http2 传输
type = getUrlArg(addition, "headerType");
host = getUrlArg(addition, strFind(addition, "sni") ? "sni" : "host");
path = getUrlArg(addition, "path");
break;
case "grpc"_hash:
host = getUrlArg(addition, "sni");
path = getUrlArg(addition, "serviceName");
@@ -1846,10 +1876,23 @@ void explodeStdVless(std::string vless, Proxy &node) {
host = getUrlArg(addition, strFind(addition, "sni") ? "sni" : "quicSecurity");
path = getUrlArg(addition, "key");
break;
case "xhttp"_hash:
type = getUrlArg(addition, "headerType");
host = getUrlArg(addition, strFind(addition, "sni") ? "sni" : "host");
path = getUrlArg(addition, "path");
for (const auto &key: xhttp_option_keys) {
std::string value = getUrlArg(addition, key);
if (value.empty()) {
value = getUrlArg(addition, replaceAllDistinct(key, "-", "_"));
}
if (!value.empty()) {
node.XHTTPOptions[key] = value;
}
}
break;
default:
return;
}
if (remarks.empty())
remarks = add + ":" + port;
sni = getUrlArg(addition, "sni");
@@ -1893,11 +1936,7 @@ void explodeShadowrocket(std::string rocket, Proxy &node) {
if (remarks.empty())
remarks = add + ":" + port;
std::string alpn = getUrlArg(addition, "alpn");
std::vector<std::string> alpnList;
if (!alpn.empty()) {
alpnList.push_back(alpn);
}
std::vector<std::string> alpnList = getUrlAlpnList(addition);
vmessConstruct(node, V2RAY_DEFAULT_GROUP, remarks, add, port, type, id, aid, net, cipher, path, host, "", tls, "",
alpnList);
}
@@ -1933,11 +1972,7 @@ void explodeKitsunebi(std::string kit, Proxy &node) {
if (remarks.empty())
remarks = add + ":" + port;
std::string alpn = getUrlArg(addition, "alpn");
std::vector<std::string> alpnList;
if (!alpn.empty()) {
alpnList.push_back(alpn);
}
std::vector<std::string> alpnList = getUrlAlpnList(addition);
vmessConstruct(node, V2RAY_DEFAULT_GROUP, remarks, add, port, type, id, aid, net, cipher, path, host, "", tls, "",
alpnList);
}
@@ -3199,11 +3234,7 @@ void explodeTuic(const std::string &tuic, Proxy &node) {
std::string link = tuic.substr(7);
string_size pos;
pos = link.rfind("#");
if (pos != std::string::npos) {
remarks = urlDecode(link.substr(pos + 1));
link.erase(pos);
}
extractRemark(link, remarks);
pos = link.rfind("?");
if (pos != std::string::npos) {
@@ -3242,7 +3273,7 @@ void explodeTuic(const std::string &tuic, Proxy &node) {
add = add.substr(1, add.length() - 2);
scv = getUrlArg(addition, "insecure");
alpn = getUrlArg(addition, "alpn");
alpn = getUrlAlpn(addition);
sni = getUrlArg(addition, "sni");
congestion_control = getUrlArg(addition, "congestion_control");
if (remarks.empty())
@@ -3262,11 +3293,7 @@ void explodeAnyTLS(std::string anytls, Proxy &node) {
anytls = anytls.substr(9);
string_size pos;
pos = anytls.rfind("#");
if (pos != anytls.npos) {
remarks = urlDecode(anytls.substr(pos + 1));
anytls.erase(pos);
}
extractRemark(anytls, remarks);
pos = anytls.rfind("?");
if (pos != anytls.npos) {
@@ -3292,14 +3319,7 @@ void explodeAnyTLS(std::string anytls, Proxy &node) {
if (remarks.empty())
remarks = add + ":" + port;
std::string alpn = getUrlArg(addition, "alpn");
if (!alpn.empty()) {
auto alpns = split(alpn, ",");
for (auto &item : alpns) {
if (!item.empty())
alpnList.emplace_back(item);
}
}
alpnList = getUrlAlpnList(addition);
fp = getUrlArg(addition, "fp");
if (fp.empty())