Skip to content

Commit b60226b

Browse files
committed
xhttp extra导入导出时兼容xray格式
1 parent 5711249 commit b60226b

2 files changed

Lines changed: 230 additions & 3 deletions

File tree

app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ fun parseV2Ray(link: String): StandardV2RayBean {
138138
bean.xhttpMode = it
139139
}
140140
url.queryParameter("extra")?.let {
141-
bean.xhttpExtra = it
141+
bean.xhttpExtra = XhttpExtraConverter.xrayToSingBox(it)
142142
}
143143
}
144144
}
@@ -254,7 +254,7 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) {
254254
xhttpMode = it
255255
}
256256
url.queryParameter("extra")?.let {
257-
xhttpExtra = it
257+
xhttpExtra = XhttpExtraConverter.xrayToSingBox(it)
258258
}
259259
}
260260
}
@@ -522,7 +522,7 @@ fun StandardV2RayBean.toUriVMessVLESSTrojan(isTrojan: Boolean): String {
522522
builder.addQueryParameter("mode", xhttpMode)
523523
}
524524
if (xhttpExtra.isNotBlank()) {
525-
builder.addQueryParameter("extra", xhttpExtra)
525+
builder.addQueryParameter("extra", XhttpExtraConverter.singBoxToXray(xhttpExtra))
526526
}
527527
}
528528

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package io.nekohasekai.sagernet.fmt.v2ray
2+
3+
import org.json.JSONObject
4+
5+
object XhttpExtraConverter {
6+
7+
fun xrayToSingBox(xrayExtra: String): String {
8+
if (xrayExtra.isBlank()) return ""
9+
return try {
10+
val xray = JSONObject(xrayExtra)
11+
if (isSingBoxFormat(xray)) return xrayExtra
12+
val singBox = JSONObject()
13+
14+
convertField(xray, singBox, "xPaddingBytes", "x_padding_bytes")
15+
convertField(xray, singBox, "scMaxEachPostBytes", "sc_max_each_post_bytes")
16+
convertField(xray, singBox, "scMinPostsIntervalMs", "sc_min_posts_interval_ms")
17+
convertField(xray, singBox, "noGRPCHeader", "no_grpc_header")
18+
19+
if (xray.has("xmux")) {
20+
val xrayXmux = xray.getJSONObject("xmux")
21+
val singBoxXmux = JSONObject()
22+
convertField(xrayXmux, singBoxXmux, "maxConcurrency", "max_concurrency")
23+
convertField(xrayXmux, singBoxXmux, "maxConnections", "max_connections")
24+
convertField(xrayXmux, singBoxXmux, "cMaxReuseTimes", "c_max_reuse_times")
25+
convertField(xrayXmux, singBoxXmux, "hMaxRequestTimes", "h_max_request_times")
26+
convertField(xrayXmux, singBoxXmux, "hMaxReusableSecs", "h_max_reusable_secs")
27+
convertField(xrayXmux, singBoxXmux, "hKeepAlivePeriod", "h_keep_alive_period")
28+
if (singBoxXmux.length() > 0) singBox.put("xmux", singBoxXmux)
29+
}
30+
31+
if (xray.has("downloadSettings")) {
32+
val xrayDown = xray.getJSONObject("downloadSettings")
33+
val singBoxDown = JSONObject()
34+
35+
xrayDown.optJSONObject("xhttpSettings")?.let { xhttpSettings ->
36+
convertField(xhttpSettings, singBoxDown, "mode", "mode")
37+
convertField(xhttpSettings, singBoxDown, "host", "host")
38+
convertField(xhttpSettings, singBoxDown, "path", "path")
39+
}
40+
convertField(xrayDown, singBoxDown, "address", "server")
41+
convertField(xrayDown, singBoxDown, "port", "server_port")
42+
43+
if (xrayDown.has("security")) {
44+
val tls = JSONObject().apply { put("enabled", true) }
45+
46+
when (xrayDown.getString("security")) {
47+
"tls" -> {
48+
xrayDown.optJSONObject("tlsSettings")?.let { tlsSettings ->
49+
convertField(tlsSettings, tls, "serverName", "server_name")
50+
convertField(tlsSettings, tls, "alpn", "alpn")
51+
convertField(tlsSettings, tls, "allowInsecure", "insecure")
52+
tlsSettings.optString("fingerprint")?.let { fp ->
53+
if (fp.isNotBlank()) {
54+
val utls = JSONObject().apply {
55+
put("enabled", true)
56+
put("fingerprint", fp)
57+
}
58+
tls.put("utls", utls)
59+
}
60+
}
61+
}
62+
}
63+
"reality" -> {
64+
xrayDown.optJSONObject("realitySettings")?.let { realitySettings ->
65+
convertField(realitySettings, tls, "serverName", "server_name")
66+
val reality = JSONObject().apply {
67+
put("enabled", true)
68+
convertField(realitySettings, this, "publicKey", "public_key")
69+
convertField(realitySettings, this, "shortId", "short_id")
70+
}
71+
tls.put("reality", reality)
72+
realitySettings.optString("fingerprint")?.let { fp ->
73+
if (fp.isNotBlank()) {
74+
val utls = JSONObject().apply {
75+
put("enabled", true)
76+
put("fingerprint", fp)
77+
}
78+
tls.put("utls", utls)
79+
}
80+
}
81+
}
82+
}
83+
}
84+
singBoxDown.put("tls", tls)
85+
}
86+
87+
xrayDown.optJSONObject("xhttpSettings")?.optJSONObject("extra")?.let { extraXmux ->
88+
val downXmux = JSONObject()
89+
convertField(extraXmux, downXmux, "maxConcurrency", "max_concurrency")
90+
convertField(extraXmux, downXmux, "maxConnections", "max_connections")
91+
convertField(extraXmux, downXmux, "cMaxReuseTimes", "c_max_reuse_times")
92+
convertField(extraXmux, downXmux, "hMaxRequestTimes", "h_max_request_times")
93+
convertField(extraXmux, downXmux, "hMaxReusableSecs", "h_max_reusable_secs")
94+
convertField(extraXmux, downXmux, "hKeepAlivePeriod", "h_keep_alive_period")
95+
if (downXmux.length() > 0) singBoxDown.put("xmux", downXmux)
96+
97+
convertField(extraXmux, singBoxDown, "xPaddingBytes", "x_padding_bytes")
98+
convertField(extraXmux, singBoxDown, "scMaxEachPostBytes", "sc_max_each_post_bytes")
99+
convertField(extraXmux, singBoxDown, "scMinPostsIntervalMs", "sc_min_posts_interval_ms")
100+
convertField(extraXmux, singBoxDown, "noGRPCHeader", "no_grpc_header")
101+
}
102+
103+
if (singBoxDown.length() > 0) singBox.put("download", singBoxDown)
104+
}
105+
106+
singBox.toString(2).replace("\\/", "/")
107+
} catch (e: Exception) {
108+
e.printStackTrace()
109+
xrayExtra
110+
}
111+
}
112+
113+
fun singBoxToXray(singBoxExtra: String): String {
114+
if (singBoxExtra.isBlank()) return ""
115+
return try {
116+
val singBox = JSONObject(singBoxExtra)
117+
if (isXrayFormat(singBox)) return singBoxExtra
118+
val xray = JSONObject()
119+
120+
convertField(singBox, xray, "x_padding_bytes", "xPaddingBytes")
121+
convertField(singBox, xray, "sc_max_each_post_bytes", "scMaxEachPostBytes")
122+
convertField(singBox, xray, "sc_min_posts_interval_ms", "scMinPostsIntervalMs")
123+
convertField(singBox, xray, "no_grpc_header", "noGRPCHeader")
124+
125+
if (singBox.has("xmux")) {
126+
val singBoxXmux = singBox.getJSONObject("xmux")
127+
val xrayXmux = JSONObject()
128+
convertField(singBoxXmux, xrayXmux, "max_concurrency", "maxConcurrency")
129+
convertField(singBoxXmux, xrayXmux, "max_connections", "maxConnections")
130+
convertField(singBoxXmux, xrayXmux, "c_max_reuse_times", "cMaxReuseTimes")
131+
convertField(singBoxXmux, xrayXmux, "h_max_request_times", "hMaxRequestTimes")
132+
convertField(singBoxXmux, xrayXmux, "h_max_reusable_secs", "hMaxReusableSecs")
133+
convertField(singBoxXmux, xrayXmux, "h_keep_alive_period", "hKeepAlivePeriod")
134+
if (xrayXmux.length() > 0) xray.put("xmux", xrayXmux)
135+
}
136+
137+
if (singBox.has("download")) {
138+
val singBoxDown = singBox.getJSONObject("download")
139+
val xrayDown = JSONObject()
140+
141+
convertField(singBoxDown, xrayDown, "server", "address")
142+
convertField(singBoxDown, xrayDown, "server_port", "port")
143+
xrayDown.put("network", "xhttp")
144+
145+
if (singBoxDown.has("tls")) {
146+
val tls = singBoxDown.getJSONObject("tls")
147+
148+
if (tls.has("reality") && tls.getJSONObject("reality").optBoolean("enabled", false)) {
149+
xrayDown.put("security", "reality")
150+
val reality = tls.getJSONObject("reality")
151+
val realitySettings = JSONObject()
152+
convertField(tls, realitySettings, "server_name", "serverName")
153+
convertField(reality, realitySettings, "public_key", "publicKey")
154+
convertField(reality, realitySettings, "short_id", "shortId")
155+
if (tls.has("utls")) {
156+
val utls = tls.getJSONObject("utls")
157+
convertField(utls, realitySettings, "fingerprint", "fingerprint")
158+
}
159+
xrayDown.put("realitySettings", realitySettings)
160+
} else {
161+
xrayDown.put("security", "tls")
162+
val tlsSettings = JSONObject()
163+
convertField(tls, tlsSettings, "server_name", "serverName")
164+
convertField(tls, tlsSettings, "alpn", "alpn")
165+
convertField(tls, tlsSettings, "insecure", "allowInsecure")
166+
if (tls.has("utls")) {
167+
val utls = tls.getJSONObject("utls")
168+
convertField(utls, tlsSettings, "fingerprint", "fingerprint")
169+
}
170+
xrayDown.put("tlsSettings", tlsSettings)
171+
}
172+
}
173+
174+
val xhttpSettings = JSONObject()
175+
convertField(singBoxDown, xhttpSettings, "mode", "mode")
176+
convertField(singBoxDown, xhttpSettings, "host", "host")
177+
convertField(singBoxDown, xhttpSettings, "path", "path")
178+
179+
val xhttpExtra = JSONObject()
180+
convertField(singBoxDown, xhttpExtra, "x_padding_bytes", "xPaddingBytes")
181+
convertField(singBoxDown, xhttpExtra, "sc_max_each_post_bytes", "scMaxEachPostBytes")
182+
convertField(singBoxDown, xhttpExtra, "sc_min_posts_interval_ms", "scMinPostsIntervalMs")
183+
convertField(singBoxDown, xhttpExtra, "no_grpc_header", "noGRPCHeader")
184+
185+
if (singBoxDown.has("xmux")) {
186+
val singBoxDownXmux = singBoxDown.getJSONObject("xmux")
187+
val xrayDownXmux = JSONObject()
188+
convertField(singBoxDownXmux, xrayDownXmux, "max_concurrency", "maxConcurrency")
189+
convertField(singBoxDownXmux, xrayDownXmux, "max_connections", "maxConnections")
190+
convertField(singBoxDownXmux, xrayDownXmux, "c_max_reuse_times", "cMaxReuseTimes")
191+
convertField(singBoxDownXmux, xrayDownXmux, "h_max_request_times", "hMaxRequestTimes")
192+
convertField(singBoxDownXmux, xrayDownXmux, "h_max_reusable_secs", "hMaxReusableSecs")
193+
convertField(singBoxDownXmux, xrayDownXmux, "h_keep_alive_period", "hKeepAlivePeriod")
194+
if (xrayDownXmux.length() > 0) xhttpExtra.put("xmux", xrayDownXmux)
195+
}
196+
197+
if (xhttpExtra.length() > 0) xhttpSettings.put("extra", xhttpExtra)
198+
xrayDown.put("xhttpSettings", xhttpSettings)
199+
200+
if (xrayDown.length() > 0) xray.put("downloadSettings", xrayDown)
201+
}
202+
203+
xray.toString(2).replace("\\/", "/")
204+
} catch (e: Exception) {
205+
e.printStackTrace()
206+
singBoxExtra
207+
}
208+
}
209+
210+
private fun isSingBoxFormat(json: JSONObject): Boolean {
211+
return json.has("x_padding_bytes") || json.has("sc_max_each_post_bytes") ||
212+
json.has("sc_min_posts_interval_ms") || json.has("sc_stream_up_server_secs") ||
213+
json.has("download")
214+
}
215+
216+
private fun isXrayFormat(json: JSONObject): Boolean {
217+
return json.has("xPaddingBytes") || json.has("scMaxEachPostBytes") ||
218+
json.has("scMinPostsIntervalMs") || json.has("scStreamUpServerSecs") ||
219+
json.has("downloadSettings")
220+
}
221+
222+
private fun convertField(from: JSONObject, to: JSONObject, fromKey: String, toKey: String) {
223+
if (from.has(fromKey)) {
224+
to.put(toKey, from.get(fromKey))
225+
}
226+
}
227+
}

0 commit comments

Comments
 (0)