Skip to content

Commit

Permalink
manager: use SuFile to load webview assets to avoid spoofing manager'…
Browse files Browse the repository at this point in the history
…s mount namespace
  • Loading branch information
tiann committed Feb 23, 2024
1 parent cbc04ff commit 9635a00
Show file tree
Hide file tree
Showing 7 changed files with 541 additions and 207 deletions.
1 change: 1 addition & 0 deletions manager/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ dependencies {

implementation(libs.com.github.topjohnwu.libsu.core)
implementation(libs.com.github.topjohnwu.libsu.service)
implementation(libs.com.github.topjohnwu.libsu.io)

implementation(libs.dev.rikka.rikkax.parcelablelist)

Expand Down
203 changes: 7 additions & 196 deletions manager/app/src/main/java/me/weishu/kernelsu/ui/screen/WebScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,32 @@ package me.weishu.kernelsu.ui.screen

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.view.Window
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.webkit.WebViewAssetLoader
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.ShellUtils
import me.weishu.kernelsu.ui.util.createRootShell
import me.weishu.kernelsu.ui.util.serveModule
import org.json.JSONArray
import org.json.JSONObject
import me.weishu.kernelsu.ui.webui.SuFilePathHandler
import me.weishu.kernelsu.ui.webui.WebViewInterface
import me.weishu.kernelsu.ui.webui.showSystemUI
import java.io.File
import java.util.concurrent.CompletableFuture

@SuppressLint("SetJavaScriptEnabled")
@Destination
@Composable
fun WebScreen(navigator: DestinationsNavigator, moduleId: String, moduleName: String) {

LaunchedEffect(Unit) {
serveModule(moduleId)
}

val context = LocalContext.current

DisposableEffect(Unit) {
Expand All @@ -58,9 +39,11 @@ fun WebScreen(navigator: DestinationsNavigator, moduleId: String, moduleName: St
}

Scaffold { innerPadding ->
val webRoot = File(context.dataDir, "webroot")
val webRoot = File("/data/adb/modules/${moduleId}/webroot")
val webViewAssetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/", WebViewAssetLoader.InternalStoragePathHandler(context, webRoot))
.addPathHandler("/",
SuFilePathHandler(context, webRoot)
)
.build()

val webViewClient = object : AccompanistWebViewClient() {
Expand All @@ -86,176 +69,4 @@ fun WebScreen(navigator: DestinationsNavigator, moduleId: String, moduleName: St
}
})
}
}

class WebViewInterface(val context: Context, private val webView: WebView) {

companion object {
var isHideSystemUI: Boolean = false
}

@JavascriptInterface
fun exec(cmd: String): String {
val shell = createRootShell()
return ShellUtils.fastCmd(shell, cmd)
}

@JavascriptInterface
fun exec(cmd: String, callbackFunc: String) {
exec(cmd, null, callbackFunc)
}

private fun processOptions(sb: StringBuilder, options: String?) {
val opts = if (options == null) JSONObject() else {
JSONObject(options)
}

val cwd = opts.optString("cwd")
if (!TextUtils.isEmpty(cwd)) {
sb.append("cd ${cwd};")
}

opts.optJSONObject("env")?.let { env ->
env.keys().forEach { key ->
sb.append("export ${key}=${env.getString(key)};")
}
}
}

@JavascriptInterface
fun exec(
cmd: String,
options: String?,
callbackFunc: String
) {
val finalCommand = StringBuilder()
processOptions(finalCommand, options)
finalCommand.append(cmd)

val shell = createRootShell()
val result = shell.newJob().add(finalCommand.toString()).to(ArrayList(), ArrayList()).exec()
val stdout = result.out.joinToString(separator = "\n")
val stderr = result.err.joinToString(separator = "\n")

val jsCode =
"javascript: (function() { try { ${callbackFunc}(${result.code}, ${
JSONObject.quote(
stdout
)
}, ${JSONObject.quote(stderr)}); } catch(e) { console.error(e); } })();"
webView.post {
webView.loadUrl(jsCode)
}
}

@JavascriptInterface
fun spawn(command: String, args: String, options: String?, callbackFunc: String) {
val finalCommand = StringBuilder()

processOptions(finalCommand, options)

if (!TextUtils.isEmpty(args)) {
finalCommand.append(command).append(" ")
JSONArray(args).let { argsArray ->
for (i in 0 until argsArray.length()) {
finalCommand.append(argsArray.getString(i))
finalCommand.append(" ")
}
}
} else {
finalCommand.append(command)
}

val shell = createRootShell()

val emitData = fun(name: String, data: String) {
val jsCode =
"javascript: (function() { try { ${callbackFunc}.${name}.emit('data', ${
JSONObject.quote(
data
)
}); } catch(e) { console.error('emitData', e); } })();"
webView.post {
webView.loadUrl(jsCode)
}
}

val stdout = object : CallbackList<String>() {
override fun onAddElement(s: String) {
emitData("stdout", s)
}
}

val stderr = object : CallbackList<String>() {
override fun onAddElement(s: String) {
emitData("stderr", s)
}
}

val future = shell.newJob().add(finalCommand.toString()).to(stdout, stderr).enqueue()
val completableFuture = CompletableFuture.supplyAsync {
future.get()
}

completableFuture.thenAccept { result ->
val emitExitCode =
"javascript: (function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();"
webView.post {
webView.loadUrl(emitExitCode)
}

if (result.code != 0) {
val emitErrCode =
"javascript: (function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${
JSONObject.quote(
result.err.joinToString(
"\n"
)
)
};${callbackFunc}.emit('error', err); } catch(e) { console.error('emitErr', e); } })();"
webView.post {
webView.loadUrl(emitErrCode)
}
}
}
}

@JavascriptInterface
fun toast(msg: String) {
webView.post {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
}

@JavascriptInterface
fun fullScreen(enable: Boolean) {
if (context is Activity) {
Handler(Looper.getMainLooper()).post {
if (enable) {
hideSystemUI(context.window)
} else {
showSystemUI(context.window)
}
isHideSystemUI = enable
}
}
}

}

private fun hideSystemUI(window: Window) {
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}

private fun showSystemUI(window: Window) {
WindowCompat.setDecorFitsSystemWindows(window, true)
WindowInsetsControllerCompat(
window,
window.decorView
).show(WindowInsetsCompat.Type.systemBars())
}
17 changes: 6 additions & 11 deletions manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ fun getRootShell(): Shell {
return KsuCli.SHELL
}

fun createRootShell(): Shell {
fun createRootShell(globalMnt: Boolean = false): Shell {
Shell.enableVerboseLogging = BuildConfig.DEBUG
val builder = Shell.Builder.create()
return try {
builder.build(getKsuDaemonPath(), "debug", "su")
if (globalMnt) {
builder.build(getKsuDaemonPath(), "debug", "su", "-g")
} else {
builder.build(getKsuDaemonPath(), "debug", "su")
}
} catch (e: Throwable) {
Log.e(TAG, "su failed: ", e)
builder.build("sh")
Expand Down Expand Up @@ -131,15 +135,6 @@ fun installModule(
}
}

fun serveModule(id: String): Boolean {
// we should use a new root shell to avoid blocking the global shell
val shell = createRootShell()
return ShellUtils.fastCmdResult(
shell,
"${getKsuDaemonPath()} module link-manager $id ${android.os.Process.myPid()} ${BuildConfig.APPLICATION_ID}"
)
}

fun reboot(reason: String = "") {
val shell = getRootShell()
if (reason == "recovery") {
Expand Down
Loading

0 comments on commit 9635a00

Please sign in to comment.