owenclave / app/src/main/java/io/nekohasekai/sagernet/group/ClashYAMLParser.kt

github - dev - back to tree - upstream
/****************************************************************************** * * * Copyright (C) 2024 dyhkwong * * Copyright (C) 2021 by nekohasekai * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ******************************************************************************/ package io.nekohasekai.sagernet.group import com.github.shadowsocks.plugin.PluginOptions import com.google.gson.GsonBuilder import com.google.gson.JsonObject import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.fmt.AbstractBean import io.nekohasekai.sagernet.fmt.anytls.AnyTLSBean import io.nekohasekai.sagernet.fmt.http.HttpBean import io.nekohasekai.sagernet.fmt.hysteria2.Hysteria2Bean import io.nekohasekai.sagernet.fmt.mieru.MieruBean import io.nekohasekai.sagernet.fmt.shadowquic.ShadowQUICBean import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean import io.nekohasekai.sagernet.fmt.shadowsocks.supportedShadowsocksMethod import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean import io.nekohasekai.sagernet.fmt.shadowsocksr.supportedShadowsocksRMethod import io.nekohasekai.sagernet.fmt.shadowsocksr.supportedShadowsocksRObfs import io.nekohasekai.sagernet.fmt.shadowsocksr.supportedShadowsocksRProtocol import io.nekohasekai.sagernet.fmt.socks.SOCKSBean import io.nekohasekai.sagernet.fmt.ssh.SSHBean import io.nekohasekai.sagernet.fmt.trojan.TrojanBean import io.nekohasekai.sagernet.fmt.trusttunnel.TrustTunnelBean import io.nekohasekai.sagernet.fmt.snell.SnellBean import io.nekohasekai.sagernet.fmt.tuic5.Tuic5Bean import io.nekohasekai.sagernet.fmt.tuic5.supportedTuic5CongestionControl import io.nekohasekai.sagernet.fmt.tuic5.supportedTuic5RelayMode import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean import io.nekohasekai.sagernet.fmt.v2ray.VMessBean import io.nekohasekai.sagernet.fmt.v2ray.supportedKcpQuicHeaderType import io.nekohasekai.sagernet.fmt.v2ray.supportedVmessMethod import io.nekohasekai.sagernet.fmt.v2ray.supportedXhttpMode import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean import io.nekohasekai.sagernet.ktx.* import kotlin.io.encoding.Base64 import libexclavecore.Libexclavecore fun parseClashProxies(proxies: List>): List { val beans = mutableListOf() proxies.forEach { beans.addAll(parseClashProxy(it)) } return beans } @Suppress("UNCHECKED_CAST") fun parseClashProxy(proxy: Map): List { when (proxy["type"]) { "socks5" -> { return listOf(SOCKSBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() username = proxy.getString("username") password = proxy.getString("password") if (proxy.getBoolean("tls") == true) { security = "tls" if (proxy.getBoolean("skip-cert-verify") == true) { allowInsecure = true } proxy.getString("fingerprint")?.replace(":", "")?.trim()?.takeIf { it.isNotEmpty() }?.takeIf { it.isNotEmpty() }?.also { pinnedPeerCertificateSha256 = it allowInsecure = true } val cert = proxy.getString("certificate")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----") } val key = proxy.getString("private-key")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----") } if (cert != null && key != null) { mtlsCertificate = cert mtlsCertificatePrivateKey = key } serverNameToVerify = proxy.getString("name-cert-verify") } name = proxy.getString("name") }) } "http" -> { return listOf(HttpBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() username = proxy.getString("username") password = proxy.getString("password") if (proxy.getBoolean("tls") == true) { security = "tls" // alpn = "http/1.1" // mihomo does not support HTTP/2 CONNECT sni = proxy.getString("sni") if (proxy.getBoolean("skip-cert-verify") == true) { allowInsecure = true } proxy.getString("fingerprint")?.replace(":", "")?.trim()?.takeIf { it.isNotEmpty() }?.also { pinnedPeerCertificateSha256 = it allowInsecure = true } val cert = proxy.getString("certificate")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----") } val key = proxy.getString("private-key")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----") } if (cert != null && key != null) { mtlsCertificate = cert mtlsCertificatePrivateKey = key } serverNameToVerify = proxy.getString("name-cert-verify") } name = proxy.getString("name") }) } "ss" -> { var pluginStr = "" if (proxy.contains("plugin")) { val opts = proxy.getObject("plugin-opts") val pluginOpts = PluginOptions() when (proxy.getString("plugin")) { "obfs" -> { pluginOpts.id = "obfs-local" when (opts?.getString("mode")) { null, "http" -> pluginOpts["obfs"] = "http" "tls" -> pluginOpts["obfs"] = "tls" else -> return listOf() } when (val host = opts?.getString("host")) { // https://github.com/MetaCubeX/mihomo/blob/903371719021d74cbec9cd24efb66e386c8b7595/adapter/outbound/shadowsocks.go#L307 null -> pluginOpts["obfs-host"] = "bing.com" else -> pluginOpts["obfs-host"] = host } } "v2ray-plugin" -> { pluginOpts.id = "v2ray-plugin" when (opts?.getString("mode")) { null, "websocket" -> {} else -> return listOf() } if (opts?.getBoolean("tls") == true) { pluginOpts["tls"] = null } when (val host = opts?.getString("host")) { // https://github.com/MetaCubeX/mihomo/blob/903371719021d74cbec9cd24efb66e386c8b7595/adapter/outbound/shadowsocks.go#L318 null -> pluginOpts["host"] = "bing.com" else -> pluginOpts["host"] = host } opts?.getString("path")?.let { pluginOpts["path"] = it } when (opts?.getBoolean("mux")) { false -> pluginOpts["mux"] = "0" true, null -> { /*pluginOpts["mux"] = "1"*/ } } if (opts?.getBoolean("v2ray-http-upgrade") == true) { return listOf() } } "", null -> {} else -> return listOf() } pluginStr = pluginOpts.toString(false) } return listOf(ShadowsocksBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() password = proxy.getString("password") method = when (val cipher = proxy.getString("cipher")?.lowercase()) { "dummy" -> "none" "aead_aes_128_gcm" -> "aes-128-gcm" "aead_aes_192_gcm" -> "aes-192-gcm" "aead_aes_256_gcm" -> "aes-256-gcm" "aead_chacha20_poly1305" -> "chacha20-ietf-poly1305" "aead_xchacha20_poly1305" -> "xchacha20-ietf-poly1305" in supportedShadowsocksMethod -> cipher else -> return listOf() } plugin = pluginStr name = proxy.getString("name") }) } "vmess", "vless", "trojan" -> { val bean = when (proxy["type"] as String) { "vmess" -> VMessBean() "vless" -> VLESSBean() "trojan" -> TrojanBean() else -> error("impossible") }.apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() name = proxy.getString("name") } if (bean is TrojanBean) { when (val network = proxy.getString("network")) { "ws", "grpc" -> bean.type = network else -> bean.type = "tcp" } } else if (bean is VMessBean) { when (val network = proxy.getString("network")) { "h2" -> bean.type = "http" "http" -> { bean.type = "tcp" bean.headerType = "http" } "ws", "grpc", "mekya" -> bean.type = network "kcp", "mkcp" -> bean.type = "kcp" "tlsmirror" -> return listOf() // TODO else -> bean.type = "tcp" } } else { // bean is VLESSBean when (val network = proxy.getString("network")) { "h2" -> bean.type = "http" "http" -> { bean.type = "tcp" bean.headerType = "http" } "ws", "grpc" -> bean.type = network "xhttp" -> bean.type = "splithttp" else -> bean.type = "tcp" } } if (bean is TrojanBean) { bean.security = "tls" bean.sni = proxy.getString("sni") bean.password = proxy.getString("password") } else { bean.security = if (proxy.getBoolean("tls") == true) "tls" else "none" if (bean.security == "tls") { bean.sni = proxy.getString("servername") } proxy.getString("uuid")?.also { bean.uuid = uuidOrGenerate(it) } } if (bean.security == "tls") { bean.alpn = proxy.getStringArray("alpn")?.joinToString("\n") bean.allowInsecure = proxy.getBoolean("skip-cert-verify") == true proxy.getString("fingerprint")?.replace(":", "")?.trim()?.takeIf { it.isNotEmpty() }?.also { bean.pinnedPeerCertificateSha256 = it bean.allowInsecure = true } val cert = proxy.getString("certificate")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----") } val key = proxy.getString("private-key")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----") } if (cert != null && key != null) { bean.mtlsCertificate = cert bean.mtlsCertificatePrivateKey = key } bean.serverNameToVerify = proxy.getString("name-cert-verify") proxy.getObject("ech-opts")?.also { bean.echEnabled = it.getBoolean("enable") bean.echConfig = it.getString("config") } } if (bean is VMessBean) { bean.alterId = proxy.getInt("alterId") ?: return listOf() bean.encryption = when (val cipher = proxy.getString("cipher")) { in supportedVmessMethod -> cipher else -> return listOf() } bean.experimentalAuthenticatedLength = proxy.getBoolean("authenticated-length") == true var isPacket = false var isXUDP = false if (proxy.getBoolean("packet-addr") == true) { isPacket = true isXUDP = false } if (proxy.getBoolean("xudp") == true) { isXUDP = true isPacket = false } when (proxy.getString("packet-encoding")) { "packetaddr", "packet" -> { isPacket = true isXUDP = false } "xudp" -> { isXUDP = true isPacket = false } } bean.packetEncoding = when { isPacket -> "packet" isXUDP -> "xudp" else -> "none" } } if (bean is VLESSBean) { var isPacket = false var isXUDP = true if (proxy.getBoolean("packet-addr") == true) { isPacket = true isXUDP = false } if (proxy.getBoolean("xudp") == true) { isXUDP = true isPacket = false } when (proxy.getString("packet-encoding")) { "packetaddr", "packet" -> { isPacket = true isXUDP = false } "xudp" -> { isXUDP = true isPacket = false } } bean.packetEncoding = when { isPacket -> "packet" @Suppress("KotlinConstantConditions") isXUDP -> "xudp" else -> "xudp" } (proxy.getString("flow"))?.takeIf { it.isNotEmpty() }?.also { if (it.startsWith("xtls-rprx-vision")) { bean.flow = "xtls-rprx-vision-udp443" bean.packetEncoding = "xudp" } else return listOf() } when (val encryption = proxy.getString("encryption")) { "", "none", null -> bean.encryption = "none" else -> { val parts = encryption.split(".") if (parts.size < 4 || parts[0] != "mlkem768x25519plus" || !(parts[1] == "native" || parts[1] == "xorpub" || parts[1] == "random") || !(parts[2] == "1rtt" || parts[2] == "0rtt")) { error("unsupported vless encryption") } bean.encryption = encryption } } } proxy.getObject("shadowtls-opts")?.also { return listOf() } proxy.getObject("restls-opts")?.also { return listOf() } proxy.getObject("jls-opts")?.also { return listOf() } proxy.getObject("reality-opts")?.also { bean.security = "reality" bean.realityPublicKey = it.getString("public-key")?.ifEmpty { return listOf() } bean.realityShortId = it.getString("short-id") } if (bean.type == "tcp" && bean.headerType != null && bean.headerType == "http") { proxy.getObject("http-opts")?.also { bean.path = it.getStringArray("path")?.joinToString("\n") val headers = it.getObject("headers") as? Map> bean.host = headers?.getStringArray("host")?.joinToString("\n") } } if (bean.type == "ws") { // Fuck Xray ws and httpupgrade ALPN // https://github.com/MetaCubeX/mihomo/blob/301d580d8ac7a1466633a2b3f0331bcf29c17735/component/tls/utls.go#L258-L283 bean.alpn = null if (bean is TrojanBean && (bean.security == "tls" || bean.security == "reality") && !bean.sni.isNullOrEmpty()) { bean.host = bean.sni } proxy.getObject("ws-opts")?.also { wsOpts -> bean.path = wsOpts.getString("path") bean.maxEarlyData = wsOpts.getInt("max-early-data") bean.earlyDataHeaderName = wsOpts.getString("early-data-header-name") bean.path = wsOpts.getString("path") if (!bean.path.isNullOrEmpty()) { try { val u = Libexclavecore.parseURL(bean.path) u.queryParameter("ed")?.also { ed -> u.deleteQueryParameter("ed") bean.path = u.string (ed.toIntOrNull())?.also { bean.maxEarlyData = it } bean.earlyDataHeaderName = "Sec-WebSocket-Protocol" } } catch (_: Exception) {} } if (wsOpts.getBoolean("v2ray-http-upgrade") == true) { bean.type = "httpupgrade" bean.maxEarlyData = null bean.earlyDataHeaderName = null } val headers = wsOpts.getObject("headers") as? Map headers?.getString("host")?.also { bean.host = it if (bean !is TrojanBean && (bean.security == "tls" || bean.security == "reality") && bean.sni.isNullOrEmpty()) { bean.sni = it } } } } if (bean.type == "http") { proxy.getObject("h2-opts")?.also { bean.path = it.getString("path") bean.host = it.getStringArray("host")?.joinToString("\n") } } if (bean.type == "grpc") { proxy.getObject("grpc-opts")?.also { bean.grpcServiceName = it.getString("grpc-service-name") } } if (bean.type == "splithttp") { proxy.getObject("xhttp-opts")?.also { opts -> bean.path = opts.getString("path") bean.host = opts.getString("host") opts.getString("mode")?.also { bean.splithttpMode = when (it) { in supportedXhttpMode -> it "" -> "auto" else -> return listOf() } } JsonObject().apply { opts.getBoolean("no-grpc-header")?.also { addProperty("noGRPCHeader", it) } opts.getString("x-padding-bytes")?.also { addProperty("xPaddingBytes", it) } opts.getString("sc-max-each-post-bytes")?.also { addProperty("scMaxEachPostBytes", it) } opts.getString("sc-min-posts-interval-ms")?.also { addProperty("scMinPostsIntervalMs", it) } opts.getObject("reuse-settings")?.also { xmux -> JsonObject().apply { xmux.getString("max-connections")?.also { addProperty("maxConnections", it) } xmux.getString("max-concurrency")?.also { addProperty("maxConcurrency", it) } xmux.getString("c-max-reuse-times")?.also { addProperty("cMaxReuseTimes", it) } xmux.getString("h-max-request-times")?.also { addProperty("hMaxRequestTimes", it) } xmux.getString("h-max-reusable-secs")?.also { addProperty("hMaxReusableSecs", it) } }.takeIf { !it.isEmpty }?.also { add("xmux", it) } } opts.getString("x-padding-bytes")?.also { addProperty("xPaddingBytes", it) } opts.getBoolean("x-padding-obfs-mode")?.also { addProperty("xPaddingObfsMode", it) } opts.getString("x-padding-key")?.also { addProperty("xPaddingKey", it) } opts.getString("x-padding-header")?.also { addProperty("xPaddingHeader", it) } opts.getString("x-padding-placement")?.also { addProperty("xPaddingPlacement", it) } opts.getString("x-padding-method")?.also { addProperty("xPaddingMethod", it) } opts.getString("uplink-http-method")?.also { addProperty("uplinkHTTPMethod", it) } opts.getString("session-placement")?.also { addProperty("sessionIDPlacement", it) } opts.getString("session-key")?.also { addProperty("sessionIDKey", it) } opts.getString("session-table")?.also { addProperty("sessionIDTable", it) } opts.getString("session-length")?.also { addProperty("sessionIDLength", it) } opts.getString("seq-placement")?.also { addProperty("seqPlacement", it) } opts.getString("seq-key")?.also { addProperty("seqKey", it) } opts.getString("uplink-data-placement")?.also { addProperty("uplinkDataPlacement", it) } opts.getString("uplink-data-key")?.also { addProperty("uplinkDataKey", it) } opts.getString("uplink-chunk-size")?.also { addProperty("uplinkChunkSize", it) } }.takeIf { !it.isEmpty }?.also { bean.splithttpExtra = GsonBuilder().setPrettyPrinting().create().toJson(it) } } } if (bean.type == "kcp") { proxy.getObject("mkcp-opts")?.also { bean.mKcpSeed = it.getString("seed") it.getString("type")?.let { type -> when (type) { in supportedKcpQuicHeaderType -> bean.headerType = type "", "noop" -> bean.headerType = "none" "wechat" -> bean.headerType = "wechat-video" else -> bean.headerType = "none" } } } } if (bean.type == "mekya") { proxy.getObject("mekya-opts")?.also { bean.mekyaUrl = it.getString("url") it.getObject("kcp")?.let { kcp -> bean.mekyaKcpSeed = it.getString("seed") kcp.getString("type")?.let { type -> when (type) { in supportedKcpQuicHeaderType -> bean.headerType = type "", "noop" -> bean.headerType = "none" "wechat" -> bean.headerType = "wechat-video" else -> bean.headerType = "none" } } } } } if (bean is TrojanBean) { proxy.getObject("ss-opts")?.also { if (it.getBoolean("enabled") == true) { return listOf() } } } return listOf(bean) } "ssr" -> { return listOf(ShadowsocksRBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() method = when (val cipher = proxy.getString("cipher")?.lowercase()) { "dummy" -> "none" in supportedShadowsocksRMethod -> cipher else -> return listOf() } password = proxy.getString("password") obfs = when (val it = proxy.getString("obfs")) { "tls1.2_ticket_fastauth" -> "tls1.2_ticket_auth" in supportedShadowsocksRObfs -> it else -> return listOf() } obfsParam = proxy.getString("obfs-param") protocol = when (val it = proxy.getString("protocol")) { in supportedShadowsocksRProtocol -> it else -> return listOf() } protocolParam = proxy.getString("protocol-param") name = proxy.getString("name") }) } "ssh" -> { return listOf(SSHBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() username = proxy.getString("username") proxy.getString("password")?.also { password = it authType = SSHBean.AUTH_TYPE_PASSWORD } proxy.getString("private-key")?.takeIf { it.contains("PRIVATE KEY") }?.also { privateKey = it privateKeyPassphrase = proxy.getString("private-key-passphrase") authType = SSHBean.AUTH_TYPE_PUBLIC_KEY } publicKey = proxy.getStringArray("host-key")?.joinToString("\n") name = proxy.getString("name") }) } "hysteria2" -> { proxy.getObject("realm-opts")?.also { opts -> // unsupported if (opts.getBoolean("enable") == true) return listOf() } return listOf(Hysteria2Bean().apply { serverAddress = proxy.getString("server") ?: return listOf() val port = proxy.getInt("port")?.takeIf { it > 0 } val ports = proxy.getString("ports")?.toIntRanges() if (port == null && ports == null) return listOf() serverPorts = ports?.joinToString(",") { if (it.third) it.first.toString() else "${it.first}-${it.second}" } ?: port.toString() auth = proxy.getString("password") sni = proxy.getString("sni") allowInsecure = proxy.getBoolean("skip-cert-verify") == true proxy.getString("fingerprint")?.replace(":", "")?.trim()?.takeIf { it.isNotEmpty() }?.also { pinnedPeerCertificateSha256 = it allowInsecure = true } // https://github.com/MetaCubeX/mihomo/commit/6786705212f67eebe25151778b86ab4d2793c7d9 if (!proxy.contains("ca")) { certificates = proxy.getString("ca-str")?.lines()?.joinToString("\n")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----") } } val cert = proxy.getString("certificate")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----") } val key = proxy.getString("private-key")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----") } if (cert != null && key != null) { mtlsCertificate = cert mtlsCertificatePrivateKey = key } serverNameToVerify = proxy.getString("name-cert-verify") proxy.getObject("ech-opts")?.also { echEnabled = it.getBoolean("enable") echConfig = it.getString("config") } (proxy.getString("obfs"))?.also { when (it) { "" -> {} "salamander" -> { obfsType = "salamander" obfsPassword = proxy.getString("obfs-password") } "gecko" -> { obfsType = "gecko" obfsPassword = proxy.getString("obfs-password") geckoMinPacketSize = proxy.getInt("obfs-min-packet-size")?.takeIf { it > 0 } geckoMaxPacketSize = proxy.getInt("obfs-max-packet-size")?.takeIf { it > 0 } } else -> return listOf() } } if (ports != null) { // Ref: https://github.com/MetaCubeX/mihomo/commit/80072ebb6f9933e6a87ae94e3f6917a6c82c665e val hopIntervalInt = proxy.getInt("hop-interval")?.takeIf { it > 0 } val hopIntervalString = proxy.getString("hop-interval")?.toIntRange() if (hopIntervalString != null) { if (hopIntervalString.first == hopIntervalString.second) { hopInterval = hopIntervalString.first.toLong() } else { // Ref: https://github.com/MetaCubeX/mihomo/blob/80072ebb6f9933e6a87ae94e3f6917a6c82c665e/adapter/outbound/hysteria2.go#L215-L223 var start = hopIntervalString.first var end = hopIntervalString.second if (start == 0) { start = 30 } else if (start < 5) { start = 5 } if (end < start) { end = start } hopIntervalMin = start.toLong() hopIntervalMax = end.toLong() } } else if (hopIntervalInt != null) { hopInterval = hopIntervalInt.toLong() } } name = proxy.getString("name") }) } "tuic" -> { if (proxy.getString("token") != null) { // v4, unsupported return listOf() } else { return listOf(Tuic5Bean().apply { serverAddress = proxy.getString("ip") ?: proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() uuid = proxy.getString("uuid") password = proxy.getString("password") udpRelayMode = when (val mode = proxy.getString("udp-relay-mode")) { in supportedTuic5RelayMode -> mode else -> "native" } congestionControl = when (val controller = proxy.getString("congestion-controller")) { in supportedTuic5CongestionControl -> controller else -> "cubic" } disableSNI = proxy.getBoolean("disable-sni") == true zeroRTTHandshake = proxy.getBoolean("reduce-rtt") == true allowInsecure = proxy.getBoolean("skip-cert-verify") == true sni = proxy.getString("sni") ?: (if (proxy.getString("ip") != null) proxy.getString("server") else null) // https://github.com/MetaCubeX/mihomo/blob/d5243adf8911563677d3bd190b82623c93e554b7/adapter/outbound/tuic.go#L174-L178 alpn = if (!proxy.contains("alpn")) "h3" else proxy.getStringArray("alpn")?.joinToString("\n") proxy.getString("fingerprint")?.replace(":", "")?.trim()?.takeIf { it.isNotEmpty() }?.also { pinnedPeerCertificateSha256 = it allowInsecure = true } // https://github.com/MetaCubeX/mihomo/commit/6786705212f67eebe25151778b86ab4d2793c7d9 if (!proxy.contains("ca")) { certificates = proxy.getString("ca-str")?.lines()?.joinToString("\n")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----") } } val cert = proxy.getString("certificate")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----") } val key = proxy.getString("private-key")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----") } if (cert != null && key != null) { mtlsCertificate = cert mtlsCertificatePrivateKey = key } serverNameToVerify = proxy.getString("name-cert-verify") /*proxy.getObject("ech-opts")?.also { echEnabled = it.getBoolean("enable") echConfig = it.getString("config") }*/ name = proxy.getString("name") }) } } "mieru" -> { return listOf(MieruBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port") portRange = proxy.getStringArray("port-range")?.joinToString("\n") if (serverPort == null && portRange == null) { return listOf() } username = proxy.getString("username") password = proxy.getString("password") protocol = MieruBean.PROTOCOL_TCP proxy.getString("transport")?.also { protocol = when (it) { "TCP", "" -> MieruBean.PROTOCOL_TCP "UDP" -> MieruBean.PROTOCOL_UDP else -> return listOf() } } proxy.getString("multiplexing")?.also { multiplexingLevel = when (it) { "MULTIPLEXING_OFF" -> MieruBean.MULTIPLEXING_OFF "MULTIPLEXING_LOW" -> MieruBean.MULTIPLEXING_LOW "MULTIPLEXING_MIDDLE" -> MieruBean.MULTIPLEXING_MIDDLE "MULTIPLEXING_HIGH" -> MieruBean.MULTIPLEXING_HIGH else -> MieruBean.MULTIPLEXING_DEFAULT } } proxy.getString("handshake-mode")?.also { handshakeMode = when (it) { "HANDSHAKE_STANDARD" -> MieruBean.HANDSHAKE_STANDARD "HANDSHAKE_NO_WAIT" -> MieruBean.HANDSHAKE_NO_WAIT else -> MieruBean.HANDSHAKE_DEFAULT } } proxy.getString("traffic-pattern")?.also { // How to validate its validity? trafficPattern = it } name = proxy.getString("name") }) } "snell" -> { return listOf(SnellBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() psk = proxy.getString("psk") version = proxy.getInt("version") if (version != 4) return listOf() proxy.getObject("obfs-opts")?.also { opts -> when (opts.getString("mode")) { "tls" -> { obfsMode = SnellBean.OBFS_TLS obfsHost = when (val host = opts.getString("host")) { // null -> "bing.com" else -> host } } "http" -> { obfsMode = SnellBean.OBFS_HTTP obfsHost = when (val host = opts.getString("host")) { null -> "bing.com" // https://github.com/MetaCubeX/mihomo/blob/75eeba429292d915691f61d89fc5e57a612c8844/adapter/outbound/snell.go#L158 else -> host } } null, "" -> SnellBean.OBFS_NONE else -> return listOf() } } reuse = proxy.getBoolean("reuse") name = proxy.getString("name") }) } "anytls" -> { return listOf(AnyTLSBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() password = proxy.getString("password") security = "tls" sni = proxy.getString("sni") alpn = proxy.getStringArray("alpn")?.joinToString("\n") allowInsecure = proxy.getBoolean("skip-cert-verify") == true proxy.getString("fingerprint")?.replace(":", "")?.trim()?.takeIf { it.isNotEmpty() }?.also { pinnedPeerCertificateSha256 = it allowInsecure = true } val cert = proxy.getString("certificate")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----") } val key = proxy.getString("private-key")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----") } if (cert != null && key != null) { mtlsCertificate = cert mtlsCertificatePrivateKey = key } serverNameToVerify = proxy.getString("name-cert-verify") /*proxy.getObject("ech-opts")?.also { echEnabled = it.getBoolean("enable") echConfig = it.getString("config") }*/ proxy.getObject("shadowtls-opts")?.also { return listOf() } proxy.getObject("restls-opts")?.also { return listOf() } proxy.getObject("jls-opts")?.also { return listOf() } name = proxy.getString("name") }) } "trusttunnel" -> { return listOf(TrustTunnelBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() username = proxy.getString("username") password = proxy.getString("password") sni = proxy.getString("sni") allowInsecure = proxy.getBoolean("skip-cert-verify") == true protocol = if (proxy.getBoolean("quic") == true) "quic" else "https" proxy.getString("fingerprint")?.replace(":", "")?.trim()?.takeIf { it.isNotEmpty() }?.also { pinnedPeerCertificateSha256 = it allowInsecure = true } val cert = proxy.getString("certificate")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----") } val key = proxy.getString("private-key")?.takeIf { it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----") } if (cert != null && key != null) { mtlsCertificate = cert mtlsCertificatePrivateKey = key } serverNameToVerify = proxy.getString("name-cert-verify") /*proxy.getObject("ech-opts")?.also { echEnabled = it.getBoolean("enable") echConfig = it.getString("config") }*/ name = proxy.getString("name") }) } "wireguard" -> { proxy.getObject("amnezia-wg-option")?.also { // unsupported return listOf() } val beanList = mutableListOf() val bean = WireGuardBean().apply { serverAddress = proxy.getString("server") serverPort = proxy.getInt("port") privateKey = proxy.getString("private-key") peerPublicKey = proxy.getString("public-key") peerPreSharedKey = proxy.getString("pre-shared-key") ?: proxy.getString("preshared-key") // "preshared-key" from Clash Premium mtu = (proxy.getInt("mtu"))?.takeIf { it > 0 } ?: 1408 localAddress = listOfNotNull(proxy.getString("ip"), proxy.getString("ipv6")).joinToString("\n") keepaliveInterval = proxy.getInt("persistent-keepalive") name = proxy.getString("name") proxy.getIntArray("reserved")?.also { if (it.size == 3) { reserved = listOf( it[0].toString(), it[1].toString(), it[2].toString() ).joinToString(",") } } ?: proxy.getString("reserved")?.also { try { val arr = Base64.decode(it) if (arr.size == 3) { reserved = listOf( arr[0].toUByte().toInt().toString(), arr[1].toUByte().toInt().toString(), arr[2].toUByte().toInt().toString() ).joinToString(",") } } catch (_: Exception) {} } } if (proxy.contains("server") && proxy.contains("port")) { beanList.add(bean) } proxy.getArray("peers")?.forEach { peer -> if (peer.contains("server") && peer.contains("port")) { beanList.add(bean.applyDefaultValues().clone().apply { serverAddress = peer.getString("server") serverPort = peer.getInt("port") peerPublicKey = peer.getString("public-key") peerPreSharedKey = peer.getString("pre-shared-key") peer.getIntArray("reserved")?.also { if (it.size == 3) { reserved = listOf( it[0].toString(), it[1].toString(), it[2].toString() ).joinToString(",") } } ?: peer.getString("reserved")?.also { try { val arr = Base64.decode(it) if (arr.size == 3) { reserved = listOf( arr[0].toUByte().toInt().toString(), arr[1].toUByte().toInt().toString(), arr[2].toUByte().toInt().toString() ).joinToString(",") } } catch (_: Exception) {} } }) } } return beanList } "shadowquic" -> { if (!DataStore.experimentalFlagsProperties.getBooleanProperty("shadowquic")) { return listOf() } return listOf(ShadowQUICBean().apply { serverAddress = proxy.getString("server") ?: return listOf() serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() username = proxy.getString("username") password = proxy.getString("password") sni = proxy.getString("sni") alpn = proxy.getStringArray("alpn")?.joinToString("\n") udpOverStream = proxy.getBoolean("udp-over-stream") zeroRTT = proxy.getBoolean("zero-rtt") name = proxy.getString("name") }) } else -> return listOf() } } @Suppress("UNCHECKED_CAST") fun Map.getObject(key: String): Map? { this[key]?.also { return it as? Map } for (it in this) { if (it.key.equals(key, ignoreCase = true)) { return it.value as? Map } } return null } @Suppress("UNCHECKED_CAST") fun Map.getArray(key: String): List>? { (this[key] as? List>)?.also { return it } for (it in this) { if (it.key.equals(key, ignoreCase = true)) { return it.value as? List> } } return null } private fun Map.getString(key: String): String? { if (this.contains(key)) { return when (val value = this[key]) { is String -> value is Int -> value.toString() // is Float -> value.toString() else -> null } } for (it in this) { if (it.key.equals(key, ignoreCase = true)) { return when (val value = it.value) { is String -> value is Int -> value.toString() // is Float -> value.toString() else -> null } } } return null } private fun Map.getInt(key: String): Int? { if (this.contains(key)) { return when (val value = this[key]) { is Int -> value is String -> value.convertClashStringToInt() is Float -> value.toInt() else -> null } } for (it in this) { if (it.key.equals(key, ignoreCase = true)) { return when (val value = it.value) { is Int -> value is String -> value.convertClashStringToInt() is Float -> value.toInt() else -> null } } } return null } private fun Map.getBoolean(key: String): Boolean? { if (this.contains(key)) { return when (val value = this[key]) { is Boolean -> value is Int -> value != 0 else -> null } } for (it in this) { if (it.key.equals(key, ignoreCase = true)) { return when (val value = it.value) { is Boolean -> value is Int -> value != 0 else -> null } } } return null } @Suppress("UNCHECKED_CAST") fun Map.getStringArray(key: String): List? { (this[key] as? List)?.also { return it } for (it in this) { if (it.key.equals(key, ignoreCase = true)) { return it.value as? List } } return null } @Suppress("UNCHECKED_CAST") fun Map.getIntArray(key: String): List? { (this[key] as? List)?.also { return it } for (it in this) { if (it.key.equals(key, ignoreCase = true)) { return it.value as? List } } return null } private fun String.convertClashStringToInt(): Int? { if (this.contains(":")) return null if (!this.startsWith("_") && this.replace("_", "").toFloatOrNull() != null) { return this.replace("_", "").toFloat().toInt() } val newStr = this.lowercase().removePrefix("+") if (newStr.contains("+")) return null if (newStr.startsWith("0x")) { return newStr.removePrefix("0x").replace("_", "").toIntOrNull(16) } if (newStr.startsWith("0b")) { return newStr.removePrefix("0b").replace("_", "").toIntOrNull(2) } if (newStr.startsWith("0o")) { return newStr.removePrefix("0o").replace("_", "").toIntOrNull(8) } if (newStr.startsWith("0")) { return newStr.removePrefix("0").replace("_", "").toIntOrNull(8) } if (newStr.startsWith("_")) { return null } return newStr.replace("_", "").toIntOrNull() } // Ref: https://github.com/MetaCubeX/mihomo/blob/80072ebb6f9933e6a87ae94e3f6917a6c82c665e/common/utils/ranges.go#L16-L48 private fun String.toIntRanges(): List> { // example: 200 or 200/302 or 200-400 or 200/204/401-429/501-503 var expected = this.trim() if (expected.isEmpty() || expected == "*") { return listOf() } // support: 200,302 or 200,204,401-429,501-503 expected = expected.replace(",", "/") val list = expected.split("/") val ranges = mutableListOf>() for (s in list) { if (s.isEmpty()) { continue } val r = s.toIntRange() ?: return listOf() ranges.add(r) } return ranges } // Ref: https://github.com/MetaCubeX/mihomo/blob/80072ebb6f9933e6a87ae94e3f6917a6c82c665e/common/utils/range.go#L65-L87 // Triple: start, end, isSinglePort private fun String.toIntRange(): Triple? { val s = this.trim() if (s.isEmpty()) { return Triple(0, 0, true) } val status = s.split("-") val start = status[0].trim { it == '[' || it == ' ' || it == ']' }.convertClashStringToInt() if (start == null) { return null } when (status.size) { 1 -> { // Single port return Triple(start, start, true) } 2 -> { // Port range val end = status[1].trim { it == '[' || it == ' ' || it == ']' }.convertClashStringToInt() if (end == null) { return null } return Triple(start, end, false) } else -> { return null } } }