From 3d001541e5a609524f471eb99c1bf1c7d49f8b06 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Thu, 30 May 2024 20:04:43 +0800 Subject: [PATCH] Refactor the parser --- .../com/v2ray/ang/util/AngConfigManager.kt | 641 +----------------- .../com/v2ray/ang/util/fmt/ShadowsocksFmt.kt | 159 +++++ .../kotlin/com/v2ray/ang/util/fmt/SocksFmt.kt | 69 ++ .../com/v2ray/ang/util/fmt/TrojanFmt.kt | 170 +++++ .../kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt | 170 +++++ .../kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt | 193 ++++++ .../com/v2ray/ang/util/fmt/WireguardFmt.kt | 71 ++ 7 files changed, 860 insertions(+), 613 deletions(-) create mode 100644 V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/ShadowsocksFmt.kt create mode 100644 V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/SocksFmt.kt create mode 100644 V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt create mode 100644 V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt create mode 100644 V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt create mode 100644 V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/WireguardFmt.kt diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt index ac4d1f68..94e09e47 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt @@ -3,7 +3,6 @@ package com.v2ray.ang.util import android.content.Context import android.graphics.Bitmap import android.text.TextUtils -import android.util.Log import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonPrimitive @@ -11,20 +10,18 @@ import com.google.gson.JsonSerializationContext import com.google.gson.JsonSerializer import com.google.gson.reflect.TypeToken import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.PROTOCOL_HTTP import com.v2ray.ang.AppConfig.PROTOCOL_HTTPS -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU import com.v2ray.ang.R import com.v2ray.ang.dto.* -import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_SECURITY -import com.v2ray.ang.dto.V2rayConfig.Companion.TLS -import com.v2ray.ang.extension.idnHost -import com.v2ray.ang.extension.removeWhiteSpace import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER +import com.v2ray.ang.util.fmt.ShadowsocksFmt +import com.v2ray.ang.util.fmt.SocksFmt +import com.v2ray.ang.util.fmt.TrojanFmt +import com.v2ray.ang.util.fmt.VlessFmt +import com.v2ray.ang.util.fmt.VmessFmt +import com.v2ray.ang.util.fmt.WireguardFmt import java.lang.reflect.Type -import java.net.URI import java.util.* object AngConfigManager { @@ -219,250 +216,28 @@ object AngConfigManager { //maybe Subscription if (TextUtils.isEmpty(subid) - && (str.startsWith(PROTOCOL_HTTP) || str.startsWith(PROTOCOL_HTTPS))) { + && (str.startsWith(PROTOCOL_HTTP) || str.startsWith(PROTOCOL_HTTPS)) + ) { MmkvManager.importUrlAsSubscription(str) return 0 } - var config: ServerConfig? = null - val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false - if (str.startsWith(EConfigType.VMESS.protocolScheme)) { - config = ServerConfig.create(EConfigType.VMESS) - val streamSetting = config.outboundBean?.streamSettings ?: return -1 - - - if (!tryParseNewVmess(str, config, allowInsecure)) { - if (str.indexOf("?") > 0) { - if (!tryResolveVmess4Kitsunebi(str, config)) { - return R.string.toast_incorrect_protocol - } - } else { - var result = str.replace(EConfigType.VMESS.protocolScheme, "") - result = Utils.decode(result) - if (TextUtils.isEmpty(result)) { - return R.string.toast_decoding_failed - } - val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java) - // Although VmessQRCode fields are non null, looks like Gson may still create null fields - if (TextUtils.isEmpty(vmessQRCode.add) - || TextUtils.isEmpty(vmessQRCode.port) - || TextUtils.isEmpty(vmessQRCode.id) - || TextUtils.isEmpty(vmessQRCode.net) - ) { - return R.string.toast_incorrect_protocol - } - - config.remarks = vmessQRCode.ps - config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> - vnext.address = vmessQRCode.add - vnext.port = Utils.parseInt(vmessQRCode.port) - vnext.users[0].id = vmessQRCode.id - vnext.users[0].security = - if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy - vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid) - } - val sni = streamSetting.populateTransportSettings( - vmessQRCode.net, - vmessQRCode.type, - vmessQRCode.host, - vmessQRCode.path, - vmessQRCode.path, - vmessQRCode.host, - vmessQRCode.path, - vmessQRCode.type, - vmessQRCode.path, - vmessQRCode.host - ) - - val fingerprint = vmessQRCode.fp ?: streamSetting.tlsSettings?.fingerprint - streamSetting.populateTlsSettings( - vmessQRCode.tls, allowInsecure, - if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni, - fingerprint, vmessQRCode.alpn, null, null, null - ) - } - } + val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) { + VmessFmt.parseVmess(str) } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) { - config = ServerConfig.create(EConfigType.SHADOWSOCKS) - if (!tryResolveResolveSip002(str, config)) { - var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "") - val indexSplit = result.indexOf("#") - if (indexSplit > 0) { - try { - config.remarks = - Utils.urlDecode(result.substring(indexSplit + 1, result.length)) - } catch (e: Exception) { - e.printStackTrace() - } - - result = result.substring(0, indexSplit) - } - - //part decode - val indexS = result.indexOf("@") - result = if (indexS > 0) { - Utils.decode(result.substring(0, indexS)) + result.substring( - indexS, - result.length - ) - } else { - Utils.decode(result) - } - - val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex() - val match = legacyPattern.matchEntire(result) - ?: return R.string.toast_incorrect_protocol - - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = match.groupValues[3].removeSurrounding("[", "]") - server.port = match.groupValues[4].toInt() - server.password = match.groupValues[2] - server.method = match.groupValues[1].lowercase() - } - } + ShadowsocksFmt.parseShadowsocks(str) } else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) { - var result = str.replace(EConfigType.SOCKS.protocolScheme, "") - val indexSplit = result.indexOf("#") - config = ServerConfig.create(EConfigType.SOCKS) - if (indexSplit > 0) { - try { - config.remarks = - Utils.urlDecode(result.substring(indexSplit + 1, result.length)) - } catch (e: Exception) { - e.printStackTrace() - } - - result = result.substring(0, indexSplit) - } - - //part decode - val indexS = result.indexOf("@") - if (indexS > 0) { - result = Utils.decode(result.substring(0, indexS)) + result.substring( - indexS, - result.length - ) - } else { - result = Utils.decode(result) - } - - val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex() - val match = - legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol - - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = match.groupValues[3].removeSurrounding("[", "]") - server.port = match.groupValues[4].toInt() - val socksUsersBean = - V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() - socksUsersBean.user = match.groupValues[1] - socksUsersBean.pass = match.groupValues[2] - server.users = listOf(socksUsersBean) - } + SocksFmt.parseSocks(str) } else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) { - val uri = URI(Utils.fixIllegalUrl(str)) - config = ServerConfig.create(EConfigType.TROJAN) - config.remarks = Utils.urlDecode(uri.fragment ?: "") - - var flow = "" - var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint - if (uri.rawQuery != null) { - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - - val sni = config.outboundBean?.streamSettings?.populateTransportSettings( - queryParam["type"] ?: "tcp", - queryParam["headerType"], - queryParam["host"], - queryParam["path"], - queryParam["seed"], - queryParam["quicSecurity"], - queryParam["key"], - queryParam["mode"], - queryParam["serviceName"], - queryParam["authority"] - ) - fingerprint = queryParam["fp"] ?: "" - config.outboundBean?.streamSettings?.populateTlsSettings( - queryParam["security"] ?: TLS, - allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"], - null, null, null - ) - flow = queryParam["flow"] ?: "" - } else { - config.outboundBean?.streamSettings?.populateTlsSettings( - TLS, allowInsecure, "", - fingerprint, null, null, null, null - ) - } - - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = uri.idnHost - server.port = uri.port - server.password = uri.userInfo - server.flow = flow - } + TrojanFmt.parseTrojan(str) } else if (str.startsWith(EConfigType.VLESS.protocolScheme)) { - val uri = URI(Utils.fixIllegalUrl(str)) - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - config = ServerConfig.create(EConfigType.VLESS) - val streamSetting = config.outboundBean?.streamSettings ?: return -1 - - config.remarks = Utils.urlDecode(uri.fragment ?: "") - config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> - vnext.address = uri.idnHost - vnext.port = uri.port - vnext.users[0].id = uri.userInfo - vnext.users[0].encryption = queryParam["encryption"] ?: "none" - vnext.users[0].flow = queryParam["flow"] ?: "" - } - - val sni = streamSetting.populateTransportSettings( - queryParam["type"] ?: "tcp", - queryParam["headerType"], - queryParam["host"], - queryParam["path"], - queryParam["seed"], - queryParam["quicSecurity"], - queryParam["key"], - queryParam["mode"], - queryParam["serviceName"], - queryParam["authority"] - ) - streamSetting.populateTlsSettings( - queryParam["security"] ?: "", - allowInsecure, - queryParam["sni"] ?: sni, - queryParam["fp"] ?: "", - queryParam["alpn"], - queryParam["pbk"] ?: "", - queryParam["sid"] ?: "", - queryParam["spx"] ?: "" - ) + VlessFmt.parseVless(str) } else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) { - val uri = URI(Utils.fixIllegalUrl(str)) - config = ServerConfig.create(EConfigType.WIREGUARD) - config.remarks = Utils.urlDecode(uri.fragment ?: "") - - if (uri.rawQuery != null) { - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - - config.outboundBean?.settings?.let { wireguard -> - wireguard.secretKey = uri.userInfo - wireguard.address = - (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace() - .split(",") - wireguard.peers?.get(0)?.publicKey = queryParam["publickey"] ?: "" - wireguard.peers?.get(0)?.endpoint = "${uri.idnHost}:${uri.port}" - wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: WIREGUARD_LOCAL_MTU) - wireguard.reserved = - (queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",") - .map { it.toInt() } - } - } + WireguardFmt.parseWireguard(str) + } else { + null } + if (config == null) { return R.string.toast_incorrect_protocol } @@ -485,171 +260,6 @@ object AngConfigManager { return 0 } - private fun tryParseNewVmess( - uriString: String, - config: ServerConfig, - allowInsecure: Boolean - ): Boolean { - return runCatching { - val uri = URI(Utils.fixIllegalUrl(uriString)) - check(uri.scheme == "vmess") - val (_, protocol, tlsStr, uuid, alterId) = - Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})") - .matchEntire(uri.userInfo)?.groupValues - ?: error("parse user info fail.") - val tls = tlsStr.isNotBlank() - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - - val streamSetting = config.outboundBean?.streamSettings ?: return false - config.remarks = Utils.urlDecode(uri.fragment ?: "") - config.outboundBean.settings?.vnext?.get(0)?.let { vnext -> - vnext.address = uri.idnHost - vnext.port = uri.port - vnext.users[0].id = uuid - vnext.users[0].security = DEFAULT_SECURITY - vnext.users[0].alterId = alterId.toInt() - } - var fingerprint = streamSetting.tlsSettings?.fingerprint - val sni = streamSetting.populateTransportSettings(protocol, - queryParam["type"], - queryParam["host"]?.split("|")?.get(0) ?: "", - queryParam["path"]?.takeIf { it.trim() != "/" } ?: "", - queryParam["seed"], - queryParam["security"], - queryParam["key"], - queryParam["mode"], - queryParam["serviceName"], - queryParam["authority"]) - streamSetting.populateTlsSettings( - if (tls) TLS else "", allowInsecure, sni, fingerprint, null, - null, null, null - ) - true - }.getOrElse { false } - } - - private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean { - - var result = server.replace(EConfigType.VMESS.protocolScheme, "") - val indexSplit = result.indexOf("?") - if (indexSplit > 0) { - result = result.substring(0, indexSplit) - } - result = Utils.decode(result) - - val arr1 = result.split('@') - if (arr1.count() != 2) { - return false - } - val arr21 = arr1[0].split(':') - val arr22 = arr1[1].split(':') - if (arr21.count() != 2) { - return false - } - - config.remarks = "Alien" - config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> - vnext.address = arr22[0] - vnext.port = Utils.parseInt(arr22[1]) - vnext.users[0].id = arr21[1] - vnext.users[0].security = arr21[0] - vnext.users[0].alterId = 0 - } - return true - } - - private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean { - try { - val uri = URI(Utils.fixIllegalUrl(str)) - config.remarks = Utils.urlDecode(uri.fragment ?: "") - - val method: String - val password: String - if (uri.userInfo.contains(":")) { - val arrUserInfo = uri.userInfo.split(":").map { it.trim() } - if (arrUserInfo.count() != 2) { - return false - } - method = arrUserInfo[0] - password = Utils.urlDecode(arrUserInfo[1]) - } else { - val base64Decode = Utils.decode(uri.userInfo) - val arrUserInfo = base64Decode.split(":").map { it.trim() } - if (arrUserInfo.count() < 2) { - return false - } - method = arrUserInfo[0] - password = base64Decode.substringAfter(":") - } - - val query = Utils.urlDecode(uri.query ?: "") - if (query != "") { - val queryPairs = HashMap() - val pairs = query.split(";") - Log.d(AppConfig.ANG_PACKAGE, pairs.toString()) - for (pair in pairs) { - val idx = pair.indexOf("=") - if (idx == -1) { - queryPairs[Utils.urlDecode(pair)] = ""; - } else { - queryPairs[Utils.urlDecode(pair.substring(0, idx))] = - Utils.urlDecode(pair.substring(idx + 1)) - } - } - Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString()) - var sni: String? = "" - if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") { - sni = config.outboundBean?.streamSettings?.populateTransportSettings( - "tcp", - "http", - queryPairs["obfs-host"], - queryPairs["path"], - null, - null, - null, - null, - null, - null - ) - } else if (queryPairs["plugin"] == "v2ray-plugin") { - var network = "ws"; - if (queryPairs["mode"] == "quic") { - network = "quic"; - } - sni = config.outboundBean?.streamSettings?.populateTransportSettings( - network, - null, - queryPairs["host"], - queryPairs["path"], - null, - null, - null, - null, - null, - null - ) - } - if ("tls" in queryPairs) { - config.outboundBean?.streamSettings?.populateTlsSettings( - "tls", false, sni ?: "", null, null, null, null, null - ) - } - - } - - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = uri.idnHost - server.port = uri.port - server.password = password - server.method = method - } - return true - } catch (e: Exception) { - Log.d(AppConfig.ANG_PACKAGE, e.toString()) - return false - } - } /** * share config @@ -657,211 +267,15 @@ object AngConfigManager { private fun shareConfig(guid: String): String { try { val config = MmkvManager.decodeServerConfig(guid) ?: return "" - val outbound = config.getProxyOutbound() ?: return "" - val streamSetting = - outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean() - if (config.configType != EConfigType.WIREGUARD) { - if (outbound.streamSettings == null) return "" - } + return config.configType.protocolScheme + when (config.configType) { - EConfigType.VMESS -> { - val vmessQRCode = VmessQRCode() - vmessQRCode.v = "2" - vmessQRCode.ps = config.remarks - vmessQRCode.add = outbound.getServerAddress().orEmpty() - vmessQRCode.port = outbound.getServerPort().toString() - vmessQRCode.id = outbound.getPassword().orEmpty() - vmessQRCode.aid = - outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString() - vmessQRCode.scy = - outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString() - vmessQRCode.net = streamSetting.network - vmessQRCode.tls = streamSetting.security - vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty() - vmessQRCode.alpn = - Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()) - .orEmpty() - vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty() - outbound.getTransportSettingDetails()?.let { transportDetails -> - vmessQRCode.type = transportDetails[0] - vmessQRCode.host = transportDetails[1] - vmessQRCode.path = transportDetails[2] - } - val json = Gson().toJson(vmessQRCode) - Utils.encode(json) - } - + EConfigType.VMESS -> VmessFmt.toUri(config) EConfigType.CUSTOM -> "" - - EConfigType.SHADOWSOCKS -> { - val remark = "#" + Utils.urlEncode(config.remarks) - val pw = - Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}") - val url = String.format( - "%s@%s:%s", - pw, - Utils.getIpv6Address(outbound.getServerAddress()!!), - outbound.getServerPort() - ) - url + remark - } - - EConfigType.SOCKS -> { - val remark = "#" + Utils.urlEncode(config.remarks) - val pw = - if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null) - "${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}" - else - ":" - val url = String.format( - "%s@%s:%s", - Utils.encode(pw), - Utils.getIpv6Address(outbound.getServerAddress()!!), - outbound.getServerPort() - ) - url + remark - } - - EConfigType.VLESS, - EConfigType.TROJAN -> { - val remark = "#" + Utils.urlEncode(config.remarks) - - val dicQuery = HashMap() - if (config.configType == EConfigType.VLESS) { - outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let { - if (!TextUtils.isEmpty(it)) { - dicQuery["flow"] = it - } - } - dicQuery["encryption"] = - if (outbound.getSecurityEncryption().isNullOrEmpty()) "none" - else outbound.getSecurityEncryption().orEmpty() - } else if (config.configType == EConfigType.TROJAN) { - config.outboundBean?.settings?.servers?.get(0)?.flow?.let { - if (!TextUtils.isEmpty(it)) { - dicQuery["flow"] = it - } - } - } - - dicQuery["security"] = streamSetting.security.ifEmpty { "none" } - (streamSetting.tlsSettings - ?: streamSetting.realitySettings)?.let { tlsSetting -> - if (!TextUtils.isEmpty(tlsSetting.serverName)) { - dicQuery["sni"] = tlsSetting.serverName - } - if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) { - dicQuery["alpn"] = - Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty() - } - if (!TextUtils.isEmpty(tlsSetting.fingerprint)) { - dicQuery["fp"] = tlsSetting.fingerprint!! - } - if (!TextUtils.isEmpty(tlsSetting.publicKey)) { - dicQuery["pbk"] = tlsSetting.publicKey!! - } - if (!TextUtils.isEmpty(tlsSetting.shortId)) { - dicQuery["sid"] = tlsSetting.shortId!! - } - if (!TextUtils.isEmpty(tlsSetting.spiderX)) { - dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX!!) - } - } - dicQuery["type"] = - streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK } - - outbound.getTransportSettingDetails()?.let { transportDetails -> - when (streamSetting.network) { - "tcp" -> { - dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } - if (!TextUtils.isEmpty(transportDetails[1])) { - dicQuery["host"] = Utils.urlEncode(transportDetails[1]) - } - } - - "kcp" -> { - dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } - if (!TextUtils.isEmpty(transportDetails[2])) { - dicQuery["seed"] = Utils.urlEncode(transportDetails[2]) - } - } - - "ws", "httpupgrade" -> { - if (!TextUtils.isEmpty(transportDetails[1])) { - dicQuery["host"] = Utils.urlEncode(transportDetails[1]) - } - if (!TextUtils.isEmpty(transportDetails[2])) { - dicQuery["path"] = Utils.urlEncode(transportDetails[2]) - } - } - - "http", "h2" -> { - dicQuery["type"] = "http" - if (!TextUtils.isEmpty(transportDetails[1])) { - dicQuery["host"] = Utils.urlEncode(transportDetails[1]) - } - if (!TextUtils.isEmpty(transportDetails[2])) { - dicQuery["path"] = Utils.urlEncode(transportDetails[2]) - } - } - - "quic" -> { - dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } - dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1]) - dicQuery["key"] = Utils.urlEncode(transportDetails[2]) - } - - "grpc" -> { - dicQuery["mode"] = transportDetails[0] - dicQuery["authority"] = Utils.urlEncode(transportDetails[1]) - dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2]) - } - } - } - val query = "?" + dicQuery.toList().joinToString( - separator = "&", - transform = { it.first + "=" + it.second }) - - val url = String.format( - "%s@%s:%s", - outbound.getPassword(), - Utils.getIpv6Address(outbound.getServerAddress()!!), - outbound.getServerPort() - ) - url + query + remark - } - - EConfigType.WIREGUARD -> { - val remark = "#" + Utils.urlEncode(config.remarks) - - val dicQuery = HashMap() - dicQuery["publickey"] = - Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString()) - if (outbound.settings?.reserved != null) { - dicQuery["reserved"] = Utils.urlEncode( - Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString()) - .toString() - ) - } - dicQuery["address"] = Utils.urlEncode( - Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString()) - .toString() - ) - if (outbound.settings?.mtu != null) { - dicQuery["mtu"] = outbound.settings?.mtu.toString() - } - val query = "?" + dicQuery.toList().joinToString( - separator = "&", - transform = { it.first + "=" + it.second }) - - val url = String.format( - "%s@%s:%s", - Utils.urlEncode(outbound.getPassword().toString()), - Utils.getIpv6Address(outbound.getServerAddress()!!), - outbound.getServerPort() - ) - url + query + remark - } + EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config) + EConfigType.SOCKS -> SocksFmt.toUri(config) + EConfigType.VLESS-> VlessFmt.toUri(config) + EConfigType.TROJAN-> TrojanFmt.toUri(config) + EConfigType.WIREGUARD -> WireguardFmt.toUri(config) } } catch (e: Exception) { e.printStackTrace() @@ -1023,7 +437,7 @@ object AngConfigManager { return 0 } - fun appendCustomConfigServer(server: String?, subid: String): Int { + fun appendCustomConfigServer(server: String?, subid: String): Int { if (server == null) { return 0 } @@ -1052,7 +466,8 @@ object AngConfigManager { var count = 0 for (srv in serverList) { val config = ServerConfig.create(EConfigType.CUSTOM) - config.fullConfig = Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java) + config.fullConfig = + Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java) config.remarks = config.fullConfig?.remarks ?: ("%04d-".format(count + 1) + System.currentTimeMillis() .toString()) diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/ShadowsocksFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/ShadowsocksFmt.kt new file mode 100644 index 00000000..f79ccb08 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/ShadowsocksFmt.kt @@ -0,0 +1,159 @@ +package com.v2ray.ang.util.fmt + +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.util.Utils +import java.net.URI + +object ShadowsocksFmt { + fun parseShadowsocks(str: String): ServerConfig? { + val config = ServerConfig.create(EConfigType.SHADOWSOCKS) + if (!tryResolveResolveSip002(str, config)) { + var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "") + val indexSplit = result.indexOf("#") + if (indexSplit > 0) { + try { + config.remarks = + Utils.urlDecode(result.substring(indexSplit + 1, result.length)) + } catch (e: Exception) { + e.printStackTrace() + } + + result = result.substring(0, indexSplit) + } + + //part decode + val indexS = result.indexOf("@") + result = if (indexS > 0) { + Utils.decode(result.substring(0, indexS)) + result.substring( + indexS, + result.length + ) + } else { + Utils.decode(result) + } + + val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex() + val match = legacyPattern.matchEntire(result) + ?: return null + + config.outboundBean?.settings?.servers?.get(0)?.let { server -> + server.address = match.groupValues[3].removeSurrounding("[", "]") + server.port = match.groupValues[4].toInt() + server.password = match.groupValues[2] + server.method = match.groupValues[1].lowercase() + } + } + return config + } + + fun toUri(config: ServerConfig): String { + val outbound = config.getProxyOutbound() ?: return "" + val remark = "#" + Utils.urlEncode(config.remarks) + val pw = + Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}") + val url = String.format( + "%s@%s:%s", + pw, + Utils.getIpv6Address(outbound.getServerAddress()!!), + outbound.getServerPort() + ) + return url + remark + } + + private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean { + try { + val uri = URI(Utils.fixIllegalUrl(str)) + config.remarks = Utils.urlDecode(uri.fragment ?: "") + + val method: String + val password: String + if (uri.userInfo.contains(":")) { + val arrUserInfo = uri.userInfo.split(":").map { it.trim() } + if (arrUserInfo.count() != 2) { + return false + } + method = arrUserInfo[0] + password = Utils.urlDecode(arrUserInfo[1]) + } else { + val base64Decode = Utils.decode(uri.userInfo) + val arrUserInfo = base64Decode.split(":").map { it.trim() } + if (arrUserInfo.count() < 2) { + return false + } + method = arrUserInfo[0] + password = base64Decode.substringAfter(":") + } + + val query = Utils.urlDecode(uri.query ?: "") + if (query != "") { + val queryPairs = HashMap() + val pairs = query.split(";") + Log.d(AppConfig.ANG_PACKAGE, pairs.toString()) + for (pair in pairs) { + val idx = pair.indexOf("=") + if (idx == -1) { + queryPairs[Utils.urlDecode(pair)] = ""; + } else { + queryPairs[Utils.urlDecode(pair.substring(0, idx))] = + Utils.urlDecode(pair.substring(idx + 1)) + } + } + Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString()) + var sni: String? = "" + if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") { + sni = config.outboundBean?.streamSettings?.populateTransportSettings( + "tcp", + "http", + queryPairs["obfs-host"], + queryPairs["path"], + null, + null, + null, + null, + null, + null + ) + } else if (queryPairs["plugin"] == "v2ray-plugin") { + var network = "ws"; + if (queryPairs["mode"] == "quic") { + network = "quic"; + } + sni = config.outboundBean?.streamSettings?.populateTransportSettings( + network, + null, + queryPairs["host"], + queryPairs["path"], + null, + null, + null, + null, + null, + null + ) + } + if ("tls" in queryPairs) { + config.outboundBean?.streamSettings?.populateTlsSettings( + "tls", false, sni ?: "", null, null, null, null, null + ) + } + + } + + config.outboundBean?.settings?.servers?.get(0)?.let { server -> + server.address = uri.idnHost + server.port = uri.port + server.password = password + server.method = method + } + return true + } catch (e: Exception) { + Log.d(AppConfig.ANG_PACKAGE, e.toString()) + return false + } + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/SocksFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/SocksFmt.kt new file mode 100644 index 00000000..420f8a68 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/SocksFmt.kt @@ -0,0 +1,69 @@ +package com.v2ray.ang.util.fmt + +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.util.Utils + +object SocksFmt { + fun parseSocks(str: String): ServerConfig? { + val config = ServerConfig.create(EConfigType.SOCKS) + var result = str.replace(EConfigType.SOCKS.protocolScheme, "") + val indexSplit = result.indexOf("#") + + if (indexSplit > 0) { + try { + config.remarks = + Utils.urlDecode(result.substring(indexSplit + 1, result.length)) + } catch (e: Exception) { + e.printStackTrace() + } + + result = result.substring(0, indexSplit) + } + + //part decode + val indexS = result.indexOf("@") + if (indexS > 0) { + result = Utils.decode(result.substring(0, indexS)) + result.substring( + indexS, + result.length + ) + } else { + result = Utils.decode(result) + } + + val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex() + val match = + legacyPattern.matchEntire(result) ?: return null + + config.outboundBean?.settings?.servers?.get(0)?.let { server -> + server.address = match.groupValues[3].removeSurrounding("[", "]") + server.port = match.groupValues[4].toInt() + val socksUsersBean = + V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() + socksUsersBean.user = match.groupValues[1] + socksUsersBean.pass = match.groupValues[2] + server.users = listOf(socksUsersBean) + } + + return config + } + + fun toUri(config: ServerConfig): String { + val outbound = config.getProxyOutbound() ?: return "" + val remark = "#" + Utils.urlEncode(config.remarks) + val pw = + if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null) + "${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}" + else + ":" + val url = String.format( + "%s@%s:%s", + Utils.encode(pw), + Utils.getIpv6Address(outbound.getServerAddress()!!), + outbound.getServerPort() + ) + return url + remark + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt new file mode 100644 index 00000000..b3365fcb --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt @@ -0,0 +1,170 @@ +package com.v2ray.ang.util.fmt + +import android.text.TextUtils +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object TrojanFmt { + private val settingsStorage by lazy { + MMKV.mmkvWithID( + MmkvManager.ID_SETTING, + MMKV.MULTI_PROCESS_MODE + ) + } + + fun parseTrojan(str: String): ServerConfig? { + val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false + val config = ServerConfig.create(EConfigType.TROJAN) + + val uri = URI(Utils.fixIllegalUrl(str)) + config.remarks = Utils.urlDecode(uri.fragment ?: "") + + var flow = "" + var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint + if (uri.rawQuery.isNullOrEmpty()) { + config.outboundBean?.streamSettings?.populateTlsSettings( + V2rayConfig.TLS, allowInsecure, "", + fingerprint, null, null, null, null + ) + } else { + val queryParam = uri.rawQuery.split("&") + .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } + + val sni = config.outboundBean?.streamSettings?.populateTransportSettings( + queryParam["type"] ?: "tcp", + queryParam["headerType"], + queryParam["host"], + queryParam["path"], + queryParam["seed"], + queryParam["quicSecurity"], + queryParam["key"], + queryParam["mode"], + queryParam["serviceName"], + queryParam["authority"] + ) + fingerprint = queryParam["fp"] ?: "" + config.outboundBean?.streamSettings?.populateTlsSettings( + queryParam["security"] ?: V2rayConfig.TLS, + allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"], + null, null, null + ) + flow = queryParam["flow"] ?: "" + } + + config.outboundBean?.settings?.servers?.get(0)?.let { server -> + server.address = uri.idnHost + server.port = uri.port + server.password = uri.userInfo + server.flow = flow + } + + return config + } + + fun toUri(config: ServerConfig): String { + val outbound = config.getProxyOutbound() ?: return "" + val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean() + + val remark = "#" + Utils.urlEncode(config.remarks) + val dicQuery = HashMap() + config.outboundBean?.settings?.servers?.get(0)?.flow?.let { + if (!TextUtils.isEmpty(it)) { + dicQuery["flow"] = it + } + + } + + dicQuery["security"] = streamSetting.security.ifEmpty { "none" } + (streamSetting.tlsSettings + ?: streamSetting.realitySettings)?.let { tlsSetting -> + if (!TextUtils.isEmpty(tlsSetting.serverName)) { + dicQuery["sni"] = tlsSetting.serverName + } + if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) { + dicQuery["alpn"] = + Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty() + } + if (!TextUtils.isEmpty(tlsSetting.fingerprint)) { + dicQuery["fp"] = tlsSetting.fingerprint!! + } + if (!TextUtils.isEmpty(tlsSetting.publicKey)) { + dicQuery["pbk"] = tlsSetting.publicKey!! + } + if (!TextUtils.isEmpty(tlsSetting.shortId)) { + dicQuery["sid"] = tlsSetting.shortId!! + } + if (!TextUtils.isEmpty(tlsSetting.spiderX)) { + dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX!!) + } + } + dicQuery["type"] = + streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK } + + outbound.getTransportSettingDetails()?.let { transportDetails -> + when (streamSetting.network) { + "tcp" -> { + dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } + if (!TextUtils.isEmpty(transportDetails[1])) { + dicQuery["host"] = Utils.urlEncode(transportDetails[1]) + } + } + + "kcp" -> { + dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } + if (!TextUtils.isEmpty(transportDetails[2])) { + dicQuery["seed"] = Utils.urlEncode(transportDetails[2]) + } + } + + "ws", "httpupgrade" -> { + if (!TextUtils.isEmpty(transportDetails[1])) { + dicQuery["host"] = Utils.urlEncode(transportDetails[1]) + } + if (!TextUtils.isEmpty(transportDetails[2])) { + dicQuery["path"] = Utils.urlEncode(transportDetails[2]) + } + } + + "http", "h2" -> { + dicQuery["type"] = "http" + if (!TextUtils.isEmpty(transportDetails[1])) { + dicQuery["host"] = Utils.urlEncode(transportDetails[1]) + } + if (!TextUtils.isEmpty(transportDetails[2])) { + dicQuery["path"] = Utils.urlEncode(transportDetails[2]) + } + } + + "quic" -> { + dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } + dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1]) + dicQuery["key"] = Utils.urlEncode(transportDetails[2]) + } + + "grpc" -> { + dicQuery["mode"] = transportDetails[0] + dicQuery["authority"] = Utils.urlEncode(transportDetails[1]) + dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2]) + } + } + } + val query = "?" + dicQuery.toList().joinToString( + separator = "&", + transform = { it.first + "=" + it.second }) + + val url = String.format( + "%s@%s:%s", + outbound.getPassword(), + Utils.getIpv6Address(outbound.getServerAddress()!!), + outbound.getServerPort() + ) + return url + query + remark + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt new file mode 100644 index 00000000..e689a31e --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt @@ -0,0 +1,170 @@ +package com.v2ray.ang.util.fmt + +import android.text.TextUtils +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object VlessFmt { + private val settingsStorage by lazy { + MMKV.mmkvWithID( + MmkvManager.ID_SETTING, + MMKV.MULTI_PROCESS_MODE + ) + } + + fun parseVless(str: String): ServerConfig? { + val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false + val config = ServerConfig.create(EConfigType.VLESS) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.rawQuery.isNullOrEmpty()) return null + val queryParam = uri.rawQuery.split("&") + .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } + + val streamSetting = config.outboundBean?.streamSettings ?: return null + + config.remarks = Utils.urlDecode(uri.fragment ?: "") + config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> + vnext.address = uri.idnHost + vnext.port = uri.port + vnext.users[0].id = uri.userInfo + vnext.users[0].encryption = queryParam["encryption"] ?: "none" + vnext.users[0].flow = queryParam["flow"] ?: "" + } + + val sni = streamSetting.populateTransportSettings( + queryParam["type"] ?: "tcp", + queryParam["headerType"], + queryParam["host"], + queryParam["path"], + queryParam["seed"], + queryParam["quicSecurity"], + queryParam["key"], + queryParam["mode"], + queryParam["serviceName"], + queryParam["authority"] + ) + streamSetting.populateTlsSettings( + queryParam["security"] ?: "", + allowInsecure, + queryParam["sni"] ?: sni, + queryParam["fp"] ?: "", + queryParam["alpn"], + queryParam["pbk"] ?: "", + queryParam["sid"] ?: "", + queryParam["spx"] ?: "" + ) + + return config + } + + fun toUri(config: ServerConfig): String { + val outbound = config.getProxyOutbound() ?: return "" + val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean() + + val remark = "#" + Utils.urlEncode(config.remarks) + val dicQuery = HashMap() + outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let { + if (!TextUtils.isEmpty(it)) { + dicQuery["flow"] = it + } + } + dicQuery["encryption"] = + if (outbound.getSecurityEncryption().isNullOrEmpty()) "none" + else outbound.getSecurityEncryption().orEmpty() + + + dicQuery["security"] = streamSetting.security.ifEmpty { "none" } + (streamSetting.tlsSettings + ?: streamSetting.realitySettings)?.let { tlsSetting -> + if (!TextUtils.isEmpty(tlsSetting.serverName)) { + dicQuery["sni"] = tlsSetting.serverName + } + if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) { + dicQuery["alpn"] = + Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty() + } + if (!TextUtils.isEmpty(tlsSetting.fingerprint)) { + dicQuery["fp"] = tlsSetting.fingerprint!! + } + if (!TextUtils.isEmpty(tlsSetting.publicKey)) { + dicQuery["pbk"] = tlsSetting.publicKey!! + } + if (!TextUtils.isEmpty(tlsSetting.shortId)) { + dicQuery["sid"] = tlsSetting.shortId!! + } + if (!TextUtils.isEmpty(tlsSetting.spiderX)) { + dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX!!) + } + } + dicQuery["type"] = + streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK } + + outbound.getTransportSettingDetails()?.let { transportDetails -> + when (streamSetting.network) { + "tcp" -> { + dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } + if (!TextUtils.isEmpty(transportDetails[1])) { + dicQuery["host"] = Utils.urlEncode(transportDetails[1]) + } + } + + "kcp" -> { + dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } + if (!TextUtils.isEmpty(transportDetails[2])) { + dicQuery["seed"] = Utils.urlEncode(transportDetails[2]) + } + } + + "ws", "httpupgrade" -> { + if (!TextUtils.isEmpty(transportDetails[1])) { + dicQuery["host"] = Utils.urlEncode(transportDetails[1]) + } + if (!TextUtils.isEmpty(transportDetails[2])) { + dicQuery["path"] = Utils.urlEncode(transportDetails[2]) + } + } + + "http", "h2" -> { + dicQuery["type"] = "http" + if (!TextUtils.isEmpty(transportDetails[1])) { + dicQuery["host"] = Utils.urlEncode(transportDetails[1]) + } + if (!TextUtils.isEmpty(transportDetails[2])) { + dicQuery["path"] = Utils.urlEncode(transportDetails[2]) + } + } + + "quic" -> { + dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } + dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1]) + dicQuery["key"] = Utils.urlEncode(transportDetails[2]) + } + + "grpc" -> { + dicQuery["mode"] = transportDetails[0] + dicQuery["authority"] = Utils.urlEncode(transportDetails[1]) + dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2]) + } + } + } + val query = "?" + dicQuery.toList().joinToString( + separator = "&", + transform = { it.first + "=" + it.second }) + + val url = String.format( + "%s@%s:%s", + outbound.getPassword(), + Utils.getIpv6Address(outbound.getServerAddress()!!), + outbound.getServerPort() + ) + return url + query + remark + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt new file mode 100644 index 00000000..83dbe679 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt @@ -0,0 +1,193 @@ +package com.v2ray.ang.util.fmt + +import android.text.TextUtils +import android.util.Log +import com.google.gson.Gson +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.dto.VmessQRCode +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object VmessFmt { + private val settingsStorage by lazy { + MMKV.mmkvWithID( + MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE + ) + } + + fun parseVmess(str: String): ServerConfig? { + val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false + val config = ServerConfig.create(EConfigType.VMESS) + val streamSetting = config.outboundBean?.streamSettings ?: return null + + if (!tryParseNewVmess(str, config, allowInsecure)) { + if (str.indexOf("?") > 0) { + if (!tryResolveVmess4Kitsunebi(str, config)) { + Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol") + return null + } + } else { + var result = str.replace(EConfigType.VMESS.protocolScheme, "") + result = Utils.decode(result) + if (TextUtils.isEmpty(result)) { + Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed") + return null + } + val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java) + // Although VmessQRCode fields are non null, looks like Gson may still create null fields + if (TextUtils.isEmpty(vmessQRCode.add) || TextUtils.isEmpty(vmessQRCode.port) || TextUtils.isEmpty( + vmessQRCode.id + ) || TextUtils.isEmpty(vmessQRCode.net) + ) { + Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol") + return null + } + + config.remarks = vmessQRCode.ps + config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> + vnext.address = vmessQRCode.add + vnext.port = Utils.parseInt(vmessQRCode.port) + vnext.users[0].id = vmessQRCode.id + vnext.users[0].security = + if (TextUtils.isEmpty(vmessQRCode.scy)) V2rayConfig.DEFAULT_SECURITY else vmessQRCode.scy + vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid) + } + val sni = streamSetting.populateTransportSettings( + vmessQRCode.net, + vmessQRCode.type, + vmessQRCode.host, + vmessQRCode.path, + vmessQRCode.path, + vmessQRCode.host, + vmessQRCode.path, + vmessQRCode.type, + vmessQRCode.path, + vmessQRCode.host + ) + + val fingerprint = vmessQRCode.fp ?: streamSetting.tlsSettings?.fingerprint + streamSetting.populateTlsSettings( + vmessQRCode.tls, + allowInsecure, + if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni, + fingerprint, + vmessQRCode.alpn, + null, + null, + null + ) + } + } + return config + } + + fun toUri(config: ServerConfig): String { + val outbound = config.getProxyOutbound() ?: return "" + val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean() + + val vmessQRCode = VmessQRCode() + vmessQRCode.v = "2" + vmessQRCode.ps = config.remarks + vmessQRCode.add = outbound.getServerAddress().orEmpty() + vmessQRCode.port = outbound.getServerPort().toString() + vmessQRCode.id = outbound.getPassword().orEmpty() + vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString() + vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString() + vmessQRCode.net = streamSetting.network + vmessQRCode.tls = streamSetting.security + vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty() + vmessQRCode.alpn = + Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty() + vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty() + outbound.getTransportSettingDetails()?.let { transportDetails -> + vmessQRCode.type = transportDetails[0] + vmessQRCode.host = transportDetails[1] + vmessQRCode.path = transportDetails[2] + } + val json = Gson().toJson(vmessQRCode) + return Utils.encode(json) + } + + private fun tryParseNewVmess( + uriString: String, config: ServerConfig, allowInsecure: Boolean + ): Boolean { + return runCatching { + val uri = URI(Utils.fixIllegalUrl(uriString)) + check(uri.scheme == "vmess") + val (_, protocol, tlsStr, uuid, alterId) = Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})").matchEntire( + uri.userInfo + )?.groupValues ?: error("parse user info fail.") + val tls = tlsStr.isNotBlank() + val queryParam = uri.rawQuery.split("&") + .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } + + val streamSetting = config.outboundBean?.streamSettings ?: return false + config.remarks = Utils.urlDecode(uri.fragment ?: "") + config.outboundBean.settings?.vnext?.get(0)?.let { vnext -> + vnext.address = uri.idnHost + vnext.port = uri.port + vnext.users[0].id = uuid + vnext.users[0].security = V2rayConfig.DEFAULT_SECURITY + vnext.users[0].alterId = alterId.toInt() + } + var fingerprint = streamSetting.tlsSettings?.fingerprint + val sni = streamSetting.populateTransportSettings(protocol, + queryParam["type"], + queryParam["host"]?.split("|")?.get(0) ?: "", + queryParam["path"]?.takeIf { it.trim() != "/" } ?: "", + queryParam["seed"], + queryParam["security"], + queryParam["key"], + queryParam["mode"], + queryParam["serviceName"], + queryParam["authority"]) + streamSetting.populateTlsSettings( + if (tls) V2rayConfig.TLS else "", + allowInsecure, + sni, + fingerprint, + null, + null, + null, + null + ) + true + }.getOrElse { false } + } + + private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean { + + var result = server.replace(EConfigType.VMESS.protocolScheme, "") + val indexSplit = result.indexOf("?") + if (indexSplit > 0) { + result = result.substring(0, indexSplit) + } + result = Utils.decode(result) + + val arr1 = result.split('@') + if (arr1.count() != 2) { + return false + } + val arr21 = arr1[0].split(':') + val arr22 = arr1[1].split(':') + if (arr21.count() != 2) { + return false + } + + config.remarks = "Alien" + config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> + vnext.address = arr22[0] + vnext.port = Utils.parseInt(arr22[1]) + vnext.users[0].id = arr21[1] + vnext.users[0].security = arr21[0] + vnext.users[0].alterId = 0 + } + return true + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/WireguardFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/WireguardFmt.kt new file mode 100644 index 00000000..90c1ebe9 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/WireguardFmt.kt @@ -0,0 +1,71 @@ +package com.v2ray.ang.util.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.extension.removeWhiteSpace +import com.v2ray.ang.util.Utils +import java.net.URI + +object WireguardFmt { + fun parseWireguard(str: String): ServerConfig? { + val config = ServerConfig.create(EConfigType.WIREGUARD) + val uri = URI(Utils.fixIllegalUrl(str)) + config.remarks = Utils.urlDecode(uri.fragment ?: "") + + if (uri.rawQuery != null) { + val queryParam = uri.rawQuery.split("&") + .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } + + config.outboundBean?.settings?.let { wireguard -> + wireguard.secretKey = uri.userInfo + wireguard.address = + (queryParam["address"] + ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace() + .split(",") + wireguard.peers?.get(0)?.publicKey = queryParam["publickey"] ?: "" + wireguard.peers?.get(0)?.endpoint = "${uri.idnHost}:${uri.port}" + wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) + wireguard.reserved = + (queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",") + .map { it.toInt() } + } + } + + return config + } + + fun toUri(config: ServerConfig): String { + val outbound = config.getProxyOutbound() ?: return "" + + val remark = "#" + Utils.urlEncode(config.remarks) + val dicQuery = HashMap() + dicQuery["publickey"] = + Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString()) + if (outbound.settings?.reserved != null) { + dicQuery["reserved"] = Utils.urlEncode( + Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString()) + .toString() + ) + } + dicQuery["address"] = Utils.urlEncode( + Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString()) + .toString() + ) + if (outbound.settings?.mtu != null) { + dicQuery["mtu"] = outbound.settings?.mtu.toString() + } + val query = "?" + dicQuery.toList().joinToString( + separator = "&", + transform = { it.first + "=" + it.second }) + + val url = String.format( + "%s@%s:%s", + Utils.urlEncode(outbound.getPassword().toString()), + Utils.getIpv6Address(outbound.getServerAddress()!!), + outbound.getServerPort() + ) + return url + query + remark + } +} \ No newline at end of file