mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-07 11:09:49 +08:00
Refactor RunCommandService
- The `FileUtils` and `PluginUtils` have been added to provide utility functions. - The executable and working directory validation has been added to check for existence and missing permissions. - The `expandPath()` function is removed from `RunCommandService`. - Working directory will automatically be created if under `TermuxConstants.TERMUX_FILES_DIR_PATH` if missing. - Better logging has been added. This will later be used to notify the user in foreground. - Javadocs have been updated.
This commit is contained in:
@@ -10,27 +10,79 @@ import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.settings.properties.SharedProperties;
|
||||
import com.termux.app.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.app.utils.FileUtils;
|
||||
import com.termux.app.utils.Logger;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Third-party apps that are not part of termux world can run commands in termux context by either
|
||||
* sending an intent to RunCommandService or becoming a plugin host for the termux-tasker plugin
|
||||
* client.
|
||||
*
|
||||
* For the {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent to work, there are 2 main requirements:
|
||||
* For the {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent to work, here are the requirements:
|
||||
*
|
||||
* 1. The `allow-external-apps` property must be set to "true" in ~/.termux/termux.properties in
|
||||
* termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/`
|
||||
* directory.
|
||||
* 2. The intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND`
|
||||
* 1. `com.termux.permission.RUN_COMMAND` permission (Mandatory)
|
||||
* The Intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND`
|
||||
* permission in its `AndroidManifest.xml` and it should be granted by user to the app through the
|
||||
* app's App Info permissions page in android settings, likely under Additional Permissions.
|
||||
* app's `App Info` `Permissions` page in Android Settings, likely under `Additional Permissions`.
|
||||
* This is a security measure to prevent any other apps from running commands in `Termux` context
|
||||
* which do not have the required permission granted to them.
|
||||
*
|
||||
* For `Tasker` you can grant it with:
|
||||
* `Android Settings` -> `Apps` -> `Tasker` -> `Permissions` -> `Additional permissions` ->
|
||||
* `Run commands in Termux environment`.
|
||||
*
|
||||
* 2. `allow-external-apps` property (Mandatory)
|
||||
* The `allow-external-apps` property must be set to "true" in `~/.termux/termux.properties` in
|
||||
* Termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/`
|
||||
* directory. Check https://github.com/termux/termux-tasker#allow-external-apps-property-optional
|
||||
* for more info.
|
||||
*
|
||||
* 3. `Draw Over Apps` permission (Optional)
|
||||
* For android `>= 10` there are new
|
||||
* [restrictions](https://developer.android.com/guide/components/activities/background-starts)
|
||||
* that prevent activities from starting from the background. This prevents the background
|
||||
* {@link TermuxService} from starting a terminal session in the foreground and running the
|
||||
* commands until the user manually clicks `Termux` notification in the status bar dropdown
|
||||
* notifications list. This only affects commands that are to be executed in a terminal
|
||||
* session and not the background ones. `Termux` version `>= 0.100`
|
||||
* requests the `Draw Over Apps` permission so that users can bypass this restriction so
|
||||
* that commands can automatically start running without user intervention.
|
||||
* You can grant `Termux` the `Draw Over Apps` permission from its `App Info` activity:
|
||||
* `Android Settings` -> `Apps` -> `Termux` -> `Advanced` -> `Draw over other apps`.
|
||||
*
|
||||
* 4. `Storage` permission (Optional)
|
||||
* Termux app must be granted `Storage` permission if the executable is accessing or working
|
||||
* directory is set to path in external shared storage. The common paths which usually refer to
|
||||
* it are `~/storage`, `/sdcard`, `/storage/emulated/0` etc.
|
||||
* You can grant `Termux` the `Storage` permission from its `App Info` activity:
|
||||
* For Android version < 11:
|
||||
* `Android Settings` -> `Apps` -> `Termux` -> `Permissions` -> `Storage`.
|
||||
* For Android version >= 11
|
||||
* `Android Settings` -> `Apps` -> `Termux` -> `Permissions` -> `Files and media` ->
|
||||
* `Allowed management of all files`.
|
||||
* NOTE: For Android version >= 11, sometimes you will get `Permission Denied` errors for
|
||||
* external shared storage even when you have granted `Files and media` permission. To solve
|
||||
* this, Deny the permission and then Allow it again and restart Termux.
|
||||
* Also check https://wiki.termux.com/wiki/Termux-setup-storage
|
||||
*
|
||||
* 5. Battery Optimizations (May be mandatory depending on device)
|
||||
* Some devices kill apps aggressively or prevent apps from starting from background.
|
||||
* If Termux is running into such problems, then exempt it from such restrictions.
|
||||
* The user may also disable battery optimizations for Termux to reduce the chances of Termux
|
||||
* being killed by Android even further due to violation of not being able to call
|
||||
* `startForeground()` within ~5s of service start in android >= 8.
|
||||
* Check https://dontkillmyapp.com/ for device specfic info to opt-out of battery optimiations.
|
||||
*
|
||||
* You may also want to check https://github.com/termux/termux-tasker
|
||||
*
|
||||
*
|
||||
*
|
||||
@@ -55,21 +107,6 @@ import com.termux.app.utils.Logger;
|
||||
* The "$PREFIX/" will expand to {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and
|
||||
* "~/" will expand to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}, followed by a forward slash "/".
|
||||
*
|
||||
*
|
||||
* To automatically bring termux session to foreground and start termux commands that were started
|
||||
* with background mode "false" in android >= 10 without user having to click the notification
|
||||
* manually requires termux to be granted draw over apps permission due to new restrictions
|
||||
* of starting activities from the background, this also applies to Termux:Tasker plugin.
|
||||
*
|
||||
* Check https://github.com/termux/termux-tasker for more details on allow-external-apps and draw
|
||||
* over apps and other limitations.
|
||||
*
|
||||
*
|
||||
* To reduce the chance of termux being killed by android even further due to violation of not
|
||||
* being able to call startForeground() within ~5s of service start in android >= 8, the user
|
||||
* may disable battery optimizations for termux.
|
||||
*
|
||||
*
|
||||
* If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.termux`
|
||||
* package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
|
||||
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
|
||||
@@ -129,44 +166,80 @@ public class RunCommandService extends Service {
|
||||
// Run again in case service is already started and onCreate() is not called
|
||||
runStartForeground();
|
||||
|
||||
String errmsg;
|
||||
|
||||
// If invalid action passed, then just return
|
||||
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||
Logger.logError(LOG_TAG, "Invalid intent action to RunCommandService: \"" + intent.getAction() + "\"");
|
||||
errmsg = this.getString(R.string.run_command_service_invalid_action, intent.getAction());
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// If allow-external-apps property is not set to "true"
|
||||
if (!SharedProperties.isPropertyValueTrue(this, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) {
|
||||
Logger.logError(LOG_TAG, "RunCommandService requires allow-external-apps property to be set to \"true\" in \"" + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH + "\" file");
|
||||
// If "allow-external-apps" property to not set to "true", then just return
|
||||
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||
if (errmsg != null) {
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
|
||||
|
||||
String commandPath = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||
// If invalid commandPath passed, then just return
|
||||
if (commandPath == null || commandPath.isEmpty()) {
|
||||
Logger.logError(LOG_TAG, "Invalid coommand path to RunCommandService: \"" + commandPath + "\"");
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
Uri programUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(getExpandedTermuxPath(commandPath)).build();
|
||||
|
||||
|
||||
|
||||
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, programUri);
|
||||
execIntent.setClass(this, TermuxService.class);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS));
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false));
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION));
|
||||
|
||||
String executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||
String[] arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
|
||||
boolean inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||
String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
|
||||
if (workingDirectory != null && !workingDirectory.isEmpty()) {
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, getExpandedTermuxPath(workingDirectory));
|
||||
String sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||
|
||||
// Get canonical path of executable
|
||||
executable = FileUtils.getCanonicalPath(executable, null, true);
|
||||
|
||||
// If executable is not a regular file, or is not readable or executable, then just return
|
||||
// Setting of missing read and execute permissions is not done
|
||||
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executable,
|
||||
null, PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS,
|
||||
false, false);
|
||||
if (errmsg != null) {
|
||||
errmsg += "\n" + this.getString(R.string.executable_absolute_path, executable);
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// If workingDirectory is not null or empty
|
||||
if (workingDirectory != null && !workingDirectory.isEmpty()) {
|
||||
// Get canonical path of workingDirectory
|
||||
workingDirectory = FileUtils.getCanonicalPath(workingDirectory, null, true);
|
||||
|
||||
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
||||
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
||||
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||
// for working directories.
|
||||
errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, workingDirectory,
|
||||
TermuxConstants.TERMUX_FILES_DIR_PATH, PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS,
|
||||
true, true, false,
|
||||
true);
|
||||
if (errmsg != null) {
|
||||
errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, workingDirectory);
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
PluginUtils.dumpExecutionIntentToLog(Log.VERBOSE, LOG_TAG, "RUN_COMMAND Intent", executable, Arrays.asList(arguments), workingDirectory, inBackground, new HashMap<String, Object>() {{
|
||||
put("sessionAction", sessionAction);
|
||||
}});
|
||||
|
||||
Uri executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executable)).build();
|
||||
|
||||
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
|
||||
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executableUri);
|
||||
execIntent.setClass(this, TermuxService.class);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, arguments);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, inBackground);
|
||||
if (workingDirectory != null && !workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, workingDirectory);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, sessionAction);
|
||||
|
||||
|
||||
// Start TERMUX_SERVICE and pass it execution intent
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.startForegroundService(execIntent);
|
||||
} else {
|
||||
@@ -223,15 +296,4 @@ public class RunCommandService extends Service {
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
/** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */
|
||||
public static String getExpandedTermuxPath(String path) {
|
||||
if(path != null && !path.isEmpty()) {
|
||||
path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/");
|
||||
path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
376
app/src/main/java/com/termux/app/utils/FileUtils.java
Normal file
376
app/src/main/java/com/termux/app/utils/FileUtils.java
Normal file
@@ -0,0 +1,376 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxConstants;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
private static final String LOG_TAG = "FileUtils";
|
||||
|
||||
/**
|
||||
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
|
||||
*
|
||||
* @param path The {@code path} to expand.
|
||||
* @return Returns the {@code expand path}.
|
||||
*/
|
||||
public static String getExpandedTermuxPath(String path) {
|
||||
if(path != null && !path.isEmpty()) {
|
||||
path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/");
|
||||
path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
|
||||
*
|
||||
* @param path The {@code path} to unexpand.
|
||||
* @return Returns the {@code unexpand path}.
|
||||
*/
|
||||
public static String getUnExpandedTermuxPath(String path) {
|
||||
if(path != null && !path.isEmpty()) {
|
||||
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/");
|
||||
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@code expandPath} is enabled, then input path is first attempted to be expanded by calling
|
||||
* {@link #getExpandedTermuxPath(String)}.
|
||||
*
|
||||
* Then if path is already an absolute path, then it is used as is to get canonical path.
|
||||
* If path is not an absolute path and {code prefixForNonAbsolutePath} is not {@code null}, then
|
||||
* {code prefixForNonAbsolutePath} + "/" is prefixed before path before getting canonical path.
|
||||
* If path is not an absolute path and {code prefixForNonAbsolutePath} is {@code null}, then
|
||||
* "/" is prefixed before path before getting canonical path.
|
||||
*
|
||||
* If an exception is raised to get the canonical path, then absolute path is returned.
|
||||
*
|
||||
* @param path The {@code path} to convert.
|
||||
* @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
|
||||
* can be set to {@code null} if non-absolute paths should
|
||||
* be prefixed with "/". The call to {@link File#getCanonicalPath()}
|
||||
* will automatically do this anyways.
|
||||
* @return Returns the {@code canonical path}.
|
||||
*/
|
||||
public static String getCanonicalPath(String path, String prefixForNonAbsolutePath, boolean expandPath) {
|
||||
if (path == null) path = "";
|
||||
|
||||
if(expandPath)
|
||||
path = getExpandedTermuxPath(path);
|
||||
|
||||
String absolutePath;
|
||||
|
||||
// If path is already an absolute path
|
||||
if (path.startsWith("/") ) {
|
||||
absolutePath = path;
|
||||
} else {
|
||||
if (prefixForNonAbsolutePath != null)
|
||||
absolutePath = prefixForNonAbsolutePath + "/" + path;
|
||||
else
|
||||
absolutePath = "/" + path;
|
||||
}
|
||||
|
||||
try {
|
||||
return new File(absolutePath).getCanonicalPath();
|
||||
} catch(Exception e) {
|
||||
}
|
||||
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes one or more forward slashes "//" with single slash "/"
|
||||
* Removes "./"
|
||||
* Removes trailing forward slash "/"
|
||||
*
|
||||
* @param path The {@code path} to convert.
|
||||
* @return Returns the {@code normalized path}.
|
||||
*/
|
||||
public static String normalizePath(String path) {
|
||||
if (path == null) return null;
|
||||
|
||||
path = path.replaceAll("/+", "/");
|
||||
path = path.replaceAll("\\./", "");
|
||||
|
||||
if (path.endsWith("/")) {
|
||||
path = path.substring(0, path.length() - 1);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether path is in {@code dirPath}.
|
||||
*
|
||||
* @param path The {@code path} to check.
|
||||
* @param dirPath The {@code directory path} to check in.
|
||||
* @param ensureUnder If set to {@code true}, then it will be ensured that {@code path} is
|
||||
* under the directory and does not equal it.
|
||||
* @return Returns {@code true} if path in {@code dirPath}, otherwise returns {@code false}.
|
||||
*/
|
||||
public static boolean isPathInDirPath(String path, String dirPath, boolean ensureUnder) {
|
||||
if (path == null || dirPath == null) return false;
|
||||
|
||||
try {
|
||||
path = new File(path).getCanonicalPath();
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String normalizedDirPath = normalizePath(dirPath);
|
||||
|
||||
if(ensureUnder)
|
||||
return !path.equals(normalizedDirPath) && path.startsWith(normalizedDirPath + "/");
|
||||
else
|
||||
return path.startsWith(normalizedDirPath + "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the existence and permissions of regular file at path.
|
||||
*
|
||||
* If the {@code parentDirPath} is not {@code null}, then setting of missing permissions will
|
||||
* only be done if {@code path} is under {@code parentDirPath}.
|
||||
*
|
||||
* @param context The {@link Context} to get error string.
|
||||
* @param path The {@code path} for file to validate.
|
||||
* @param parentDirPath The optional {@code parent directory path} to restrict operations to.
|
||||
* This can optionally be {@code null}.
|
||||
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
||||
* @param setMissingPermissions The {@code boolean} that decides if missing permissions are to be
|
||||
* automatically set.
|
||||
* @param ignoreErrorsIfPathIsUnderParentDirPath The {@code boolean} that decides if permission
|
||||
* errors are to be ignored if path is under
|
||||
* {@code parentDirPath}.
|
||||
* @return Returns the {@code errmsg} if path is not a regular file, or validating permissions
|
||||
* failed, otherwise {@code null}.
|
||||
*/
|
||||
public static String validateRegularFileExistenceAndPermissions(final Context context, final String path, final String parentDirPath, String permissionsToCheck, final boolean setMissingPermissions, final boolean ignoreErrorsIfPathIsUnderParentDirPath) {
|
||||
if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_file);
|
||||
|
||||
try {
|
||||
File file = new File(path);
|
||||
|
||||
// If file exits but not a regular file
|
||||
if (file.exists() && !file.isFile()) {
|
||||
return context.getString(R.string.non_regular_file_found);
|
||||
}
|
||||
|
||||
boolean isPathUnderParentDirPath = false;
|
||||
if (parentDirPath != null) {
|
||||
// The path can only be under parent directory path
|
||||
isPathUnderParentDirPath = isPathInDirPath(path, parentDirPath, true);
|
||||
}
|
||||
|
||||
// If setMissingPermissions is enabled and path is a regular file
|
||||
if (setMissingPermissions && permissionsToCheck != null && file.isFile()) {
|
||||
// If there is not parentDirPath restriction or path is under parentDirPath
|
||||
if (parentDirPath == null || (isPathUnderParentDirPath && new File(parentDirPath).isDirectory())) {
|
||||
setMissingFilePermissions(path, permissionsToCheck);
|
||||
}
|
||||
}
|
||||
|
||||
// If path is not a regular file
|
||||
// Regular files cannot be automatically created so we do not ignore if missing
|
||||
if (!file.isFile()) {
|
||||
return context.getString(R.string.no_regular_file_found);
|
||||
}
|
||||
|
||||
// If there is not parentDirPath restriction or path is not under parentDirPath or
|
||||
// if permission errors must not be ignored for paths under parentDirPath
|
||||
if (parentDirPath == null || !isPathUnderParentDirPath || !ignoreErrorsIfPathIsUnderParentDirPath) {
|
||||
if (permissionsToCheck != null) {
|
||||
// Check if permissions are missing
|
||||
return checkMissingFilePermissions(context, path, permissionsToCheck, "File", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Some function calls may throw SecurityException, etc
|
||||
catch (Exception e) {
|
||||
return context.getString(R.string.validate_file_existence_and_permissions_failed_with_exception, path, e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the existence and permissions of directory at path.
|
||||
*
|
||||
* If the {@code parentDirPath} is not {@code null}, then creation of missing directory and
|
||||
* setting of missing permissions will only be done if {@code path} is under
|
||||
* {@code parentDirPath} or equals {@code parentDirPath}.
|
||||
*
|
||||
* @param context The {@link Context} to get error string.
|
||||
* @param path The {@code path} for file to validate.
|
||||
* @param parentDirPath The optional {@code parent directory path} to restrict operations to.
|
||||
* This can optionally be {@code null}.
|
||||
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
||||
* @param createDirectoryIfMissing The {@code boolean} that decides if directory
|
||||
* should be created if its missing.
|
||||
* @param setMissingPermissions The {@code boolean} that decides if missing permissions are to be
|
||||
* automatically set.
|
||||
* @param ignoreErrorsIfPathIsInParentDirPath The {@code boolean} that decides if existence
|
||||
* and permission errors are to be ignored if path is
|
||||
* in {@code parentDirPath}.
|
||||
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
|
||||
* error is to be ignored. This allows making an attempt to set
|
||||
* executable permissions, but ignoring if it fails.
|
||||
* @return Returns the {@code errmsg} if path is not a directory, or validating permissions
|
||||
* failed, otherwise {@code null}.
|
||||
*/
|
||||
public static String validateDirectoryExistenceAndPermissions(final Context context, final String path, final String parentDirPath, String permissionsToCheck, final boolean createDirectoryIfMissing, final boolean setMissingPermissions, final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
|
||||
if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_directory);
|
||||
|
||||
try {
|
||||
File file = new File(path);
|
||||
|
||||
// If file exits but not a directory file
|
||||
if (file.exists() && !file.isDirectory()) {
|
||||
return context.getString(R.string.non_directory_file_found);
|
||||
}
|
||||
|
||||
boolean isPathInParentDirPath = false;
|
||||
if (parentDirPath != null) {
|
||||
// The path can be equal to parent directory path or under it
|
||||
isPathInParentDirPath = isPathInDirPath(path, parentDirPath, false);
|
||||
}
|
||||
|
||||
if (createDirectoryIfMissing || setMissingPermissions) {
|
||||
// If there is not parentDirPath restriction or path is in parentDirPath
|
||||
if (parentDirPath == null || (isPathInParentDirPath && new File(parentDirPath).isDirectory())) {
|
||||
// If createDirectoryIfMissing is enabled and no file exists at path, then create directory
|
||||
if (createDirectoryIfMissing && !file.exists()) {
|
||||
Logger.logVerbose(LOG_TAG, "Creating missing directory at path: \"" + path + "\"");
|
||||
// If failed to create directory
|
||||
if (!file.mkdirs()) {
|
||||
return context.getString(R.string.creating_missing_directory_failed, path);
|
||||
}
|
||||
}
|
||||
|
||||
// If setMissingPermissions is enabled and path is a directory
|
||||
if (setMissingPermissions && permissionsToCheck != null && file.isDirectory()) {
|
||||
setMissingFilePermissions(path, permissionsToCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is not parentDirPath restriction or path is not in parentDirPath or
|
||||
// if existence or permission errors must not be ignored for paths in parentDirPath
|
||||
if (parentDirPath == null || !isPathInParentDirPath || !ignoreErrorsIfPathIsInParentDirPath) {
|
||||
// If path is not a directory
|
||||
// Directories can be automatically created so we can ignore if missing with above check
|
||||
if (!file.isDirectory()) {
|
||||
return context.getString(R.string.no_directory_found);
|
||||
}
|
||||
|
||||
if (permissionsToCheck != null) {
|
||||
// Check if permissions are missing
|
||||
return checkMissingFilePermissions(context, path, permissionsToCheck, "Directory", ignoreIfNotExecutable);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Some function calls may throw SecurityException, etc
|
||||
catch (Exception e) {
|
||||
return context.getString(R.string.validate_directory_existence_and_permissions_failed_with_exception, path, e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set missing permissions for file at path.
|
||||
*
|
||||
* @param path The {@code path} for file to set permissions to.
|
||||
* @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
||||
*/
|
||||
public static void setMissingFilePermissions(String path, String permissionsToSet) {
|
||||
if (path == null || path.isEmpty()) return;
|
||||
|
||||
if (!isValidPermissingString(permissionsToSet)) {
|
||||
Logger.logError(LOG_TAG, "Invalid permissionsToSet passed to setMissingFilePermissions: \"" + permissionsToSet + "\"");
|
||||
return;
|
||||
}
|
||||
|
||||
File file = new File(path);
|
||||
|
||||
if (permissionsToSet.contains("r") && !file.canRead()) {
|
||||
Logger.logVerbose(LOG_TAG, "Setting missing read permissions for file at path: \"" + path + "\"");
|
||||
file.setReadable(true);
|
||||
}
|
||||
|
||||
if (permissionsToSet.contains("w") && !file.canWrite()) {
|
||||
Logger.logVerbose(LOG_TAG, "Setting missing write permissions for file at path: \"" + path + "\"");
|
||||
file.setWritable(true);
|
||||
}
|
||||
|
||||
if (permissionsToSet.contains("x") && !file.canExecute()) {
|
||||
Logger.logVerbose(LOG_TAG, "Setting missing execute permissions for file at path: \"" + path + "\"");
|
||||
file.setExecutable(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checking missing permissions for file at path.
|
||||
*
|
||||
* @param context The {@link Context} to get error string.
|
||||
* @param path The {@code path} for file to check permissions for.
|
||||
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
||||
* @param fileType The label for the type of file to use for error string.
|
||||
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
|
||||
* error is to be ignored.
|
||||
* @return Returns the {@code errmsg} if validating permissions failed, otherwise {@code null}.
|
||||
*/
|
||||
public static String checkMissingFilePermissions(Context context, String path, String permissionsToCheck, String fileType, boolean ignoreIfNotExecutable) {
|
||||
if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_path);
|
||||
|
||||
if (!isValidPermissingString(permissionsToCheck)) {
|
||||
Logger.logError(LOG_TAG, "Invalid permissionsToCheck passed to checkMissingFilePermissions: \"" + permissionsToCheck + "\"");
|
||||
return context.getString(R.string.invalid_file_permissions_string_to_check);
|
||||
}
|
||||
|
||||
if (fileType == null || fileType.isEmpty()) fileType = "File";
|
||||
|
||||
File file = new File(path);
|
||||
|
||||
// If file is not readable
|
||||
if (permissionsToCheck.contains("r") && !file.canRead()) {
|
||||
return context.getString(R.string.file_not_readable, fileType);
|
||||
}
|
||||
|
||||
// If file is not writable
|
||||
if (permissionsToCheck.contains("w") && !file.canWrite()) {
|
||||
return context.getString(R.string.file_not_writable, fileType);
|
||||
}
|
||||
// If file is not executable
|
||||
// This canExecute() will give "avc: granted { execute }" warnings for target sdk 29
|
||||
else if (permissionsToCheck.contains("x") && !file.canExecute() && !ignoreIfNotExecutable) {
|
||||
return context.getString(R.string.file_not_executable, fileType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether string exactly matches the 3 character permission string that
|
||||
* contains the "r", "w", "x" or "-" in-order.
|
||||
*
|
||||
* @param string The {@link String} to check.
|
||||
* @return Returns {@code true} if string exactly matches a permission string, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean isValidPermissingString(String string) {
|
||||
if (string == null || string.isEmpty()) return false;
|
||||
return Pattern.compile("^([r-])[w-][x-]$", 0).matcher(string).matches();
|
||||
}
|
||||
|
||||
}
|
180
app/src/main/java/com/termux/app/utils/PluginUtils.java
Normal file
180
app/src/main/java/com/termux/app/utils/PluginUtils.java
Normal file
@@ -0,0 +1,180 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxConstants;
|
||||
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.settings.properties.SharedProperties;
|
||||
import com.termux.app.settings.properties.TermuxPropertyConstants;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PluginUtils {
|
||||
|
||||
/** Plugin variable for stdout value of termux command */
|
||||
public static final String PLUGIN_VARIABLE_STDOUT = "%stdout"; // Default: "%stdout"
|
||||
/** Plugin variable for stderr value of termux command */
|
||||
public static final String PLUGIN_VARIABLE_STDERR = "%stderr"; // Default: "%stderr"
|
||||
/** Plugin variable for exit code value of termux command */
|
||||
public static final String PLUGIN_VARIABLE_EXIT_CODE = "%result"; // Default: "%result"
|
||||
/** Plugin variable for err value of termux command */
|
||||
public static final String PLUGIN_VARIABLE_ERR = "%err"; // Default: "%err"
|
||||
/** Plugin variable for errmsg value of termux command */
|
||||
public static final String PLUGIN_VARIABLE_ERRMSG = "%errmsg"; // Default: "%errmsg"
|
||||
|
||||
/** Intent {@code Parcelable} extra containing original intent received from plugin host app by FireReceiver */
|
||||
public static final String EXTRA_ORIGINAL_INTENT = "originalIntent"; // Default: "originalIntent"
|
||||
|
||||
|
||||
|
||||
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
|
||||
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
|
||||
/** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions.
|
||||
* Execute permissions should be attempted to be set, but ignored if they are missing */
|
||||
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A regex to validate if a string matches a valid plugin host variable name with the percent sign "%" prefix.
|
||||
* Valid values: A string containing a percent sign character "%", followed by 1 alphanumeric character,
|
||||
* followed by 2 or more alphanumeric or underscore "_" characters but does not end with an underscore "_"
|
||||
*/
|
||||
public static final String PLUGIN_HOST_VARIABLE_NAME_MATCH_EXPRESSION = "%[a-zA-Z0-9][a-zA-Z0-9_]{2,}(?<!_)";
|
||||
|
||||
|
||||
|
||||
private static final String LOG_TAG = "PluginUtils";
|
||||
|
||||
/**
|
||||
* Send execution result of commands to the {@link PendingIntent} creator received by
|
||||
* execution service if {@code pendingIntent} is not {@code null}
|
||||
*
|
||||
* @param logLevel The log level to dump the result.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param context The {@link Context} that will be used to send result intent to the PluginResultsService.
|
||||
* @param pendingIntent The {@link PendingIntent} sent by creator to the execution service.
|
||||
* @param stdout The value for {@link TERMUX_SERVICE#EXTRA_STDOUT} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
||||
* @param stderr The value for {@link TERMUX_SERVICE#EXTRA_STDERR} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
||||
* @param exitCode The value for {@link TERMUX_SERVICE#EXTRA_EXIT_CODE} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
||||
* @param errCode The value for {@link TERMUX_SERVICE#EXTRA_ERR} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
||||
* @param errmsg The value for {@link TERMUX_SERVICE#EXTRA_ERRMSG} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
||||
*/
|
||||
public static void sendExecuteResultToResultsService(final int logLevel, final String logTag, final Context context, final PendingIntent pendingIntent, final String stdout, final String stderr, final String exitCode, final String errCode, final String errmsg) {
|
||||
String label;
|
||||
|
||||
if(pendingIntent == null)
|
||||
label = "Execution Result";
|
||||
else
|
||||
label = "Sending execution result to " + pendingIntent.getCreatorPackage();
|
||||
|
||||
Logger.logMesssage(logLevel, logTag, label + ":\n" +
|
||||
TERMUX_SERVICE.EXTRA_STDOUT + ":\n```\n" + stdout + "\n```\n" +
|
||||
TERMUX_SERVICE.EXTRA_STDERR + ":\n```\n" + stderr + "\n```\n" +
|
||||
TERMUX_SERVICE.EXTRA_EXIT_CODE + ": `" + exitCode + "`\n" +
|
||||
TERMUX_SERVICE.EXTRA_ERR + ": `" + errCode + "`\n" +
|
||||
TERMUX_SERVICE.EXTRA_ERRMSG + ": `" + errmsg + "`");
|
||||
|
||||
// If pendingIntent is null, then just return
|
||||
if(pendingIntent == null) return;
|
||||
|
||||
final Bundle resultBundle = new Bundle();
|
||||
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_STDOUT, stdout);
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_STDERR, stderr);
|
||||
if (exitCode != null && !exitCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_EXIT_CODE, Integer.parseInt(exitCode));
|
||||
if (errCode != null && !errCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_ERR, Integer.parseInt(errCode));
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_ERRMSG, errmsg);
|
||||
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_BUNDLE, resultBundle);
|
||||
|
||||
if(context != null) {
|
||||
try {
|
||||
pendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
// The caller doesn't want the result? That's fine, just ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
||||
*
|
||||
* @param context The {@link Context} to get error string.
|
||||
* @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}.
|
||||
*/
|
||||
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||
String errmsg = null;
|
||||
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) {
|
||||
errmsg = context.getString(R.string.run_command_service_allow_external_apps_ungranted_warning);
|
||||
}
|
||||
|
||||
return errmsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send execution result of commands to the {@link PendingIntent} creator received by
|
||||
* execution service if {@code pendingIntent} is not {@code null}
|
||||
*
|
||||
* @param logLevel The log level to dump the result.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param executable The executable received.
|
||||
* @param arguments_list The arguments list for executable received.
|
||||
* @param workingDirectory The working directory for the command received.
|
||||
* @param inBackground The command should be run in background.
|
||||
* @param additionalExtras The {@link HashMap} for additional extras received. The key will be
|
||||
* used as the label to log the value. The object will be converted
|
||||
* to {@link String} with a call to {@code value.toString()}.
|
||||
*/
|
||||
public static void dumpExecutionIntentToLog(int logLevel, String logTag, String label, String executable, List<String> arguments_list, String workingDirectory, boolean inBackground, HashMap<String, Object> additionalExtras) {
|
||||
if (label == null) label = "Execution Intent";
|
||||
|
||||
StringBuilder executionIntentDump = new StringBuilder();
|
||||
|
||||
executionIntentDump.append(label).append(":\n");
|
||||
executionIntentDump.append("Executable: `").append(executable).append("`\n");
|
||||
executionIntentDump.append("Arguments:").append(getArgumentsStringForLog(arguments_list)).append("\n");
|
||||
executionIntentDump.append("Working Directory: `").append(workingDirectory).append("`\n");
|
||||
executionIntentDump.append("inBackground: `").append(inBackground).append("`");
|
||||
|
||||
if(additionalExtras != null) {
|
||||
for (Map.Entry<String, Object> entry : additionalExtras.entrySet()) {
|
||||
executionIntentDump.append("\n").append(entry.getKey()).append(": `").append(entry.getValue()).append("`");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.logMesssage(logLevel, logTag, executionIntentDump.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts arguments list to log friendly format. If arguments are null or of size 0, then
|
||||
* nothing is returned. Otherwise following format is returned:
|
||||
*
|
||||
* ```
|
||||
* Arg 0: `value`
|
||||
* Arg 1: 'value`
|
||||
* ```
|
||||
*
|
||||
* @param arguments_list The arguments list.
|
||||
* @return Returns the formatted arguments list.
|
||||
*/
|
||||
public static String getArgumentsStringForLog(List<String> arguments_list) {
|
||||
if (arguments_list==null || arguments_list.size() == 0) return "";
|
||||
|
||||
StringBuilder arguments_list_string = new StringBuilder("\n```\n");
|
||||
for(int i = 0; i != arguments_list.size(); i++) {
|
||||
arguments_list_string.append("Arg ").append(i).append(": `").append(arguments_list.get(i)).append("`\n");
|
||||
}
|
||||
arguments_list_string.append("```");
|
||||
|
||||
return arguments_list_string.toString();
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@
|
||||
<!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling">
|
||||
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
|
||||
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
|
||||
<!ENTITY TERMUX_PROPERTIES_PRIMARY_PATH_SHORT "~/.termux/termux.properties">
|
||||
]>
|
||||
|
||||
<resources>
|
||||
@@ -64,6 +65,29 @@
|
||||
<string name="file_received_edit_button">Edit</string>
|
||||
<string name="file_received_open_folder_button">Open folder</string>
|
||||
|
||||
<string name="executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
|
||||
<string name="working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
|
||||
|
||||
<string name="executable_required">Executable required.</string>
|
||||
<string name="null_or_empty_path">The path is null or empty.</string>
|
||||
<string name="null_or_empty_file">The file is null or empty.</string>
|
||||
<string name="null_or_empty_executable">The executable is null or empty.</string>
|
||||
<string name="null_or_empty_directory">The directory is null or empty.</string>
|
||||
<string name="invalid_file_permissions_string_to_check">The file permission string to check is invalid.</string>
|
||||
<string name="no_regular_file_found">Regular file not found at path.</string>
|
||||
<string name="no_directory_found">Directory not found at path.</string>
|
||||
<string name="file_not_readable">%1$s at path is not readable. Permission Denied.</string>
|
||||
<string name="file_not_writable">%1$s at path is not writable. Permission Denied.</string>
|
||||
<string name="file_not_executable">%1$s at path is not executable. Permission Denied.</string>
|
||||
<string name="non_regular_file_found">Non-regular file found at path.</string>
|
||||
<string name="non_directory_file_found">Non-directory file found at path.</string>
|
||||
<string name="creating_missing_directory_failed">Failed to create missing directory at path: \"%1$s\"</string>
|
||||
<string name="validate_file_existence_and_permissions_failed_with_exception">Validating file existence and permissions fafiled: \"%1$s\"\nException: %2$s</string>
|
||||
<string name="validate_directory_existence_and_permissions_failed_with_exception">Validating directory existence and permissions fafiled: \"%1$s\"\nException: %2$s</string>
|
||||
|
||||
<string name="run_command_service_invalid_action">Invalid intent action to RunCommandService: \"%1$s\"</string>
|
||||
<string name="run_command_service_invalid_command_path">Invalid coommand path to RunCommandService: \"%1$s\"</string>
|
||||
<string name="run_command_service_allow_external_apps_ungranted_warning">RunCommandService require allow-external-apps property to be set to \"true\" in &TERMUX_PROPERTIES_PRIMARY_PATH_SHORT; file.</string>
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user