tailscale: Add tailssh server
This commit is contained in:
@@ -2,6 +2,7 @@ package io.nekohasekai.sfa.bg;
|
|||||||
|
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import io.nekohasekai.sfa.bg.INeighborTableCallback;
|
import io.nekohasekai.sfa.bg.INeighborTableCallback;
|
||||||
|
import io.nekohasekai.sfa.bg.IRootShellSession;
|
||||||
import io.nekohasekai.sfa.bg.ParceledListSlice;
|
import io.nekohasekai.sfa.bg.ParceledListSlice;
|
||||||
|
|
||||||
interface IRootService {
|
interface IRootService {
|
||||||
@@ -16,4 +17,8 @@ interface IRootService {
|
|||||||
void registerNeighborTableCallback(in INeighborTableCallback callback) = 4;
|
void registerNeighborTableCallback(in INeighborTableCallback callback) = 4;
|
||||||
|
|
||||||
oneway void unregisterNeighborTableCallback(in INeighborTableCallback callback) = 5;
|
oneway void unregisterNeighborTableCallback(in INeighborTableCallback callback) = 5;
|
||||||
|
|
||||||
|
IRootShellSession openShellSession(String user, String command, in String[] env, String term, int rows, int cols) = 6;
|
||||||
|
|
||||||
|
String lookupSFTPServer() = 7;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package io.nekohasekai.sfa.bg;
|
||||||
|
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
interface IRootShellSession {
|
||||||
|
ParcelFileDescriptor getMasterFD();
|
||||||
|
void resize(int rows, int cols);
|
||||||
|
void signal(int sig);
|
||||||
|
int waitFor();
|
||||||
|
void close();
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package io.nekohasekai.sfa.bg
|
||||||
|
|
||||||
|
import io.nekohasekai.libbox.Int32Iterator
|
||||||
|
|
||||||
|
class IntArrayIterator(private val array: IntArray) : Int32Iterator {
|
||||||
|
private var index = 0
|
||||||
|
|
||||||
|
override fun len(): Int = array.size
|
||||||
|
|
||||||
|
override fun hasNext(): Boolean = index < array.size
|
||||||
|
|
||||||
|
override fun next(): Int = array[index++]
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package io.nekohasekai.sfa.bg
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -15,12 +16,17 @@ import io.nekohasekai.libbox.NeighborEntryIterator
|
|||||||
import io.nekohasekai.libbox.NeighborUpdateListener
|
import io.nekohasekai.libbox.NeighborUpdateListener
|
||||||
import io.nekohasekai.libbox.NetworkInterfaceIterator
|
import io.nekohasekai.libbox.NetworkInterfaceIterator
|
||||||
import io.nekohasekai.libbox.PlatformInterface
|
import io.nekohasekai.libbox.PlatformInterface
|
||||||
|
import io.nekohasekai.libbox.PlatformUser
|
||||||
|
import io.nekohasekai.libbox.ShellSession
|
||||||
import io.nekohasekai.libbox.StringIterator
|
import io.nekohasekai.libbox.StringIterator
|
||||||
import io.nekohasekai.libbox.TunOptions
|
import io.nekohasekai.libbox.TunOptions
|
||||||
import io.nekohasekai.libbox.WIFIState
|
import io.nekohasekai.libbox.WIFIState
|
||||||
import io.nekohasekai.sfa.Application
|
import io.nekohasekai.sfa.Application
|
||||||
|
import io.nekohasekai.sfa.ktx.toList
|
||||||
|
import io.nekohasekai.sfa.ktx.toStringIterator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.io.File
|
||||||
import java.net.Inet6Address
|
import java.net.Inet6Address
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.InterfaceAddress
|
import java.net.InterfaceAddress
|
||||||
@@ -205,6 +211,99 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun usePlatformShell(): Boolean = true
|
||||||
|
|
||||||
|
override fun checkPlatformShell() {
|
||||||
|
val available = RootClient.rootAvailable.value ?: runBlocking(Dispatchers.IO) {
|
||||||
|
RootClient.checkRootAvailable()
|
||||||
|
}
|
||||||
|
if (!available) {
|
||||||
|
error("missing root permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openShellSession(
|
||||||
|
user: PlatformUser?,
|
||||||
|
command: String?,
|
||||||
|
environ: StringIterator?,
|
||||||
|
term: String?,
|
||||||
|
rows: Int,
|
||||||
|
cols: Int,
|
||||||
|
): ShellSession {
|
||||||
|
user!!
|
||||||
|
val envList = environ?.toList().orEmpty()
|
||||||
|
if (user.uid == Process.myUid()) {
|
||||||
|
val resolved = ResolvedUser(user.username, user.uid, user.gid, user.homeDir)
|
||||||
|
val shell = UserResolver.findShell(resolved)
|
||||||
|
val shellEnv = buildBasicEnvironment(envList.toTypedArray(), shell, resolved.homeDir, term)
|
||||||
|
val args = if (command.isNullOrEmpty()) {
|
||||||
|
arrayOf("-" + File(shell).name)
|
||||||
|
} else {
|
||||||
|
arrayOf(File(shell).name, "-c", command)
|
||||||
|
}
|
||||||
|
val argsIter = args.asIterable().toStringIterator()
|
||||||
|
val envIter = shellEnv.asIterable().toStringIterator()
|
||||||
|
return if (term.isNullOrEmpty()) {
|
||||||
|
Libbox.openNativePipeSession(
|
||||||
|
shell,
|
||||||
|
resolved.homeDir,
|
||||||
|
argsIter,
|
||||||
|
envIter,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Libbox.openNativeShellSession(
|
||||||
|
shell,
|
||||||
|
resolved.homeDir,
|
||||||
|
argsIter,
|
||||||
|
envIter,
|
||||||
|
term,
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val rootSession = runBlocking(Dispatchers.IO) {
|
||||||
|
RootClient.openShellSession(
|
||||||
|
user.username,
|
||||||
|
command,
|
||||||
|
envList.toTypedArray(),
|
||||||
|
term,
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return RootShellSessionWrapper(rootSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun readSystemSSHHostKey(): io.nekohasekai.libbox.StringBox {
|
||||||
|
error("not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lookupSFTPServer(): io.nekohasekai.libbox.StringBox {
|
||||||
|
val path = runBlocking(Dispatchers.IO) {
|
||||||
|
RootClient.lookupSFTPServer()
|
||||||
|
}
|
||||||
|
val result = io.nekohasekai.libbox.StringBox()
|
||||||
|
result.value = path
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lookupUser(username: String?): io.nekohasekai.libbox.PlatformUser {
|
||||||
|
val resolved = UserResolver.resolve(Application.packageManager, username!!)
|
||||||
|
val platformUser = io.nekohasekai.libbox.PlatformUser()
|
||||||
|
platformUser.username = resolved.packageName
|
||||||
|
platformUser.uid = resolved.uid
|
||||||
|
platformUser.gid = resolved.gid
|
||||||
|
platformUser.homeDir = resolved.homeDir
|
||||||
|
return platformUser
|
||||||
|
}
|
||||||
|
|
||||||
override fun registerMyInterface(name: String?) {
|
override fun registerMyInterface(name: String?) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +315,29 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class RootShellSessionWrapper(
|
||||||
|
private val rootSession: IRootShellSession,
|
||||||
|
) : ShellSession {
|
||||||
|
private val masterPfd: ParcelFileDescriptor = rootSession.masterFD
|
||||||
|
|
||||||
|
override fun masterFD(): Int = masterPfd.fd
|
||||||
|
|
||||||
|
override fun resize(rows: Int, cols: Int) {
|
||||||
|
rootSession.resize(rows, cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun signal(signal: Int) {
|
||||||
|
rootSession.signal(signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitExit(): Int = rootSession.waitFor()
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
masterPfd.close()
|
||||||
|
rootSession.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class NeighborEntryArray(private val iterator: Iterator<LibboxNeighborEntry>) : NeighborEntryIterator {
|
private class NeighborEntryArray(private val iterator: Iterator<LibboxNeighborEntry>) : NeighborEntryIterator {
|
||||||
override fun hasNext(): Boolean = iterator.hasNext()
|
override fun hasNext(): Boolean = iterator.hasNext()
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,31 @@ object RootClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun lookupSFTPServer(): String {
|
||||||
|
val svc = bindService()
|
||||||
|
try {
|
||||||
|
return svc.lookupSFTPServer()
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw e.rethrowAsRuntime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun openShellSession(
|
||||||
|
user: String,
|
||||||
|
command: String?,
|
||||||
|
env: Array<String>,
|
||||||
|
term: String?,
|
||||||
|
rows: Int,
|
||||||
|
cols: Int,
|
||||||
|
): IRootShellSession {
|
||||||
|
val svc = bindService()
|
||||||
|
try {
|
||||||
|
return svc.openShellSession(user, command, env, term, rows, cols)
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw e.rethrowAsRuntime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun unregisterNeighborTableCallback(callback: INeighborTableCallback) {
|
suspend fun unregisterNeighborTableCallback(callback: INeighborTableCallback) {
|
||||||
try {
|
try {
|
||||||
service?.unregisterNeighborTableCallback(callback)
|
service?.unregisterNeighborTableCallback(callback)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.nekohasekai.sfa.bg
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
@@ -12,8 +13,11 @@ import io.nekohasekai.libbox.Libbox
|
|||||||
import io.nekohasekai.libbox.NeighborEntryIterator
|
import io.nekohasekai.libbox.NeighborEntryIterator
|
||||||
import io.nekohasekai.libbox.NeighborSubscription
|
import io.nekohasekai.libbox.NeighborSubscription
|
||||||
import io.nekohasekai.libbox.NeighborUpdateListener
|
import io.nekohasekai.libbox.NeighborUpdateListener
|
||||||
|
import io.nekohasekai.libbox.ShellSession
|
||||||
import io.nekohasekai.sfa.BuildConfig
|
import io.nekohasekai.sfa.BuildConfig
|
||||||
|
import io.nekohasekai.sfa.ktx.toStringIterator
|
||||||
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
|
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.lang.reflect.Proxy
|
import java.lang.reflect.Proxy
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
@@ -91,6 +95,149 @@ class RootServer : RootService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun openShellSession(
|
||||||
|
user: String?,
|
||||||
|
command: String?,
|
||||||
|
env: Array<out String>?,
|
||||||
|
term: String?,
|
||||||
|
rows: Int,
|
||||||
|
cols: Int,
|
||||||
|
): IRootShellSession {
|
||||||
|
val resolved = UserResolver.resolve(packageManager, user!!)
|
||||||
|
val shell: String
|
||||||
|
val shellEnv: Array<String>
|
||||||
|
val cwd: String
|
||||||
|
if (resolved.packageName == UserResolver.TERMUX_PACKAGE) {
|
||||||
|
val termuxPrefix = File(UserResolver.TERMUX_PREFIX)
|
||||||
|
val actualShell = UserResolver.findTermuxShell(termuxPrefix, resolved.homeDir)
|
||||||
|
cwd = resolved.homeDir
|
||||||
|
shellEnv =
|
||||||
|
buildTermuxEnvironment(env, actualShell, cwd, termuxPrefix.absolutePath, term)
|
||||||
|
shell = if (command.isNullOrEmpty()) {
|
||||||
|
val loginBin = File(termuxPrefix, "bin/login")
|
||||||
|
if (loginBin.canExecute()) loginBin.absolutePath else actualShell
|
||||||
|
} else {
|
||||||
|
actualShell
|
||||||
|
}
|
||||||
|
} else if (resolved.uid == 0) {
|
||||||
|
val termuxPrefix = File(UserResolver.TERMUX_PREFIX)
|
||||||
|
val termuxAvailable = File(termuxPrefix, "bin").isDirectory
|
||||||
|
if (termuxAvailable) {
|
||||||
|
shell = UserResolver.findTermuxShell(termuxPrefix, UserResolver.TERMUX_HOME)
|
||||||
|
cwd = UserResolver.TERMUX_HOME
|
||||||
|
shellEnv = buildTermuxEnvironment(
|
||||||
|
env,
|
||||||
|
shell,
|
||||||
|
cwd,
|
||||||
|
termuxPrefix.absolutePath,
|
||||||
|
term,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
shell = "/system/bin/sh"
|
||||||
|
cwd = "/data/local"
|
||||||
|
shellEnv = buildBasicEnvironment(env, shell, cwd, term)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shell = UserResolver.findShell(resolved)
|
||||||
|
cwd = resolved.homeDir
|
||||||
|
shellEnv = buildBasicEnvironment(env, shell, cwd, term)
|
||||||
|
}
|
||||||
|
val args: Array<String>
|
||||||
|
if (command.isNullOrEmpty()) {
|
||||||
|
args = arrayOf("-" + File(shell).name)
|
||||||
|
} else {
|
||||||
|
args = arrayOf(File(shell).name, "-c", command)
|
||||||
|
}
|
||||||
|
val supplementaryGids = if (resolved.packageName == "root" || resolved.packageName == "shell") {
|
||||||
|
intArrayOf()
|
||||||
|
} else {
|
||||||
|
packageManager.getPackageGids(resolved.packageName)
|
||||||
|
}
|
||||||
|
val argsIter = args.asIterable().toStringIterator()
|
||||||
|
val envIter = shellEnv.asIterable().toStringIterator()
|
||||||
|
val groupsIter = IntArrayIterator(supplementaryGids)
|
||||||
|
val isPipe = term.isNullOrEmpty()
|
||||||
|
val session = if (isPipe) {
|
||||||
|
Libbox.openNativePipeSession(
|
||||||
|
shell,
|
||||||
|
cwd,
|
||||||
|
argsIter,
|
||||||
|
envIter,
|
||||||
|
resolved.uid,
|
||||||
|
resolved.gid,
|
||||||
|
groupsIter,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Libbox.openNativeShellSession(
|
||||||
|
shell, cwd, argsIter, envIter,
|
||||||
|
term, rows, cols,
|
||||||
|
resolved.uid, resolved.gid, groupsIter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return RootShellSession(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lookupSFTPServer(): String {
|
||||||
|
val termuxPrefix = File(UserResolver.TERMUX_PREFIX)
|
||||||
|
for (name in arrayOf("libexec/sftp-server", "lib/openssh/sftp-server")) {
|
||||||
|
val candidate = File(termuxPrefix, name)
|
||||||
|
if (candidate.canExecute()) return candidate.absolutePath
|
||||||
|
}
|
||||||
|
throw IOException("sftp-server not found, install openssh in Termux")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildTermuxEnvironment(
|
||||||
|
sshEnv: Array<out String>?,
|
||||||
|
shell: String,
|
||||||
|
home: String,
|
||||||
|
prefix: String,
|
||||||
|
term: String?,
|
||||||
|
): Array<String> {
|
||||||
|
val env = parseEnvArray(sshEnv)
|
||||||
|
env["HOME"] = home
|
||||||
|
env["PREFIX"] = prefix
|
||||||
|
env["PATH"] = "$prefix/bin"
|
||||||
|
env["TMPDIR"] = "$prefix/tmp"
|
||||||
|
env["SHELL"] = shell
|
||||||
|
env["LANG"] = "en_US.UTF-8"
|
||||||
|
env["COLORTERM"] = "truecolor"
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
|
env["LD_LIBRARY_PATH"] = "$prefix/lib"
|
||||||
|
} else {
|
||||||
|
env.remove("LD_LIBRARY_PATH")
|
||||||
|
}
|
||||||
|
val termuxExec = File("$prefix/lib/libtermux-exec.so")
|
||||||
|
if (termuxExec.exists()) {
|
||||||
|
env["LD_PRELOAD"] = termuxExec.absolutePath
|
||||||
|
}
|
||||||
|
if (!term.isNullOrEmpty()) {
|
||||||
|
env["TERM"] = term
|
||||||
|
}
|
||||||
|
addAndroidSystemEnvironment(env)
|
||||||
|
return env.map { (k, v) -> "$k=$v" }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RootShellSession(
|
||||||
|
private val session: ShellSession,
|
||||||
|
) : IRootShellSession.Stub() {
|
||||||
|
|
||||||
|
override fun getMasterFD(): ParcelFileDescriptor = ParcelFileDescriptor.fromFd(session.masterFD())
|
||||||
|
|
||||||
|
override fun resize(rows: Int, cols: Int) {
|
||||||
|
session.resize(rows, cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun signal(sig: Int) {
|
||||||
|
session.signal(sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitFor(): Int = session.waitExit()
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun broadcastEnrichedEntries(rawList: List<Pair<String, String>>) {
|
private fun broadcastEnrichedEntries(rawList: List<Pair<String, String>>) {
|
||||||
@@ -223,3 +370,46 @@ class RootServer : RootService() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun parseEnvArray(sshEnv: Array<out String>?): MutableMap<String, String> {
|
||||||
|
val env = mutableMapOf<String, String>()
|
||||||
|
sshEnv?.forEach { entry ->
|
||||||
|
val idx = entry.indexOf('=')
|
||||||
|
if (idx > 0) env[entry.substring(0, idx)] = entry.substring(idx + 1)
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun buildBasicEnvironment(
|
||||||
|
sshEnv: Array<out String>?,
|
||||||
|
shell: String,
|
||||||
|
home: String,
|
||||||
|
term: String?,
|
||||||
|
): Array<String> {
|
||||||
|
val env = parseEnvArray(sshEnv)
|
||||||
|
env["HOME"] = home
|
||||||
|
env["PATH"] = "/system/bin:/system/xbin:/vendor/bin"
|
||||||
|
env["SHELL"] = shell
|
||||||
|
env["TMPDIR"] = "/data/local/tmp"
|
||||||
|
if (!term.isNullOrEmpty()) {
|
||||||
|
env["TERM"] = term
|
||||||
|
}
|
||||||
|
addAndroidSystemEnvironment(env)
|
||||||
|
return env.map { (k, v) -> "$k=$v" }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun addAndroidSystemEnvironment(env: MutableMap<String, String>) {
|
||||||
|
val androidVars = arrayOf(
|
||||||
|
"ANDROID_ASSETS", "ANDROID_DATA", "ANDROID_ROOT", "ANDROID_STORAGE",
|
||||||
|
"EXTERNAL_STORAGE", "ASEC_MOUNTPOINT", "LOOP_MOUNTPOINT",
|
||||||
|
"ANDROID_RUNTIME_ROOT", "ANDROID_ART_ROOT",
|
||||||
|
"ANDROID_I18N_ROOT", "ANDROID_TZDATA_ROOT",
|
||||||
|
"BOOTCLASSPATH", "DEX2OATBOOTCLASSPATH", "SYSTEMSERVERCLASSPATH",
|
||||||
|
)
|
||||||
|
for (name in androidVars) {
|
||||||
|
val value = System.getenv(name)
|
||||||
|
if (value != null) {
|
||||||
|
env[name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package io.nekohasekai.sfa.bg
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Process
|
||||||
|
import io.nekohasekai.sfa.BuildConfig
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
data class ResolvedUser(
|
||||||
|
val packageName: String,
|
||||||
|
val uid: Int,
|
||||||
|
val gid: Int,
|
||||||
|
val homeDir: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
object UserResolver {
|
||||||
|
|
||||||
|
const val TERMUX_PACKAGE = "com.termux"
|
||||||
|
const val TERMUX_PREFIX = "/data/data/com.termux/files/usr"
|
||||||
|
const val TERMUX_HOME = "/data/data/com.termux/files/home"
|
||||||
|
|
||||||
|
fun resolve(pm: PackageManager, username: String): ResolvedUser = when (username) {
|
||||||
|
"root" -> ResolvedUser("root", Process.ROOT_UID, Process.ROOT_UID, "/")
|
||||||
|
"shell" -> ResolvedUser("shell", Process.SHELL_UID, Process.SHELL_UID, "/data/local")
|
||||||
|
"termux" -> resolvePackage(pm, TERMUX_PACKAGE)
|
||||||
|
"sing-box" -> resolvePackage(pm, BuildConfig.APPLICATION_ID)
|
||||||
|
else -> resolvePackage(pm, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePackage(pm: PackageManager, packageName: String): ResolvedUser {
|
||||||
|
val appInfo = pm.getApplicationInfo(packageName, 0)
|
||||||
|
val homeDir = when (packageName) {
|
||||||
|
TERMUX_PACKAGE -> TERMUX_HOME
|
||||||
|
else -> appInfo.dataDir
|
||||||
|
}
|
||||||
|
return ResolvedUser(packageName, appInfo.uid, appInfo.uid, homeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findShell(resolved: ResolvedUser): String {
|
||||||
|
if (resolved.packageName == TERMUX_PACKAGE) {
|
||||||
|
return findTermuxShell(
|
||||||
|
File(TERMUX_PREFIX),
|
||||||
|
resolved.homeDir,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return "/system/bin/sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findTermuxShell(prefix: File, homeDir: String): String {
|
||||||
|
val dotTermuxShell = File(homeDir, ".termux/shell")
|
||||||
|
if (dotTermuxShell.canExecute()) {
|
||||||
|
return dotTermuxShell.canonicalPath
|
||||||
|
}
|
||||||
|
val binDir = File(prefix, "bin")
|
||||||
|
for (name in arrayOf("bash", "zsh", "fish", "sh")) {
|
||||||
|
val candidate = File(binDir, name)
|
||||||
|
if (candidate.canExecute()) return candidate.absolutePath
|
||||||
|
}
|
||||||
|
return "/system/bin/sh"
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -354,6 +354,12 @@ private fun createSessionClient(viewModel: TailscaleSSHTerminalViewModel, viewRe
|
|||||||
if (state.sessions.size > 1) {
|
if (state.sessions.size > 1) {
|
||||||
val managed = state.sessions.firstOrNull { it.terminalSession === finishedSession }
|
val managed = state.sessions.firstOrNull { it.terminalSession === finishedSession }
|
||||||
if (managed != null) {
|
if (managed != null) {
|
||||||
|
if (managed.id == state.activeSessionId) {
|
||||||
|
val sshSession = finishedSession as TailscaleSSHTerminalSession
|
||||||
|
if (sshSession.getSSHExitCode() != 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
viewModel.removeSession(managed.id)
|
viewModel.removeSession(managed.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user