/******************************************************************************
* *
* Copyright (C) 2024 dyhkwong *
* *
* 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.Gson
import com.google.gson.JsonArray
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.naive.NaiveBean
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.snell.SnellBean
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.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.supportedVmessMethod
import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
import io.nekohasekai.sagernet.ktx.*
import kotlin.io.encoding.Base64
import kotlin.time.Duration
import kotlin.time.DurationUnit
fun parseSingBoxOutbound(outbound: JsonObject): List
{
when (val type = outbound.getString("type", ignoreCase = false)) {
"shadowsocks", "trojan", "vmess", "vless", "socks", "http" -> {
val v2rayBean = when (type) {
"shadowsocks" -> ShadowsocksBean()
"trojan" -> TrojanBean()
"vmess" -> VMessBean()
"vless" -> VLESSBean()
"socks" -> SOCKSBean()
"http" -> HttpBean()
else -> return listOf()
}.apply {
outbound.getString("tag", ignoreCase = false)?.also {
name = it
}
outbound.getString("server")?.also {
serverAddress = it
} ?: return listOf()
outbound.getInt("server_port")?.also {
serverPort = it
} ?: return listOf()
}
when (type) {
"trojan", "vmess", "vless" -> {
outbound.getObject("transport")?.takeIf { !it.isEmpty }?.also { transport ->
when (transport.getString("type")) {
"ws" -> {
v2rayBean.type = "ws"
transport.getString("path")?.also {
v2rayBean.path = it
}
transport.getObject("headers")?.also { headers ->
headers.getStringArray("host")?.get(0)?.also {
v2rayBean.host = it
} ?: headers.getString("host")?.also {
v2rayBean.host = it
}
}
transport.getInt("max_early_data")?.also {
v2rayBean.maxEarlyData = it
}
transport.getString("early_data_header_name")?.also {
v2rayBean.earlyDataHeaderName = it
}
}
"http" -> {
v2rayBean.type = "tcp"
v2rayBean.headerType = "http"
// Difference from v2ray-core
// TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.
outbound.getObject("tls")?.also {
if (it.getBoolean("enabled") == true) {
v2rayBean.type = "http"
v2rayBean.headerType = null
}
}
transport.getString("path")?.also {
v2rayBean.path = it
}
transport.getStringArray("host")?.also {
v2rayBean.host = it.joinToString("\n")
} ?: transport.getString("host")?.also {
v2rayBean.host = it
}
}
"quic" -> {
v2rayBean.type = "quic"
}
"grpc" -> {
v2rayBean.type = "grpc"
transport.getString("service_name")?.also {
v2rayBean.grpcServiceName = it
}
}
"httpupgrade" -> {
v2rayBean.type = "httpupgrade"
transport.getString("host")?.also {
v2rayBean.host = it
}
transport.getString("path")?.also {
v2rayBean.path = it
}
}
else -> return listOf()
}
}
}
}
when (type) {
"trojan", "vmess", "vless", "http" -> {
outbound.getObject("tls")?.also { tls ->
(tls.getBoolean("enabled"))?.also { enabled ->
if (enabled) {
v2rayBean.security = "tls"
tls.getString("server_name")?.also {
v2rayBean.sni = it
}
tls.getBoolean("insecure")?.also {
v2rayBean.allowInsecure = it
}
tls.getStringArray("alpn")?.also {
v2rayBean.alpn = it.joinToString("\n")
} ?: tls.getString("alpn")?.also {
v2rayBean.alpn = it
}
if (v2rayBean.alpn == null && v2rayBean.type == "quic") {
// https://github.com/SagerNet/sing-box/pull/1934
v2rayBean.alpn = "h3"
}
/*if (v2rayBean.alpn == null && type == "http") {
// sing-box does not support HTTP/2 CONNECT
v2rayBean.alpn = "http/1.1"
}*/
if (!tls.contains("certificate_path")) {
var cert: String? = null
tls.getStringArray("certificate")?.also { certificate ->
cert = certificate.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
} ?: tls.getString("certificate")?.also { certificate ->
cert = certificate.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
}
if (cert != null) {
v2rayBean.certificates = cert
}
}
if (!tls.contains("client_certificate_path") && !tls.contains("client_key_path")) {
var cert: String? = null
tls.getStringArray("client_certificate")?.also { clientCert ->
cert = clientCert.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
} ?: tls.getString("client_certificate")?.also { clientCert ->
cert = clientCert.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
}
var key: String? = null
tls.getStringArray("client_key")?.also { clientKey ->
key = clientKey.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----")
}
} ?: tls.getString("client_key")?.also { clientKey ->
key = clientKey.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----")
}
}
if (cert != null && key != null) {
v2rayBean.mtlsCertificate = cert
v2rayBean.mtlsCertificatePrivateKey = key
}
}
tls.getByteArrayArray("certificate_public_key_sha256")?.takeIf { it.isNotEmpty() }?.also {
v2rayBean.pinnedPeerCertificatePublicKeySha256 = it.joinToString("\n") { Base64.encode(it) }
v2rayBean.allowInsecure = true
} ?: tls.getByteArray("certificate_public_key_sha256")?.takeIf { it.isNotEmpty() }?.also {
v2rayBean.pinnedPeerCertificatePublicKeySha256 = Base64.encode(it)
v2rayBean.allowInsecure = true
}
tls.getObject("reality")?.also { reality ->
reality.getBoolean("enabled")?.also { enabled ->
if (enabled) {
v2rayBean.security = "reality"
reality.getString("public_key")?.ifEmpty { return listOf() }?.also {
v2rayBean.realityPublicKey = it
}
reality.getString("short_id")?.also {
v2rayBean.realityShortId = it
}
}
}
}
if (v2rayBean is VLESSBean || v2rayBean is TrojanBean || v2rayBean is VMessBean) {
tls.getObject("ech")?.also { ech ->
ech.getBoolean("enabled")?.also { enabled ->
if (enabled) {
v2rayBean.echEnabled = true
ech.getStringArray("config")?.also {
v2rayBean.echConfig = parseECHConfigPem(it.joinToString("\n"))
} ?: ech.getString("config")?.also {
v2rayBean.echConfig = parseECHConfigPem(it)
}
}
}
}
}
}
}
}
}
}
when (type) {
"socks" -> {
v2rayBean as SOCKSBean
outbound.getString("version")?.also {
v2rayBean.protocol = when (it) {
"4" -> SOCKSBean.PROTOCOL_SOCKS4
"4a" -> SOCKSBean.PROTOCOL_SOCKS4A
"", "5" -> SOCKSBean.PROTOCOL_SOCKS5
else -> return listOf()
}
}
outbound.getString("username")?.also {
v2rayBean.username = it
}
if (v2rayBean.protocol == SOCKSBean.PROTOCOL_SOCKS5) {
outbound.getString("password")?.also {
v2rayBean.password = it
}
}
}
"http" -> {
v2rayBean as HttpBean
outbound.getString("path")?.also {
if (it != "" && it != "/") {
// unsupported
return listOf()
}
}
outbound.getString("username")?.also {
v2rayBean.username = it
}
outbound.getString("password")?.also {
v2rayBean.password = it
}
}
"shadowsocks" -> {
v2rayBean as ShadowsocksBean
outbound.getString("method")?.also {
if (it !in supportedShadowsocksMethod) return listOf()
v2rayBean.method = it
}
outbound.getString("password")?.also {
v2rayBean.password = it
}
outbound.getString("plugin")?.takeIf { it.isNotEmpty() }?.also { pluginId ->
if (pluginId != "obfs-local" && pluginId != "v2ray-plugin") return listOf()
v2rayBean.plugin = PluginOptions(pluginId, outbound.getString("plugin_opts")).toString(trimId = false)
}
}
"trojan" -> {
v2rayBean as TrojanBean
outbound.getString("password")?.also {
v2rayBean.password = it
}
}
"vmess" -> {
v2rayBean as VMessBean
outbound.getString("uuid")?.also {
v2rayBean.uuid = uuidOrGenerate(it)
}
outbound.getString("security")?.also {
if (it !in supportedVmessMethod) return listOf()
v2rayBean.encryption = it
}
outbound.getInt("alter_id")?.also {
v2rayBean.alterId = it
}
outbound.getBoolean("global_padding")?.also {
v2rayBean.experimentalAuthenticatedLength = it
}
v2rayBean.packetEncoding = when (outbound.getString("packet_encoding")) {
"packetaddr" -> "packet"
"xudp" -> "xudp"
else -> "none"
}
}
"vless" -> {
v2rayBean as VLESSBean
outbound.getString("uuid")?.also {
v2rayBean.uuid = uuidOrGenerate(it)
}
v2rayBean.packetEncoding = when (outbound.getString("packet_encoding")) {
"packetaddr" -> "packet"
"xudp", null -> "xudp"
else -> "none"
}
outbound.getString("flow")?.also {
when (it) {
"" -> {}
"xtls-rprx-vision" -> {
v2rayBean.flow = "xtls-rprx-vision-udp443"
v2rayBean.packetEncoding = "xudp"
}
else -> return listOf()
}
}
}
}
return listOf(v2rayBean)
}
"hysteria2" -> {
outbound.getObject("realm")?.also {
// unsupported
return listOf()
}
val hysteria2Bean = Hysteria2Bean().apply {
outbound.getString("tag", ignoreCase = false)?.also {
name = it
}
outbound.getString("server")?.also {
serverAddress = it
} ?: return listOf()
outbound.getInt("server_port")?.takeIf { it > 0 }?.also {
serverPorts = it.toString()
}
outbound.getStringArray("server_ports")?.takeIf { it.isNotEmpty() }?.also {
serverPorts = it.joinToString(",").replace(":", "-")
} ?: outbound.getString("server_ports")?.also {
serverPorts = it.replace(":", "-")
}
if (!serverPorts.isValidHysteriaPort()) {
return listOf()
}
outbound.getString("hop_interval_max")?.also { interval ->
try {
val duration = Duration.parse(interval)
hopIntervalMax = duration.toLong(DurationUnit.SECONDS).takeIf { it > 0 }
} catch (_: Exception) {}
}
outbound.getString("hop_interval")?.also { interval ->
try {
val duration = Duration.parse(interval)
if (hopIntervalMax != null) {
hopIntervalMin = duration.toLong(DurationUnit.SECONDS).takeIf { it > 0 }
} else {
hopInterval = duration.toLong(DurationUnit.SECONDS).takeIf { it > 0 }
}
} catch (_: Exception) {}
}
outbound.getString("password")?.also {
auth = it
}
outbound.getObject("tls")?.also { tls ->
if (tls.getBoolean("enabled") != true) {
return listOf()
}
if (tls.getObject("reality")?.getBoolean("enabled") == true) {
return listOf()
}
tls.getString("server_name")?.also {
sni = it
}
tls.getBoolean("insecure")?.also {
allowInsecure = it
}
if (!tls.contains("certificate_path")) {
var cert: String? = null
tls.getStringArray("certificate")?.also { certificate ->
cert = certificate.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
} ?: tls.getString("certificate")?.also { certificate ->
cert = certificate.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
}
if (cert != null) {
certificates = cert
}
}
if (!tls.contains("client_certificate_path") && !tls.contains("client_key_path")) {
var cert: String? = null
tls.getStringArray("client_certificate")?.also { clientCert ->
cert = clientCert.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
} ?: tls.getString("client_certificate")?.also { clientCert ->
cert = clientCert.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
}
var key: String? = null
tls.getStringArray("client_key")?.also { clientKey ->
key = clientKey.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----")
}
} ?: tls.getString("client_key")?.also { clientKey ->
key = clientKey.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----")
}
}
if (cert != null && key != null) {
mtlsCertificate = cert
mtlsCertificatePrivateKey = key
}
}
tls.getByteArrayArray("certificate_public_key_sha256")?.takeIf { it.isNotEmpty() }?.also {
pinnedPeerCertificatePublicKeySha256 = it.joinToString("\n") { Base64.encode(it) }
allowInsecure = true
} ?: tls.getByteArray("certificate_public_key_sha256")?.takeIf { it.isNotEmpty() }?.also {
pinnedPeerCertificatePublicKeySha256 = Base64.encode(it)
allowInsecure = true
}
tls.getObject("ech")?.also { ech ->
ech.getBoolean("enabled")?.also { enabled ->
if (enabled) {
echEnabled = true
ech.getStringArray("config")?.also {
echConfig = parseECHConfigPem(it.joinToString("\n"))
} ?: ech.getString("config")?.also {
echConfig = parseECHConfigPem(it)
}
}
}
}
} ?: return listOf()
outbound.getObject("obfs")?.also { obfuscation ->
obfuscation.getString("type")?.takeIf { it.isNotEmpty() }?.also { type ->
when (type) {
"salamander" -> {
obfsType = "salamander"
obfuscation.getString("password")?.also {
obfsPassword = it
}
}
"gecko" -> {
obfsType = "gecko"
obfuscation.getString("password")?.also {
obfsPassword = it
}
obfuscation.getInt("min_packet_size")?.takeIf { it > 0 }?.also {
geckoMinPacketSize = it
}
obfuscation.getInt("max_packet_size")?.takeIf { it > 0 }?.also {
geckoMaxPacketSize = it
}
}
else -> return listOf()
}
}
}
}
return listOf(hysteria2Bean)
}
"tuic" -> {
val tuic5Bean = Tuic5Bean().apply {
outbound.getString("tag", ignoreCase = false)?.also {
name = it
}
outbound.getString("server")?.also {
serverAddress = it
} ?: return listOf()
outbound.getInt("server_port")?.also {
serverPort = it
} ?: return listOf()
outbound.getString("uuid")?.also {
uuid = it
}
outbound.getString("password")?.also {
password = it
}
outbound.getString("congestion_control")?.also {
congestionControl = if (it in supportedTuic5CongestionControl) it else "cubic"
}
outbound.getString("udp_relay_mode")?.also {
udpRelayMode = if (it in supportedTuic5RelayMode) it else "native"
}
outbound.getBoolean("zero_rtt_handshake")?.also {
zeroRTTHandshake = it
}
outbound.getObject("tls")?.also { tls ->
if (tls.getBoolean("enabled") != true) {
return listOf()
}
if (tls.getObject("reality")?.getBoolean("enabled") == true) {
return listOf()
}
tls.getString("server_name")?.also {
sni = it
}
tls.getStringArray("alpn")?.also {
alpn = it.joinToString("\n")
} ?: tls.getString("alpn")?.also {
alpn = it
}
tls.getBoolean("insecure")?.also {
allowInsecure = it
}
tls.getBoolean("disable_sni")?.also {
disableSNI = it
}
if (!tls.contains("certificate_path")) {
var cert: String? = null
tls.getStringArray("certificate")?.also { certificate ->
cert = certificate.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
} ?: tls.getString("certificate")?.also { certificate ->
cert = certificate.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
}
if (cert != null) {
certificates = cert
}
}
if (!tls.contains("client_certificate_path") && !tls.contains("client_key_path")) {
var cert: String? = null
tls.getStringArray("client_certificate")?.also { clientCert ->
cert = clientCert.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
} ?: tls.getString("client_certificate")?.also { clientCert ->
cert = clientCert.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
}
var key: String? = null
tls.getStringArray("client_key")?.also { clientKey ->
key = clientKey.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----")
}
} ?: tls.getString("client_key")?.also { clientKey ->
key = clientKey.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----")
}
}
if (cert != null && key != null) {
mtlsCertificate = cert
mtlsCertificatePrivateKey = key
}
}
tls.getByteArrayArray("certificate_public_key_sha256")?.takeIf { it.isNotEmpty() }?.also {
pinnedPeerCertificatePublicKeySha256 = it.joinToString("\n") { Base64.encode(it) }
allowInsecure = true
} ?: tls.getByteArray("certificate_public_key_sha256")?.takeIf { it.isNotEmpty() }?.also {
pinnedPeerCertificatePublicKeySha256 = Base64.encode(it)
allowInsecure = true
}
/*tls.getObject("ech")?.also { ech ->
ech.getBoolean("enabled")?.also { enabled ->
if (enabled) {
echEnabled = true
ech.getStringArray("config")?.also {
echConfig = parseECHConfigPem(it.joinToString("\n"))
} ?: ech.getString("config")?.also {
echConfig = parseECHConfigPem(it)
}
}
}
}*/
} ?: return listOf()
}
return listOf(tuic5Bean)
}
"ssh" -> {
val sshBean = SSHBean().apply {
outbound.getString("tag", ignoreCase = false)?.also {
name = it
}
outbound.getString("server")?.also {
serverAddress = it
} ?: return listOf()
outbound.getInt("server_port")?.takeIf { it > 0 }?.also {
serverPort = it
} ?: {
// https://github.com/SagerNet/sing-box/blob/5f739c4e66163ce88dbf75456e698c191d7069a6/protocol/ssh/outbound.go#L68
serverPort = 22
}
outbound.getString("user")?.takeIf { it.isNotEmpty() }?.also {
username = it
} ?: {
// https://github.com/SagerNet/sing-box/blob/5f739c4e66163ce88dbf75456e698c191d7069a6/protocol/ssh/outbound.go#L71
username = "root"
}
if (outbound.getString("password")?.isNotEmpty() == true) {
authType = SSHBean.AUTH_TYPE_PASSWORD
outbound.getString("password")?.also {
password = it
}
}
if (outbound.getString("private_key")?.isNotEmpty() == true) {
authType = SSHBean.AUTH_TYPE_PUBLIC_KEY
outbound.getStringArray("private_key")?.also {
privateKey = it.joinToString("\n")
} ?: outbound.getString("private_key")?.also {
privateKey = it
}
outbound.getString("private_key_passphrase")?.also {
privateKeyPassphrase = it
}
}
outbound.getStringArray("host_key")?.also {
publicKey = it.joinToString("\n")
} ?: outbound.getString("host_key")?.also {
publicKey = it
}
}
return listOf(sshBean)
}
"ssr" -> {
// removed in v1.6.0
val ssrBean = ShadowsocksRBean().apply {
outbound.getString("tag", ignoreCase = false)?.also {
name = it
}
outbound.getString("server")?.also {
serverAddress = it
} ?: return listOf()
outbound.getInt("server_port")?.also {
serverPort = it
} ?: return listOf()
outbound.getString("method")?.also {
if (it !in supportedShadowsocksRMethod) return listOf()
method = it
}
outbound.getString("password")?.also {
password = it
}
outbound.getString("obfs")?.also {
obfs = when (it) {
"tls1.2_ticket_fastauth" -> "tls1.2_ticket_auth"
in supportedShadowsocksRObfs -> it
else -> return listOf()
}
}
outbound.getString("obfs_param")?.also {
obfsParam = it
}
outbound.getString("protocol")?.also {
if (it !in supportedShadowsocksRProtocol) return listOf()
protocol = it
}
outbound.getString("protocol_param")?.also {
protocolParam = it
}
}
return listOf(ssrBean)
}
"anytls" -> {
val anytlsBean = AnyTLSBean().apply {
outbound.getString("tag", ignoreCase = false)?.also {
name = it
}
outbound.getString("server")?.also {
serverAddress = it
} ?: return listOf()
outbound.getInt("server_port")?.also {
serverPort = it
} ?: return listOf()
outbound.getString("password")?.also {
password = it
}
outbound.getObject("tls")?.also { tls ->
(tls.getBoolean("enabled"))?.also { enabled ->
if (enabled) {
security = "tls"
tls.getString("server_name")?.also {
sni = it
}
tls.getBoolean("insecure")?.also {
allowInsecure = it
}
tls.getStringArray("alpn")?.also {
alpn = it.joinToString("\n")
} ?: tls.getString("alpn")?.also {
alpn = it
}
if (!tls.contains("certificate_path")) {
var cert: String? = null
tls.getStringArray("certificate")?.also { certificate ->
cert = certificate.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
} ?: tls.getString("certificate")?.also { certificate ->
cert = certificate.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
}
if (cert != null) {
certificates = cert
}
}
if (!tls.contains("client_certificate_path") && !tls.contains("client_key_path")) {
var cert: String? = null
tls.getStringArray("client_certificate")?.also { clientCert ->
cert = clientCert.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
} ?: tls.getString("client_certificate")?.also { clientCert ->
cert = clientCert.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
}
var key: String? = null
tls.getStringArray("client_key")?.also { clientKey ->
key = clientKey.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----")
}
} ?: tls.getString("client_key")?.also { clientKey ->
key = clientKey.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" PRIVATE KEY-----")
}
}
if (cert != null && key != null) {
mtlsCertificate = cert
mtlsCertificatePrivateKey = key
}
}
tls.getByteArrayArray("certificate_public_key_sha256")?.takeIf { it.isNotEmpty() }?.also {
pinnedPeerCertificatePublicKeySha256 = it.joinToString("\n") { Base64.encode(it) }
allowInsecure = true
} ?: tls.getByteArray("certificate_public_key_sha256")?.takeIf { it.isNotEmpty() }?.also {
pinnedPeerCertificatePublicKeySha256 = Base64.encode(it)
allowInsecure = true
}
tls.getObject("reality")?.also { reality ->
reality.getBoolean("enabled")?.also { enabled ->
if (enabled) {
security = "reality"
reality.getString("public_key")?.also {
realityPublicKey = it
}
reality.getString("short_id")?.also {
realityShortId = it
}
}
}
}
/*tls.getObject("ech")?.also { ech ->
ech.getBoolean("enabled")?.also { enabled ->
if (enabled) {
echEnabled = true
ech.getStringArray("config")?.also {
echConfig = parseECHConfigPem(it.joinToString("\n"))
} ?: ech.getString("config")?.also {
echConfig = parseECHConfigPem(it)
}
}
}
}*/
} else {
security = "none"
}
}
}
}
return listOf(anytlsBean)
}
"naive" -> {
val naiveBean = NaiveBean().apply {
outbound.getString("tag", ignoreCase = false)?.also {
name = it
}
outbound.getString("server")?.also {
serverAddress = it
} ?: return listOf()
outbound.getInt("server_port")?.also {
serverPort = it
} ?: return listOf()
outbound.getString("username")?.also {
username = it
}
outbound.getString("password")?.also {
password = it
}
outbound.getBoolean("quic")?.takeIf { it }?.also {
proto = "quic"
}
outbound.getObject("tls")?.also { tls ->
tls.getString("server_name")?.also {
sni = it
}
if (!tls.contains("certificate_path")) {
var cert: String? = null
tls.getStringArray("certificate")?.also { certificate ->
cert = certificate.joinToString("\n").takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
} ?: tls.getString("certificate")?.also { certificate ->
cert = certificate.takeIf {
it.contains("-----BEGIN ") && it.contains("-----END ") && it.contains(" CERTIFICATE-----")
}
}
if (cert != null) {
certificate = cert
}
}
}
}
return listOf(naiveBean)
}
"snell" -> {
val snellBean = SnellBean().apply {
outbound.getString("tag", ignoreCase = false)?.also {
name = it
}
outbound.getString("server")?.also {
serverAddress = it
} ?: return listOf()
outbound.getInt("server_port")?.also {
serverPort = it
} ?: return listOf()
version = outbound.getInt("version")
if (version != 4 && version != 6) return listOf()
outbound.getString("psk")?.also {
psk = it
}
if (DataStore.experimentalFlagsProperties.getBooleanProperty("singSnellUserKey")) {
outbound.getString("userkey")?.also {
userKey = it
}
}
outbound.getBoolean("reuse")?.also {
reuse = it
}
when (version) {
4 -> when (outbound.getString("obfs_mode")?.lowercase()) {
null, "", "none" -> {
obfsMode = SnellBean.OBFS_NONE
}
"http" -> {
obfsMode = SnellBean.OBFS_HTTP
obfsHost = outbound.getString("obfs_host")
}
"tls" -> {
obfsMode = SnellBean.OBFS_TLS
obfsHost = outbound.getString("obfs_host")
}
else -> return listOf()
}
6 -> mode = when (outbound.getString("mode")) {
null, "", "default" -> SnellBean.MODE_DEFAULT
"unshaped" -> SnellBean.MODE_UNSHAPED
"unsafe-raw" -> SnellBean.MODE_UNSAFE_RAW
else -> return listOf()
}
}
}
return listOf(snellBean)
}
"wireguard" -> {
if (outbound.contains("address")) {
// wireguard endpoint format introduced in 1.11.0-alpha.19
return listOf()
}
val beanList = mutableListOf()
val bean = WireGuardBean().apply {
outbound.getString("tag", ignoreCase = false)?.also {
name = it
}
outbound.getString("private_key")?.also {
privateKey = it
}
outbound.getString("peer_public_key")?.also {
peerPublicKey = it
}
outbound.getString("pre_shared_key")?.also {
peerPreSharedKey = it
}
mtu = 1408
outbound.getInt("mtu")?.takeIf { it > 0 }?.also {
mtu = it
}
outbound.getStringArray("local_address")?.also {
localAddress = it.joinToString("\n")
} ?: outbound.getString("local_address")?.also {
localAddress = it
} ?: return listOf()
outbound.getIntArray("reserved")?.also {
if (it.size == 3) {
reserved = listOf(it[0].toString(), it[1].toString(), it[2].toString()).joinToString(",")
}
} ?: outbound.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 (outbound.contains("server")) {
outbound.getString("server")?.also {
bean.serverAddress = it
} ?: return listOf()
outbound.getInt("server_port")?.also {
bean.serverPort = it
} ?: return listOf()
beanList.add(bean)
}
outbound.getArray("peers")?.forEach { peer ->
beanList.add(bean.applyDefaultValues().clone().apply {
peer.getString("server")?.also {
serverAddress = it
}
peer.getInt("server_port")?.also {
serverPort = it
}
peer.getString("public_key")?.also {
peerPublicKey = it
}
peer.getString("pre_shared_key")?.also {
peerPreSharedKey = it
}
peer.getInt("persistent_keepalive_interval")?.takeIf { it > 0 }?.also {
keepaliveInterval = it
}
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
}
else -> return listOf()
}
}
fun parseSingBoxEndpoint(endpoint: JsonObject): List {
when (endpoint.getString("type", ignoreCase = false)) {
"wireguard" -> {
val beanList = mutableListOf()
if (endpoint.contains("local_address")) {
// legacy wireguard outbound format
return listOf()
}
val bean = WireGuardBean().apply {
endpoint.getString("tag", ignoreCase = false)?.also {
name = it
}
endpoint.getString("private_key")?.also {
privateKey = it
}
mtu = 1408
endpoint.getInt("mtu")?.takeIf { it > 0 }?.also {
mtu = it
}
endpoint.getStringArray("address")?.also {
localAddress = it.joinToString("\n")
} ?: endpoint.getString("address")?.also {
localAddress = it
} ?: return listOf()
}
endpoint.getArray("peers")?.forEach { peer ->
beanList.add(bean.applyDefaultValues().clone().apply {
peer.getString("address")?.also {
serverAddress = it
}
peer.getInt("port")?.also {
serverPort = it
}
peer.getString("public_key")?.also {
peerPublicKey = it
}
peer.getString("pre_shared_key")?.also {
peerPreSharedKey = it
}
peer.getInt("persistent_keepalive_interval")?.takeIf { it > 0 }?.also {
keepaliveInterval = it
}
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
}
else -> return listOf()
}
}
private fun JsonObject.contains(key: String, ignoreCase: Boolean = true): Boolean {
val value = get(key)
when {
value == null -> if (!ignoreCase) return false
value.isJsonNull -> if (!ignoreCase) return false
else -> return true
}
for ((k, v) in entrySet()) {
if (k.equals(key, ignoreCase = true) && !v.isJsonNull) {
return true
}
}
return false
}
private fun JsonObject.getString(key: String, ignoreCase: Boolean = true): String? {
val value = get(key)
when {
value == null -> if (!ignoreCase) return null
value.isJsonNull -> if (!ignoreCase) return null
value.isJsonPrimitive -> return if (value.asJsonPrimitive.isString) value.asString else null
else -> return null
}
for ((k, v) in entrySet()) {
if (k.equals(key, ignoreCase = true) && v.isJsonPrimitive && v.asJsonPrimitive.isString) {
return v.asString
}
}
return null
}
private fun JsonObject.getInt(key: String, ignoreCase: Boolean = true): Int? {
val value = get(key)
when {
value == null -> if (!ignoreCase) return null
value.isJsonNull -> if (!ignoreCase) return null
value.isJsonPrimitive -> {
return if (value.asJsonPrimitive.isNumber) {
try {
value.asInt
} catch (_: Exception) {
null
}
} else {
null
}
}
}
for ((k, v) in entrySet()) {
if (k.equals(key, ignoreCase = true) && v.isJsonPrimitive && v.asJsonPrimitive.isNumber) {
try {
return v.asInt
} catch (_: Exception) {}
}
}
return null
}
private fun JsonObject.getBoolean(key: String, ignoreCase: Boolean = true): Boolean? {
val value = get(key)
when {
value == null -> if (!ignoreCase) return null
value.isJsonNull -> if (!ignoreCase) return null
value.isJsonPrimitive -> return if (value.asJsonPrimitive.isBoolean) value.asBoolean else null
else -> return null
}
for ((k, v) in entrySet()) {
if (k.equals(key, ignoreCase = true) && v.isJsonPrimitive && v.asJsonPrimitive.isBoolean) {
return v.asBoolean
}
}
return null
}
private fun JsonObject.getObject(key: String, ignoreCase: Boolean = true): JsonObject? {
val value = get(key)
when {
value == null -> if (!ignoreCase) return null
value.isJsonNull -> if (!ignoreCase) return null
value.isJsonObject -> return value.asJsonObject
else -> return null
}
for ((k, v) in entrySet()) {
if (k.equals(key, ignoreCase = true) && v.isJsonObject) {
return v.asJsonObject
}
}
return null
}
private fun JsonObject.getJsonArray(key: String, ignoreCase: Boolean = true): JsonArray? {
val value = get(key)
when {
value == null -> if (!ignoreCase) return null
value.isJsonNull -> if (!ignoreCase) return null
value.isJsonArray -> return value.asJsonArray
else -> return null
}
for ((k, v) in entrySet()) {
if (k.equals(key, ignoreCase = true) && v.isJsonArray) {
return v.asJsonArray
}
}
return null
}
private fun JsonObject.getArray(key: String, ignoreCase: Boolean = true): List? {
val jsonArray = getJsonArray(key, ignoreCase) ?: return null
return try {
Gson().fromJson(jsonArray, Array::class.java)?.asList()
} catch (_: Exception) {
null
}
}
private fun JsonObject.getStringArray(key: String, ignoreCase: Boolean = true): List? {
val jsonArray = getJsonArray(key, ignoreCase) ?: return null
return try {
Gson().fromJson(jsonArray, Array::class.java)?.asList()
} catch (_: Exception) {
null
}
}
private fun JsonObject.getIntArray(key: String, ignoreCase: Boolean = true): List? {
val jsonArray = getJsonArray(key, ignoreCase) ?: return null
return try {
Gson().fromJson(jsonArray, Array::class.java)?.asList()
} catch (_: Exception) {
null
}
}
private fun JsonObject.getByteArray(key: String): ByteArray? {
val value = get(key)
when {
value == null -> {}
value.isJsonNull -> {}
value.isJsonPrimitive && value.asJsonPrimitive.isString -> {
return try {
Base64.decode(value.asString.toByteArray())
} catch (_: Exception) {
null
}
}
value.isJsonArray -> return try {
Gson().fromJson(value, ByteArray::class.java)
} catch (_: Exception) {
null
}
else -> return null
}
for ((k, v) in entrySet()) {
if (k.equals(key, ignoreCase = true)) {
when {
v.isJsonPrimitive && v.asJsonPrimitive.isString -> {
try {
return Base64.decode(v.asString.toByteArray())
} catch (_: Exception) {}
}
v.isJsonArray -> {
try {
return Gson().fromJson(v, ByteArray::class.java)
} catch (_: Exception) {}
}
}
}
}
return null
}
private fun JsonObject.getByteArrayArray(key: String): Array? {
val jsonArray = getJsonArray(key, ignoreCase = true) ?: return null
val ret = mutableListOf()
for (value in jsonArray) {
when {
value.isJsonPrimitive && value.asJsonPrimitive.isString -> {
ret.add(
try {
Base64.decode(value.asString.toByteArray())
} catch (_: Exception) {
return null
}
)
}
value.isJsonArray -> {
ret.add(
try {
Gson().fromJson(value, ByteArray::class.java)
} catch (_: Exception) {
return null
}
)
}
value.isJsonNull -> continue
else -> return null
}
}
return ret.toTypedArray()
}
// this is not strict, but enough
private fun parseECHConfigPem(pem: String): String? {
if (pem.split("-----BEGIN ECH CONFIGS-----").size - 1 != 1) {
return null
}
if (pem.split("-----END ECH CONFIGS-----").size - 1 != 1) {
return null
}
return try {
Base64.encode(Base64.decode(pem
.substringAfter("-----BEGIN ECH CONFIGS-----")
.substringBefore("-----END ECH CONFIGS-----")
.replace("\r", "")
.replace("\n", "")
))
} catch (_: Exception) {
null
}
}