diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt index 8f87af2cb..9763c7208 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt @@ -193,6 +193,7 @@ object AppConfig { const val HYSTERIA = "hysteria://" const val HYSTERIA2 = "hysteria2://" const val HY2 = "hy2://" + const val V2RAYNFMTS = "v2rayn://" /** Give a good name to this, IDK*/ const val VPN = "VPN" diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayNShareItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayNShareItem.kt new file mode 100644 index 000000000..b2e2c433f --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayNShareItem.kt @@ -0,0 +1,167 @@ +package com.v2ray.ang.dto + +import com.v2ray.ang.dto.entities.ProfileItem +import com.v2ray.ang.enums.BalancerStrategyType +import com.v2ray.ang.enums.EConfigType +import com.v2ray.ang.enums.NetworkType + +@Suppress("PropertyName") +data class V2rayNShareItem( + // val IndexId: String?, + val ConfigType: Int?, + // val CoreType: Int?, + val ConfigVersion: Int?, + val Subid: String?, + val IsSub: Boolean?, + val PreSocksPort: Int?, + // val DisplayLog: Boolean?, + val Remarks: String?, + val Address: String?, + val Port: Int?, + val Password: String?, + val Username: String?, + val Network: String?, + val StreamSecurity: String?, + val AllowInsecure: String?, + val Sni: String?, + val Alpn: String?, + val Fingerprint: String?, + val PublicKey: String?, + val ShortId: String?, + val SpiderX: String?, + val Mldsa65Verify: String?, + // val MuxEnabled: Boolean?, + // val Cert: String?, + val CertSha: String?, + val EchConfigList: String?, + val VerifyPeerCertByName: String?, + val Finalmask: String?, + val ProtoExtraObj: V2rayNProtocolExtraShareItem?, + val TransportExtraObj: V2rayNTransportExtraShareItem?, +) { + data class V2rayNProtocolExtraShareItem( + val AlterId: Int?, + val VmessSecurity: String?, + val Flow: String?, + val VlessEncryption: String?, + val SsMethod: String?, + val WgPublicKey: String?, + val WgPresharedKey: String?, + val WgInterfaceAddress: String?, + val WgReserved: String?, + val WgMtu: Int?, + val SalamanderPass: String?, + val UpMbps: Int?, + val DownMbps: Int?, + val Ports: String?, + val HopInterval: String?, + val GroupType: String?, + val ChildItems: String?, + val SubChildItems: String?, + val Filter: String?, + val MultipleLoad: Int?, + ) + data class V2rayNTransportExtraShareItem( + val RawHeaderType: String?, + val Host: String?, + val Path: String?, + val XhttpMode: String?, + val XhttpExtra: String?, + val GrpcAuthority: String?, + val GrpcServiceName: String?, + val GrpcMode: String?, + val KcpHeaderType: String?, + val KcpSeed: String?, + val KcpMtu: Int?, + ) + + fun toProfileItem(): ProfileItem { + val configType = when (ConfigType) { + 1 -> EConfigType.VMESS + 2 -> EConfigType.CUSTOM + 3 -> EConfigType.SHADOWSOCKS + 4 -> EConfigType.SOCKS + 5 -> EConfigType.VLESS + 6 -> EConfigType.TROJAN + 7 -> EConfigType.HYSTERIA2 + // 8 -> EConfigType.TUIC + 9 -> EConfigType.WIREGUARD + 10 -> EConfigType.HTTP + 101 -> EConfigType.POLICYGROUP + 102 -> EConfigType.PROXYCHAIN + else -> error("Unknown ConfigType: $ConfigType") + } + val network = if (configType == EConfigType.HYSTERIA2) "hysteria" else + when (Network) { + "raw" -> "tcp" + else -> NetworkType.fromString(Network).type + } + val profile = ProfileItem( + configType = configType, + remarks = Remarks.orEmpty(), + server = Address.orEmpty(), + serverPort = Port.toString(), + password = Password.orEmpty(), + method = when (configType) { + EConfigType.VMESS -> ProtoExtraObj?.VmessSecurity.orEmpty() + EConfigType.SHADOWSOCKS -> ProtoExtraObj?.SsMethod.orEmpty() + EConfigType.VLESS -> ProtoExtraObj?.VlessEncryption.orEmpty() + else -> null + }, + flow = ProtoExtraObj?.Flow.orEmpty(), + username = Username.orEmpty(), + network = network, + headerType = when (network) { + NetworkType.TCP.type -> TransportExtraObj?.RawHeaderType + NetworkType.KCP.type -> TransportExtraObj?.KcpHeaderType + else -> null + }, + host = TransportExtraObj?.Host, + path = TransportExtraObj?.Path, + seed = TransportExtraObj?.KcpSeed, + kcpMtu = TransportExtraObj?.KcpMtu, + mode = TransportExtraObj?.GrpcMode, + serviceName = TransportExtraObj?.GrpcServiceName, + authority = TransportExtraObj?.GrpcAuthority, + xhttpMode = TransportExtraObj?.XhttpMode, + xhttpExtra = TransportExtraObj?.XhttpExtra, + finalMask = Finalmask, + security = StreamSecurity, + sni = Sni, + alpn = Alpn, + fingerPrint = Fingerprint, + insecure = AllowInsecure?.toBoolean(), + echConfigList = EchConfigList, + verifyPeerCertByName = VerifyPeerCertByName, + pinnedCA256 = CertSha, + publicKey = PublicKey, + shortId = ShortId, + spiderX = SpiderX, + mldsa65Verify = Mldsa65Verify, + secretKey = if (configType == EConfigType.WIREGUARD) Password else null, + preSharedKey = ProtoExtraObj?.WgPresharedKey, + localAddress = ProtoExtraObj?.WgInterfaceAddress, + reserved = ProtoExtraObj?.WgReserved, + mtu = ProtoExtraObj?.WgMtu, + obfsPassword = ProtoExtraObj?.SalamanderPass, + portHopping = ProtoExtraObj?.Ports, + portHoppingInterval = ProtoExtraObj?.HopInterval, + bandwidthDown = ProtoExtraObj?.DownMbps?.takeIf { it > 0 }?.let { "${it}Mbps" }, + bandwidthUp = ProtoExtraObj?.UpMbps?.takeIf { it > 0 }?.let { "${it}Mbps" }, + policyGroupType = when (ProtoExtraObj?.MultipleLoad) { + 2 -> BalancerStrategyType.RANDOM.policyGroupType + 3 -> BalancerStrategyType.ROUND_ROBIN.policyGroupType + 4 -> BalancerStrategyType.LEAST_LOAD.policyGroupType + else -> BalancerStrategyType.LEAST_PING.policyGroupType + }, + // NOTE: not safe, suggest converting and rewriting + // policyGroupSubscriptionId = ProtoExtraObj?.SubChildItems, + policyGroupSubscriptionId = if (ProtoExtraObj?.SubChildItems == "self") "self" else null, + policyGroupFilter = ProtoExtraObj?.Filter, + // NOTE: proxyChainProfiles stores remarks, not IndexId + // proxyChainProfiles = ProtoExtraObj?.ChildItems, + ) + // profile.description = + return profile + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt index 659204c2f..68f5be35b 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt @@ -36,9 +36,6 @@ object Hysteria2Fmt : FmtBase() { config.security = queryParam["security"] ?: AppConfig.TLS config.obfsPassword = queryParam["obfs-password"] config.portHopping = queryParam["mport"] - if (config.portHopping.isNotNullEmpty()) { - config.portHoppingInterval = queryParam["mportHopInt"] - } config.pinnedCA256 = queryParam["pinSHA256"] } @@ -67,39 +64,6 @@ object Hysteria2Fmt : FmtBase() { if (config.portHopping.isNotNullEmpty()) { dicQuery["mport"] = config.portHopping.orEmpty() } - if (config.portHoppingInterval.isNotNullEmpty()) { - val rawInterval = config.portHoppingInterval?.trim().nullIfBlank() - val interval = if (rawInterval == null) { - null - } else { - val singleValue = rawInterval.toIntOrNull() - if (singleValue != null) { - if (singleValue < 5) { - null - } else { - rawInterval - } - } else { - val parts = rawInterval.split('-') - if (parts.size == 2) { - val start = parts[0].trim().toIntOrNull() - val end = parts[1].trim().toIntOrNull() - if (start != null && end != null) { - val minStart = maxOf(5, start) - val minEnd = maxOf(minStart, end) - (minStart + minEnd) / 2 - } else { - null - } - } else { - null - } - } - } - if (interval != null) { - dicQuery["mportHopInt"] = interval.toString() - } - } if (config.pinnedCA256.isNotNullEmpty()) { dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty() } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/V2rayNFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/V2rayNFmt.kt new file mode 100644 index 000000000..66d6077cb --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/V2rayNFmt.kt @@ -0,0 +1,22 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.V2rayNShareItem +import com.v2ray.ang.dto.entities.ProfileItem +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.LogUtil +import com.v2ray.ang.util.Utils + +object V2rayNFmt : FmtBase() { + fun parse(str: String): ProfileItem? { + try { + val jsonBase64Payload = str.substringAfterLast('/') + val jsonPayload = Utils.decode(jsonBase64Payload) + val v2rayNShareItem = JsonUtil.fromJson(jsonPayload, V2rayNShareItem::class.java) + return v2rayNShareItem?.toProfileItem() + } catch (e: Exception) { + LogUtil.e(AppConfig.TAG, "Failed to parse V2rayN format", e) + } + return null + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt index b17f10ee7..3a2e6a818 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt @@ -18,6 +18,7 @@ import com.v2ray.ang.fmt.Hysteria2Fmt import com.v2ray.ang.fmt.ShadowsocksFmt import com.v2ray.ang.fmt.SocksFmt import com.v2ray.ang.fmt.TrojanFmt +import com.v2ray.ang.fmt.V2rayNFmt import com.v2ray.ang.fmt.VlessFmt import com.v2ray.ang.fmt.VmessFmt import com.v2ray.ang.fmt.WireguardFmt @@ -42,7 +43,8 @@ object AngConfigManager { EConfigType.VLESS.protocolScheme to VlessFmt::parse, EConfigType.WIREGUARD.protocolScheme to WireguardFmt::parse, EConfigType.HYSTERIA2.protocolScheme to Hysteria2Fmt::parse, - AppConfig.HY2 to Hysteria2Fmt::parse + AppConfig.HY2 to Hysteria2Fmt::parse, + AppConfig.V2RAYNFMTS to V2rayNFmt::parse ) } @@ -494,6 +496,11 @@ object AngConfigManager { config.subscriptionId = subid config.description = generateDescription(config) + if (str.startsWith(AppConfig.V2RAYNFMTS, ignoreCase = true) + && config.policyGroupSubscriptionId == "self") { + config.policyGroupSubscriptionId = subid + } + return config } catch (e: Exception) { LogUtil.e(AppConfig.TAG, "Failed to parse config", e)