1
0
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:
tareksander 2022-05-02 00:34:31 +02:00
parent f3ebad8002
commit 868cd13af1
17 changed files with 724 additions and 4 deletions

View File

@ -35,6 +35,7 @@ android {
implementation project(":terminal-view")
implementation project(":termux-shared")
implementation project(":plugin-aidl")
}
defaultConfig {

View File

@ -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." -->

View File

@ -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;
}

View 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);
}
}
}
}

View File

@ -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
View File

@ -0,0 +1 @@
/build

49
plugin-aidl/build.gradle Normal file
View 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
}
}
}
}

View File

@ -0,0 +1,2 @@
# keep all classes
-keep class com.termux.plugin_aidl.** { *; }

24
plugin-aidl/proguard-rules.pro vendored Normal file
View 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.** { *; }

View 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>

View File

@ -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;
}

View File

@ -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}/&lt;package name of caller&gt; 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}/&lt;package name of caller&gt; with mode.
*
* @param name Name of the file.
* @þaram mode Mode to use.
*/
ParcelFileDescriptor openFile(String name, String mode) = 5;
}

View File

@ -1 +1 @@
include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view'
include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view', ':plugin-aidl'

View File

@ -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.");
}
}

View File

@ -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() {

View File

@ -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}.

View File

@ -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 */