diff --git a/RootCommands/.gitignore b/RootCommands/.gitignore new file mode 100644 index 00000000..afa9dfc4 --- /dev/null +++ b/RootCommands/.gitignore @@ -0,0 +1,33 @@ +#Android specific +bin +gen +obj +libs/armeabi +lint.xml +local.properties +release.properties +ant.properties +*.class +*.apk + +#Gradle +.gradle +build +gradle.properties +gradlew +gradlew.bat +gradle + +#Maven +target +pom.xml.* + +#Eclipse +.project +.classpath +.settings +.metadata + +#IntelliJ IDEA +.idea +*.iml diff --git a/RootCommands/build.gradle b/RootCommands/build.gradle new file mode 100644 index 00000000..00907bf0 --- /dev/null +++ b/RootCommands/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + + sourceSets { + main { + jni.srcDirs = [] + } + } + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + + diff --git a/RootCommands/src/main/AndroidManifest.xml b/RootCommands/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ee9b1995 --- /dev/null +++ b/RootCommands/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Mount.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Mount.java new file mode 100644 index 00000000..6f5ef787 --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Mount.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * 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.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class Mount { + protected final File mDevice; + protected final File mMountPoint; + protected final String mType; + protected final Set mFlags; + + Mount(File device, File path, String type, String flagsStr) { + mDevice = device; + mMountPoint = path; + mType = type; + mFlags = new HashSet(Arrays.asList(flagsStr.split(","))); + } + + public File getDevice() { + return mDevice; + } + + public File getMountPoint() { + return mMountPoint; + } + + public String getType() { + return mType; + } + + public Set getFlags() { + return mFlags; + } + + @Override + public String toString() { + return String.format("%s on %s type %s %s", mDevice, mMountPoint, mType, mFlags); + } +} diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Remounter.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Remounter.java new file mode 100644 index 00000000..00d4e2cd --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Remounter.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * 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.FileReader; +import java.io.IOException; +import java.io.LineNumberReader; +import java.util.ArrayList; +import java.util.Locale; + +import org.sufficientlysecure.rootcommands.command.SimpleCommand; +import org.sufficientlysecure.rootcommands.util.Log; + +//no modifier, this means it is package-private. Only our internal classes can use this. +class Remounter { + + private Shell shell; + + public Remounter(Shell shell) { + super(); + this.shell = shell; + } + + /** + * 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 boolean which indicates whether or not the partition has been + * remounted as specified. + */ + protected boolean remount(String file, String mountType) { + + // if the path has a trailing slash get rid of it. + if (file.endsWith("/") && !file.equals("/")) { + file = file.substring(0, file.lastIndexOf("/")); + } + // Make sure that what we are trying to remount is in the mount list. + boolean foundMount = false; + while (!foundMount) { + try { + for (Mount mount : getMounts()) { + Log.d(RootCommands.TAG, mount.getMountPoint().toString()); + + if (file.equals(mount.getMountPoint().toString())) { + foundMount = true; + break; + } + } + } catch (Exception e) { + Log.e(RootCommands.TAG, "Exception", e); + return false; + } + if (!foundMount) { + try { + file = (new File(file).getParent()).toString(); + } catch (Exception e) { + Log.e(RootCommands.TAG, "Exception", e); + return false; + } + } + } + Mount mountPoint = findMountPointRecursive(file); + + Log.d(RootCommands.TAG, "Remounting " + mountPoint.getMountPoint().getAbsolutePath() + + " as " + mountType.toLowerCase(Locale.US)); + final boolean isMountMode = mountPoint.getFlags().contains(mountType.toLowerCase(Locale.US)); + + if (!isMountMode) { + // grab an instance of the internal class + try { + SimpleCommand command = new SimpleCommand("busybox mount -o remount," + + mountType.toLowerCase(Locale.US) + " " + mountPoint.getDevice().getAbsolutePath() + + " " + mountPoint.getMountPoint().getAbsolutePath(), + "toolbox mount -o remount," + mountType.toLowerCase(Locale.US) + " " + + mountPoint.getDevice().getAbsolutePath() + " " + + mountPoint.getMountPoint().getAbsolutePath(), "mount -o remount," + + mountType.toLowerCase(Locale.US) + " " + + mountPoint.getDevice().getAbsolutePath() + " " + + mountPoint.getMountPoint().getAbsolutePath(), + "/system/bin/toolbox mount -o remount," + mountType.toLowerCase(Locale.US) + " " + + mountPoint.getDevice().getAbsolutePath() + " " + + mountPoint.getMountPoint().getAbsolutePath()); + + // execute on shell + shell.add(command).waitForFinish(); + + } catch (Exception e) { + } + + mountPoint = findMountPointRecursive(file); + } + + if (mountPoint != null) { + Log.d(RootCommands.TAG, mountPoint.getFlags() + " AND " + mountType.toLowerCase(Locale.US)); + if (mountPoint.getFlags().contains(mountType.toLowerCase(Locale.US))) { + Log.d(RootCommands.TAG, mountPoint.getFlags().toString()); + return true; + } else { + Log.d(RootCommands.TAG, mountPoint.getFlags().toString()); + } + } else { + Log.d(RootCommands.TAG, "mountPoint is null"); + } + return false; + } + + private Mount findMountPointRecursive(String file) { + try { + ArrayList mounts = getMounts(); + for (File path = new File(file); path != null;) { + for (Mount mount : mounts) { + if (mount.getMountPoint().equals(path)) { + return mount; + } + } + } + return null; + } catch (IOException e) { + throw new RuntimeException(e); + } catch (Exception e) { + Log.e(RootCommands.TAG, "Exception", e); + } + return null; + } + + /** + * This will return an ArrayList of the class Mount. The class mount contains the following + * property's: device mountPoint type flags + *

