11import { describe , expect , it } from "vitest" ;
2+ import { configToYaml } from "../../generator/yaml" ;
3+ import type { ClashConfig } from "../../types/config" ;
24import { parseVLESS } from "./vless" ;
35
46const UUID = "11111111-1111-4111-8111-111111111111" ;
@@ -95,7 +97,7 @@ describe("parseVLESS", () => {
9597 } ) ;
9698 } ) ;
9799
98- it ( "parses Shadowrocket-style encoded authority and Reality defaults " , ( ) => {
100+ it ( "parses Shadowrocket-style encoded authority and ordinary TLS aliases " , ( ) => {
99101 const encoded = Buffer . from ( `prefix:${ UUID } @shadowrocket.example.com:443` ) . toString ( "base64url" ) ;
100102 const node = parseVLESS (
101103 `vless://${ encoded } ?obfs=websocket&tls=1&xtls=2&obfsParam=cdn.example.com&path=/ws%3Fed%3D512#Shadowrocket`
@@ -108,7 +110,6 @@ describe("parseVLESS", () => {
108110 uuid : UUID ,
109111 tls : true ,
110112 flow : "xtls-rprx-vision" ,
111- "client-fingerprint" : "chrome" ,
112113 network : "ws" ,
113114 "ws-opts" : {
114115 path : "/ws" ,
@@ -117,6 +118,8 @@ describe("parseVLESS", () => {
117118 "max-early-data" : 512 ,
118119 } ,
119120 } ) ;
121+ expect ( node ) . not . toHaveProperty ( "reality-opts" ) ;
122+ expect ( node ) . not . toHaveProperty ( "client-fingerprint" ) ;
120123
121124 const encodedWithoutPrefix = Buffer . from ( `${ UUID } @shadow-simple.example.com:443` ) . toString ( "base64url" ) ;
122125 expect ( parseVLESS ( `vless://${ encodedWithoutPrefix } ?obfs=websocket&tls=1&xtls=1&tls-verification=false&remark=SimpleSR` ) ) . toMatchObject ( {
@@ -129,6 +132,129 @@ describe("parseVLESS", () => {
129132 } ) ;
130133 } ) ;
131134
135+ it ( "parses Shadowrocket ordinary TLS with compound ECH through generated YAML" , ( ) => {
136+ const encoded = Buffer . from ( `:${ UUID } @vless-ech.example.com:443` ) . toString ( "base64url" ) ;
137+ const ech = encodeURIComponent ( "cloudflare-ech.com+https://203.0.113.53/dns-query" ) ;
138+ const obfsParam = encodeURIComponent (
139+ JSON . stringify ( {
140+ "" : {
141+ xPaddingHeader : "X-Request-Signature" ,
142+ xPaddingPlacement : "queryInHeader" ,
143+ xPaddingObfsMode : true ,
144+ xPaddingKey : "_tk" ,
145+ xPaddingMethod : "tokenish" ,
146+ } ,
147+ hOsT : "cdn.example.com" ,
148+ "X-Test" : "yes" ,
149+ Numeric : 1 ,
150+ Enabled : true ,
151+ } )
152+ ) ;
153+ const node = parseVLESS (
154+ `vless://${ encoded } ?obfs=xhttp&tls=1&peer=front.example.com&path=%2Fxhttp&obfsParam=${ obfsParam } &ech=${ ech } #ShadowrocketECH`
155+ ) ;
156+
157+ expect ( node ) . toMatchObject ( {
158+ name : "ShadowrocketECH" ,
159+ type : "vless" ,
160+ server : "vless-ech.example.com" ,
161+ port : 443 ,
162+ uuid : UUID ,
163+ tls : true ,
164+ servername : "front.example.com" ,
165+ network : "xhttp" ,
166+ "xhttp-opts" : {
167+ path : "/xhttp" ,
168+ host : "cdn.example.com" ,
169+ headers : {
170+ "X-Test" : "yes" ,
171+ } ,
172+ } ,
173+ "ech-opts" : {
174+ enable : true ,
175+ "query-server-name" : "cloudflare-ech.com" ,
176+ } ,
177+ } ) ;
178+ expect ( node ) . not . toHaveProperty ( "reality-opts" ) ;
179+ expect ( Object . prototype . hasOwnProperty . call ( node [ "xhttp-opts" ] || { } , "" ) ) . toBe ( false ) ;
180+ expect ( node [ "xhttp-opts" ] ?. headers ) . not . toHaveProperty ( "Numeric" ) ;
181+ expect ( node [ "xhttp-opts" ] ?. headers ) . not . toHaveProperty ( "Enabled" ) ;
182+
183+ const generated = configToYaml ( {
184+ proxies : [ node ] ,
185+ "proxy-groups" : [ ] ,
186+ "rule-providers" : { } ,
187+ rules : [ ] ,
188+ } as unknown as ClashConfig ) ;
189+ expect ( generated ) . toContain ( "ech-opts: {enable: true, query-server-name: cloudflare-ech.com}" ) ;
190+ expect ( generated ) . not . toContain ( "config: cloudflare-ech.com+" ) ;
191+ expect ( generated ) . not . toContain ( "reality-opts" ) ;
192+ expect ( generated ) . not . toContain ( "xPaddingHeader" ) ;
193+ expect ( generated ) . not . toContain ( "Numeric" ) ;
194+ } ) ;
195+
196+ it ( "keeps explicit xHTTP host and headers ahead of Shadowrocket obfsParam fallbacks" , ( ) => {
197+ const encoded = Buffer . from ( `${ UUID } @xhttp-precedence.example.com:443` ) . toString ( "base64url" ) ;
198+ const obfsParam = encodeURIComponent (
199+ JSON . stringify ( { Host : "fallback.example.com" , "X-Test" : "fallback" , "X-Fallback" : "yes" } )
200+ ) ;
201+ const explicitHeaders = encodeURIComponent ( JSON . stringify ( { "X-Test" : "explicit" , "X-Explicit" : "yes" } ) ) ;
202+
203+ expect (
204+ parseVLESS (
205+ `vless://${ encoded } ?obfs=xhttp&tls=1&xhttpHost=explicit.example.com&xhttpHeaders=${ explicitHeaders } &obfsParam=${ obfsParam } #XHTTPPrecedence`
206+ )
207+ ) . toMatchObject ( {
208+ "xhttp-opts" : {
209+ host : "explicit.example.com" ,
210+ headers : {
211+ "X-Test" : "explicit" ,
212+ "X-Fallback" : "yes" ,
213+ "X-Explicit" : "yes" ,
214+ } ,
215+ } ,
216+ } ) ;
217+
218+ expect (
219+ parseVLESS ( `vless://${ encoded } ?obfs=xhttp&tls=1&obfsParam=legacy.example.com#XHTTPLegacy` )
220+ ) . toMatchObject ( {
221+ "xhttp-opts" : {
222+ host : "legacy.example.com" ,
223+ } ,
224+ } ) ;
225+ } ) ;
226+
227+ it ( "infers implicit Shadowrocket Reality only from a public key" , ( ) => {
228+ const encoded = Buffer . from ( `${ UUID } @shadow-reality.example.com:443` ) . toString ( "base64url" ) ;
229+ const publicKey = "A" . repeat ( 43 ) ;
230+
231+ expect ( parseVLESS ( `vless://${ encoded } ?tls=1&pbk=${ publicKey } #ImplicitReality` ) ) . toMatchObject ( {
232+ name : "ImplicitReality" ,
233+ tls : true ,
234+ "client-fingerprint" : "chrome" ,
235+ "reality-opts" : {
236+ "public-key" : publicKey ,
237+ } ,
238+ } ) ;
239+ expect ( ( ) =>
240+ parseVLESS (
241+ `vless://${ encoded } ?tls=1&pbk=${ publicKey } &ech=${ encodeURIComponent ( "cloudflare-ech.com" ) } #BadRealityECH`
242+ )
243+ ) . toThrow ( "VLESS 启用 ECH 需要 security=tls" ) ;
244+
245+ const explicitTls = parseVLESS (
246+ `vless://${ encoded } ?security=tls&tls=1&pbk=${ publicKey } &ech=${ encodeURIComponent ( "cloudflare-ech.com" ) } #ExplicitTLS`
247+ ) ;
248+ expect ( explicitTls ) . toMatchObject ( {
249+ tls : true ,
250+ "ech-opts" : {
251+ enable : true ,
252+ "query-server-name" : "cloudflare-ech.com" ,
253+ } ,
254+ } ) ;
255+ expect ( explicitTls ) . not . toHaveProperty ( "reality-opts" ) ;
256+ } ) ;
257+
132258 it ( "parses TCP, H2, HTTP Upgrade, and Reality detail variants" , ( ) => {
133259 const publicKey = "A" . repeat ( 43 ) ;
134260
0 commit comments