librootkotlinx

Fixes #14, #27, #114, #117.
This commit is contained in:
Mygod
2020-06-21 05:33:39 +08:00
parent 7b1f610f9a
commit ad218d7ec6
51 changed files with 1781 additions and 574 deletions

View File

@@ -0,0 +1,185 @@
package be.mygod.vpnhotspot.root
import android.content.Context
import android.os.Build
import android.os.Parcelable
import android.os.RemoteException
import android.util.Log
import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.*
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.net.TetheringManager
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.produce
import java.io.File
import java.io.FileOutputStream
import java.io.InterruptedIOException
import java.util.concurrent.Executor
@Parcelize
class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCacheDir) : RootCommandNoResult {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) {
FileOutputStream(path, true).use { out ->
val process = ProcessBuilder("sh").redirectErrorStream(true).start()
process.outputStream.bufferedWriter().use { commands ->
// https://android.googlesource.com/platform/external/iptables/+/android-7.0.0_r1/iptables/Android.mk#34
val iptablesSave = if (Build.VERSION.SDK_INT >= 24) "iptables-save" else
File(cacheDir, "iptables-save").absolutePath.also {
commands.appendln("ln -sf /system/bin/iptables $it")
}
val ip6tablesSave = if (Build.VERSION.SDK_INT >= 24) "ip6tables-save" else
File(cacheDir, "ip6tables-save").absolutePath.also {
commands.appendln("ln -sf /system/bin/ip6tables $it")
}
commands.appendln("""
|echo dumpsys ${Context.WIFI_P2P_SERVICE}
|dumpsys ${Context.WIFI_P2P_SERVICE}
|echo
|echo dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|echo
|echo iptables -t filter
|$iptablesSave -t filter
|echo
|echo iptables -t nat
|$iptablesSave -t nat
|echo
|echo ip6tables-save
|$ip6tablesSave
|echo
|echo ip rule
|ip rule
|echo
|echo ip neigh
|ip neigh
|echo
|echo iptables -nvx -L vpnhotspot_fwd
|${Routing.IPTABLES} -nvx -L vpnhotspot_fwd
|echo
|echo iptables -nvx -L vpnhotspot_acl
|${Routing.IPTABLES} -nvx -L vpnhotspot_acl
|echo
|echo logcat-su
|logcat -d
""".trimMargin())
}
process.inputStream.copyTo(out)
check(process.waitFor() == 0)
}
null
}
}
sealed class ProcessData : Parcelable {
@Parcelize
data class StdoutLine(val line: String) : ProcessData()
@Parcelize
data class StderrLine(val line: String) : ProcessData()
@Parcelize
data class Exit(val code: Int) : ProcessData()
}
@Parcelize
class ProcessListener(private val terminateRegex: Regex,
private vararg val command: String) : RootCommandChannel<ProcessData> {
override fun create(scope: CoroutineScope) = scope.produce(Dispatchers.IO, capacity) {
val process = ProcessBuilder(*command).start()
val parent = Job() // we need to destroy process before joining, so we cannot use coroutineScope
try {
launch(parent) {
try {
process.inputStream.bufferedReader().forEachLine {
check(offer(ProcessData.StdoutLine(it)))
if (terminateRegex.containsMatchIn(it)) process.destroy()
}
} catch (_: InterruptedIOException) { }
}
launch(parent) {
try {
process.errorStream.bufferedReader().forEachLine { check(offer(ProcessData.StderrLine(it))) }
} catch (_: InterruptedIOException) { }
}
launch(parent) { check(offer(ProcessData.Exit(process.waitFor()))) }
parent.join()
} finally {
parent.cancel()
if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly()
parent.join()
}
}
}
@Parcelize
class ReadArp : RootCommand<ParcelableString> {
override suspend fun execute() = withContext(Dispatchers.IO) {
ParcelableString(File("/proc/net/arp").bufferedReader().readText())
}
}
@Parcelize
@RequiresApi(30)
class StartTethering(private val type: Int, private val showProvisioningUi: Boolean) : RootCommand<ParcelableInt?> {
override suspend fun execute(): ParcelableInt? {
val future = CompletableDeferred<Int?>()
val callback = object : TetheringManager.StartTetheringCallback {
override fun onTetheringStarted() {
future.complete(null)
}
override fun onTetheringFailed(error: Int?) {
future.complete(error!!)
}
}
TetheringManager.startTethering(type, true, showProvisioningUi, Executor {
GlobalScope.launch(Dispatchers.Unconfined) { it.run() }
}, TetheringManager.proxy(callback))
return future.await()?.let { ParcelableInt(it) }
}
}
@Deprecated("Old API since API 30")
@Parcelize
@RequiresApi(24)
@Suppress("DEPRECATION")
class StartTetheringLegacy(private val cacheDir: File, private val type: Int,
private val showProvisioningUi: Boolean) : RootCommand<ParcelableBoolean> {
override suspend fun execute(): ParcelableBoolean {
val future = CompletableDeferred<Boolean>()
val callback = object : TetheringManager.StartTetheringCallback {
override fun onTetheringStarted() {
future.complete(true)
}
override fun onTetheringFailed(error: Int?) {
check(error == null)
future.complete(false)
}
}
TetheringManager.startTetheringLegacy(type, showProvisioningUi, callback, cacheDir = cacheDir)
return ParcelableBoolean(future.await())
}
}
@Parcelize
@RequiresApi(24)
class StopTethering(private val type: Int) : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
TetheringManager.stopTethering(type)
return null
}
}
@Parcelize
class SettingsGlobalPut(val name: String, val value: String) : RootCommandNoResult {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) {
val process = ProcessBuilder("settings", "put", "global", name, value).redirectErrorStream(true).start()
val error = process.inputStream.bufferedReader().readText()
check(process.waitFor() == 0)
if (error.isNotEmpty()) throw RemoteException(error)
null
}
}

