diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt index fb81a00e..23ca7b8f 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt @@ -1,6 +1,8 @@ package com.v2ray.ang import android.content.Context +import android.content.pm.PackageManager +import android.os.Build import androidx.multidex.MultiDexApplication import androidx.work.Configuration import androidx.work.WorkManager @@ -41,4 +43,10 @@ class AngApplication : MultiDexApplication() { SettingsManager.initRoutingRulesets(this) } + + fun getPackageInfo(packageName: String) = packageManager.getPackageInfo( + packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES + else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES + )!! + } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/Hysteria2Bean.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/Hysteria2Bean.kt new file mode 100644 index 00000000..fe598185 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/Hysteria2Bean.kt @@ -0,0 +1,17 @@ +package com.v2ray.ang.dto + +data class Hysteria2Bean( + val server: String?, + val auth: String?, + val lazy: Boolean? = true, + val socks5: Socks5Bean?, + val tls: TlsBean? +) { + data class Socks5Bean( + val listen: String?, + ) + + data class TlsBean( + val sni: String? + ) +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt index 77ace45f..8f839409 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt @@ -1,6 +1,9 @@ package com.v2ray.ang.extension +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.Build import android.widget.Toast import com.v2ray.ang.AngApplication @@ -56,4 +59,26 @@ val URI.idnHost: String fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "") -fun String.toLongEx(): Long = toLongOrNull() ?: 0 \ No newline at end of file +fun String.toLongEx(): Long = toLongOrNull() ?: 0 + +fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + callback() + if (onetime) context.unregisterReceiver(this) + } + }.apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(this, IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + }, Context.RECEIVER_EXPORTED) + } else { + registerReceiver(this, IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + }) + } + } \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/NativePlugin.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/NativePlugin.kt new file mode 100644 index 00000000..7a072cd0 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/NativePlugin.kt @@ -0,0 +1,32 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * 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 com.v2ray.ang.plugin + +import android.content.pm.ResolveInfo + +class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) { + init { + check(resolveInfo.providerInfo != null) + } + + override val componentInfo get() = resolveInfo.providerInfo!! +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/Plugin.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/Plugin.kt new file mode 100644 index 00000000..04294ac6 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/Plugin.kt @@ -0,0 +1,43 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * 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 com.v2ray.ang.plugin + +import android.graphics.drawable.Drawable + +abstract class Plugin { + abstract val id: String + abstract val label: CharSequence + abstract val version: Int + abstract val versionName: String + open val icon: Drawable? get() = null + open val defaultConfig: String? get() = null + open val packageName: String get() = "" + open val directBootAware: Boolean get() = true + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return id == (other as Plugin).id + } + + override fun hashCode() = id.hashCode() +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/PluginContract.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/PluginContract.kt new file mode 100644 index 00000000..5aa253f8 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/PluginContract.kt @@ -0,0 +1,33 @@ +/****************************************************************************** + * * + * 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 com.v2ray.ang.plugin + +object PluginContract { + + const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN" + const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY" + const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id" + const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path" + const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable" + + const val COLUMN_PATH = "path" + const val COLUMN_MODE = "mode" + const val SCHEME = "plugin" +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/PluginList.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/PluginList.kt new file mode 100644 index 00000000..9c247286 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/PluginList.kt @@ -0,0 +1,53 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * 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 com.v2ray.ang.plugin + +import android.content.Intent +import android.content.pm.PackageManager +import com.v2ray.ang.AngApplication + +class PluginList : ArrayList() { + init { + addAll( + AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA) + .filter { it.providerInfo.exported }.map { NativePlugin(it) }) + } + + val lookup = mutableMapOf().apply { + for (plugin in this@PluginList.toList()) { + fun check(old: Plugin?) { + if (old != null && old != plugin) { + this@PluginList.remove(old) + } + /* if (old != null && old !== plugin) { + val packages = this@PluginList.filter { it.id == plugin.id } + .joinToString { it.packageName } + val message = "Conflicting plugins found from: $packages" + Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show() + throw IllegalStateException(message) + }*/ + } + check(put(plugin.id, plugin)) + } + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/PluginManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/PluginManager.kt new file mode 100644 index 00000000..19fb6f2e --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/PluginManager.kt @@ -0,0 +1,219 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * 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 com.v2ray.ang.plugin + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.ContentResolver +import android.content.Intent +import android.content.pm.ComponentInfo +import android.content.pm.PackageManager +import android.content.pm.ProviderInfo +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.system.Os +import android.widget.Toast +import androidx.core.os.bundleOf +import com.v2ray.ang.AngApplication +import com.v2ray.ang.extension.listenForPackageChanges +import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID +import java.io.File +import java.io.FileNotFoundException + +object PluginManager { + + class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin) + private var receiver: BroadcastReceiver? = null + private var cachedPlugins: PluginList? = null + fun fetchPlugins() = synchronized(this) { + if (receiver == null) receiver = AngApplication.application.listenForPackageChanges { + synchronized(this) { + receiver = null + cachedPlugins = null + } + } + if (cachedPlugins == null) cachedPlugins = PluginList() + cachedPlugins!! + } + + private fun buildUri(id: String, authority: String) = Uri.Builder() + .scheme(PluginContract.SCHEME) + .authority(authority) + .path("/$id") + .build() + + data class InitResult( + val path: String, + ) + + @Throws(Throwable::class) + fun init(pluginId: String): InitResult? { + if (pluginId.isEmpty()) return null + var throwable: Throwable? = null + + try { + val result = initNative(pluginId) + if (result != null) return result + } catch (t: Throwable) { + if (throwable == null) throwable = t //Logs.w(t) + } + + throw throwable ?: PluginNotFoundException(pluginId) + } + + private fun initNative(pluginId: String): InitResult? { + var flags = PackageManager.GET_META_DATA + if (Build.VERSION.SDK_INT >= 24) { + flags = + flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE + } + var providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags) + .filter { it.providerInfo.exported } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags) + .filter { it.providerInfo.exported } + } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags) + .filter { it.providerInfo.exported } + } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags) + .filter { it.providerInfo.exported } + } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA + ).filter { + it.providerInfo.exported && + it.providerInfo.metaData.containsKey(METADATA_KEY_ID) && + it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId + } + if (providers.size > 1) { + providers = listOf(providers[0]) // What if there is more than one? + } + } + if (providers.isEmpty()) return null + if (providers.size > 1) { + val message = + "Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}" + Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show() + throw IllegalStateException(message) + } + val provider = providers.single().providerInfo + var failure: Throwable? = null + try { + initNativeFaster(provider)?.also { return InitResult(it) } + } catch (t: Throwable) { + // Logs.w("Initializing native plugin faster mode failed") + failure = t + } + + val uri = Uri.Builder().apply { + scheme(ContentResolver.SCHEME_CONTENT) + authority(provider.authority) + }.build() + try { + return initNativeFast(AngApplication.application.contentResolver, + pluginId, + uri)?.let { InitResult(it) } + } catch (t: Throwable) { + // Logs.w("Initializing native plugin fast mode failed") + failure?.also { t.addSuppressed(it) } + failure = t + } + + try { + return initNativeSlow(AngApplication.application.contentResolver, + pluginId, + uri)?.let { InitResult(it) } + } catch (t: Throwable) { + failure?.also { t.addSuppressed(it) } + throw t + } + } + + private fun initNativeFaster(provider: ProviderInfo): String? { + return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH) + ?.let { relativePath -> + File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply { + check(canExecute()) + }.absolutePath + } + } + + private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? { + return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf()) + ?.getString(PluginContract.EXTRA_ENTRY)?.also { + check(File(it).canExecute()) + } + } + + @SuppressLint("Recycle") + private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? { + var initialized = false + fun entryNotFound(): Nothing = + throw IndexOutOfBoundsException("Plugin entry binary not found") + + val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin") + (cr.query(uri, + arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), + null, + null, + null) + ?: return null).use { cursor -> + if (!cursor.moveToFirst()) entryNotFound() + pluginDir.deleteRecursively() + if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory") + val pluginDirPath = pluginDir.absolutePath + '/' + do { + val path = cursor.getString(0) + val file = File(pluginDir, path) + check(file.absolutePath.startsWith(pluginDirPath)) + cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream -> + file.outputStream().use { outStream -> inStream.copyTo(outStream) } + } + Os.chmod(file.absolutePath, when (cursor.getType(1)) { + Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1) + Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8) + else -> throw IllegalArgumentException("File mode should be of type int") + }) + if (path == pluginId) initialized = true + } while (cursor.moveToNext()) + } + if (!initialized) entryNotFound() + return File(pluginDir, pluginId).absolutePath + } + + fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) { + is String -> value + is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo) + .getString(value) + null -> null + else -> error("meta-data $key has invalid type ${value.javaClass}") + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/ResolvedPlugin.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/ResolvedPlugin.kt new file mode 100644 index 00000000..3b2f2003 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/ResolvedPlugin.kt @@ -0,0 +1,45 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * 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 com.v2ray.ang.plugin + +import android.content.pm.ComponentInfo +import android.content.pm.ResolveInfo +import android.graphics.drawable.Drawable +import android.os.Build +import com.v2ray.ang.AngApplication +import com.v2ray.ang.plugin.PluginManager.loadString + +abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() { + protected abstract val componentInfo: ComponentInfo + + override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! } + override val version by lazy { + AngApplication.application.getPackageInfo(componentInfo.packageName).versionCode + } + override val versionName: String by lazy { + AngApplication.application.getPackageInfo(componentInfo.packageName).versionName!! + } + override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager) + override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager) + override val packageName: String get() = componentInfo.packageName + override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt index 86a09153..97819a40 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt @@ -24,6 +24,7 @@ import com.v2ray.ang.ui.MainActivity import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager.settingsStorage +import com.v2ray.ang.util.PluginUtil import com.v2ray.ang.util.Utils import com.v2ray.ang.util.V2rayConfigUtil import go.Seq @@ -161,6 +162,8 @@ object V2RayServiceManager { if (v2rayPoint.isRunning) { MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") showNotification() + + PluginUtil.runPlugin(service, config) } else { MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") cancelNotification() @@ -188,6 +191,7 @@ object V2RayServiceManager { } catch (e: Exception) { Log.d(ANG_PACKAGE, e.toString()) } + PluginUtil.stopPlugin() } private class ReceiveMessageHandler : BroadcastReceiver() { diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt index d0904b84..ab31d8f0 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt @@ -292,10 +292,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList true } -// R.id.import_manually_hysteria2 -> { -// importManually(EConfigType.HYSTERIA2.value) -// true -// } + R.id.import_manually_hysteria2 -> { + importManually(EConfigType.HYSTERIA2.value) + true + } R.id.import_config_custom_clipboard -> { importConfigCustomClipboard() diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/PluginUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/PluginUtil.kt new file mode 100644 index 00000000..e26f0016 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/PluginUtil.kt @@ -0,0 +1,93 @@ +package com.v2ray.ang.util + +import android.content.Context +import android.os.SystemClock +import android.util.Log +import com.google.gson.Gson +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.util.MmkvManager.settingsStorage +import com.v2ray.ang.util.fmt.Hysteria2Fmt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +object PluginUtil { + //private const val HYSTERIA2 = "hysteria2-plugin" + private const val HYSTERIA2 = "libhysteria2.so" + private const val packageName = "PluginUtil" + private lateinit var process: Process + +// fun initPlugin(name: String): PluginManager.InitResult { +// return PluginManager.init(name)!! +// } + + fun runPlugin(context: Context, config: ServerConfig?) { + Log.d(packageName, "runPlugin") + + val outbound = config?.getProxyOutbound() ?: return + if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) { + Log.d(packageName, "runPlugin $HYSTERIA2") + + val socksPort = 100 + Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) + val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return + + val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json") + Log.d(packageName, "runPlugin ${configFile.absolutePath}") + + configFile.parentFile?.mkdirs() + configFile.writeText(Gson().toJson(hy2Config)) + Log.d(packageName, Gson().toJson(hy2Config)) + + runHy2(context, configFile) + } + } + + fun stopPlugin() { + stopHy2() + } + + private fun runHy2(context: Context, configFile: File) { + val cmd = mutableListOf( + File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath, + //initPlugin(HYSTERIA2).path, + "--disable-update-check", + "--config", + configFile.absolutePath, + "--log-level", + "warn", + "client" + ) + Log.d(packageName, cmd.toString()) + + try { + val proBuilder = ProcessBuilder(cmd) + proBuilder.redirectErrorStream(true) + process = proBuilder + .directory(context.filesDir) + .start() + + CoroutineScope(Dispatchers.IO).launch { + Thread.sleep(500L) + Log.d(packageName, "$HYSTERIA2 check") + process.waitFor() + Log.d(packageName, "$HYSTERIA2 exited") + } + Log.d(packageName, process.toString()) + + } catch (e: Exception) { + Log.d(packageName, e.toString()) + } + } + + private fun stopHy2() { + try { + Log.d(packageName, "$HYSTERIA2 destroy") + process?.destroy() + } catch (e: Exception) { + Log.d(packageName, e.toString()) + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt index eb668174..27f0ab0c 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt @@ -6,6 +6,7 @@ import android.util.Log import com.google.gson.Gson import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM import com.v2ray.ang.AppConfig.TAG_BLOCKED import com.v2ray.ang.AppConfig.TAG_DIRECT @@ -41,7 +42,7 @@ object V2rayConfigUtil { } val result = getV2rayNonCustomConfig(context, config) - Log.d(ANG_PACKAGE, result.content) + //Log.d(ANG_PACKAGE, result.content) return result } catch (e: Exception) { e.printStackTrace() @@ -110,7 +111,7 @@ object V2rayConfigUtil { v2rayConfig.inbounds.forEach { curInbound -> if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) { //bind all inbounds to localhost if the user requests - curInbound.listen = "127.0.0.1" + curInbound.listen = LOOPBACK } } v2rayConfig.inbounds[0].port = socksPort @@ -145,6 +146,28 @@ object V2rayConfigUtil { } private fun outbounds(v2rayConfig: V2rayConfig, outbound: V2rayConfig.OutboundBean): Boolean { + if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) { + val socksPort = 100 + Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) + val outboundNew = V2rayConfig.OutboundBean( + mux = null, + protocol = EConfigType.SOCKS.name.lowercase(), + settings = V2rayConfig.OutboundBean.OutSettingsBean( + servers = listOf( + V2rayConfig.OutboundBean.OutSettingsBean.ServersBean( + address = LOOPBACK, + port = socksPort + ) + ) + ) + ) + if (v2rayConfig.outbounds.isNotEmpty()) { + v2rayConfig.outbounds[0] = outboundNew + } else { + v2rayConfig.outbounds.add(outboundNew) + } + return true + } + val ret = updateOutboundWithGlobalSettings(outbound) if (!ret) return false @@ -247,7 +270,7 @@ object V2rayConfigUtil { V2rayConfig.InboundBean( tag = "dns-in", port = localDnsPort, - listen = "127.0.0.1", + listen = LOOPBACK, protocol = "dokodemo-door", settings = dnsInboundSettings, sniffing = null @@ -335,7 +358,7 @@ object V2rayConfigUtil { //block dns val blkDomain = userRule2Domain(TAG_BLOCKED) if (blkDomain.size > 0) { - hosts.putAll(blkDomain.map { it to "127.0.0.1" }) + hosts.putAll(blkDomain.map { it to LOOPBACK }) } // hardcode googleapi rule to fix play store problems diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/Hysteria2Fmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/Hysteria2Fmt.kt index cbb30fae..e1042515 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/Hysteria2Fmt.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/Hysteria2Fmt.kt @@ -72,4 +72,17 @@ object Hysteria2Fmt { ) return url + query + remark } + + fun toNativeConfig(config: ServerConfig, socksPort: Int): Hysteria2Bean? { + val outbound = config.getProxyOutbound() ?: return null + val bean = Hysteria2Bean( + server = outbound.getServerAddressAndPort(), + auth = outbound.getPassword(), + socks5 = Hysteria2Bean.Socks5Bean( + listen = "$LOOPBACK:${socksPort}" + ), + tls = Hysteria2Bean.TlsBean(outbound.getServerAddress()) + ) + return bean + } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_main.xml b/V2rayNG/app/src/main/res/menu/menu_main.xml index 7d25d00e..1144010e 100644 --- a/V2rayNG/app/src/main/res/menu/menu_main.xml +++ b/V2rayNG/app/src/main/res/menu/menu_main.xml @@ -48,10 +48,10 @@ android:id="@+id/import_manually_wireguard" android:title="@string/menu_item_import_config_manually_wireguard" app:showAsAction="never" /> - - - - +