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

github - dev - back to tree - upstream
/****************************************************************************** * * * Copyright (C) 2024 dyhkwong * * Copyright (C) 2021 by nekohasekai * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ******************************************************************************/ package io.nekohasekai.sagernet.group import androidx.core.net.toUri import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.database.GroupManager import io.nekohasekai.sagernet.database.ProxyEntity import io.nekohasekai.sagernet.database.ProxyGroup import io.nekohasekai.sagernet.database.SagerDatabase import io.nekohasekai.sagernet.database.SubscriptionBean import io.nekohasekai.sagernet.fmt.AbstractBean import io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocksConfig import io.nekohasekai.sagernet.fmt.wireguard.parseWireGuardConfig import io.nekohasekai.sagernet.ktx.* import libexclavecore.Libexclavecore import org.yaml.snakeyaml.DumperOptions import org.yaml.snakeyaml.LoaderOptions import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.constructor.Constructor import org.yaml.snakeyaml.nodes.MappingNode import org.yaml.snakeyaml.nodes.Node import org.yaml.snakeyaml.nodes.ScalarNode import org.yaml.snakeyaml.nodes.SequenceNode import org.yaml.snakeyaml.nodes.Tag import org.yaml.snakeyaml.representer.Representer import org.yaml.snakeyaml.resolver.Resolver import java.util.regex.Pattern @Suppress("EXPERIMENTAL_API_USAGE") object RawUpdater : GroupUpdater() { override suspend fun doUpdate( proxyGroup: ProxyGroup, subscription: SubscriptionBean, userInterface: GroupManager.Interface?, byUser: Boolean ) { val link = subscription.link var proxies: List if (link.startsWith("content://", ignoreCase = true)) { val contentText = app.contentResolver.openInputStream(link.toUri()) ?.bufferedReader() ?.readText() proxies = contentText?.let { parseRaw(contentText) } ?: error(app.getString(R.string.no_proxies_found_in_subscription)) } else { val response = Libexclavecore.newHttpClient().apply { if (SagerNet.started && DataStore.startedProfile > 0) { useUDS(SagerNet.deviceStorage.noBackupFilesDir.toString() + "/ipc.sock") } }.newRequest().apply { setURL(subscription.link) if (subscription.customUserAgent.isNotEmpty()) { setUserAgent(subscription.customUserAgent) } else { setUserAgent(USER_AGENT) } if (subscription.httpHeaders.isNotEmpty()) { for (header in subscription.httpHeaders.replace("\r\n", "\n").split("\n")) { if (header.isEmpty()) continue if (!header.contains(":")) error("invalid http header") setHeader(header.substringBefore(":"), header.substringAfter(":").trimStart()) } } }.execute() proxies = parseRaw(response.contentString) ?: error(app.getString(R.string.no_proxies_found)) val subscriptionUserinfo = response.getHeader("Subscription-Userinfo") if (subscriptionUserinfo.isNotEmpty()) { fun get(regex: String): String? { return regex.toRegex().findAll(subscriptionUserinfo).mapNotNull { if (it.groupValues.size > 1) it.groupValues[1] else null }.firstOrNull() } var used = 0L try { val upload = get("upload=([0-9]+)")?.toLong() ?: -1L if (upload > 0L) { used += upload } val download = get("download=([0-9]+)")?.toLong() ?: -1L if (download > 0L) { used += download } val total = get("total=([0-9]+)")?.toLong() ?: -1L subscription.apply { if (upload > 0L || download > 0L) { bytesUsed = used bytesRemaining = if (total > 0L) total - used else -1L } else { bytesUsed = -1L bytesRemaining = -1L } expiryDate = get("expire=([0-9]+)")?.toLong() ?: -1L } } catch (_: Exception) { } } else { subscription.apply { bytesUsed = -1L bytesRemaining = -1L expiryDate = -1L } } } proxies.forEach { it.applyDefaultValues() } if (subscription.nameFilter.isNotEmpty()) { val pattern = Regex(subscription.nameFilter) proxies = proxies.filter { !pattern.containsMatchIn(it.name) } } if (subscription.nameFilter1.isNotEmpty()) { val pattern = Regex(subscription.nameFilter1) proxies = proxies.filter { pattern.containsMatchIn(it.name) } } val proxiesMap = LinkedHashMap() for (proxy in proxies) { var index = 0 var name = proxy.displayName() while (proxiesMap.containsKey(name)) { println("Exists name: $name") index++ name = name.replace(" (${index - 1})", "") name = "$name ($index)" proxy.name = name } proxiesMap[proxy.displayName()] = proxy } proxies = proxiesMap.values.toList() val exists = SagerDatabase.proxyDao.getByGroup(proxyGroup.id) val duplicate = ArrayList() if (subscription.deduplication) { val uniqueProxies = LinkedHashSet() val uniqueNames = HashMap() for (p in proxies) { val proxy = Protocols.Deduplication(p, p.javaClass.toString()) if (!uniqueProxies.add(proxy)) { val index = uniqueProxies.indexOf(proxy) if (uniqueNames.containsKey(proxy)) { val name = uniqueNames[proxy]!!.replace(" ($index)", "") if (name.isNotEmpty()) { duplicate.add("$name ($index)") uniqueNames[proxy] = "" } } duplicate.add(p.displayName() + " ($index)") } else { uniqueNames[proxy] = p.displayName() } } uniqueProxies.retainAll(uniqueNames.keys) proxies = uniqueProxies.toList().map { it.bean } } val nameMap = proxies.associateBy { bean -> bean.displayName() } val toDelete = ArrayList() val toReplace = exists.mapNotNull { entity -> val name = entity.displayName() if (nameMap.contains(name)) name to entity else let { toDelete.add(entity) null } }.toMap() val toUpdate = ArrayList() val added = mutableListOf() val updated = mutableMapOf() val deleted = toDelete.map { it.displayName() } var userOrder = 1L var changed = toDelete.size for ((name, bean) in nameMap.entries) { if (toReplace.contains(name)) { val entity = toReplace[name]!! val existsBean = entity.requireBean() existsBean.applyFeatureSettings(bean) when { existsBean != bean -> { changed++ entity.putBean(bean) toUpdate.add(entity) updated[entity.displayName()] = name } entity.userOrder != userOrder -> { entity.putBean(bean) toUpdate.add(entity) entity.userOrder = userOrder } } } else { changed++ SagerDatabase.proxyDao.addProxy(ProxyEntity( groupId = proxyGroup.id, userOrder = userOrder ).apply { putBean(bean) }) added.add(name) } userOrder++ } SagerDatabase.proxyDao.updateProxy(toUpdate) SagerDatabase.proxyDao.deleteProxy(toDelete) subscription.lastUpdated = System.currentTimeMillis() / 1000 SagerDatabase.groupDao.updateGroup(proxyGroup) finishUpdate(proxyGroup) if (byUser && userInterface != null) { userInterface.onUpdateSuccess(proxyGroup, changed, added, updated, deleted, duplicate) } } @Suppress("UNCHECKED_CAST") fun parseRaw(text: String): List? { try { val options = DumperOptions() val yaml = Yaml(YAMLConstructor(LoaderOptions()), Representer(options), options, object : Resolver() { override fun addImplicitResolver(tag: Tag, regexp: Pattern, first: String?, limit: Int) { when (tag) { Tag.FLOAT -> {} Tag.BOOL -> super.addImplicitResolver(tag, Pattern.compile("^(?:true|True|TRUE|false|False|FALSE)$"), "tTfF", limit) else -> super.addImplicitResolver(tag, regexp, first, limit) } } }).apply { // https://github.com/SagerNet/SagerNet/blob/70e684bae81d4bb4203e860ab88c4319e88f944d/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt#L229 // IDK why but `!` is obviously widely used in Clash ecology // https://github.com/search?q=!%3Cstr%3E&type=code // addTypeDescription(TypeDescription(String::class.java, "str")) }.loadAs(text, Map::class.java) (yaml["proxies"] as? List>)?.let { proxies -> parseClashProxies(proxies).takeIf { it.isNotEmpty() }?.let { return it } } } catch (_: Exception) {} try { parseJSONConfig(text).takeIf { it.isNotEmpty() }?.let { return it } } catch (_: Exception) {} try { parseShareLinks(text.decodeBase64()).takeIf { it.isNotEmpty() }?.let { return it } } catch (_: Exception) {} try { parseShareLinks(text).takeIf { it.isNotEmpty() }?.let { return it } } catch (_: Exception) {} try { parseWireGuardConfig(text).takeIf { it.isNotEmpty() }?.let { return it } } catch (_: Exception) {} return null } @Suppress("UNCHECKED_CAST") private fun parseJSONConfig(text: String): List { val jsonElement = parseJson(stripJson(text, stripTrailingCommas = true)) if (jsonElement.isJsonArray) { // https://github.com/XTLS/Xray-core/discussions/3765 WTF val beans = ArrayList() jsonElement.asJsonArray.forEach { if (!it.isJsonObject) { return listOf() } val prefix = it.asJsonObject.getString("remarks", ignoreCase = true) val outbounds = it.asJsonObject.getArray("outbounds", ignoreCase = true) outbounds?.forEach { outbound -> val parsed = parseV2RayOutbound(outbound) if (!prefix.isNullOrEmpty()) { parsed.forEach { bean -> bean.initializeDefaultValues() bean.name = "$prefix ${bean.displayName()}" } } beans.addAll(parsed) } } return beans } if (!jsonElement.isJsonObject) { return listOf() } val jsonObject = jsonElement.asJsonObject val beans = ArrayList() when { jsonObject.contains("protocol", ignoreCase = true) -> { // V2Ray JSONv4 outbound or V2Ray JSONv5 outbound return parseV2Ray5Outbound(jsonObject).takeIf { it.isNotEmpty() } ?: parseV2RayOutbound(jsonObject) } jsonObject.contains("proxies", ignoreCase = true) -> { // Clash YAML return listOf() } jsonObject.getInt("version") != null && jsonObject.contains("servers") -> { // SIP008 val element = parseJson(text) if (!element.isJsonObject) { return listOf() } element.asJsonObject.getArray("servers")?.forEach { server -> parseShadowsocksConfig(server)?.let { beans.add(it) } } return beans } jsonObject.contains("type") -> { // sing-box outbound/endpoint return parseSingBoxEndpoint(jsonObject).takeIf { it.isNotEmpty() } ?: parseSingBoxOutbound(jsonObject) } else -> { val outbounds = jsonObject.getArray("outbounds", ignoreCase = true) val endpoints = jsonObject.getArray("endpoints", ignoreCase = true) val isV2Ray = !outbounds.isNullOrEmpty() && outbounds[0].contains("protocol", ignoreCase = true) if (isV2Ray) { // V2Ray JSONv4 or V2Ray JSONv5 outbounds.forEach { beans.addAll(parseV2Ray5Outbound(it).takeIf { it.isNotEmpty() } ?: parseV2RayOutbound(it)) } return beans } val isSingBox = !endpoints.isNullOrEmpty() || (!outbounds.isNullOrEmpty() && outbounds[0].contains("type")) if (isSingBox) { // sing-box outbounds?.forEach { beans.addAll(parseSingBoxOutbound(it)) } endpoints?.forEach { beans.addAll(parseSingBoxEndpoint(it)) } return beans } return listOf() } } } } class YAMLConstructor( options: LoaderOptions, ) : Constructor(options) { override fun constructObject(node: Node): Any? { when (node.tag) { Tag.NULL, Tag.BOOL, Tag.INT, Tag.FLOAT, Tag.STR, Tag.SEQ, Tag.MAP, Tag.BINARY, Tag.TIMESTAMP, Tag.SET, Tag.OMAP, Tag.PAIRS, Tag.YAML, Tag.MERGE -> { return super.constructObject(node) } } // ignore unknown tags return when (node) { is MappingNode -> constructMapping(node) is SequenceNode -> constructSequence(node) is ScalarNode -> constructScalar(node) else -> null } } }