Skip to content

Commit 1be32ae

Browse files
committed
feat: 统一 h2 host 兼容逻辑并补齐 Egern fingerprint_sha256 下发
- 解析层把 VMess/VLESS 的 h2 传输 Host 改为统一字段: 将 h2 的 host 从 headers 里搬到 h2-opts.host,支持逗号分隔多 Host,清理空 headers,并保持 path 默认值兼容。 - 同步修正 VMess 解析: V2rayN 中 `net=http` 识别为 h2 处理,VMess/VLESS 生成时支持 host 列表和默认 `path='/'`,避免历史格式不一致。 - 统一 Clash / ClashMeta / Shadowrocket / Stash 的 h2-opts 处理: 兼容 `h2-opts.host` 与 `h2-opts.headers.host|Host`,输出统一为 `host` 数组并移除多余 host header。 - Egern 生产端增强: 支持 h2 作为受限网络分支,添加 http2 transport 输出(含 path/headers/sni/skip_tls_verify), 并将 `tls-fingerprint` 转换为 `fingerprint_sha256`,支持根配置与 transport 透传(http2/tls/wss),空值/空白值会被剔除。 - 测试同步覆盖: 增加/调整 parser、structured producer、text producer 测试,覆盖 h2 host 多值、Host header 保留/提升、以及 fingerprint_sha256 生效与不生效场景。
1 parent baccae7 commit 1be32ae

12 files changed

Lines changed: 764 additions & 51 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.23.21",
3+
"version": "2.23.22",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"packageManager": "pnpm@11.0.9",

