tor-android/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Toolbox.java

825 lines
27 KiB
Java

/*
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks (RootTools)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.rootcommands;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.sufficientlysecure.rootcommands.command.ExecutableCommand;
import org.sufficientlysecure.rootcommands.command.Command;
import org.sufficientlysecure.rootcommands.command.SimpleCommand;
import org.sufficientlysecure.rootcommands.util.BrokenBusyboxException;
import org.sufficientlysecure.rootcommands.util.Log;
import android.os.StatFs;
import android.os.SystemClock;
/**
* All methods in this class are working with Androids toolbox. Toolbox is similar to busybox, but
* normally shipped on every Android OS. You can find toolbox commands on
* https://github.com/CyanogenMod/android_system_core/tree/ics/toolbox
*
* This means that these commands are designed to work on every Android OS, with a _working_ toolbox
* binary on it. They don't require busybox!
*
*/
public class Toolbox {
private Shell shell;
/**
* All methods in this class are working with Androids toolbox. Toolbox is similar to busybox,
* but normally shipped on every Android OS.
*
* @param shell
* where to execute commands on
*/
public Toolbox(Shell shell) {
super();
this.shell = shell;
}
/**
* Checks if user accepted root access
*
* (commands: id)
*
* @return true if user has given root access
* @throws IOException
* @throws TimeoutException
* @throws BrokenBusyboxException
*/
public boolean isRootAccessGiven() throws BrokenBusyboxException, TimeoutException, IOException {
SimpleCommand idCommand = new SimpleCommand("id");
shell.add(idCommand).waitForFinish();
if (idCommand.getOutput().contains("uid=0")) {
return true;
} else {
return false;
}
}
/**
* This command class gets all pids to a given process name
*/
private class PsCommand extends Command {
private String processName;
private ArrayList<String> pids;
private String psRegex;
private Pattern psPattern;
public PsCommand(String processName) {
super("ps");
this.processName = processName;
pids = new ArrayList<String>();
/**
* regex to get pid out of ps line, example:
*
* <pre>
* root 24736 1 12140 584 ffffffff 40010d14 S /data/data/org.adaway/files/blank_webserver
* ^\\S \\s ([0-9]+) .* processName $
* </pre>
*/
psRegex = "^\\S+\\s+([0-9]+).*" + Pattern.quote(processName) + "$";
psPattern = Pattern.compile(psRegex);
}
public ArrayList<String> getPids() {
return pids;
}
public String getPidsString() {
StringBuilder sb = new StringBuilder();
for (String s : pids) {
sb.append(s);
sb.append(" ");
}
return sb.toString();
}
@Override
public void output(int id, String line) {
// general check if line contains processName
if (line.contains(processName)) {
Matcher psMatcher = psPattern.matcher(line);
// try to match line exactly
try {
if (psMatcher.find()) {
String pid = psMatcher.group(1);
// add to pids list
pids.add(pid);
Log.d(RootCommands.TAG, "Found pid: " + pid);
} else {
Log.d(RootCommands.TAG, "Matching in ps command failed!");
}
} catch (Exception e) {
Log.e(RootCommands.TAG, "Error with regex!", e);
}
}
}
@Override
public void afterExecution(int id, int exitCode) {
}
}
/**
* This method can be used to kill a running process
*
* (commands: ps, kill)
*
* @param processName
* name of process to kill
* @return <code>true</code> if process was found and killed successfully
* @throws IOException
* @throws TimeoutException
* @throws BrokenBusyboxException
*/
public boolean killAll(String processName) throws BrokenBusyboxException, TimeoutException,
IOException {
Log.d(RootCommands.TAG, "Killing process " + processName);
PsCommand psCommand = new PsCommand(processName);
shell.add(psCommand).waitForFinish();
// kill processes
if (!psCommand.getPids().isEmpty()) {
// example: kill -9 1234 1222 5343
SimpleCommand killCommand = new SimpleCommand("kill -9 "
+ psCommand.getPidsString());
shell.add(killCommand).waitForFinish();
if (killCommand.getExitCode() == 0) {
return true;
} else {
return false;
}
} else {
Log.d(RootCommands.TAG, "No pid found! Nothing was killed!");
return false;
}
}
/**
* Kill a running executable
*
* See README for more information how to use your own executables!
*
* @param executableName
* @return
* @throws BrokenBusyboxException
* @throws TimeoutException
* @throws IOException
*/
public boolean killAllExecutable(String executableName) throws BrokenBusyboxException,
TimeoutException, IOException {
return killAll(ExecutableCommand.EXECUTABLE_PREFIX + executableName + ExecutableCommand.EXECUTABLE_SUFFIX);
}
/**
* This method can be used to to check if a process is running
*
* @param processName
* name of process to check
* @return <code>true</code> if process was found
* @throws IOException
* @throws BrokenBusyboxException
* @throws TimeoutException
* (Could not determine if the process is running)
*/
public boolean isProcessRunning(String processName) throws BrokenBusyboxException,
TimeoutException, IOException {
PsCommand psCommand = new PsCommand(processName);
shell.add(psCommand).waitForFinish();
// if pids are available process is running!
if (!psCommand.getPids().isEmpty()) {
return true;
} else {
return false;
}
}
/**
* Checks if binary is running
*
* @param binaryName
* @return
* @throws BrokenBusyboxException
* @throws TimeoutException
* @throws IOException
*/
public boolean isBinaryRunning(String binaryName) throws BrokenBusyboxException,
TimeoutException, IOException {
return isProcessRunning(ExecutableCommand.EXECUTABLE_PREFIX + binaryName
+ ExecutableCommand.EXECUTABLE_SUFFIX);
}
/**
* Ls command to get permissions or symlinks
*/
private class LsCommand extends Command {
private String fileName;
private String permissionRegex;
private Pattern permissionPattern;
private String symlinkRegex;
private Pattern symlinkPattern;
private String symlink;
private String permissions;
public String getSymlink() {
return symlink;
}
public String getPermissions() {
return permissions;
}
public LsCommand(String file) {
super("ls -l " + file);
// get only filename:
this.fileName = (new File(file)).getName();
Log.d(RootCommands.TAG, "fileName: " + fileName);
/**
* regex to get pid out of ps line, example:
*
* <pre>
* with busybox:
* lrwxrwxrwx 1 root root 15 Aug 13 12:14 dev/stdin -> /proc/self/fd/0
*
* with toolbox:
* lrwxrwxrwx root root 15 Aug 13 12:14 stdin -> /proc/self/fd/0
*
* Regex:
* ^.*?(\\S{10}) .* $
* </pre>
*/
permissionRegex = "^.*?(\\S{10}).*$";
permissionPattern = Pattern.compile(permissionRegex);
/**
* regex to get symlink
*
* <pre>
* -> /proc/self/fd/0
* ^.*?\\-\\> \\s+ (.*) $
* </pre>
*/
symlinkRegex = "^.*?\\-\\>\\s+(.*)$";
symlinkPattern = Pattern.compile(symlinkRegex);
}
/**
* Converts permission string from ls command to numerical value. Example: -rwxrwxrwx gets
* to 777
*
* @param permissions
* @return
*/
private String convertPermissions(String permissions) {
int owner = getGroupPermission(permissions.substring(1, 4));
int group = getGroupPermission(permissions.substring(4, 7));
int world = getGroupPermission(permissions.substring(7, 10));
return "" + owner + group + world;
}
/**
* Calculates permission for one group
*
* @param permission
* @return value of permission string
*/
private int getGroupPermission(String permission) {
int value = 0;
if (permission.charAt(0) == 'r') {
value += 4;
}
if (permission.charAt(1) == 'w') {
value += 2;
}
if (permission.charAt(2) == 'x') {
value += 1;
}
return value;
}
@Override
public void output(int id, String line) {
// general check if line contains file
if (line.contains(fileName)) {
// try to match line exactly
try {
Matcher permissionMatcher = permissionPattern.matcher(line);
if (permissionMatcher.find()) {
permissions = convertPermissions(permissionMatcher.group(1));
Log.d(RootCommands.TAG, "Found permissions: " + permissions);
} else {
Log.d(RootCommands.TAG, "Permissions were not found in ls command!");
}
// try to parse for symlink
Matcher symlinkMatcher = symlinkPattern.matcher(line);
if (symlinkMatcher.find()) {
/*
* TODO: If symlink points to a file in the same directory the path is not
* absolute!!!
*/
symlink = symlinkMatcher.group(1);
Log.d(RootCommands.TAG, "Symlink found: " + symlink);
} else {
Log.d(RootCommands.TAG, "No symlink found!");
}
} catch (Exception e) {
Log.e(RootCommands.TAG, "Error with regex!", e);
}
}
}
@Override
public void afterExecution(int id, int exitCode) {
}
}
/**
* @param file
* String that represent the file, including the full path to the file and its name.
* @param followSymlinks
* @return File permissions as String, for example: 777, returns null on error
* @throws IOException
* @throws TimeoutException
* @throws BrokenBusyboxException
*
*/
public String getFilePermissions(String file) throws BrokenBusyboxException, TimeoutException,
IOException {
Log.d(RootCommands.TAG, "Checking permissions for " + file);
String permissions = null;
if (fileExists(file)) {
Log.d(RootCommands.TAG, file + " was found.");
LsCommand lsCommand = new LsCommand(file);
shell.add(lsCommand).waitForFinish();
permissions = lsCommand.getPermissions();
}
return permissions;
}
/**
* Sets permission of file
*
* @param file
* absolute path to file
* @param permissions
* String like 777
* @return true if command worked
* @throws BrokenBusyboxException
* @throws TimeoutException
* @throws IOException
*/
public boolean setFilePermissions(String file, String permissions)
throws BrokenBusyboxException, TimeoutException, IOException {
Log.d(RootCommands.TAG, "Set permissions of " + file + " to " + permissions);
SimpleCommand chmodCommand = new SimpleCommand("chmod " + permissions + " " + file);
shell.add(chmodCommand).waitForFinish();
if (chmodCommand.getExitCode() == 0) {
return true;
} else {
return false;
}
}
/**
* This will return a String that represent the symlink for a specified file.
*
* @param file
* The path to the file to get the Symlink for. (must have absolute path)
*
* @return A String that represent the symlink for a specified file or null if no symlink
* exists.
* @throws IOException
* @throws TimeoutException
* @throws BrokenBusyboxException
*/
public String getSymlink(String file) throws BrokenBusyboxException, TimeoutException,
IOException {
Log.d(RootCommands.TAG, "Find symlink for " + file);
String symlink = null;
LsCommand lsCommand = new LsCommand(file);
shell.add(lsCommand).waitForFinish();
symlink = lsCommand.getSymlink();
return symlink;
}
/**
* Copys a file to a destination. Because cp is not available on all android devices, we use dd
* or cat.
*
* @param source
* example: /data/data/org.adaway/files/hosts
* @param destination
* example: /system/etc/hosts
* @param remountAsRw
* remounts the destination as read/write before writing to it
* @param preserveFileAttributes
* tries to copy file attributes from source to destination, if only cat is available
* only permissions are preserved
* @return true if it was successfully copied
* @throws BrokenBusyboxException
* @throws IOException
* @throws TimeoutException
*/
public boolean copyFile(String source, String destination, boolean remountAsRw,
boolean preservePermissions) throws BrokenBusyboxException, IOException,
TimeoutException {
/*
* dd can only copy files, but we can not check if the source is a file without invoking
* shell commands, because from Java we probably have no read access, thus we only check if
* they are ending with trailing slashes
*/
if (source.endsWith("/") || destination.endsWith("/")) {
throw new FileNotFoundException("dd can only copy files!");
}
// remount destination as read/write before copying to it
if (remountAsRw) {
if (!remount(destination, "RW")) {
Log.d(RootCommands.TAG,
"Remounting failed! There is probably no need to remount this partition!");
}
}
// get permissions of source before overwriting
String permissions = null;
if (preservePermissions) {
permissions = getFilePermissions(source);
}
boolean commandSuccess = false;
SimpleCommand ddCommand = new SimpleCommand("dd if=" + source + " of="
+ destination);
shell.add(ddCommand).waitForFinish();
if (ddCommand.getExitCode() == 0) {
commandSuccess = true;
} else {
// try cat if dd fails
SimpleCommand catCommand = new SimpleCommand("cat " + source + " > "
+ destination);
shell.add(catCommand).waitForFinish();
if (catCommand.getExitCode() == 0) {
commandSuccess = true;
}
}
// set back permissions from source to destination
if (preservePermissions) {
setFilePermissions(destination, permissions);
}
// remount destination back to read only
if (remountAsRw) {
if (!remount(destination, "RO")) {
Log.d(RootCommands.TAG,
"Remounting failed! There is probably no need to remount this partition!");
}
}
return commandSuccess;
}
public static final int REBOOT_HOTREBOOT = 1;
public static final int REBOOT_REBOOT = 2;
public static final int REBOOT_SHUTDOWN = 3;
public static final int REBOOT_RECOVERY = 4;
/**
* Shutdown or reboot device. Possible actions are REBOOT_HOTREBOOT, REBOOT_REBOOT,
* REBOOT_SHUTDOWN, REBOOT_RECOVERY
*
* @param action
* @throws IOException
* @throws TimeoutException
* @throws BrokenBusyboxException
*/
public void reboot(int action) throws BrokenBusyboxException, TimeoutException, IOException {
if (action == REBOOT_HOTREBOOT) {
killAll("system_server");
// or: killAll("zygote");
} else {
String command;
switch (action) {
case REBOOT_REBOOT:
command = "reboot";
break;
case REBOOT_SHUTDOWN:
command = "reboot -p";
break;
case REBOOT_RECOVERY:
command = "reboot recovery";
break;
default:
command = "reboot";
break;
}
SimpleCommand rebootCommand = new SimpleCommand(command);
shell.add(rebootCommand).waitForFinish();
if (rebootCommand.getExitCode() == -1) {
Log.e(RootCommands.TAG, "Reboot failed!");
}
}
}
/**
* This command checks if a file exists
*/
private class FileExistsCommand extends Command {
private String file;
private boolean fileExists = false;
public FileExistsCommand(String file) {
super("ls " + file);
this.file = file;
}
public boolean isFileExists() {
return fileExists;
}
@Override
public void output(int id, String line) {
if (line.trim().equals(file)) {
fileExists = true;
}
}
@Override
public void afterExecution(int id, int exitCode) {
}
}
/**
* Use this to check whether or not a file exists on the filesystem.
*
* @param file
* String that represent the file, including the full path to the file and its name.
*
* @return a boolean that will indicate whether or not the file exists.
* @throws IOException
* @throws TimeoutException
* @throws BrokenBusyboxException
*
*/
public boolean fileExists(String file) throws BrokenBusyboxException, TimeoutException,
IOException {
FileExistsCommand fileExistsCommand = new FileExistsCommand(file);
shell.add(fileExistsCommand).waitForFinish();
if (fileExistsCommand.isFileExists()) {
return true;
} else {
return false;
}
}
public abstract class WithPermissions {
abstract void whileHavingPermissions();
}
/**
* Execute user defined Java code while having temporary permissions on a file
*
* @param file
* @param withPermissions
* @throws BrokenBusyboxException
* @throws TimeoutException
* @throws IOException
*/
public void withPermission(String file, String permission, WithPermissions withPermissions)
throws BrokenBusyboxException, TimeoutException, IOException {
String oldPermissions = getFilePermissions(file);
// set permissions (If set to 666, then Dalvik VM can also write to that file!)
setFilePermissions(file, permission);
// execute user defined code
withPermissions.whileHavingPermissions();
// set back to old permissions
setFilePermissions(file, oldPermissions);
}
/**
* Execute user defined Java code while having temporary write permissions on a file using chmod
* 666
*
* @param file
* @param withWritePermissions
* @throws BrokenBusyboxException
* @throws TimeoutException
* @throws IOException
*/
public void withWritePermissions(String file, WithPermissions withWritePermissions)
throws BrokenBusyboxException, TimeoutException, IOException {
withPermission(file, "666", withWritePermissions);
}
/**
* Sets system clock using /dev/alarm
*
* @param millis
* @throws BrokenBusyboxException
* @throws TimeoutException
* @throws IOException
*/
public void setSystemClock(final long millis) throws BrokenBusyboxException, TimeoutException,
IOException {
withWritePermissions("/dev/alarm", new WithPermissions() {
@Override
void whileHavingPermissions() {
SystemClock.setCurrentTimeMillis(millis);
}
});
}
/**
* Adjust system clock by offset using /dev/alarm
*
* @param offset
* @throws BrokenBusyboxException
* @throws TimeoutException
* @throws IOException
*/
public void adjustSystemClock(final long offset) throws BrokenBusyboxException,
TimeoutException, IOException {
withWritePermissions("/dev/alarm", new WithPermissions() {
@Override
void whileHavingPermissions() {
SystemClock.setCurrentTimeMillis(System.currentTimeMillis() + offset);
}
});
}
/**
* This will take a path, which can contain the file name as well, and attempt to remount the
* underlying partition.
*
* For example, passing in the following string:
* "/system/bin/some/directory/that/really/would/never/exist" will result in /system ultimately
* being remounted. However, keep in mind that the longer the path you supply, the more work
* this has to do, and the slower it will run.
*
* @param file
* file path
* @param mountType
* mount type: pass in RO (Read only) or RW (Read Write)
* @return a <code>boolean</code> which indicates whether or not the partition has been
* remounted as specified.
*/
public boolean remount(String file, String mountType) {
// Recieved a request, get an instance of Remounter
Remounter remounter = new Remounter(shell);
// send the request
return (remounter.remount(file, mountType));
}
/**
* This will tell you how the specified mount is mounted. rw, ro, etc...
*
* @param The
* mount you want to check
*
* @return <code>String</code> What the mount is mounted as.
* @throws Exception
* if we cannot determine how the mount is mounted.
*/
public String getMountedAs(String path) throws Exception {
ArrayList<Mount> mounts = Remounter.getMounts();
if (mounts != null) {
for (Mount mount : mounts) {
if (path.contains(mount.getMountPoint().getAbsolutePath())) {
Log.d(RootCommands.TAG, (String) mount.getFlags().toArray()[0]);
return (String) mount.getFlags().toArray()[0];
}
}
throw new Exception();
} else {
throw new Exception();
}
}
/**
* Check if there is enough space on partition where target is located
*
* @param size
* size of file to put on partition
* @param target
* path where to put the file
*
* @return true if it will fit on partition of target, false if it will not fit.
*/
public boolean hasEnoughSpaceOnPartition(String target, long size) {
try {
// new File(target).getFreeSpace() (API 9) is not working on data partition
// get directory without file
String directory = new File(target).getParent().toString();
StatFs stat = new StatFs(directory);
long blockSize = stat.getBlockSize();
long availableBlocks = stat.getAvailableBlocks();
long availableSpace = availableBlocks * blockSize;
Log.i(RootCommands.TAG, "Checking for enough space: Target: " + target
+ ", directory: " + directory + " size: " + size + ", availableSpace: "
+ availableSpace);
if (size < availableSpace) {
return true;
} else {
Log.e(RootCommands.TAG, "Not enough space on partition!");
return false;
}
} catch (Exception e) {
// if new StatFs(directory) fails catch IllegalArgumentException and just return true as
// workaround
Log.e(RootCommands.TAG, "Problem while getting available space on partition!", e);
return true;
}
}
/**
* TODO: Not tested!
*
* @param toggle
* @throws IOException
* @throws TimeoutException
* @throws BrokenBusyboxException
*/
public void toggleAdbDaemon(boolean toggle) throws BrokenBusyboxException, TimeoutException,
IOException {
SimpleCommand disableAdb = new SimpleCommand("setprop persist.service.adb.enable 0",
"stop adbd");
SimpleCommand enableAdb = new SimpleCommand("setprop persist.service.adb.enable 1",
"stop adbd", "sleep 1", "start adbd");
if (toggle) {
shell.add(enableAdb).waitForFinish();
} else {
shell.add(disableAdb).waitForFinish();
}
}
}