+ * These will provide you with any information you need to work with the mount points. + * + * @return ArrayList an ArrayList of the class Mount. + * @throws Exception + * if we cannot return the mount points. + */ + protected static ArrayList getMounts() throws Exception { + + final String tempFile = "/data/local/RootToolsMounts"; + + // copy /proc/mounts to tempfile. Directly reading it does not work on 4.3 + Shell shell = Shell.startRootShell(); + Toolbox tb = new Toolbox(shell); + tb.copyFile("/proc/mounts", tempFile, false, false); + tb.setFilePermissions(tempFile, "777"); + shell.close(); + + LineNumberReader lnr = null; + lnr = new LineNumberReader(new FileReader(tempFile)); + String line; + ArrayList mounts = new ArrayList(); + while ((line = lnr.readLine()) != null) { + + Log.d(RootCommands.TAG, line); + + String[] fields = line.split(" "); + mounts.add(new Mount(new File(fields[0]), // device + new File(fields[1]), // mountPoint + fields[2], // fstype + fields[3] // flags + )); + } + lnr.close(); + + return mounts; + } +} diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/RootCommands.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/RootCommands.java new file mode 100644 index 00000000..25576281 --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/RootCommands.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * 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 org.sufficientlysecure.rootcommands.util.Log; + +public class RootCommands { + public static boolean DEBUG = false; + public static int DEFAULT_TIMEOUT = 10000; + + public static final String TAG = "RootCommands"; + + /** + * General method to check if user has su binary and accepts root access for this program! + * + * @return true if everything worked + */ + public static boolean rootAccessGiven() { + boolean rootAccess = false; + + try { + Shell rootShell = Shell.startRootShell(); + + Toolbox tb = new Toolbox(rootShell); + if (tb.isRootAccessGiven()) { + rootAccess = true; + } + + rootShell.close(); + } catch (Exception e) { + Log.e(TAG, "Problem while checking for root access!", e); + } + + return rootAccess; + } +} diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Shell.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Shell.java new file mode 100644 index 00000000..8091dee6 --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Shell.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks, Jeremy Lakeman (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.BufferedReader; +import java.io.Closeable; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import org.sufficientlysecure.rootcommands.command.Command; +import org.sufficientlysecure.rootcommands.util.Log; +import org.sufficientlysecure.rootcommands.util.RootAccessDeniedException; +import org.sufficientlysecure.rootcommands.util.Utils; + +public class Shell implements Closeable { + private final Process shellProcess; + private final BufferedReader stdOutErr; + private final DataOutputStream outputStream; + private final List commands = new ArrayList(); + private boolean close = false; + + private static final String LD_LIBRARY_PATH = System.getenv("LD_LIBRARY_PATH"); + private static final String token = "F*D^W@#FGF"; + + /** + * Start root shell + * + * @param customEnv + * @param baseDirectory + * @return + * @throws IOException + */ + public static Shell startRootShell(ArrayList customEnv, String baseDirectory) + throws IOException, RootAccessDeniedException { + Log.d(RootCommands.TAG, "Starting Root Shell!"); + + // On some versions of Android (ICS) LD_LIBRARY_PATH is unset when using su + // We need to pass LD_LIBRARY_PATH over su for some commands to work correctly. + if (customEnv == null) { + customEnv = new ArrayList(); + } + customEnv.add("LD_LIBRARY_PATH=" + LD_LIBRARY_PATH); + + Shell shell = new Shell(Utils.getSuPath(), customEnv, baseDirectory); + + return shell; + } + + /** + * Start root shell without custom environment and base directory + * + * @return + * @throws IOException + */ + public static Shell startRootShell() throws IOException, RootAccessDeniedException { + return startRootShell(null, null); + } + + /** + * Start default sh shell + * + * @param customEnv + * @param baseDirectory + * @return + * @throws IOException + */ + public static Shell startShell(ArrayList customEnv, String baseDirectory) + throws IOException { + Log.d(RootCommands.TAG, "Starting Shell!"); + Shell shell = new Shell("sh", customEnv, baseDirectory); + return shell; + } + + /** + * Start default sh shell without custom environment and base directory + * + * @return + * @throws IOException + */ + public static Shell startShell() throws IOException { + return startShell(null, null); + } + + /** + * Start custom shell defined by shellPath + * + * @param shellPath + * @param customEnv + * @param baseDirectory + * @return + * @throws IOException + */ + public static Shell startCustomShell(String shellPath, ArrayList customEnv, + String baseDirectory) throws IOException { + Log.d(RootCommands.TAG, "Starting Custom Shell!"); + Shell shell = new Shell(shellPath, customEnv, baseDirectory); + + return shell; + } + + /** + * Start custom shell without custom environment and base directory + * + * @param shellPath + * @return + * @throws IOException + */ + public static Shell startCustomShell(String shellPath) throws IOException { + return startCustomShell(shellPath, null, null); + } + + private Shell(String shell, ArrayList customEnv, String baseDirectory) + throws IOException, RootAccessDeniedException { + Log.d(RootCommands.TAG, "Starting shell: " + shell); + + // start shell process! + shellProcess = Utils.runWithEnv(shell, customEnv, baseDirectory); + + // StdErr is redirected to StdOut, defined in Command.getCommand() + stdOutErr = new BufferedReader(new InputStreamReader(shellProcess.getInputStream())); + outputStream = new DataOutputStream(shellProcess.getOutputStream()); + + outputStream.write("echo Started\n".getBytes()); + outputStream.flush(); + + while (true) { + String line = stdOutErr.readLine(); + if (line == null) + throw new RootAccessDeniedException( + "stdout line is null! Access was denied or this executeable is not a shell!"); + if ("".equals(line)) + continue; + if ("Started".equals(line)) + break; + + destroyShellProcess(); + throw new IOException("Unable to start shell, unexpected output \"" + line + "\""); + } + + new Thread(inputRunnable, "Shell Input").start(); + new Thread(outputRunnable, "Shell Output").start(); + } + + private Runnable inputRunnable = new Runnable() { + public void run() { + try { + writeCommands(); + } catch (IOException e) { + Log.e(RootCommands.TAG, "IO Exception", e); + } + } + }; + + private Runnable outputRunnable = new Runnable() { + public void run() { + try { + readOutput(); + } catch (IOException e) { + Log.e(RootCommands.TAG, "IOException", e); + } catch (InterruptedException e) { + Log.e(RootCommands.TAG, "InterruptedException", e); + } + } + }; + + /** + * Destroy shell process considering that the process could already be terminated + */ + private void destroyShellProcess() { + try { + // Yes, this really is the way to check if the process is + // still running. + shellProcess.exitValue(); + } catch (IllegalThreadStateException e) { + // Only call destroy() if the process is still running; + // Calling it for a terminated process will not crash, but + // (starting with at least ICS/4.0) spam the log with INFO + // messages ala "Failed to destroy process" and "kill + // failed: ESRCH (No such process)". + shellProcess.destroy(); + } + + Log.d(RootCommands.TAG, "Shell destroyed"); + } + + /** + * Writes queued commands one after another into the opened shell. After an execution a token is + * written to seperate command output on read + * + * @throws IOException + */ + private void writeCommands() throws IOException { + try { + int commandIndex = 0; + while (true) { + DataOutputStream out; + synchronized (commands) { + while (!close && commandIndex >= commands.size()) { + commands.wait(); + } + out = this.outputStream; + } + if (commandIndex < commands.size()) { + Command next = commands.get(commandIndex); + next.writeCommand(out); + String line = "\necho " + token + " " + commandIndex + " $?\n"; + out.write(line.getBytes()); + out.flush(); + commandIndex++; + } else if (close) { + out.write("\nexit 0\n".getBytes()); + out.flush(); + Log.d(RootCommands.TAG, "Closing shell"); + shellProcess.waitFor(); + out.close(); + return; + } else { + Thread.sleep(50); + } + } + } catch (InterruptedException e) { + Log.e(RootCommands.TAG, "interrupted while writing command", e); + } + } + + /** + * Reads output line by line, seperated by token written after every command + * + * @throws IOException + * @throws InterruptedException + */ + private void readOutput() throws IOException, InterruptedException { + Command command = null; + + // index of current command + int commandIndex = 0; + while (true) { + String lineStdOut = stdOutErr.readLine(); + + // terminate on EOF + if (lineStdOut == null) + break; + + if (command == null) { + + // break on close after last command + if (commandIndex >= commands.size()) { + if (close) + break; + continue; + } + + // get current command + command = commands.get(commandIndex); + } + + int pos = lineStdOut.indexOf(token); + if (pos > 0) { + command.processOutput(lineStdOut.substring(0, pos)); + } + if (pos >= 0) { + lineStdOut = lineStdOut.substring(pos); + String fields[] = lineStdOut.split(" "); + int id = Integer.parseInt(fields[1]); + if (id == commandIndex) { + command.setExitCode(Integer.parseInt(fields[2])); + + // go to next command + commandIndex++; + command = null; + continue; + } + } + command.processOutput(lineStdOut); + } + Log.d(RootCommands.TAG, "Read all output"); + shellProcess.waitFor(); + stdOutErr.close(); + destroyShellProcess(); + + while (commandIndex < commands.size()) { + if (command == null) { + command = commands.get(commandIndex); + } + command.terminated("Unexpected Termination!"); + commandIndex++; + command = null; + } + } + + /** + * Add command to shell queue + * + * @param command + * @return + * @throws IOException + */ + public Command add(Command command) throws IOException { + if (close) + throw new IOException("Unable to add commands to a closed shell"); + synchronized (commands) { + commands.add(command); + // set shell on the command object, to know where the command is running on + command.addedToShell(this, (commands.size() - 1)); + commands.notifyAll(); + } + + return command; + } + + /** + * Close shell + * + * @throws IOException + */ + public void close() throws IOException { + synchronized (commands) { + this.close = true; + commands.notifyAll(); + } + } + + /** + * Returns number of queued commands + * + * @return + */ + public int getCommandsSize() { + return commands.size(); + } + +} \ No newline at end of file diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/SystemCommands.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/SystemCommands.java new file mode 100644 index 00000000..355e6952 --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/SystemCommands.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * 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 android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.location.LocationManager; +import android.os.PowerManager; +import android.provider.Settings; + +/** + * This methods work when the apk is installed as a system app (under /system/app) + */ +public class SystemCommands { + Context context; + + public SystemCommands(Context context) { + super(); + this.context = context; + } + + /** + * Get GPS status + * + * @return + */ + public boolean getGPS() { + return ((LocationManager) context.getSystemService(Context.LOCATION_SERVICE)) + .isProviderEnabled(LocationManager.GPS_PROVIDER); + } + + /** + * Enable/Disable GPS + * + * @param value + */ + @TargetApi(8) + public void setGPS(boolean value) { + ContentResolver localContentResolver = context.getContentResolver(); + Settings.Secure.setLocationProviderEnabled(localContentResolver, + LocationManager.GPS_PROVIDER, value); + } + + /** + * TODO: Not ready yet + */ + @TargetApi(8) + public void reboot() { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + pm.reboot("recovery"); + pm.reboot(null); + + // not working: + // reboot(null); + } + + /** + * Reboot the device immediately, passing 'reason' (may be null) to the underlying __reboot + * system call. Should not return. + * + * Taken from com.android.server.PowerManagerService.reboot + */ + // public void reboot(String reason) { + // + // // final String finalReason = reason; + // Runnable runnable = new Runnable() { + // public void run() { + // synchronized (this) { + // // ShutdownThread.reboot(mContext, finalReason, false); + // try { + // Class clazz = Class.forName("com.android.internal.app.ShutdownThread"); + // + // // if (mReboot) { + // Method method = clazz.getMethod("reboot", Context.class, String.class, + // Boolean.TYPE); + // method.invoke(null, context, null, false); + // + // // if (mReboot) { + // // Method method = clazz.getMethod("reboot", Context.class, String.class, + // // Boolean.TYPE); + // // method.invoke(null, mContext, mReason, mConfirm); + // // } else { + // // Method method = clazz.getMethod("shutdown", Context.class, Boolean.TYPE); + // // method.invoke(null, mContext, mConfirm); + // // } + // } catch (Exception e) { + // e.printStackTrace(); + // } + // } + // + // } + // }; + // // ShutdownThread must run on a looper capable of displaying the UI. + // mHandler.post(runnable); + // + // // PowerManager.reboot() is documented not to return so just wait for the inevitable. + // // synchronized (runnable) { + // // while (true) { + // // try { + // // runnable.wait(); + // // } catch (InterruptedException e) { + // // } + // // } + // // } + // } + +} diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Toolbox.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Toolbox.java new file mode 100644 index 00000000..fe8d2b4c --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Toolbox.java @@ -0,0 +1,824 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * 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 pids; + private String psRegex; + private Pattern psPattern; + + public PsCommand(String processName) { + super("ps"); + this.processName = processName; + pids = new ArrayList(); + + /** + * regex to get pid out of ps line, example: + * + *

+             *  root    24736    1   12140  584   ffffffff 40010d14 S /data/data/org.adaway/files/blank_webserver
+             * ^\\S \\s ([0-9]+)                          .*                                      processName    $
+             * 
+ */ + psRegex = "^\\S+\\s+([0-9]+).*" + Pattern.quote(processName) + "$"; + psPattern = Pattern.compile(psRegex); + } + + public ArrayList 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 true 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 true 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: + * + *
+             * 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})                     .*                                                  $
+             * 
+ */ + permissionRegex = "^.*?(\\S{10}).*$"; + permissionPattern = Pattern.compile(permissionRegex); + + /** + * regex to get symlink + * + *
+             *     ->           /proc/self/fd/0
+             * ^.*?\\-\\> \\s+  (.*)           $
+             * 
+ */ + 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 boolean 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 String 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 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(); + } + } + +} diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/Command.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/Command.java new file mode 100644 index 00000000..33e0f196 --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/Command.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks, Jeremy Lakeman (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.command; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.TimeoutException; + +import org.sufficientlysecure.rootcommands.RootCommands; +import org.sufficientlysecure.rootcommands.Shell; +import org.sufficientlysecure.rootcommands.util.BrokenBusyboxException; +import org.sufficientlysecure.rootcommands.util.Log; + +public abstract class Command { + final String command[]; + boolean finished = false; + boolean brokenBusyboxDetected = false; + int exitCode; + int id; + int timeout = RootCommands.DEFAULT_TIMEOUT; + Shell shell = null; + + public Command(String... command) { + this.command = command; + } + + public Command(int timeout, String... command) { + this.command = command; + this.timeout = timeout; + } + + /** + * This is called from Shell after adding it + * + * @param shell + * @param id + */ + public void addedToShell(Shell shell, int id) { + this.shell = shell; + this.id = id; + } + + /** + * Gets command string executed on the shell + * + * @return + */ + public String getCommand() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < command.length; i++) { + // redirect stderr to stdout + sb.append(command[i] + " 2>&1"); + sb.append('\n'); + } + Log.d(RootCommands.TAG, "Sending command(s): " + sb.toString()); + return sb.toString(); + } + + public void writeCommand(OutputStream out) throws IOException { + out.write(getCommand().getBytes()); + } + + public void processOutput(String line) { + Log.d(RootCommands.TAG, "ID: " + id + ", Output: " + line); + + /* + * Try to detect broken toolbox/busybox binaries (see + * https://code.google.com/p/busybox-android/issues/detail?id=1) + * + * It is giving "Value too large for defined data type" on certain file operations (e.g. ls + * and chown) in certain directories (e.g. /data/data) + */ + if (line.contains("Value too large for defined data type")) { + Log.e(RootCommands.TAG, "Busybox is broken with high probability due to line: " + line); + brokenBusyboxDetected = true; + } + + // now execute specific output parsing + output(id, line); + } + + public abstract void output(int id, String line); + + public void processAfterExecution(int exitCode) { + Log.d(RootCommands.TAG, "ID: " + id + ", ExitCode: " + exitCode); + + afterExecution(id, exitCode); + } + + public abstract void afterExecution(int id, int exitCode); + + public void commandFinished(int id) { + Log.d(RootCommands.TAG, "Command " + id + " finished."); + } + + public void setExitCode(int code) { + synchronized (this) { + exitCode = code; + finished = true; + commandFinished(id); + this.notifyAll(); + } + } + + /** + * Close the shell + * + * @param reason + */ + public void terminate(String reason) { + try { + shell.close(); + Log.d(RootCommands.TAG, "Terminating the shell."); + terminated(reason); + } catch (IOException e) { + } + } + + public void terminated(String reason) { + setExitCode(-1); + Log.d(RootCommands.TAG, "Command " + id + " did not finish, because of " + reason); + } + + /** + * Waits for this command to finish and forwards exitCode into afterExecution method + * + * @param timeout + * @throws TimeoutException + * @throws BrokenBusyboxException + */ + public void waitForFinish() throws TimeoutException, BrokenBusyboxException { + synchronized (this) { + while (!finished) { + try { + this.wait(timeout); + } catch (InterruptedException e) { + Log.e(RootCommands.TAG, "InterruptedException in waitForFinish()", e); + } + + if (!finished) { + finished = true; + terminate("Timeout"); + throw new TimeoutException("Timeout has occurred."); + } + } + + if (brokenBusyboxDetected) { + throw new BrokenBusyboxException(); + } + + processAfterExecution(exitCode); + } + } + +} \ No newline at end of file diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/ExecutableCommand.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/ExecutableCommand.java new file mode 100644 index 00000000..d6c8e610 --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/ExecutableCommand.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * 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.command; + +import java.io.File; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; + +public abstract class ExecutableCommand extends Command { + public static final String EXECUTABLE_PREFIX = "lib"; + public static final String EXECUTABLE_SUFFIX = "_exec.so"; + + /** + * This class provides a way to use your own binaries! + * + * Include your own executables, renamed from * to lib*_exec.so, in your libs folder under the + * architecture directories. Now they will be deployed by Android the same way libraries are + * deployed! + * + * See README for more information how to use your own executables! + * + * @param context + * @param executableName + * @param parameters + */ + public ExecutableCommand(Context context, String executableName, String parameters) { + super(getLibDirectory(context) + File.separator + EXECUTABLE_PREFIX + executableName + + EXECUTABLE_SUFFIX + " " + parameters); + } + + /** + * Get full path to lib directory of app + * + * @return dir as String + */ + @SuppressLint("NewApi") + private static String getLibDirectory(Context context) { + if (Build.VERSION.SDK_INT >= 9) { + return context.getApplicationInfo().nativeLibraryDir; + } else { + return context.getApplicationInfo().dataDir + File.separator + "lib"; + } + } + + public abstract void output(int id, String line); + + public abstract void afterExecution(int id, int exitCode); + +} \ No newline at end of file diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleCommand.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleCommand.java new file mode 100644 index 00000000..9049040f --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleCommand.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * 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.command; + +public class SimpleCommand extends Command { + private StringBuilder sb = new StringBuilder(); + + public SimpleCommand(String... command) { + super(command); + } + + @Override + public void output(int id, String line) { + sb.append(line).append('\n'); + } + + @Override + public void afterExecution(int id, int exitCode) { + } + + public String getOutput() { + return sb.toString(); + } + + public int getExitCode() { + return exitCode; + } + +} \ No newline at end of file diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleExecutableCommand.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleExecutableCommand.java new file mode 100644 index 00000000..95d2faef --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleExecutableCommand.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * 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.command; + +import android.content.Context; + +public class SimpleExecutableCommand extends ExecutableCommand { + private StringBuilder sb = new StringBuilder(); + + public SimpleExecutableCommand(Context context, String executableName, String parameters) { + super(context, executableName, parameters); + } + + @Override + public void output(int id, String line) { + sb.append(line).append('\n'); + } + + @Override + public void afterExecution(int id, int exitCode) { + } + + public String getOutput() { + return sb.toString(); + } + + public int getExitCode() { + return exitCode; + } + +} \ No newline at end of file diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/BrokenBusyboxException.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/BrokenBusyboxException.java new file mode 100644 index 00000000..e982b242 --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/BrokenBusyboxException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * 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.util; + +import java.io.IOException; + +public class BrokenBusyboxException extends IOException { + private static final long serialVersionUID = 8337358201589488409L; + + public BrokenBusyboxException() { + super(); + } + + public BrokenBusyboxException(String detailMessage) { + super(detailMessage); + } + +} diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Log.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Log.java new file mode 100644 index 00000000..a25fbf4c --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Log.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * 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.util; + +import org.sufficientlysecure.rootcommands.RootCommands; + +/** + * Wraps Android Logging to enable or disable debug output using Constants + * + */ +public final class Log { + + public static void v(String tag, String msg) { + if (RootCommands.DEBUG) { + android.util.Log.v(tag, msg); + } + } + + public static void v(String tag, String msg, Throwable tr) { + if (RootCommands.DEBUG) { + android.util.Log.v(tag, msg, tr); + } + } + + public static void d(String tag, String msg) { + if (RootCommands.DEBUG) { + android.util.Log.d(tag, msg); + } + } + + public static void d(String tag, String msg, Throwable tr) { + if (RootCommands.DEBUG) { + android.util.Log.d(tag, msg, tr); + } + } + + public static void i(String tag, String msg) { + if (RootCommands.DEBUG) { + android.util.Log.i(tag, msg); + } + } + + public static void i(String tag, String msg, Throwable tr) { + if (RootCommands.DEBUG) { + android.util.Log.i(tag, msg, tr); + } + } + + public static void w(String tag, String msg) { + android.util.Log.w(tag, msg); + } + + public static void w(String tag, String msg, Throwable tr) { + android.util.Log.w(tag, msg, tr); + } + + public static void w(String tag, Throwable tr) { + android.util.Log.w(tag, tr); + } + + public static void e(String tag, String msg) { + android.util.Log.e(tag, msg); + } + + public static void e(String tag, String msg, Throwable tr) { + android.util.Log.e(tag, msg, tr); + } + +} diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/RootAccessDeniedException.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/RootAccessDeniedException.java new file mode 100644 index 00000000..35f353d1 --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/RootAccessDeniedException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * 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.util; + +import java.io.IOException; + +public class RootAccessDeniedException extends IOException { + private static final long serialVersionUID = 9088998884166225540L; + + public RootAccessDeniedException() { + super(); + } + + public RootAccessDeniedException(String detailMessage) { + super(detailMessage); + } + +} diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/UnsupportedArchitectureException.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/UnsupportedArchitectureException.java new file mode 100644 index 00000000..96ad0309 --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/UnsupportedArchitectureException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * 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.util; + +public class UnsupportedArchitectureException extends Exception { + private static final long serialVersionUID = 7826528799780001655L; + + public UnsupportedArchitectureException() { + super(); + } + + public UnsupportedArchitectureException(String detailMessage) { + super(detailMessage); + } + +} diff --git a/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Utils.java b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Utils.java new file mode 100644 index 00000000..87f32bbc --- /dev/null +++ b/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Utils.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (c) 2012 Michael Elsdörfer (Android Autostarts) + * 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.util; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; + +import org.sufficientlysecure.rootcommands.RootCommands; + +public class Utils { + /* + * The emulator and ADP1 device both have a su binary in /system/xbin/su, but it doesn't allow + * apps to use it (su app_29 $ su su: uid 10029 not allowed to su). + * + * Cyanogen used to have su in /system/bin/su, in newer versions it's a symlink to + * /system/xbin/su. + * + * The Archos tablet has it in /data/bin/su, since they don't have write access to /system yet. + */ + static final String[] BinaryPlaces = { "/data/bin/", "/system/bin/", "/system/xbin/", "/sbin/", + "/data/local/xbin/", "/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", + "/data/local/" }; + + /** + * Determine the path of the su executable. + * + * Code from https://github.com/miracle2k/android-autostarts, use under Apache License was + * agreed by Michael Elsdörfer + */ + public static String getSuPath() { + for (String p : BinaryPlaces) { + File su = new File(p + "su"); + if (su.exists()) { + Log.d(RootCommands.TAG, "su found at: " + p); + return su.getAbsolutePath(); + } else { + Log.v(RootCommands.TAG, "No su in: " + p); + } + } + Log.d(RootCommands.TAG, "No su found in a well-known location, " + "will just use \"su\"."); + return "su"; + } + + /** + * This code is adapted from java.lang.ProcessBuilder.start(). + * + * The problem is that Android doesn't allow us to modify the map returned by + * ProcessBuilder.environment(), even though the docstring indicates that it should. This is + * because it simply returns the SystemEnvironment object that System.getenv() gives us. The + * relevant portion in the source code is marked as "// android changed", so presumably it's not + * the case in the original version of the Apache Harmony project. + * + * Note that simply passing the environment variables we want to Process.exec won't be good + * enough, since that would override the environment we inherited completely. + * + * We needed to be able to set a CLASSPATH environment variable for our new process in order to + * use the "app_process" command directly. Note: "app_process" takes arguments passed on to the + * Dalvik VM as well; this might be an alternative way to set the class path. + * + * Code from https://github.com/miracle2k/android-autostarts, use under Apache License was + * agreed by Michael Elsdörfer + */ + public static Process runWithEnv(String command, ArrayList customAddedEnv, + String baseDirectory) throws IOException { + + Map environment = System.getenv(); + String[] envArray = new String[environment.size() + + (customAddedEnv != null ? customAddedEnv.size() : 0)]; + int i = 0; + for (Map.Entry entry : environment.entrySet()) { + envArray[i++] = entry.getKey() + "=" + entry.getValue(); + } + if (customAddedEnv != null) { + for (String entry : customAddedEnv) { + envArray[i++] = entry; + } + } + + Process process; + if (baseDirectory == null) { + process = Runtime.getRuntime().exec(command, envArray, null); + } else { + process = Runtime.getRuntime().exec(command, envArray, new File(baseDirectory)); + } + return process; + } +} diff --git a/orbotservice/build.gradle b/orbotservice/build.gradle index cfc0a533..62ceffd7 100644 --- a/orbotservice/build.gradle +++ b/orbotservice/build.gradle @@ -27,6 +27,7 @@ android { dependencies { compile project(':jsocksAndroid') + compile project(':RootCommands') compile 'com.android.support:appcompat-v7:23.4.0' compile fileTree(dir: 'libs', include: ['*.jar','*.so']) testCompile 'junit:junit:4.12' diff --git a/orbotservice/src/main/java/org/torproject/android/service/TorService.java b/orbotservice/src/main/java/org/torproject/android/service/TorService.java index 87d814d5..0dab4804 100644 --- a/orbotservice/src/main/java/org/torproject/android/service/TorService.java +++ b/orbotservice/src/main/java/org/torproject/android/service/TorService.java @@ -35,6 +35,8 @@ import android.text.TextUtils; import android.util.Log; import android.widget.RemoteViews; +import org.sufficientlysecure.rootcommands.Shell; +import org.sufficientlysecure.rootcommands.command.SimpleCommand; import org.torproject.android.control.ConfigEntry; import org.torproject.android.control.TorControlConnection; import org.torproject.android.service.transproxy.TorTransProxy; @@ -60,6 +62,7 @@ import java.io.PrintStream; import java.io.PrintWriter; import java.net.Socket; import java.text.Normalizer; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -81,7 +84,6 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon private TorControlConnection conn = null; private Socket torConnSocket = null; private int mLastProcessId = -1; - private Process mProcPolipo; private int mPortHTTP = HTTP_PROXY_PORT_DEFAULT; private int mPortSOCKS = SOCKS_PROXY_PORT_DEFAULT; @@ -123,6 +125,8 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon public static File fileXtables; public static File fileTorRc; + private Shell mShell; + private Shell mShellPolipo; public void debug(String msg) { @@ -374,6 +378,16 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon public void onDestroy() { stopTor(); unregisterReceiver(mNetworkStateReceiver); + + try + { + mShell.close(); + } + catch (IOException ioe) + { + Log.d(TAG, "Error closing shell",ioe); + } + super.onDestroy(); } @@ -500,11 +514,10 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon conn = null; } - if (mProcPolipo != null) + if (mShellPolipo != null) { - mProcPolipo.destroy(); - int exitValue = mProcPolipo.waitFor(); - logNotice("Polipo exited with value: " + exitValue); + mShellPolipo.close(); + //logNotice("Polipo exited with value: " + exitValue); } @@ -545,6 +558,8 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon appBinHome = getDir(TorServiceConstants.DIRECTORY_TOR_BINARY, Application.MODE_PRIVATE); appCacheHome = getDir(TorServiceConstants.DIRECTORY_TOR_DATA,Application.MODE_PRIVATE); + mShell = Shell.startShell(); + fileTor= new File(appBinHome, TorServiceConstants.TOR_ASSET_KEY); filePolipo = new File(appBinHome, TorServiceConstants.POLIPO_ASSET_KEY); fileObfsclient = new File(appBinHome, TorServiceConstants.OBFSCLIENT_ASSET_KEY); @@ -768,32 +783,19 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon { customEnv.add("TOR_PT_PROXY=socks5://" + OrbotVpnManager.sSocksProxyLocalhost + ":" + OrbotVpnManager.sSocksProxyServerPort); } - - // String baseDirectory = fileTor.getParent(); - // Shell shellUser = Shell.startShell(customEnv, baseDirectory); - + boolean success = runTorShellCmd(); - - if (success) - { - if (mPortHTTP != -1) - runPolipoShellCmd(); - - if (Prefs.useRoot() && Prefs.useTransparentProxying()) - { - disableTransparentProxy(); - enableTransparentProxy(); - + if (mPortHTTP != -1) + runPolipoShellCmd(); - } - - getHiddenServiceHostname (); - } - else - { - showToolbarNotification(getString(R.string.unable_to_start_tor), ERROR_NOTIFY_ID, R.drawable.ic_stat_notifyerr); - } + if (Prefs.useRoot() && Prefs.useTransparentProxying()) + { + disableTransparentProxy(); + enableTransparentProxy(); + } + + getHiddenServiceHostname (); } catch (Exception e) { logException("Unable to start Tor: " + e.toString(), e); @@ -914,14 +916,19 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon mTransProxy = new TorTransProxy(this, fileXtables); mTransProxy.setTransparentProxyingAll(this, false); + ArrayList apps = TorTransProxy.getApps(this, TorServiceUtils.getSharedPrefs(getApplicationContext())); mTransProxy.setTransparentProxyingByApp(this, apps, false); - + + mTransProxy.closeShell(); + mTransProxy = null; + return true; } private boolean runTorShellCmd() throws Exception { + boolean result = true; String torrcPath = new File(appBinHome, TORRC_ASSET_KEY).getCanonicalPath(); @@ -936,9 +943,8 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon debug(torCmdString); - Process proc = exec(torCmdString + " --verify-config", true); + int exitCode = exec(torCmdString + " --verify-config", true); - int exitCode = proc.exitValue(); String output = ""; // String output = shellTorCommand.getOutput(); @@ -949,8 +955,7 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon } - proc = exec(torCmdString, true); - exitCode = proc.exitValue(); + exitCode = exec(torCmdString, true); output = "";// shellTorCommand.getOutput(); if (exitCode != 0) @@ -977,7 +982,7 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon } - return true; + return result; } @@ -986,14 +991,15 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon mExecutor.execute(runn); } - private Process exec (String cmd, boolean wait) throws Exception + private int exec (String cmd, boolean wait) throws Exception { - Process proc = Runtime.getRuntime().exec(cmd); + SimpleCommand command = new SimpleCommand(cmd); + mShell.add(command); if (wait) - proc.waitFor(); + command.waitForFinish(); - return proc; + return command.getExitCode(); } private void updatePolipoConfig () throws FileNotFoundException, IOException @@ -1024,7 +1030,12 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon String polipoConfigPath = new File(appBinHome, POLIPOCONFIG_ASSET_KEY).getCanonicalPath(); String cmd = (filePolipo.getCanonicalPath() + " -c " + polipoConfigPath); - mProcPolipo = exec(cmd,false); + if (mShellPolipo != null) + mShellPolipo.close(); + + mShellPolipo = Shell.startShell(); + SimpleCommand cmdPolipo = new SimpleCommand(cmd); + mShellPolipo.add(cmdPolipo); sendCallbackLogMessage(getString(R.string.privoxy_is_running_on_port_) + mPortHTTP); diff --git a/orbotservice/src/main/java/org/torproject/android/service/transproxy/TorTransProxy.java b/orbotservice/src/main/java/org/torproject/android/service/transproxy/TorTransProxy.java index 037868a9..da0a4a7b 100644 --- a/orbotservice/src/main/java/org/torproject/android/service/transproxy/TorTransProxy.java +++ b/orbotservice/src/main/java/org/torproject/android/service/transproxy/TorTransProxy.java @@ -16,6 +16,8 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import org.sufficientlysecure.rootcommands.Shell; +import org.sufficientlysecure.rootcommands.command.SimpleCommand; import org.torproject.android.service.OrbotConstants; import org.torproject.android.service.util.Prefs; import org.torproject.android.service.TorService; @@ -32,18 +34,15 @@ public class TorTransProxy implements TorServiceConstants { private int mTransProxyPort = TOR_TRANSPROXY_PORT_DEFAULT; private int mDNSPort = TOR_DNS_PORT_DEFAULT; - private Process mProcess = null; - - private DataOutputStream mProcessOutput = null; - + private Shell mShell; public TorTransProxy (TorService torService, File fileXTables) throws IOException { mTorService = torService; mFileXtables = fileXTables; - mProcess = Runtime.getRuntime().exec("su"); - mProcessOutput = new DataOutputStream(mProcess.getOutputStream()); + // start root shell + mShell = Shell.startRootShell(); } @@ -562,24 +561,22 @@ public class TorTransProxy implements TorServiceConstants { private int executeCommand (String cmdString) throws Exception { - mProcessOutput.writeBytes(cmdString + "\n"); - mProcessOutput.flush(); + SimpleCommand command = new SimpleCommand(cmdString); - logMessage(cmdString); + mShell.add(command).waitForFinish(); + + logMessage("Command Exec: " + cmdString); + logMessage("Output: " + command.getOutput()); + logMessage("Exit code: " + command.getExitCode()); return 0; } - public int doExit () throws Exception + public void closeShell () throws IOException { - mProcessOutput.writeBytes("exit\n"); - mProcessOutput.flush(); - - return mProcess.waitFor(); - + mShell.close(); } - - + public int enableTetheringRules (Context context) throws Exception { @@ -800,7 +797,8 @@ public class TorTransProxy implements TorServiceConstants { executeCommand (script.toString()); script = new StringBuilder(); - + + /** if (Prefs.useDebugLogging()) { //XXX: Comment the following rules for non-debug builds @@ -827,8 +825,8 @@ public class TorTransProxy implements TorServiceConstants { executeCommand (script.toString()); script = new StringBuilder(); - - } + + }**/ //allow access to transproxy port script.append(ipTablesPath); diff --git a/settings.gradle b/settings.gradle index 9984a03e..7453139d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':jsocksAndroid', ':orbotservice' +include ':jsocksAndroid', ':orbotservice', ':RootCommands' include ':app'