|
| 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