owenclave / app/src/main/java/io/nekohasekai/sagernet/group/AgeUpdater.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.ktx.* import libexclavecore.Libexclavecore import org.yaml.snakeyaml.DumperOptions import org.yaml.snakeyaml.LoaderOptions import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.nodes.Tag import org.yaml.snakeyaml.representer.Representer import org.yaml.snakeyaml.resolver.Resolver import java.util.regex.Pattern object AgeUpdater : 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 content = app.contentResolver.openInputStream(link.toUri())?.readBytes() val data = Libexclavecore.ageArmerDecrypt(content, subscription.agePrivateKey) proxies = data?.let { parseRaw(String(data)) } ?: 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() val data = Libexclavecore.ageArmerDecrypt(response.content, subscription.agePrivateKey) proxies = data?.let { parseRaw(String(data)) } ?: 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).firstNotNullOfOrNull { if (it.groupValues.size > 1) it.groupValues[1] else null } } 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 { // Mihomo config, share link collection and Base64-encoded share link collection only. // Mihomo only supports these. Do not extend its scope. 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 { parseShareLinks(text.decodeBase64()).takeIf { it.isNotEmpty() }?.let { return it } } catch (_: Exception) {} try { parseShareLinks(text).takeIf { it.isNotEmpty() }?.let { return it } } catch (_: Exception) {} return null } }