backend/src/core/proxy-utils/index.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -819,24 +819,28 @@ function lastParse(proxy) {
819819
if (proxy.network) {
820820
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
821821
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
822-
if (proxy.network === 'h2') {
823-
if (!transporthost && transportHost) {
824-
proxy[`${proxy.network}-opts`].headers.host = transportHost;
825-
delete proxy[`${proxy.network}-opts`].headers.Host;
826-
}
827-
} else if (transporthost && !transportHost) {
822+
if (proxy.network !== 'h2' && transporthost && !transportHost) {
828823
proxy[`${proxy.network}-opts`].headers.Host = transporthost;
829824
delete proxy[`${proxy.network}-opts`].headers.host;
830825
}
831826
}
832827
if (proxy.network === 'h2') {
833-
const host = proxy['h2-opts']?.headers?.host;
834-
const path = proxy['h2-opts']?.path;
835-
if (host && !Array.isArray(host)) {
836-
proxy['h2-opts'].headers.host = [host];
828+
const h2Opts = proxy['h2-opts'];
829+
const host =
830+
h2Opts?.host ?? h2Opts?.headers?.host ?? h2Opts?.headers?.Host;
831+
const path = h2Opts?.path;
832+
if (host) {
833+
h2Opts.host = Array.isArray(host) ? host : [host];
834+
}
835+
if (h2Opts?.headers) {
836+
delete h2Opts.headers.host;
837+
delete h2Opts.headers.Host;
838+
if (Object.keys(h2Opts.headers).length === 0) {
839+
delete h2Opts.headers;
840+
}
837841
}
838842
if (Array.isArray(path)) {
839-
proxy['h2-opts'].path = path[0];
843+
h2Opts.path = path[0];
840844
}
841845
}
842846

@@ -872,7 +876,10 @@ function lastParse(proxy) {
872876
if (proxy.tls && !proxy.sni && proxy.sni !== '') {
873877
// 传输层若有设置就使用
874878
if (proxy.network) {
875-
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
879+
let transportHost =
880+
proxy.network === 'h2'
881+
? proxy['h2-opts']?.host
882+
: proxy[`${proxy.network}-opts`]?.headers?.Host;
876883
transportHost = Array.isArray(transportHost)
877884
? transportHost[0]
878885
: transportHost;

backend/src/core/proxy-utils/parsers/index.js

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ function parseEarlyDataSize(value) {
102102
return parsed;
103103
}
104104

105+
function splitURIHostList(host) {
106+
if (Array.isArray(host)) {
107+
return host.flatMap((item) => splitURIHostList(item) || []);
108+
}
109+
if (typeof host !== 'string') {
110+
return host == null ? undefined : [host];
111+
}
112+
const hosts = host
113+
.split(',')
114+
.map((item) => item.trim())
115+
.filter(Boolean);
116+
return hosts.length > 0 ? hosts : undefined;
117+
}
118+
105119
function parseWireGuardURIAddressValue(value) {
106120
if (value == null) return null;
107121
const raw = `${value}`.trim();
@@ -740,11 +754,12 @@ function URI_VMess() {
740754
if (params.net === 'ws' || params.obfs === 'websocket') {
741755
proxy.network = 'ws';
742756
} else if (
743-
['http'].includes(params.net) ||
744757
['http'].includes(params.obfs) ||
745758
['http'].includes(params.type)
746759
) {
747760
proxy.network = 'http';
761+
} else if (params.net === 'http') {
762+
proxy.network = 'h2';
748763
} else if (['grpc', 'kcp', 'quic'].includes(params.net)) {
749764
proxy.network = params.net;
750765
} else if (
@@ -805,6 +820,10 @@ function URI_VMess() {
805820
} else {
806821
transportPath = '/';
807822
}
823+
} else if (proxy.network === 'h2') {
824+
if (!transportPath) {
825+
transportPath = '/';
826+
}
808827
}
809828
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点
810829
if (
@@ -832,8 +851,18 @@ function URI_VMess() {
832851
} else {
833852
const opts = {
834853
path: getIfNotBlank(transportPath),
835-
headers: { Host: getIfNotBlank(transportHost) },
836854
};
855+
const normalizedTransportHost =
856+
getIfNotBlank(transportHost);
857+
if (proxy.network === 'h2') {
858+
const h2Hosts =
859+
splitURIHostList(normalizedTransportHost);
860+
if (h2Hosts) {
861+
opts.host = h2Hosts;
862+
}
863+
} else {
864+
opts.headers = { Host: normalizedTransportHost };
865+
}
837866
if (httpupgrade) {
838867
opts['v2ray-http-upgrade'] = true;
839868
httpUpgradeEd =
@@ -1860,6 +1889,15 @@ function URI_VLESS() {
18601889
delete opts.headers;
18611890
}
18621891
}
1892+
const h2Host = opts.headers?.Host ?? opts.headers?.host;
1893+
if (['h2'].includes(proxy.network) && h2Host) {
1894+
opts.host = splitURIHostList(h2Host);
1895+
delete opts.headers.Host;
1896+
delete opts.headers.host;
1897+
if (Object.keys(opts.headers).length === 0) {
1898+
delete opts.headers;
1899+
}
1900+
}
18631901
}
18641902
if (params.serviceName) {
18651903
opts[`${proxy.network}-service-name`] = params.serviceName;
@@ -1880,6 +1918,8 @@ function URI_VLESS() {
18801918
pathEarlyData = extracted.ed;
18811919
}
18821920
opts.path = transportPath;
1921+
} else if (proxy.network === 'h2') {
1922+
opts.path = '/';
18831923
}
18841924
if (proxy.network === 'http' && params.method) {
18851925
opts.method = params.method;

backend/src/core/proxy-utils/producers/clash.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,27 @@ export default function Clash_Producer() {
138138
) {
139139
proxy['h2-opts'].path = path[0];
140140
}
141-
let host = proxy['h2-opts']?.headers?.host;
141+
let host =
142+
proxy['h2-opts']?.host ??
143+
proxy['h2-opts']?.headers?.host ??
144+
proxy['h2-opts']?.headers?.Host;
142145
if (
143-
isPresent(proxy, 'h2-opts.headers.Host') &&
144-
!Array.isArray(host)
146+
(isPresent(proxy, 'h2-opts.host') ||
147+
isPresent(proxy, 'h2-opts.headers.host') ||
148+
isPresent(proxy, 'h2-opts.headers.Host'))
145149
) {
146-
proxy['h2-opts'].headers.host = [host];
150+
proxy['h2-opts'].host = Array.isArray(host)
151+
? host
152+
: [host];
153+
}
154+
if (proxy['h2-opts']?.headers) {
155+
delete proxy['h2-opts'].headers.host;
156+
delete proxy['h2-opts'].headers.Host;
157+
if (
158+
Object.keys(proxy['h2-opts'].headers).length === 0
159+
) {
160+
delete proxy['h2-opts'].headers;
161+
}
147162
}
148163
}
149164
if (['ws'].includes(proxy.network)) {

backend/src/core/proxy-utils/producers/clashmeta.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,27 @@ export default function ClashMeta_Producer() {
310310
) {
311311
proxy['h2-opts'].path = path[0];
312312
}
313-
let host = proxy['h2-opts']?.headers?.host;
313+
let host =
314+
proxy['h2-opts']?.host ??
315+
proxy['h2-opts']?.headers?.host ??
316+
proxy['h2-opts']?.headers?.Host;
314317
if (
315-
isPresent(proxy, 'h2-opts.headers.Host') &&
316-
!Array.isArray(host)
318+
(isPresent(proxy, 'h2-opts.host') ||
319+
isPresent(proxy, 'h2-opts.headers.host') ||
320+
isPresent(proxy, 'h2-opts.headers.Host'))
317321
) {
318-
proxy['h2-opts'].headers.host = [host];
322+
proxy['h2-opts'].host = Array.isArray(host)
323+
? host
324+
: [host];
325+
}
326+
if (proxy['h2-opts']?.headers) {
327+
delete proxy['h2-opts'].headers.host;
328+
delete proxy['h2-opts'].headers.Host;
329+
if (
330+
Object.keys(proxy['h2-opts'].headers).length === 0
331+
) {
332+
delete proxy['h2-opts'].headers;
333+
}
319334
}
320335
}
321336
if (['ws'].includes(proxy.network)) {

backend/src/core/proxy-utils/producers/egern.js

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,15 @@ export default function Egern_Producer() {
6161
'2022-blake3-aes-256-gcm',
6262
].includes(proxy.cipher))) ||
6363
(proxy.type === 'vmess' &&
64-
!['http', 'ws', 'tcp'].includes(proxy.network) &&
64+
!['h2', 'http', 'ws', 'tcp'].includes(proxy.network) &&
6565
proxy.network) ||
6666
(proxy.type === 'trojan' &&
6767
!['http', 'ws', 'tcp'].includes(proxy.network) &&
6868
proxy.network) ||
6969
(proxy.type === 'vless' &&
70-
((!['http', 'ws', 'tcp'].includes(proxy.network) &&
70+
((!['h2', 'http', 'ws', 'tcp'].includes(
71+
proxy.network,
72+
) &&
7173
proxy.network) ||
7274
(typeof proxy.flow !== 'undefined' &&
7375
!['xtls-rprx-vision', ''].includes(
@@ -132,6 +134,24 @@ export default function Egern_Producer() {
132134
}
133135
: {}),
134136
};
137+
} else if (proxy.type === 'https') {
138+
proxy = {
139+
type: 'https',
140+
name: proxy.name,
141+
server: proxy.server,
142+
port: proxy.port,
143+
username: proxy.username,
144+
password: proxy.password,
145+
...(hasHeaders(proxy)
146+
? {
147+
headers: proxy.headers,
148+
}
149+
: {}),
150+
tfo: proxy.tfo || proxy['fast-open'],
151+
next_hop: proxy.next_hop,
152+
sni: proxy.sni,
153+
skip_tls_verify: proxy['skip-cert-verify'],
154+
};
135155
} else if (proxy.type === 'socks5') {
136156
proxy = {
137157
type: 'socks5',
@@ -312,13 +332,8 @@ export default function Egern_Producer() {
312332
path: Array.isArray(proxy['h2-opts']?.path)
313333
? proxy['h2-opts']?.path[0]
314334
: proxy['h2-opts']?.path,
315-
headers: {
316-
Host: Array.isArray(
317-
proxy['h2-opts']?.headers?.Host,
318-
)
319-
? proxy['h2-opts']?.headers?.Host[0]
320-
: proxy['h2-opts']?.headers?.Host,
321-
},
335+
headers: getH2Headers(proxy['h2-opts']),
336+
sni: proxy.sni,
322337
skip_tls_verify: proxy['skip-cert-verify'],
323338
},
324339
};
@@ -393,6 +408,18 @@ export default function Egern_Producer() {
393408
skip_tls_verify: proxy['skip-cert-verify'],
394409
},
395410
};
411+
} else if (proxy.network === 'h2') {
412+
proxy.transport = {
413+
http2: {
414+
method: proxy['h2-opts']?.method,
415+
path: Array.isArray(proxy['h2-opts']?.path)
416+
? proxy['h2-opts']?.path[0]
417+
: proxy['h2-opts']?.path,
418+
headers: getH2Headers(proxy['h2-opts']),
419+
sni: proxy.sni,
420+
skip_tls_verify: proxy['skip-cert-verify'],
421+
},
422+
};
396423
} else if (proxy.network === 'tcp' || !proxy.network) {
397424
let reality;
398425
if (
@@ -518,6 +545,16 @@ export default function Egern_Producer() {
518545
};
519546
}
520547
}
548+
const fingerprintSha256 = getFingerprintSha256(original);
549+
if (fingerprintSha256) {
550+
if (supportsRootFingerprintSha256(original, proxy)) {
551+
proxy.fingerprint_sha256 = fingerprintSha256;
552+
}
553+
addTransportFingerprintSha256(
554+
proxy.transport,
555+
fingerprintSha256,
556+
);
557+
}
521558
if (
522559
[
523560
'socks5',
@@ -611,3 +648,72 @@ function hasHeaders(proxy) {
611648
Object.keys(proxy.headers).length > 0
612649
);
613650
}
651+
652+
function getFirstHeaderValue(headers, ...keys) {
653+
for (const key of keys) {
654+
const value = getFirstValue(headers?.[key]);
655+
if (value) return value;
656+
}
657+
return undefined;
658+
}
659+
660+
function getFirstH2Host(h2Opts) {
661+
return (
662+
getFirstValue(h2Opts?.host) ||
663+
getFirstHeaderValue(h2Opts?.headers, 'host', 'Host')
664+
);
665+
}
666+
667+
function getH2Headers(h2Opts) {
668+
const headers = {};
669+
if (
670+
h2Opts?.headers &&
671+
typeof h2Opts.headers === 'object' &&
672+
!Array.isArray(h2Opts.headers)
673+
) {
674+
for (const [key, value] of Object.entries(h2Opts.headers)) {
675+
if (/^host$/i.test(key)) continue;
676+
const headerValue = getFirstValue(value);
677+
if (headerValue != null) {
678+
headers[key] = headerValue;
679+
}
680+
}
681+
}
682+
const host = getFirstH2Host(h2Opts);
683+
if (host) {
684+
headers.Host = host;
685+
}
686+
return Object.keys(headers).length > 0 ? headers : undefined;
687+
}
688+
689+
function getFirstValue(value) {
690+
if (Array.isArray(value)) return value[0];
691+
if (value != null) return value;
692+
return undefined;
693+
}
694+
695+
function getFingerprintSha256(proxy) {
696+
const fingerprint = proxy?.['tls-fingerprint'];
697+
if (typeof fingerprint !== 'string') return undefined;
698+
const trimmedFingerprint = fingerprint.trim();
699+
return trimmedFingerprint.length > 0 ? trimmedFingerprint : undefined;
700+
}
701+
702+
function supportsRootFingerprintSha256(original, proxy) {
703+
return (
704+
['anytls', 'https', 'hysteria2', 'trojan', 'tuic'].includes(
705+
original.type,
706+
) ||
707+
(original.type === 'http' && proxy.type === 'https')
708+
);
709+
}
710+
711+
function addTransportFingerprintSha256(transport, fingerprintSha256) {
712+
if (!transport) return;
713+
714+
for (const key of ['http2', 'tls', 'wss']) {
715+
if (transport[key]) {
716+
transport[key].fingerprint_sha256 = fingerprintSha256;
717+
}
718+
}
719+
}

0 commit comments

Comments
 (0)