mirror of
https://github.com/termux/termux-app
synced 2024-06-17 22:57:08 +00:00
Added: New dangerous permission com.termux.permission.TERMUX_PLUGIN, signature permission com.termux.permission.TERMUX_SIGNATURE, PluginService bound service for plugins.
The methods for PluginService are defined in AIDL in the new module plugin-aidl. The module can be imported by plugins to get the method definitions. The TERMUX_PLUGIN permission is needed to bind to the PluginService, and external apps have to be enabled in the config. AIDL methods: setCallbackBinder and setCallbackService: Used to get a Binder for the PluginService to identify each plugin and get notified when it dies. runTask: Runs a command as a Termux task in the background if the caller also has the RUN_COMMAND permission. listenOnSocketFile: Creates a server socket in the app directory for the plugins (TermuxConstants#TERMUX_APPS_DIR_PATH/<package name>) and returns the file descriptor, which can be used for a LocalServerSocket. openFile: Opens a file in the app directory for the plugin and returns the file descriptor.
This commit is contained in:
parent
f3ebad8002
commit
868cd13af1
|
@ -35,6 +35,7 @@ android {
|
|||
|
||||
implementation project(":terminal-view")
|
||||
implementation project(":termux-shared")
|
||||
implementation project(":plugin-aidl")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
|
|
@ -20,6 +20,17 @@
|
|||
android:label="@string/permission_run_command_label"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<permission
|
||||
android:name="${TERMUX_PACKAGE_NAME}.permission.TERMUX_PLUGIN"
|
||||
android:description="@string/permission_plugin_description"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/permission_plugin_label"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<permission
|
||||
android:name="${TERMUX_PACKAGE_NAME}.permission.TERMUX_SIGNATURE"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
@ -207,6 +218,10 @@
|
|||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".app.plugin.PluginService"
|
||||
android:exported="true"
|
||||
android:permission="${TERMUX_PACKAGE_NAME}.permission.TERMUX_PLUGIN"/>
|
||||
|
||||
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8 mark the
|
||||
app with "This app is optimized to run in full screen." -->
|
||||
|
|
|
@ -68,7 +68,7 @@ import java.util.List;
|
|||
public final class TermuxService extends Service implements AppShell.AppShellClient, TermuxSession.TermuxSessionClient {
|
||||
|
||||
/** This service is only bound from inside the same process and never uses IPC. */
|
||||
class LocalBinder extends Binder {
|
||||
public class LocalBinder extends Binder {
|
||||
public final TermuxService service = TermuxService.this;
|
||||
}
|
||||
|
||||
|
|
325
app/src/main/java/com/termux/app/plugin/PluginService.java
Normal file
325
app/src/main/java/com/termux/app/plugin/PluginService.java
Normal file
|
@ -0,0 +1,325 @@
|
|||
package com.termux.app.plugin;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.plugin_aidl.IPluginCallback;
|
||||
import com.termux.plugin_aidl.IPluginService;
|
||||
import com.termux.shared.android.BinderUtils;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.net.socket.local.ILocalSocketManager;
|
||||
import com.termux.shared.net.socket.local.LocalClientSocket;
|
||||
import com.termux.shared.net.socket.local.LocalServerSocket;
|
||||
import com.termux.shared.net.socket.local.LocalSocketManager;
|
||||
import com.termux.shared.net.socket.local.LocalSocketRunConfig;
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This is a bound service that can be used by plugins.
|
||||
* The available methods are defined in {@link com.termux.plugin_aidl.IPluginService}.
|
||||
*/
|
||||
public class PluginService extends Service
|
||||
{
|
||||
private final static String LOG_TAG = "PluginService";
|
||||
|
||||
/**
|
||||
* Internal representation of a connected plugin for the service.
|
||||
*/
|
||||
private class Plugin {
|
||||
int pid, uid;
|
||||
|
||||
@NonNull IPluginCallback callback;
|
||||
int cachedCallbackVersion;
|
||||
|
||||
Plugin(int pid, int uid, @NonNull IPluginCallback callback) throws RemoteException {
|
||||
this.pid = pid;
|
||||
this.uid = uid;
|
||||
this.callback = callback;
|
||||
callback.asBinder().linkToDeath(() -> mConnectedPlugins.remove(pid), 0); // remove self when the callback binder dies
|
||||
cachedCallbackVersion = callback.getCallbackVersion();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return pid + uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof Plugin)) return false;
|
||||
Plugin p = (Plugin) obj;
|
||||
return p.pid == pid && p.uid == uid;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
if (! bindService(new Intent(this, TermuxService.class), mTermuxServiceConnection, Context.BIND_AUTO_CREATE)) {
|
||||
Logger.logError("Could not bind to TermuxService");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
unbindService(mTermuxServiceConnection);
|
||||
}
|
||||
|
||||
// map of connected clients by PID
|
||||
private final Map<Integer, Plugin> mConnectedPlugins = Collections.synchronizedMap(new HashMap<>());
|
||||
private final PluginServiceBinder mBinder = new PluginServiceBinder();
|
||||
private TermuxService mTermuxService; // can be null if TermuxService gets temporarily destroyed
|
||||
private final ServiceConnection mTermuxServiceConnection = new ServiceConnection()
|
||||
{
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
mTermuxService = ((TermuxService.LocalBinder) service).service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
mTermuxService = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindingDied(ComponentName name) {
|
||||
mTermuxService = null;
|
||||
Logger.logError("Binding to TermuxService died"); // this should never happen, as TermuxService is in the same process
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNullBinding(ComponentName name) {
|
||||
// this should never happen, as TermuxService returns its Binder
|
||||
Logger.logError("TermuxService onBind returned no Binder");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
// If TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", don't enable binding
|
||||
String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
|
||||
if (errmsg != null) {
|
||||
return null;
|
||||
}
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
/**
|
||||
* If not already, this creates an entry in the map of connected plugins for the current Binder client.
|
||||
*/
|
||||
private void addClient(@NonNull IPluginCallback callback) {
|
||||
int pid = Binder.getCallingPid(), uid = Binder.getCallingPid();
|
||||
if (pid == Process.myPid()) return; // no client connected
|
||||
if (mConnectedPlugins.get(pid) != null) return; // client already in list
|
||||
try {
|
||||
mConnectedPlugins.put(pid, new Plugin(pid, uid, callback));
|
||||
} catch (RemoteException ignored) {}
|
||||
}
|
||||
|
||||
|
||||
private void checkClient() throws IllegalStateException {
|
||||
if (! mConnectedPlugins.containsKey(Binder.getCallingPid())) Logger.logDebug("client not in list");
|
||||
if (! mConnectedPlugins.containsKey(Binder.getCallingPid())) throw new IllegalStateException("Please call setCallbackBinder first");
|
||||
}
|
||||
|
||||
|
||||
|
||||
private class PluginServiceBinder extends IPluginService.Stub {
|
||||
|
||||
/**
|
||||
* Convenience function to throw an {@link IllegalArgumentException} if external apps aren't enabled.
|
||||
*/
|
||||
private void externalAppsOrThrow() {
|
||||
// If TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", then throw exception
|
||||
String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(PluginService.this, LOG_TAG);
|
||||
if (errmsg != null) {
|
||||
throw new IllegalArgumentException(errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String pluginDirOrThrow() {
|
||||
String pdir = BinderUtils.getCallingPluginDir(mTermuxService);
|
||||
if (pdir == null) throw new NullPointerException("Could not get apps dir of calling package");
|
||||
return pdir;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String fileInPluginDirOrThrow(String name) {
|
||||
String pluginDir = pluginDirOrThrow();
|
||||
String filePath = FileUtils.getCanonicalPath(pluginDir + "/" +name, null);
|
||||
if (FileUtils.isPathInDirPath(filePath, pluginDir, true)) {
|
||||
return filePath;
|
||||
} else {
|
||||
throw new IllegalArgumentException("A plugin cannot access paths outside of the plugin directory: "+filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCallbackBinder(IPluginCallback callback) {
|
||||
externalAppsOrThrow();
|
||||
if (callback == null) throw new NullPointerException("Passed callback binder is null");
|
||||
addClient(callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCallbackService(String componentNameString) {
|
||||
externalAppsOrThrow();
|
||||
if (componentNameString == null) throw new NullPointerException("Passed componentName is null");
|
||||
|
||||
String callerPackageName = BinderUtils.getCallerPackageNameOrNull(PluginService.this);
|
||||
if (callerPackageName == null) throw new NullPointerException("Caller package is null");
|
||||
|
||||
ComponentName componentName = ComponentName.createRelative(callerPackageName, componentNameString);
|
||||
Intent callbackStartIntent = new Intent();
|
||||
callbackStartIntent.setComponent(componentName);
|
||||
|
||||
|
||||
final boolean[] bindingFinished = {false};
|
||||
final IBinder[] callbackBinder = new IBinder[] {null};
|
||||
|
||||
ServiceConnection con = new ServiceConnection()
|
||||
{
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
callbackBinder[0] = service;
|
||||
bindingFinished[0] = true;
|
||||
synchronized (callbackBinder) {
|
||||
callbackBinder.notifyAll();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
unbindService(this);
|
||||
}
|
||||
@Override
|
||||
public void onBindingDied(ComponentName name) {
|
||||
unbindService(this);
|
||||
}
|
||||
@Override
|
||||
public void onNullBinding(ComponentName name) {
|
||||
bindingFinished[0] = true;
|
||||
synchronized (callbackBinder) {
|
||||
callbackBinder.notifyAll();
|
||||
}
|
||||
unbindService(this);
|
||||
}
|
||||
};
|
||||
|
||||
PluginService.this.bindService(callbackStartIntent, con, Context.BIND_ALLOW_OOM_MANAGEMENT);
|
||||
|
||||
while (! bindingFinished[0]) {
|
||||
try {
|
||||
synchronized (callbackBinder) {
|
||||
callbackBinder.wait();
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
if (callbackBinder[0] == null) {
|
||||
throw new IllegalArgumentException("Could not bind callback service: "+componentNameString);
|
||||
}
|
||||
|
||||
addClient(IPluginCallback.Stub.asInterface(callbackBinder[0]));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor[] runTask(String commandPath, String[] arguments, ParcelFileDescriptor stdin, String workdir, String commandLabel, String commandDescription, String commandHelp) {
|
||||
externalAppsOrThrow();
|
||||
if (commandPath == null) throw new NullPointerException("Passed commandPath is null");
|
||||
checkClient();
|
||||
BinderUtils.enforceRunCommandPermission(PluginService.this);
|
||||
|
||||
|
||||
// TODO run the task with mTermuxService
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ParcelFileDescriptor listenOnSocketFile(String name) {
|
||||
externalAppsOrThrow();
|
||||
if (name == null) throw new NullPointerException("Passed name is null");
|
||||
checkClient();
|
||||
|
||||
String socketPath = fileInPluginDirOrThrow(name);
|
||||
|
||||
|
||||
// passing null for localSocketManagerClient here should be okay, because the handler methods don't get called
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
LocalSocketRunConfig conf = new LocalSocketRunConfig(BinderUtils.getCallerPackageName(PluginService.this) + " socket: " + name, socketPath, null);
|
||||
LocalSocketManager m = new LocalSocketManager(PluginService.this, conf);
|
||||
Error err = m.createSocket();
|
||||
if (err != null) {
|
||||
throw new UnsupportedOperationException("Error: "+err.getErrorLogString());
|
||||
}
|
||||
|
||||
ParcelFileDescriptor socketPfd;
|
||||
|
||||
try {
|
||||
socketPfd = ParcelFileDescriptor.fromFd(conf.getFD());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
} finally {
|
||||
m.getServerSocket().closeServerSocket(true);
|
||||
}
|
||||
|
||||
return socketPfd;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(String name, String mode) {
|
||||
externalAppsOrThrow();
|
||||
if (name == null) throw new NullPointerException("Passed name is null");
|
||||
if (mode == null) throw new NullPointerException("Passed mode is null");
|
||||
checkClient();
|
||||
|
||||
String filePath = fileInPluginDirOrThrow(name);
|
||||
|
||||
Error err = FileUtils.createRegularFile("("+BinderUtils.getCallerPackageName(PluginService.this)+")", filePath, "rw", true, false);
|
||||
if (err != null) {
|
||||
throw new UnsupportedOperationException(err.getErrorLogString());
|
||||
}
|
||||
try {
|
||||
return ParcelFileDescriptor.open(new File(filePath), ParcelFileDescriptor.parseMode(mode));
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -23,7 +23,9 @@
|
|||
<string name="permission_run_command_description">execute arbitrary commands within &TERMUX_APP_NAME;
|
||||
environment and access files</string>
|
||||
|
||||
|
||||
<!-- Termux TERMUX_PLUGIN permission -->
|
||||
<string name="permission_plugin_label">Connect as a plugin to &TERMUX_APP_NAME;</string>
|
||||
<string name="permission_plugin_description">manage an app directory under /data/data/&TERMUX_PACKAGE_NAME;/files/apps</string>
|
||||
|
||||
<!-- Termux Bootstrap Packages Installation -->
|
||||
<string name="bootstrap_installer_body">Installing bootstrap packages…</string>
|
||||
|
|
1
plugin-aidl/.gitignore
vendored
Normal file
1
plugin-aidl/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
49
plugin-aidl/build.gradle
Normal file
49
plugin-aidl/build.gradle
Normal file
|
@ -0,0 +1,49 @@
|
|||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs, "build/generated/aidl_source_output_dir/release/out"
|
||||
classifier "sources"
|
||||
dependsOn "compileReleaseAidl"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'plugin-aidl'
|
||||
version = '1.0.0'
|
||||
artifact sourceJar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2
plugin-aidl/consumer-rules.pro
Normal file
2
plugin-aidl/consumer-rules.pro
Normal file
|
@ -0,0 +1,2 @@
|
|||
# keep all classes
|
||||
-keep class com.termux.plugin_aidl.** { *; }
|
24
plugin-aidl/proguard-rules.pro
vendored
Normal file
24
plugin-aidl/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
# keep all classes
|
||||
-keep class com.termux.plugin_aidl.** { *; }
|
5
plugin-aidl/src/main/AndroidManifest.xml
Normal file
5
plugin-aidl/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.termux.plugin_aidl">
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,40 @@
|
|||
package com.termux.plugin_aidl;
|
||||
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
/**
|
||||
* This can be passed from a plugin to Termux to enable callback functionality.
|
||||
* To maintain backwards compatibility callbacks cannot be removed (without deprecation).
|
||||
* New callbacks can be added and CURRENT_CALLBACK_VERSION incremented, but the callbacks can only be called
|
||||
* when the callback version via getCallbackVersion() is at least the new value of CURRENT_CALLBACK_VERSION.
|
||||
* Previous callback versions have to be supported by Termux, so an out-of-date plugin doesn't receive transactions it doesn't recognize.
|
||||
* In the javadoc write the first and last (if there is one) callback version code where the callback will be called.
|
||||
*/
|
||||
interface IPluginCallback {
|
||||
|
||||
/**
|
||||
* This defines to most up-to-date version code for callback binders.
|
||||
*/
|
||||
const int CURRENT_CALLBACK_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Returns the callback version supported by this Binder.
|
||||
* This is the first method called after Termux receives the Binder.
|
||||
* Only methods compatible with the callback version will be called.
|
||||
*/
|
||||
int getCallbackVersion() = 1;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package com.termux.plugin_aidl;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.app.PendingIntent;
|
||||
|
||||
import com.termux.plugin_aidl.IPluginCallback;
|
||||
|
||||
|
||||
/**
|
||||
* All available methods in {@link com.termux.app.plugin.PluginService}.
|
||||
* When modifying methods here, use the following scheme to maintain backwards compatibility:
|
||||
* <ul>
|
||||
* <li>Append the version code of the last app version where the method exists with the current method signature as a postfix to the method you want to modify.</li>
|
||||
* <li>Also rename the method in {@link com.termux.app.plugin.PluginService.PluginServiceBinder}.</li>
|
||||
* <li>create a method with the original name, a new transaction id and the updated method signature.
|
||||
* You should always count the transaction id up from the last created method to prevent clashing with old methods.</li>
|
||||
* </ul>
|
||||
* The old implementation should be kept for backwards compatibility, at least for some time.<br>
|
||||
* Example:<br>
|
||||
* Version code of the last app release: 118<br>
|
||||
* Method: String foo(String bar) = 33;<br>
|
||||
* Renamed: String foo_118(String bar) = 33;<br>
|
||||
* New method: boolean foo(String bar) = 34;<br><br>
|
||||
*/
|
||||
interface IPluginService {
|
||||
|
||||
|
||||
/**
|
||||
* This or {@link com.termux.plugin_aidl.IPluginService#setCallbackService} has to be called before any other method.
|
||||
* It initializes the internal representation of the connected plugin and sets the callback binder.
|
||||
*/
|
||||
void setCallbackBinder(IPluginCallback callback) = 1;
|
||||
|
||||
/**
|
||||
* This or {@link com.termux.plugin_aidl.IPluginService#setCallbackBinder} has to be called before any other method.
|
||||
* It initialized the internal representation of the connected plugin and sets the callback binder to the binder returned by the bound service.
|
||||
*
|
||||
* @param componentName This is the relative part of a component name string.
|
||||
* The package name is always taken from the calling binder package for security reasons.
|
||||
*/
|
||||
void setCallbackService(String componentName) = 2;
|
||||
|
||||
|
||||
/**
|
||||
* Runs a command like through a RUN_COMMAND intent.
|
||||
* For documentation of the parameters, see <a href="https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent#run_command-intent-command-extras">the wiki</a>.
|
||||
* If a parameter is null it is treated the same as if the extra isn't in the intent.
|
||||
* <br><br>
|
||||
* This method runs synchronously and returns stout in [0] of the result array and stderr in [1].
|
||||
*/
|
||||
ParcelFileDescriptor[] runTask(String commandPath, in String[] arguments, in ParcelFileDescriptor stdin, String workdir, String commandLabel, String commandDescription, String commandHelp) = 3;
|
||||
|
||||
/**
|
||||
* This creates a socket file with name under {@link TermuxConstants#TERMUX_APPS_DIR_PATH}/<package name of caller> and listens on it.
|
||||
*
|
||||
* @param name Name of the socket file.
|
||||
* @return The file descriptor of the created local server socket.
|
||||
*/
|
||||
ParcelFileDescriptor listenOnSocketFile(String name) = 4;
|
||||
|
||||
|
||||
/**
|
||||
* Opens a file under{@link TermuxConstants#TERMUX_APPS_DIR_PATH}/<package name of caller> with mode.
|
||||
*
|
||||
* @param name Name of the file.
|
||||
* @þaram mode Mode to use.
|
||||
*/
|
||||
ParcelFileDescriptor openFile(String name, String mode) = 5;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1 +1 @@
|
|||
include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view'
|
||||
include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view', ':plugin-aidl'
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package com.termux.shared.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Binder;
|
||||
import android.os.Process;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
public class BinderUtils
|
||||
{
|
||||
/**
|
||||
* Get the package name of the calling process for the current Binder transaction of this thread.
|
||||
* Returns null if no Binder transaction is being processed.
|
||||
*
|
||||
* @param c The context used to get the {@link android.content.pm.PackageManager}.
|
||||
* @return The calling package name, or null if the thread isn't processing a Binder transaction.
|
||||
*/
|
||||
public static String getCallerPackageNameOrNull(Context c) {
|
||||
if (c.getApplicationInfo().uid == Binder.getCallingUid()) {
|
||||
if (Process.myPid() == Binder.getCallingPid()) {
|
||||
return null; // same process as Termux
|
||||
} else {
|
||||
// calling process has the same uid: search all processes with the same uid for the pid
|
||||
for (String pack : c.getPackageManager().getPackagesForUid(Binder.getCallingUid())) {
|
||||
if (String.valueOf(Binder.getCallingPid()).equals(PackageUtils.getPackagePID(c, pack))) {
|
||||
return pack;
|
||||
}
|
||||
}
|
||||
return c.getPackageName(); // process could not be found under shared uid processes, user Termux as default
|
||||
}
|
||||
} else {
|
||||
return c.getPackageManager().getNameForUid(Binder.getCallingUid()); // foreign uid, getNameForUid is correct
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package name of the calling process for the current Binder transaction of this thread.
|
||||
* Returns {@link Context#getPackageName()} if no Binder transaction is being processed.
|
||||
*
|
||||
* @param c The context used to get the {@link android.content.pm.PackageManager}.
|
||||
* @return The calling package name, or {@link Context#getPackageName()} if the thread isn't processing a Binder transaction.
|
||||
*/
|
||||
public static String getCallerPackageName(Context c) {
|
||||
String packageName = getCallerPackageNameOrNull(c);
|
||||
if (packageName == null) {
|
||||
return c.getPackageName();
|
||||
} else {
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param c The context used to get the {@link android.content.pm.PackageManager}.
|
||||
* @return The plugin directory of the calling package name under {@link TermuxConstants#TERMUX_APPS_DIR_PATH}, or null if {@link #getCallerPackageNameOrNull(Context)} returns null.
|
||||
*/
|
||||
public static String getCallingPluginDir(Context c) {
|
||||
String packageName = getCallerPackageNameOrNull(c);
|
||||
if (packageName == null) {
|
||||
return null;
|
||||
}
|
||||
return TermuxConstants.TERMUX_APPS_DIR_PATH + "/" + packageName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Throws a {@link SecurityException} if the calling process of the current Binder transaction doesn't have the RUN_COMMAND permission
|
||||
* or the current thread isn't processing a Binder transaction.
|
||||
*
|
||||
* @param c The context used to get the {@link android.content.pm.PackageManager}.
|
||||
*/
|
||||
public static void enforceRunCommandPermission(Context c) {
|
||||
c.enforceCallingPermission(TermuxConstants.PERMISSION_RUN_COMMAND, "Calling package "+ getCallerPackageName(c)+" does not have the" + TermuxConstants.PERMISSION_RUN_COMMAND + " permission.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -122,6 +122,75 @@ public class LocalServerSocket implements Closeable {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the socket and binds to it, but doesn't start the socket listener.
|
||||
* You can use this to pass the server socket file descriptor to another application.
|
||||
*/
|
||||
public synchronized Error createSocket() {
|
||||
Logger.logDebug(LOG_TAG, "start");
|
||||
|
||||
String path = mLocalSocketRunConfig.getPath();
|
||||
if (path == null || path.isEmpty()) {
|
||||
return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NULL_OR_EMPTY.getError(mLocalSocketRunConfig.getTitle());
|
||||
}
|
||||
if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) {
|
||||
path = FileUtils.getCanonicalPath(path, null);
|
||||
}
|
||||
|
||||
// On Linux, sun_path is 108 bytes (UNIX_PATH_MAX) in size, so do an early check here to
|
||||
// prevent useless parent directory creation since createServerSocket() call will fail since
|
||||
// there is a native check as well.
|
||||
if (path.getBytes(StandardCharsets.UTF_8).length > 108) {
|
||||
return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_TOO_LONG.getError(mLocalSocketRunConfig.getTitle(), path);
|
||||
}
|
||||
|
||||
int backlog = mLocalSocketRunConfig.getBacklog();
|
||||
if (backlog <= 0) {
|
||||
return LocalSocketErrno.ERRNO_SERVER_SOCKET_BACKLOG_INVALID.getError(mLocalSocketRunConfig.getTitle(), backlog);
|
||||
}
|
||||
|
||||
Error error;
|
||||
|
||||
// If server socket is not in abstract namespace
|
||||
if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) {
|
||||
if (!path.startsWith("/"))
|
||||
return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NOT_ABSOLUTE.getError(mLocalSocketRunConfig.getTitle(), path);
|
||||
|
||||
// Create the server socket file parent directory and set SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS if missing
|
||||
String socketParentPath = new File(path).getParent();
|
||||
error = FileUtils.validateDirectoryFileExistenceAndPermissions(mLocalSocketRunConfig.getTitle() + " server socket file parent",
|
||||
socketParentPath,
|
||||
null, true,
|
||||
SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS, true, true,
|
||||
false, false);
|
||||
if (error != null)
|
||||
return error;
|
||||
|
||||
|
||||
// Delete the server socket file to stop any existing servers and for bind() to succeed
|
||||
error = deleteServerSocketFile();
|
||||
if (error != null)
|
||||
return error;
|
||||
}
|
||||
|
||||
// Create the server socket
|
||||
JniResult result = LocalSocketManager.createServerSocket(mLocalSocketRunConfig.getLogTitle() + " (server)",
|
||||
path.getBytes(StandardCharsets.UTF_8), backlog);
|
||||
if (result == null || result.retval != 0) {
|
||||
return LocalSocketErrno.ERRNO_CREATE_SERVER_SOCKET_FAILED.getError(mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result));
|
||||
}
|
||||
|
||||
int fd = result.intData;
|
||||
if (fd < 0) {
|
||||
return LocalSocketErrno.ERRNO_SERVER_SOCKET_FD_INVALID.getError(fd, mLocalSocketRunConfig.getTitle());
|
||||
}
|
||||
|
||||
// Update fd to signify that server socket has been created successfully
|
||||
mLocalSocketRunConfig.setFD(fd);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Stop server. */
|
||||
public synchronized Error stop() {
|
||||
|
|
|
@ -85,6 +85,25 @@ public class LocalSocketManager {
|
|||
mIsRunning = true;
|
||||
return mServerSocket.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the {@link LocalServerSocket} but don't start the handler thread.
|
||||
* You can use this to pass the server socket file descriptor to another application.
|
||||
*/
|
||||
public synchronized Error createSocket() {
|
||||
Logger.logDebugExtended(LOG_TAG, "start\n" + mLocalSocketRunConfig);
|
||||
|
||||
if (!localSocketLibraryLoaded) {
|
||||
try {
|
||||
Logger.logDebug(LOG_TAG, "Loading \"" + LOCAL_SOCKET_LIBRARY + "\" library");
|
||||
System.loadLibrary(LOCAL_SOCKET_LIBRARY);
|
||||
localSocketLibraryLoaded = true;
|
||||
} catch (Exception e) {
|
||||
return LocalSocketErrno.ERRNO_START_LOCAL_SOCKET_LIB_LOAD_FAILED_WITH_EXCEPTION.getError(e, LOCAL_SOCKET_LIBRARY, e.getMessage());
|
||||
}
|
||||
}
|
||||
return mServerSocket.createSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the {@link LocalServerSocket} and stop listening for new {@link LocalClientSocket}.
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.util.Formatter;
|
|||
import java.util.List;
|
||||
|
||||
/*
|
||||
* Version: v0.52.0
|
||||
* Version: v0.53.0
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Changelog
|
||||
|
@ -277,6 +277,8 @@ import java.util.List;
|
|||
*
|
||||
* - 0.52.0 (2022-06-18)
|
||||
* - Added `TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY`.
|
||||
* - 0.53.0 (2022-08-22)
|
||||
* - Added `PERMISSION_TERMUX_PLUGIN` and `PERMISSION_TERMUX_SIGNATURE`.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -879,6 +881,19 @@ public final class TermuxConstants {
|
|||
/** Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by
|
||||
* 3rd party apps to run various commands in Termux app context */
|
||||
public static final String PERMISSION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".permission.RUN_COMMAND"; // Default: "com.termux.permission.RUN_COMMAND"
|
||||
|
||||
/**
|
||||
* Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by
|
||||
* 3rd party apps to connect to the {@link com.termux.app.plugin.PluginService}.
|
||||
*/
|
||||
public static final String PERMISSION_TERMUX_PLUGIN = TERMUX_PACKAGE_NAME + ".permission.TERMUX_PLUGIN"; // Default: "com.termux.permission.TERMUX_PLUGIN"
|
||||
|
||||
/**
|
||||
* A permission only the Termux app can hold that can be used by 3rd party apps to restrict component access to Termux.
|
||||
* The 3rd party apps should also verify the signature of the package {@link TermuxConstants#TERMUX_PACKAGE_NAME}
|
||||
* and verify the request came from {@link TermuxConstants#TERMUX_PACKAGE_NAME}.
|
||||
*/
|
||||
public static final String PERMISSION_TERMUX_SIGNATURE = TERMUX_PACKAGE_NAME + ".permission.TERMUX_SIGNATURE"; // Default: "com.termux.permission.TERMUX_SIGNATURE"
|
||||
|
||||
/** Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND
|
||||
* to allow 3rd party apps to run various commands in Termux app context */
|
||||
|
|
Loading…
Reference in New Issue
Block a user