View File

@@ -0,0 +1,78 @@
package be.mygod.vpnhotspot.root
import android.net.wifi.p2p.WifiP2pManager
import android.os.Looper
import android.os.Parcelable
import android.system.Os
import android.system.OsConstants
import android.text.TextUtils
import be.mygod.librootkotlinx.ParcelableInt
import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandNoResult
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 {
@Parcelize
class SetChannel(private val oc: Int, private val forceReinit: Boolean = false) : RootCommand<ParcelableInt?> {
override suspend fun execute() = Services.p2p!!.run {
if (forceReinit) channel = null
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<Int?>()
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) }
}
}
@Parcelize
data class WriteP2pConfig(val data: String, val legacy: Boolean) : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
File(if (legacy) CONF_PATH_LEGACY else CONF_PATH_TREBLE).writeText(data)
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }!!) {
if (File(File(process, "cmdline").inputStream().bufferedReader().readText()
.split(Char.MIN_VALUE, limit = 2).first()).name == "wpa_supplicant") {
Os.kill(process.name.toInt(), OsConstants.SIGTERM)
}
}
return null
}
}
@Parcelize
class ReadP2pConfig : RootCommand<WriteP2pConfig> {
private fun test(path: String) = File(path).run {
if (canRead()) readText() else null
}
override suspend fun execute(): WriteP2pConfig {
test(CONF_PATH_TREBLE)?.let { return WriteP2pConfig(it, false) }
test(CONF_PATH_LEGACY)?.let { return WriteP2pConfig(it, true) }
throw IllegalStateException("p2p config file not found")
}
}
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
}

View File

@@ -0,0 +1,33 @@
package be.mygod.vpnhotspot.root
import android.os.Parcelable
import be.mygod.librootkotlinx.RootCommandNoResult
import be.mygod.librootkotlinx.RootServer
import be.mygod.librootkotlinx.RootSession
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BuildConfig
import be.mygod.vpnhotspot.util.Services
import eu.chainfire.librootjava.RootJava
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
object RootManager : RootSession() {
@Parcelize
class RootInit : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
Services.init(RootJava.getSystemContext())
return null
}
}
override fun createServer() = RootServer { Timber.w(it) }
override suspend fun initServer(server: RootServer) {
RootServer.DEBUG = BuildConfig.DEBUG
try {
server.init(app)
} finally {
server.readUnexpectedStderr()?.let { Timber.e(it) }
}
server.execute(RootInit())
}
}

View File

@@ -0,0 +1,59 @@
package be.mygod.vpnhotspot.root
import android.os.Parcelable
import android.util.Log
import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandOneWay
import be.mygod.vpnhotspot.net.Routing
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
object RoutingCommands {
@Parcelize
class Clean : RootCommandOneWay {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) {
val process = ProcessBuilder("sh").redirectErrorStream(true).start()
process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands)
when (val code = process.waitFor()) {
0 -> { }
else -> Log.d("RoutingCommands.Clean", "Unexpected exit code $code")
}
check(process.waitFor() == 0)
}
}
class UnexpectedOutputException(msg: String, val result: ProcessResult) : RuntimeException(msg)
@Parcelize
data class ProcessResult(val exit: Int, val out: String, val err: String) : Parcelable {
fun message(command: List<String>, out: Boolean = this.out.isNotEmpty(),
err: Boolean = this.err.isNotEmpty()): String? {
val msg = StringBuilder("${command.joinToString(" ")} exited with $exit")
if (out) msg.append("\n${this.out}")
if (err) msg.append("\n=== stderr ===\n${this.err}")
return if (exit != 0 || out || err) msg.toString() else null
}
fun check(command: List<String>, out: Boolean = this.out.isNotEmpty(),
err: Boolean = this.err.isNotEmpty()) = message(command, out, err)?.let { msg ->
throw UnexpectedOutputException(msg, this)
}
}
@Parcelize
class Process(val command: List<String>, private val redirect: Boolean = false) : RootCommand<ProcessResult> {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) {
val process = ProcessBuilder(command).redirectErrorStream(redirect).start()
coroutineScope {
val output = async { process.inputStream.bufferedReader().readText() }
val error = async { if (redirect) "" else process.errorStream.bufferedReader().readText() }
ProcessResult(process.waitFor(), output.await(), error.await())
}
}
}
}

View File

@@ -0,0 +1,19 @@
package be.mygod.vpnhotspot.root
import be.mygod.librootkotlinx.ParcelableBoolean
import be.mygod.librootkotlinx.RootCommand
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import kotlinx.android.parcel.Parcelize
object WifiApCommands {
@Parcelize
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
override suspend fun execute() = WifiApManager.configuration
}
@Parcelize
data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> {
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
}
}