From febf7f1c61db47f197edce7830c3d4f9b54bd3f7 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 1 Jul 2020 15:37:21 -0400 Subject: [PATCH] Fix requestPersistentGroupInfo and deletePersistentGroup permissions --- README.md | 4 +- build.gradle.kts | 2 +- mobile/src/main/AndroidManifest.xml | 2 + .../java/be/mygod/librootkotlinx/Utils.kt | 21 ++++ .../be/mygod/vpnhotspot/RepeaterService.kt | 116 +++++++++++------- .../vpnhotspot/net/wifi/WifiApManager.kt | 3 + .../net/wifi/WifiP2pManagerHelper.kt | 52 +++++--- .../mygod/vpnhotspot/root/RepeaterCommands.kt | 54 ++++---- 8 files changed, 170 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 36e3710b..2409c0ec 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,12 @@ Installing as system app also has the side benefit of launching root daemon less * `android.permission.LOCAL_MAC_ADDRESS` * `android.permission.MANAGE_USB` +* `android.permission.OVERRIDE_WIFI_CONFIG` +* `android.permission.READ_WIFI_CREDENTIAL` * `android.permission.TETHER_PRIVILEGED` * `android.permission.WRITE_SECURE_SETTINGS` -Whenever you install an app update, if there was a new protected permission addition (last updated in v2.10.2), you should update the app installed in system as well to make the system grant the privileged permission. +Whenever you install an app update, if there was a new protected permission addition (last updated in v2.10.4), you should update the app installed in system as well to make the system grant the privileged permission. ## Settings and How to Use Them diff --git a/build.gradle.kts b/build.gradle.kts index 86d3c69d..78880366 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ buildscript { dependencies { classpath(kotlin("gradle-plugin", kotlinVersion)) - classpath("com.android.tools.build:gradle:4.1.0-beta01") + classpath("com.android.tools.build:gradle:4.1.0-beta02") classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0") classpath("com.google.firebase:firebase-crashlytics-gradle:2.2.0") classpath("com.google.android.gms:oss-licenses-plugin:0.10.2") diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 2b3ca094..757c49d2 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -38,6 +38,8 @@ tools:ignore="ProtectedPermissions"/> + diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt b/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt index f5f11b12..c0d27f04 100644 --- a/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt +++ b/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt @@ -179,6 +179,27 @@ data class ParcelableSize(val value: Size) : Parcelable @Parcelize data class ParcelableSizeF(val value: SizeF) : Parcelable +@Parcelize +data class ParcelableArray(val value: Array) : Parcelable { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ParcelableArray + + if (!value.contentEquals(other.value)) return false + + return true + } + + override fun hashCode(): Int { + return value.contentHashCode() + } +} + +@Parcelize +data class ParcelableList(val value: List) : Parcelable + @SuppressLint("Recycle") inline fun useParcel(block: (Parcel) -> T) = Parcel.obtain().run { try { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt index 70619056..6e674779 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt @@ -5,6 +5,7 @@ import android.annotation.TargetApi import android.app.Service import android.content.Intent import android.content.SharedPreferences +import android.content.pm.PackageManager import android.net.wifi.WpsInfo import android.net.wifi.p2p.* import android.os.Build @@ -32,6 +33,7 @@ import kotlinx.coroutines.* import timber.log.Timber import java.lang.reflect.InvocationTargetException import java.net.NetworkInterface +import java.util.concurrent.atomic.AtomicBoolean /** * Service for handling Wi-Fi P2P. `supported` must be checked before this service is started otherwise it would crash. @@ -168,6 +170,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene override val coroutineContext = dispatcher + Job() private var routingManager: RoutingManager? = null private var persistNextGroup = false + private val deinitPending = AtomicBoolean(true) var status = Status.IDLE private set(value) { @@ -197,41 +200,42 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene override fun onBind(intent: Intent) = binder - private fun setOperatingChannel(forceReinit: Boolean = false, oc: Int = operatingChannel) = try { + private fun setOperatingChannel(oc: Int = operatingChannel) { val channel = channel - if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show() - // we don't care about listening channel - else p2pManager.setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener { - override fun onSuccess() { } - override fun onFailure(reason: Int) { - if (reason == WifiP2pManager.ERROR && Build.VERSION.SDK_INT >= 30) launch(start = CoroutineStart.UNDISPATCHED) { - val rootReason = try { - RootManager.use { - if (forceReinit) it.execute(RepeaterCommands.Deinit()) - it.execute(RepeaterCommands.SetChannel(oc)) - } - } catch (e: Exception) { - Timber.w(e) - SmartSnackbar.make(e).show() - null - } ?: return@launch - SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, rootReason.value)).show() - } else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show() + if (channel != null) launch(start = CoroutineStart.UNDISPATCHED) { + val reason = try { + // we don't care about listening channel + p2pManager.setWifiP2pChannels(channel, 0, oc) ?: return@launch + } catch (e: InvocationTargetException) { + if (oc != 0) { + val message = getString(R.string.repeater_set_oc_failure, e.message) + SmartSnackbar.make(message).show() + Timber.w(RuntimeException("Failed to set operating channel $oc", e)) + } else Timber.w(e) + return@launch } - }) - } catch (e: InvocationTargetException) { - if (oc != 0) { - val message = getString(R.string.repeater_set_oc_failure, e.message) - SmartSnackbar.make(message).show() - Timber.w(RuntimeException("Failed to set operating channel $oc", e)) - } else Timber.w(e) + if (reason == WifiP2pManager.ERROR && Build.VERSION.SDK_INT >= 30) { + val rootReason = try { + RootManager.use { + if (deinitPending.getAndSet(false)) it.execute(RepeaterCommands.Deinit()) + it.execute(RepeaterCommands.SetChannel(oc)) + } + } catch (e: Exception) { + Timber.w(e) + SmartSnackbar.make(e).show() + null + } ?: return@launch + SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, rootReason.value)).show() + } else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show() + } else SmartSnackbar.make(R.string.repeater_failure_disconnected).show() } override fun onChannelDisconnected() { channel = null + deinitPending.set(true) if (status != Status.DESTROYED) try { channel = p2pManager.initialize(this, Looper.getMainLooper(), this) - if (!safeMode) setOperatingChannel(true) + if (!safeMode) setOperatingChannel() } catch (e: RuntimeException) { Timber.w(e) launch(Dispatchers.Main) { @@ -244,7 +248,11 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (!safeMode) when (key) { KEY_OPERATING_CHANNEL -> setOperatingChannel() - KEY_SAFE_MODE -> setOperatingChannel(true) + KEY_SAFE_MODE -> { + deinitPending.set(true) + setOperatingChannel() + onPersistentGroupsChanged() + } } } @@ -257,27 +265,43 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene null } ?: return@launch val channel = channel ?: return@launch + fun Collection.filterUselessGroups(): List { + if (isNotEmpty()) persistentSupported = true + val ownedGroups = filter { + if (!it.isGroupOwner) return@filter false + val address = MacAddressCompat.fromString(it.owner.deviceAddress) + // WifiP2pServiceImpl only removes self address + Build.VERSION.SDK_INT >= 29 && address == MacAddressCompat.ANY_ADDRESS || address == ownerAddress + } + val main = ownedGroups.minBy { it.networkId } + // do not replace current group if it's better + if (binder.group?.passphrase == null) binder.group = main + return if (main != null) ownedGroups.filter { it.networkId != main.networkId } else emptyList() + } + fun Int?.print(group: WifiP2pGroup) { + if (this == null) Timber.i("Removed redundant owned group: $group") + else SmartSnackbar.make(formatReason(R.string.repeater_clean_pog_failure, this)).show() + } + // we only get empty list on permission denial. Is there a better permission check? + if (Build.VERSION.SDK_INT < 30 || checkSelfPermission("android.permission.READ_WIFI_CREDENTIAL") == + PackageManager.PERMISSION_GRANTED) try { + for (group in p2pManager.requestPersistentGroupInfo(channel).filterUselessGroups()) { + p2pManager.deletePersistentGroup(channel, group.networkId).print(group) + } + return@launch + } catch (e: ReflectiveOperationException) { + Timber.w(e) + } try { - p2pManager.requestPersistentGroupInfo(channel) { groups -> - if (groups.isNotEmpty()) persistentSupported = true - val ownedGroups = groups.filter { - if (!it.isGroupOwner) return@filter false - val address = MacAddressCompat.fromString(it.owner.deviceAddress) - // WifiP2pServiceImpl only removes self address - Build.VERSION.SDK_INT >= 29 && address == MacAddressCompat.ANY_ADDRESS || address == ownerAddress - } - val main = ownedGroups.minBy { it.networkId } - // do not replace current group if it's better - if (binder.group?.passphrase == null) binder.group = main - if (main != null) ownedGroups.filter { it.networkId != main.networkId }.forEach { - p2pManager.deletePersistentGroup(channel, it.networkId, object : WifiP2pManager.ActionListener { - override fun onSuccess() = Timber.i("Removed redundant owned group: $it") - override fun onFailure(reason: Int) = SmartSnackbar.make( - formatReason(R.string.repeater_clean_pog_failure, reason)).show() - }) + RootManager.use { server -> + if (deinitPending.getAndSet(false)) server.execute(RepeaterCommands.Deinit()) + @Suppress("UNCHECKED_CAST") + val groups = server.execute(RepeaterCommands.RequestPersistentGroupInfo()).value as List + for (group in groups.filterUselessGroups()) { + server.execute(RepeaterCommands.DeletePersistentGroup(group.networkId))?.value.print(group) } } - } catch (e: ReflectiveOperationException) { + } catch (e: Exception) { Timber.w(e) SmartSnackbar.make(e).show() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt index 19b462ef..07ba35b5 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt @@ -22,6 +22,9 @@ object WifiApManager { WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java) } + /** + * Requires NETWORK_SETTINGS permission (or root) on API 30+, and OVERRIDE_WIFI_CONFIG on API 29-. + */ val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") { (getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat() ?: SoftApConfigurationCompat.empty() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt index c62f7c6a..ce248d6a 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt @@ -7,12 +7,25 @@ import android.net.wifi.p2p.WifiP2pManager import android.os.Build import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.util.callSuper +import kotlinx.coroutines.CompletableDeferred import timber.log.Timber import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.lang.reflect.Proxy object WifiP2pManagerHelper { + private class ResultListener : WifiP2pManager.ActionListener { + val future = CompletableDeferred() + + override fun onSuccess() { + future.complete(null) + } + + override fun onFailure(reason: Int) { + future.complete(reason) + } + } + const val UNSUPPORTED = -2 val ACTION_WIFI_P2P_PERSISTENT_GROUPS_CHANGED = if (Build.VERSION.SDK_INT >= 30) { "android.net.wifi.p2p.action.WIFI_P2P_PERSISTENT_GROUPS_CHANGED" @@ -28,14 +41,18 @@ object WifiP2pManagerHelper { WifiP2pManager::class.java.getDeclaredMethod("setWifiP2pChannels", WifiP2pManager.Channel::class.java, Int::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java) } - fun WifiP2pManager.setWifiP2pChannels(c: WifiP2pManager.Channel, lc: Int, oc: Int, - listener: WifiP2pManager.ActionListener) { + /** + * Requires one of NETWORK_SETTING, NETWORK_STACK, or OVERRIDE_WIFI_CONFIG permission since API 30. + */ + suspend fun WifiP2pManager.setWifiP2pChannels(c: WifiP2pManager.Channel, lc: Int, oc: Int): Int? { + val result = ResultListener() try { - setWifiP2pChannels(this, c, lc, oc, listener) + setWifiP2pChannels(this, c, lc, oc, result) } catch (_: NoSuchMethodException) { app.logEvent("NoSuchMethod_setWifiP2pChannels") - listener.onFailure(UNSUPPORTED) + return UNSUPPORTED } + return result.future.await() } /** @@ -66,14 +83,18 @@ object WifiP2pManagerHelper { WifiP2pManager::class.java.getDeclaredMethod("deletePersistentGroup", WifiP2pManager.Channel::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java) } - fun WifiP2pManager.deletePersistentGroup(c: WifiP2pManager.Channel, netId: Int, - listener: WifiP2pManager.ActionListener) { + /** + * Requires one of NETWORK_SETTING, NETWORK_STACK, or READ_WIFI_CREDENTIAL permission since API 30. + */ + suspend fun WifiP2pManager.deletePersistentGroup(c: WifiP2pManager.Channel, netId: Int): Int? { + val result = ResultListener() try { - deletePersistentGroup(this, c, netId, listener) + deletePersistentGroup(this, c, netId, result) } catch (_: NoSuchMethodException) { app.logEvent("NoSuchMethod_deletePersistentGroup") - listener.onFailure(UNSUPPORTED) + return UNSUPPORTED } + return result.future.await() } private val interfacePersistentGroupInfoListener by lazy @SuppressLint("PrivateApi") { @@ -89,21 +110,24 @@ object WifiP2pManagerHelper { /** * Request a list of all the persistent p2p groups stored in system. * + * Requires one of NETWORK_SETTING, NETWORK_STACK, or READ_WIFI_CREDENTIAL permission since API 30. + * * @param c is the channel created at {@link #initialize} * @param listener for callback when persistent group info list is available. Can be null. */ - fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel, - listener: (Collection) -> Unit) { - val proxy = Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader, + suspend fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel): Collection { + val result = CompletableDeferred>() + requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader, arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler { override fun invoke(proxy: Any, method: Method, args: Array?): Any? = when (method.name) { "onPersistentGroupInfoAvailable" -> { if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args")) - @Suppress("UNCHECKED_CAST") listener(getGroupList(args!![0]) as Collection) + @Suppress("UNCHECKED_CAST") + result.complete(getGroupList(args!![0]) as Collection) } else -> callSuper(interfacePersistentGroupInfoListener, proxy, method, args) } - }) - requestPersistentGroupInfo(this, c, proxy) + })) + return result.await() } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt index e968baab..9bfdaa23 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt @@ -7,13 +7,15 @@ import android.system.Os import android.system.OsConstants import android.text.TextUtils import be.mygod.librootkotlinx.ParcelableInt +import be.mygod.librootkotlinx.ParcelableList import be.mygod.librootkotlinx.RootCommand import be.mygod.librootkotlinx.RootCommandNoResult +import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup +import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels import be.mygod.vpnhotspot.util.Services import eu.chainfire.librootjava.RootJava import kotlinx.android.parcel.Parcelize -import kotlinx.coroutines.CompletableDeferred import java.io.File object RepeaterCommands { @@ -25,30 +27,24 @@ object RepeaterCommands { } } + @Parcelize + data class DeletePersistentGroup(val netId: Int) : RootCommand { + override suspend fun execute() = Services.p2p!!.run { + deletePersistentGroup(obtainChannel(), netId)?.let { ParcelableInt(it) } + } + } + + @Parcelize + class RequestPersistentGroupInfo : RootCommand { + override suspend fun execute() = Services.p2p!!.run { + ParcelableList(requestPersistentGroupInfo(obtainChannel()).toList()) + } + } + @Parcelize class SetChannel(private val oc: Int) : RootCommand { override suspend fun execute() = Services.p2p!!.run { - val uninitializer = object : WifiP2pManager.ChannelListener { - var target: WifiP2pManager.Channel? = null - override fun onChannelDisconnected() { - if (target == channel) channel = null - } - } - val channel = channel ?: initialize(RootJava.getSystemContext(), - Looper.getMainLooper(), uninitializer) - uninitializer.target = channel - RepeaterCommands.channel = channel // cache the instance until invalidated - val future = CompletableDeferred() - setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener { - override fun onSuccess() { - future.complete(null) - } - - override fun onFailure(reason: Int) { - future.complete(reason) - } - }) - future.await()?.let { ParcelableInt(it) } + setWifiP2pChannels(obtainChannel(), 0, oc)?.let { ParcelableInt(it) } } } @@ -82,4 +78,18 @@ object RepeaterCommands { private const val CONF_PATH_TREBLE = "/data/vendor/wifi/wpa/p2p_supplicant.conf" private const val CONF_PATH_LEGACY = "/data/misc/wifi/p2p_supplicant.conf" private var channel: WifiP2pManager.Channel? = null + + private fun WifiP2pManager.obtainChannel(): WifiP2pManager.Channel { + channel?.let { return it } + val uninitializer = object : WifiP2pManager.ChannelListener { + var target: WifiP2pManager.Channel? = null + override fun onChannelDisconnected() { + if (target == channel) channel = null + } + } + return initialize(RootJava.getSystemContext(), Looper.getMainLooper(), uninitializer).also { + uninitializer.target = it + channel = it // cache the instance until invalidated + } + } }