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

github - dev - back to tree - upstream
/****************************************************************************** * * * 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.fmt.v2ray import com.google.gson.GsonBuilder import com.google.gson.JsonArray import com.google.gson.JsonObject import io.nekohasekai.sagernet.fmt.trojan.TrojanBean import io.nekohasekai.sagernet.ktx.* import libexclavecore.Libexclavecore import java.util.Base64 import kotlin.collections.filter import kotlin.collections.isNotEmpty import kotlin.text.isNotEmpty val supportedVmessMethod = arrayOf( "auto", "aes-128-gcm", "chacha20-poly1305", "none", "zero" ) val supportedVlessFlow = arrayOf( "xtls-rprx-vision", "xtls-rprx-vision-udp443" ) val legacyVlessFlow = arrayOf( "xtls-rprx-origin", "xtls-rprx-origin-udp443", "xtls-rprx-direct", "xtls-rprx-direct-udp443", "xtls-rprx-splice", "xtls-rprx-splice-udp443" ) val supportedQuicSecurity = arrayOf( "none", "aes-128-gcm", "chacha20-poly1305" ) val supportedKcpQuicHeaderType = arrayOf( "none", "srtp", "utp", "wechat-video", "dtls", "wireguard" ) val supportedXhttpMode = arrayOf( "auto", "packet-up", "stream-up", "stream-one" ) val nonRawTransportName = arrayOf( "kcp", "mkcp", "ws", "websocket", "h2", "http", "quic", "grpc", "gun", "meek", "httpupgrade", "splithttp", "xhttp", "hysteria2", "hy2", "mekya" ) fun parseV2Ray(link: String): StandardV2RayBean { // https://github.com/XTLS/Xray-core/issues/91 // https://github.com/XTLS/Xray-core/discussions/716 val url = Libexclavecore.parseURL(link) val bean = when (url.scheme) { "vmess" -> VMessBean() "vless" -> VLESSBean() "trojan" -> TrojanBean() else -> error("impossible") } if (url.scheme == "vmess" && url.port == 0 && url.username.isEmpty() && url.password.isEmpty()) { val decoded = link.substring("vmess://".length).substringBefore("#").decodeBase64() try { return parseV2RayN(parseJson(decoded).asJsonObject) } catch (_: Exception) {} if (decoded.filterNot { it.isWhitespace() }.contains("=vmess,")) { // vmess://{BASE64_ENCODED} // name = vmess, example.com, 8388, aes-128-gcm, 00000000-0000-0000-0000-000000000000, param=value // quan? error("known unsupported format") } if (decoded.contains("@")) { // vmess://{BASE64_ENCODED}?param=value&remarks=name // aes-128-gcm:00000000-0000-0000-0000-000000000000@example.com:8388 // rocket? error("known unsupported format") } error("unknown format") } if (url.scheme == "vmess" && url.password.isNotEmpty()) { // https://github.com/v2fly/v2fly-github-io/issues/26 error("known unsupported format") } bean.serverAddress = url.host.ifEmpty { error("empty host") } bean.serverPort = url.port bean.name = url.fragment if (bean is TrojanBean) { // https://github.com/trojan-gfw/igniter/issues/318 when { url.username.isEmpty() && url.password.isEmpty() -> { if (link.substring("trojan://".length).substringBefore("@") == ":") { bean.password = ":" } } url.username.isNotEmpty() && url.password.isEmpty() -> { bean.password = if (link.substring("trojan://".length).substringBefore("@").endsWith(":")) { url.username + ":" } else { url.username } } url.username.isEmpty() && url.password.isNotEmpty() -> { bean.password = ":" + url.password } url.username.isNotEmpty() && url.password.isNotEmpty() -> { bean.password = url.username + ":" + url.password } } } else { bean.uuid = uuidOrGenerate(url.username) } if (bean is VMessBean) { url.queryParameter("encryption")?.let { if (it !in supportedVmessMethod) error("unsupported vmess encryption") bean.encryption = it } } if (bean is VLESSBean) { when (val encryption = url.queryParameter("encryption")) { "none", null -> bean.encryption = "none" "" -> error("unsupported vless encryption") 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 } } } when (val security = url.queryParameter("security")) { null -> bean.security = if (bean is TrojanBean) "tls" else "none" "none", "tls", "reality" -> bean.security = security "xtls" -> bean.security = "tls" else -> { // Do not throw error. Some links are stupid. bean.security = if (bean is TrojanBean) "tls" else "none" } } when (bean.security) { "none" -> { if (bean is VLESSBean) { url.queryParameter("flow")?.let { when (it) { in supportedVlessFlow -> { bean.flow = "xtls-rprx-vision-udp443" bean.packetEncoding = "xudp" } in legacyVlessFlow, "", "none" -> null else -> error("unsupported vless flow") } } } } "tls" -> { url.queryParameter("sni")?.let { bean.sni = it } url.queryParameter("alpn")?.let { bean.alpn = it.split(",").joinToString("\n") } if (bean is VLESSBean) { url.queryParameter("flow")?.let { when (it) { in supportedVlessFlow -> { bean.flow = "xtls-rprx-vision-udp443" bean.packetEncoding = "xudp" } in legacyVlessFlow, "", "none" -> null else -> error("unsupported vless flow") } } } // bad format from where? url.queryParameter("allowInsecure")?.let { if (it == "1" || it == "true") { bean.allowInsecure = true // non-standard } } url.queryParameter("insecure")?.let { if (it == "1" || it == "true") { bean.allowInsecure = true // non-standard } } url.queryParameter("allow_insecure")?.let { if (it == "1" || it == "true") { bean.allowInsecure = true // non-standard } } url.queryParameter("pcs")?.takeIf { it.isNotEmpty() }?.let { pcs -> bean.pinnedPeerCertificateSha256 = pcs.split(",") .mapNotNull { it.trim().ifEmpty { null }?.replace(":", "") } .joinToString("\n") if (!bean.pinnedPeerCertificateSha256.isNullOrEmpty()) { bean.allowInsecure = true } } url.queryParameter("vcn")?.takeIf { it.isNotEmpty() }?.let { vcn -> bean.serverNameToVerify = vcn.split(",") .filter { it.isNotEmpty() }.takeIf { it.isNotEmpty() } ?.joinToString("\n") } url.queryParameter("ech")?.let { bean.echEnabled = true try { Base64.getDecoder().decode(it) bean.echConfig = it } catch (_: Exception) {} } } "reality" -> { url.queryParameter("sni")?.let { bean.sni = it } url.queryParameter("pbk")?.ifEmpty { error("empty reality public key") }?.let { bean.realityPublicKey = it } url.queryParameter("sid")?.let { bean.realityShortId = it } url.queryParameter("pqv")?.let { bean.realityMldsa65Verify = it } if (bean is VLESSBean) { url.queryParameter("flow")?.let { when (it) { in supportedVlessFlow -> { bean.flow = "xtls-rprx-vision-udp443" bean.packetEncoding = "xudp" } "", "none" -> null else -> error("unsupported vless flow") } } } } } bean.type = url.queryParameter("type") when (bean.type) { "tcp", "raw", null -> { bean.type = "tcp" url.queryParameter("headerType")?.let { headerType -> // invented by v2rayN(G) when (headerType) { "none" -> {} "http" -> { bean.headerType = headerType url.queryParameter("host")?.let { bean.host = it.split(",").joinToString("\n") } } else -> error("unsupported headerType") } } } "kcp" -> { url.queryParameter("seed")?.let { bean.mKcpSeed = it } url.queryParameter("headerType")?.let { if (it !in supportedKcpQuicHeaderType) error("unsupported headerType") bean.headerType = it } } "http" -> { url.queryParameter("host")?.let { // The proposal says "省略时复用 remote-host", but this is not correct except for the breaking change below. // will not follow the breaking change in https://github.com/XTLS/Xray-core/commit/0a252ac15d34e7c23a1d3807a89bfca51cbb559b // "若有多个域名,可使用英文逗号隔开,但中间及前后不可有空格。" bean.host = it.split(",").joinToString("\n") } url.queryParameter("path")?.let { bean.path = it } } "xhttp", "splithttp" -> { bean.type = "splithttp" url.queryParameter("extra")?.let { extra -> try { val json = parseJson(extra).asJsonObject if (!json.isEmpty) { // fuck RPRX `extra` bean.splithttpExtra = GsonBuilder().setPrettyPrinting().create().toJson(json) } } catch (_: Exception) {} } url.queryParameter("host")?.let { bean.host = it } url.queryParameter("path")?.let { bean.path = it } url.queryParameter("mode")?.let { bean.splithttpMode = when (it) { in supportedXhttpMode -> it "" -> "auto" else -> error("unsupported xhttp mode") } } } "httpupgrade" -> { // Fuck Xray httpupgrade ALPN // https://github.com/XTLS/Xray-core/blob/1bdb488c9ec09ea51e6899697d5b7437f3cf6eb2/transport/internet/tls/tls.go#L94-L131 bean.alpn = null url.queryParameter("host")?.let { // will not follow the breaking change in // https://github.com/XTLS/Xray-core/commit/a2b773135a860f63e990874c551b099dfc888471 bean.host = it } url.queryParameter("path")?.let { path -> bean.path = path try { // RPRX's smart-assed invention. This of course will break under some conditions. val u = Libexclavecore.parseURL(path) u.queryParameter("ed")?.let { u.deleteQueryParameter("ed") bean.path = u.string } } catch (_: Exception) {} } url.queryParameter("eh")?.let { bean.earlyDataHeaderName = it // non-standard, invented by SagerNet and adopted by some other software } url.queryParameter("ed")?.toIntOrNull()?.let { bean.maxEarlyData = it // non-standard, invented by SagerNet and adopted by some other software } } "ws" -> { // Fuck Xray ws ALPN // https://github.com/XTLS/Xray-core/blob/1bdb488c9ec09ea51e6899697d5b7437f3cf6eb2/transport/internet/tls/tls.go#L94-L131 bean.alpn = null url.queryParameter("host")?.let { // will not follow the breaking change in // https://github.com/XTLS/Xray-core/commit/a2b773135a860f63e990874c551b099dfc888471 bean.host = it } url.queryParameter("path")?.let { path -> bean.path = path try { // RPRX's smart-assed invention. This of course will break under some conditions. val u = Libexclavecore.parseURL(path) u.queryParameter("ed")?.let { ed -> u.deleteQueryParameter("ed") bean.path = u.string bean.maxEarlyData = ed.toIntOrNull() bean.earlyDataHeaderName = "Sec-WebSocket-Protocol" } } catch (_: Exception) {} } url.queryParameter("eh")?.let { bean.earlyDataHeaderName = it // non-standard, invented by SagerNet and adopted by some other software } url.queryParameter("ed")?.toIntOrNull()?.let { bean.maxEarlyData = it // non-standard, invented by SagerNet and adopted by some other software } } "quic" -> { url.queryParameter("headerType")?.let { if (it !in supportedKcpQuicHeaderType) error("unsupported headerType") bean.headerType = it } url.queryParameter("quicSecurity")?.let { quicSecurity -> if (quicSecurity !in supportedQuicSecurity) error("unsupported quicSecurity") bean.quicSecurity = quicSecurity url.queryParameter("key")?.let { bean.quicKey = it } } } "grpc" -> { url.queryParameter("serviceName")?.let { // Xray hijacks the share link standard, uses escaped `serviceName` and some other non-standard `serviceName`s and breaks the compatibility with other implementations. // Fixing the compatibility with Xray will break the compatibility with V2Ray and others. // So do not fix the compatibility with Xray. bean.grpcServiceName = it } url.queryParameter("mode")?.takeIf { it == "multi" }?.let { // Xray private bean.grpcMultiMode = true } } "meek" -> { // https://github.com/v2fly/v2ray-core/discussions/2638 url.queryParameter("url")?.let { bean.meekUrl = it } } "mekya" -> { // not a standard url.queryParameter("headerType")?.let { if (it !in supportedKcpQuicHeaderType) error("unsupported headerType") bean.mekyaKcpHeaderType = it } url.queryParameter("seed")?.let { bean.mekyaKcpSeed = it } url.queryParameter("url")?.let { bean.mekyaUrl = it } } "hysteria2", "hysteria" -> error("unsupported") else -> bean.type = "tcp" } url.queryParameter("fm")?.let { finalmask -> // fuck RPRX try { val json = parseJson(finalmask).asJsonObject if (!json.isEmpty) { when (bean.type) { "tcp", "ws", "grpc", "httpupgrade" -> { // ban Xray TCP finalmask json.getArray("tcp", ignoreCase = true)?.takeIf { it.isNotEmpty() }?.also { error("unsupported") } } "kcp" -> { json.getArray("udp", ignoreCase = true)?.takeIf { it.isNotEmpty() }?.also { udpMasks -> if (udpMasks.size !in 1..2) error("unsupported") var isMkcpLegacy = false when (udpMasks.last().getString("type", ignoreCase = true)) { "mkcp-original" -> {} "mkcp-aes128gcm" -> { udpMasks.last().getObject("settings", ignoreCase = true)?.also { settings -> settings.getString("password", ignoreCase = true).orEmpty().also { if (it.isEmpty()) error("unsupported") bean.mKcpSeed = it } } } "mkcp-legacy" -> { isMkcpLegacy = true udpMasks.last().getObject("settings", ignoreCase = true)?.also { settings -> settings.getString("header", ignoreCase = true).orEmpty().lowercase().also { when (it) { "dtls", "srtp", "utp", "wireguard" -> bean.headerType = it "wechat" -> bean.headerType = "wechat-video" else -> error("unsupported") } } } } else -> error("unsupported") } if (udpMasks.size == 2) { when (val type = udpMasks.first().getString("type", ignoreCase = true)) { null -> {} "header-wechat" -> { if (isMkcpLegacy) error("unsupported") bean.headerType = "wechat-video" } "header-dtls", "header-srtp", "header-utp", "header-wireguard" -> { if (isMkcpLegacy) error("unsupported") bean.headerType = type.removePrefix("header-") } "mkcp-legacy" -> { if (!isMkcpLegacy) error("unsupported") udpMasks.first().getObject("settings", ignoreCase = true)?.also { settings -> settings.getString("header", ignoreCase = true).orEmpty().also { if (it.isNotEmpty()) error("unsupported") } settings.getString("value", ignoreCase = true).orEmpty().also { bean.mKcpSeed = it } } } else -> error("unsupported") } } } } "splithttp" -> { if (bean.alpn != "h3") { // ban Xray TCP finalmask json.getArray("tcp", ignoreCase = true)?.takeIf { it.isNotEmpty() }?.also { error("unsupported") } } else { // ban Xray UDP finalmask json.getArray("udp", ignoreCase = true)?.takeIf { it.isNotEmpty() }?.also { error("unsupported") } // ban Xray QUIC port hopping json.getObject("quicParams")?.also { quicParams -> quicParams.getObject("udphop")?.also { udphop -> udphop.getInt("ports")?.also { error("unsupported") } ?: udphop.getString("ports")?.takeIf { it.isNotEmpty() }?.also { it.split(",").joinToString(",") { it.trim() } .takeIf { it.isValidHysteriaPort(disallowFromGreaterThanTo = true) } ?.also { error("unsupported") } } } } } } } } } catch (e: Exception) { throw e } } return bean } private fun parseV2RayN(json: JsonObject): VMessBean { // https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link val bean = VMessBean().apply { serverAddress = json.getString("add")?.ifEmpty { error("empty host") } ?: error("missing server address") serverPort = (json.getString("port")?.toIntOrNull() ?: json.getInt("port"))?: error("invalid port") uuid = json.getString("id")?.let { uuidOrGenerate(it) } alterId = json.getString("aid")?.toIntOrNull() ?: json.getInt("aid") json.getString("scy")?.takeIf { it.isNotEmpty() }?.let { if (it !in supportedVmessMethod) error("unsupported vmess encryption") encryption = it } name = json.getString("ps")?.takeIf { it.isNotEmpty() } } val net = json.getString("net") bean.type = when (net) { "h2" -> "http" "xhttp" -> "splithttp" "tcp", "kcp", "ws", "http", "quic", "grpc", "httpupgrade", "splithttp" -> net else -> "tcp" } val type = json.getString("type")?.takeIf { it.isNotEmpty() } val host = json.getString("host")?.takeIf { it.isNotEmpty() } val path = json.getString("path")?.takeIf { it.isNotEmpty() } when (bean.type) { "tcp" -> { bean.host = host?.split(",")?.joinToString("\n") // "http(tcp)->host中间逗号(,)隔开" bean.path = path?.split(",")?.joinToString("\n") // See https://github.com/owenewans/owenclave/issues/357 type?.let { if (it != "http" && it != "none") error("unsupported headerType") bean.headerType = it } } "kcp" -> { bean.mKcpSeed = path type?.let { if (it !in supportedKcpQuicHeaderType) error("unsupported headerType") bean.headerType = it } } "ws" -> { bean.host = host bean.path = path try { // RPRX's smart-assed invention. This of course will break under some conditions. val u = Libexclavecore.parseURL(bean.path) u.queryParameter("ed")?.let { ed -> u.deleteQueryParameter("ed") bean.path = u.string bean.maxEarlyData = ed.toIntOrNull() bean.earlyDataHeaderName = "Sec-WebSocket-Protocol" } } catch (_: Exception) {} } "httpupgrade" -> { bean.host = host bean.path = path try { // RPRX's smart-assed invention. This of course will break under some conditions. val u = Libexclavecore.parseURL(bean.path) u.queryParameter("ed")?.let { u.deleteQueryParameter("ed") bean.path = u.string } } catch (_: Exception) {} } "http" -> { bean.host = host?.split(",")?.joinToString("\n") // "http(tcp)->host中间逗号(,)隔开" bean.path = path } "quic" -> { bean.quicSecurity = host bean.quicKey = path type?.let { if (it !in supportedKcpQuicHeaderType) error("unsupported headerType") bean.headerType = it } } "grpc" -> { // Xray hijacks the share link standard, uses escaped `serviceName` and some other non-standard `serviceName`s and breaks the compatibility with other implementations. // Fixing the compatibility with Xray will break the compatibility with V2Ray and others. // So do not fix the compatibility with Xray. bean.grpcServiceName = bean.path type?.let { if (it == "multi") { bean.grpcMultiMode = true // Xray private } } } "splithttp" -> { bean.host = host bean.path = path type?.let { when (it) { "" -> bean.splithttpMode = "auto" in supportedXhttpMode -> bean.splithttpMode = it else -> error("unsupported xhttp mode") } } } } when (val security = json.getString("tls")) { "tls" -> { bean.security = security bean.name = json.getString("ps")?.takeIf { it.isNotEmpty() } // See https://github.com/2dust/v2rayNG/blob/5db2df77a01144b8f3d40116f8c183153f181d05/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt#L1077-L1242 bean.sni = json.getString("sni")?.takeIf { it.isNotEmpty() } ?: host?.split(",")?.get(0) bean.alpn = json.getString("alpn")?.takeIf { it.isNotEmpty() }?.split(",")?.joinToString("\n") json.getString("insecure")?.takeIf { it == "1" }?.let { bean.allowInsecure = true } json.getInt("insecure")?.takeIf { it == 1 }?.let { bean.allowInsecure = true } json.getString("pcs")?.takeIf { it.isNotEmpty() }?.let { pcs -> bean.pinnedPeerCertificateSha256 = pcs.split(",") .mapNotNull { it.trim().ifEmpty { null }?.replace(":", "") } .joinToString("\n") if (!bean.pinnedPeerCertificateSha256.isNullOrEmpty()) { bean.allowInsecure = true } } json.getString("vcn")?.takeIf { it.isNotEmpty() }?.let { vcn -> bean.serverNameToVerify = vcn.split(",") .filter { it.isNotEmpty() }.takeIf { it.isNotEmpty() } ?.joinToString("\n") } } "reality" -> { error("v2rayN(G) style link lacks REALITY public key support and does not work at all.") } else -> bean.security = "none" } // https://github.com/2dust/v2rayN/blob/737d563ebb66d44504c3a9f51b7dcbb382991dfd/v2rayN/v2rayN/Handler/ConfigHandler.cs#L701-L743 if (!json.contains("v") || (json.getString("v") != null && json.getString("v")!!.toIntOrNull() != null && json.getString("v")!!.toIntOrNull()!! < 2) || (json.getInt("v") != null && json.getInt("v")!! < 2)) { when (net) { "ws", "h2" -> { host?.replace(" ", "")?.split(";")?.let { if (it.isNotEmpty()) { bean.path = it[0] bean.host = "" } if (it.size > 1) { bean.path = it[0] bean.host = it[1] } } } } } return bean } fun StandardV2RayBean.toUri(): String? { val builder = Libexclavecore.newURL( when (this) { is VMessBean -> "vmess" is VLESSBean -> "vless" is TrojanBean -> "trojan" else -> error("impossible") } ).apply { setHostPort(serverAddress.ifEmpty { error("empty server address") }, serverPort) if (name.isNotEmpty()) { fragment = name } } when (this) { is TrojanBean -> { if (password.isNotEmpty()) { builder.username = password } } is VMessBean -> { builder.username = uuidOrGenerate(uuid) builder.addQueryParameter("encryption", encryption) if (alterId > 0) { error("unsupported vmess alterId") } } is VLESSBean -> { builder.username = uuidOrGenerate(uuid) when (encryption) { "none" -> builder.addQueryParameter("encryption", "none") "" -> error("unsupported vless encryption") 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") } builder.addQueryParameter("encryption", encryption) } } } } when (type) { "tcp" -> { // do not add `type=tcp` for Trojan if possible if (this !is TrojanBean || headerType == "http") { builder.addQueryParameter("type", "tcp") } } "splithttp" -> { builder.addQueryParameter("type", "xhttp") } "kcp", "ws", "http", "httpupgrade", "quic", "grpc", "meek", "meyka" -> { builder.addQueryParameter("type", type) } else -> error("unsupported transport") } when (type) { "tcp" -> { if (headerType == "http") { // invented by v2rayNG builder.addQueryParameter("headerType", headerType) if (host.isNotEmpty()) { builder.addQueryParameter("host", host.listByLineOrComma().joinToString(",")) } // See https://github.com/owenewans/owenclave/issues/357 /*if (path.isNotEmpty()) { builder.addQueryParameter("path", path.listByLineOrComma().joinToString(",")) }*/ } } "kcp" -> { if (headerType != "none") { builder.addQueryParameter("headerType", headerType) } if (mKcpSeed.isNotEmpty()) { builder.addQueryParameter("seed", mKcpSeed) } // fuck rprx finalmask builder.addQueryParameter("fm", JsonObject().apply { add("udp", JsonArray().apply { add(JsonObject().apply { addProperty("type", "mkcp-legacy") if (mKcpSeed.isNotEmpty()) { add("settings", JsonObject().apply { addProperty("value", mKcpSeed) }) } }) when (headerType) { "none" -> {} "srtp", "utp", "dtls", "wireguard" -> { add(JsonObject().apply { addProperty("type", "mkcp-legacy") add("settings", JsonObject().apply { addProperty("header", headerType) }) }) } "wechat-video" -> { add(JsonObject().apply { addProperty("type", "mkcp-legacy") add("settings", JsonObject().apply { addProperty("header", "wechat") }) }) } } }) }.toString()) } "ws" -> { if (host.isNotEmpty()) { builder.addQueryParameter("host", host) } if (path.isNotEmpty()) { builder.addQueryParameter("path", path) } if (earlyDataHeaderName.isNotEmpty()) { // non-standard, invented by SagerNet and adopted by some other software builder.addQueryParameter("eh", earlyDataHeaderName) } if (maxEarlyData > 0) { // non-standard, invented by SagerNet and adopted by some other software builder.addQueryParameter("ed", maxEarlyData.toString()) } } "http" -> { if (host.isNotEmpty()) { builder.addQueryParameter("host", host.listByLineOrComma().joinToString(",")) } if (path.isNotEmpty()) { builder.addQueryParameter("path", path) } } "httpupgrade" -> { if (host.isNotEmpty()) { builder.addQueryParameter("host", host) } if (path.isNotEmpty()) { builder.addQueryParameter("path", path) } if (earlyDataHeaderName.isNotEmpty()) { // non-standard, invented by SagerNet and adopted by some other software builder.addQueryParameter("eh", earlyDataHeaderName) } if (maxEarlyData > 0) { // non-standard, invented by SagerNet and adopted by some other software builder.addQueryParameter("ed", maxEarlyData.toString()) } } "splithttp" -> { if (host.isNotEmpty()) { builder.addQueryParameter("host", host) } if (path.isNotEmpty()) { builder.addQueryParameter("path", path) } builder.addQueryParameter("mode", splithttpMode) if (splithttpExtra.isNotEmpty()) { parseJson(splithttpExtra).asJsonObject?.takeIf { !it.isEmpty }?.let { // fuck RPRX `extra` builder.addQueryParameter("extra", it.toString()) } } } "quic" -> { if (headerType != "none") { builder.addQueryParameter("headerType", headerType) } if (quicSecurity.isNotEmpty() && quicSecurity != "none") { builder.addQueryParameter("quicSecurity", quicSecurity) builder.addQueryParameter("key", quicKey) } } "grpc" -> { if (grpcServiceName.isNotEmpty()) { builder.addQueryParameter("serviceName", grpcServiceName) } if (grpcMultiMode) { builder.addQueryParameter("mode", "multi") // Xray private } } "meek" -> { // https://github.com/v2fly/v2ray-core/discussions/2638 if (meekUrl.isNotEmpty()) { builder.addQueryParameter("url", meekUrl) } } "mekya" -> { // not a standard if (headerType != "none") { builder.addQueryParameter("headerType", mekyaKcpHeaderType) } if (mekyaKcpSeed.isNotEmpty()) { builder.addQueryParameter("seed", mekyaKcpSeed) } if (mekyaUrl.isNotEmpty()) { builder.addQueryParameter("url", mekyaUrl) } } } when (security) { "tls" -> { // do not add `security=tls` for Trojan if possible if (this !is TrojanBean) { builder.addQueryParameter("security", security) } } else -> { builder.addQueryParameter("security", security) } } when (security) { "none" -> { if (this is VLESSBean && flow.isNotEmpty()) { builder.addQueryParameter("flow", flow.removeSuffix("-udp443")) } } "tls" -> { if (sni.isNotEmpty()) { if (this !is TrojanBean || sni != serverAddress) { // do not add `sni` for Trojan if possible builder.addQueryParameter("sni", sni) } } if (alpn.isNotEmpty()) { builder.addQueryParameter("alpn", alpn.listByLineOrComma().joinToString(",")) } // as pinned certificate is not exportable, only add `allowInsecure=1` if pinned certificate is not used if (allowInsecure && pinnedPeerCertificateSha256.isEmpty() && pinnedPeerCertificatePublicKeySha256.isEmpty() && pinnedPeerCertificateChainSha256.isEmpty() && serverNameToVerify.listByLineOrComma().isEmpty()) { // bad format from where? builder.addQueryParameter("allowInsecure", "1") } if (pinnedPeerCertificateSha256.isNotEmpty()) { builder.addQueryParameter("pcs", pinnedPeerCertificateSha256.listByLineOrComma().joinToString(":")) } if (serverNameToVerify.isNotEmpty()) { val serverNames = serverNameToVerify.listByLineOrComma() if (serverNames.contains("")) error("serverNameToVerify contains empty value") builder.addQueryParameter("vcn", serverNames.joinToString(",")) } if (this is VLESSBean && flow.isNotEmpty()) { builder.addQueryParameter("flow", flow.removeSuffix("-udp443")) } } "reality" -> { if (sni.isNotEmpty()) { builder.addQueryParameter("sni", sni) } builder.addQueryParameter("pbk", realityPublicKey.ifEmpty { error("empty reality public key") }) if (realityShortId.isNotEmpty()) { builder.addQueryParameter("sid", realityShortId) } if (realityMldsa65Verify.isNotEmpty()) { builder.addQueryParameter("pqv", realityMldsa65Verify) } builder.addQueryParameter("fp", "chrome") // "chrome" is only a placeholder because "若使用 REALITY,此项不可省略。". if (this is VLESSBean && flow.isNotEmpty()) { builder.addQueryParameter("flow", flow.removeSuffix("-udp443")) } } } return builder.string }