2018-01-01 09:06:37 +01:00
|
|
|
package com.wireguard.android.util;
|
2017-08-01 02:00:05 +02:00
|
|
|
|
2017-08-01 06:21:59 +02:00
|
|
|
import android.content.Context;
|
2017-12-16 06:17:27 +01:00
|
|
|
import android.system.OsConstants;
|
2017-08-01 02:00:05 +02:00
|
|
|
import android.util.Log;
|
|
|
|
|
2018-01-01 09:06:37 +01:00
|
|
|
import com.wireguard.android.Application.ApplicationContext;
|
|
|
|
import com.wireguard.android.Application.ApplicationScope;
|
|
|
|
|
2017-08-01 02:00:05 +02:00
|
|
|
import java.io.BufferedReader;
|
2017-12-16 06:17:27 +01:00
|
|
|
import java.io.File;
|
2017-08-01 02:00:05 +02:00
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.io.InputStreamReader;
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
import java.util.List;
|
2017-11-30 21:46:56 +01:00
|
|
|
import java.util.regex.Matcher;
|
2018-01-01 09:06:37 +01:00
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
import javax.inject.Inject;
|
2017-08-01 02:00:05 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper class for running commands as root.
|
|
|
|
*/
|
|
|
|
|
2018-01-01 09:06:37 +01:00
|
|
|
@ApplicationScope
|
2017-12-19 02:42:00 +01:00
|
|
|
public class RootShell {
|
2018-01-01 09:06:37 +01:00
|
|
|
private static final Pattern ERRNO_EXTRACTOR = Pattern.compile("error=(\\d+)");
|
2017-08-01 02:00:05 +02:00
|
|
|
/**
|
|
|
|
* Setup commands that are run at the beginning of each root shell. The trap command ensures
|
|
|
|
* access to the return value of the last command, since su itself always exits with 0.
|
|
|
|
*/
|
2017-12-16 06:17:27 +01:00
|
|
|
private static final String TAG = "WireGuard/RootShell";
|
|
|
|
private static final String[][] libraryNamedExecutables = {
|
2018-01-01 09:06:37 +01:00
|
|
|
{"libwg.so", "wg"},
|
|
|
|
{"libwg-quick.so", "wg-quick"}
|
2017-12-16 06:17:27 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
private final String preamble;
|
2017-08-01 06:21:59 +02:00
|
|
|
|
2018-01-01 09:06:37 +01:00
|
|
|
@Inject
|
|
|
|
public RootShell(@ApplicationContext final Context context) {
|
2017-12-16 06:17:27 +01:00
|
|
|
final String binDir = context.getCacheDir().getPath() + "/bin";
|
|
|
|
final String tmpDir = context.getCacheDir().getPath() + "/tmp";
|
2017-12-19 02:42:00 +01:00
|
|
|
final String libDir = context.getApplicationInfo().nativeLibraryDir;
|
2017-12-16 06:17:27 +01:00
|
|
|
|
|
|
|
new File(binDir).mkdirs();
|
|
|
|
new File(tmpDir).mkdirs();
|
|
|
|
|
2017-12-19 02:42:00 +01:00
|
|
|
StringBuilder builder = new StringBuilder();
|
2017-12-16 06:17:27 +01:00
|
|
|
for (final String[] libraryNamedExecutable : libraryNamedExecutables) {
|
2017-12-19 02:42:00 +01:00
|
|
|
final String arg1 = "'" + libDir + "/" + libraryNamedExecutable[0] + "'";
|
|
|
|
final String arg2 = "'" + binDir + "/" + libraryNamedExecutable[1] + "'";
|
|
|
|
builder.append(String.format("[ %s -ef %s ] || ln -sf %s %s || exit 31;", arg1, arg2, arg1, arg2));
|
2017-12-16 06:17:27 +01:00
|
|
|
}
|
2017-12-19 02:42:00 +01:00
|
|
|
builder.append(String.format("export PATH=\"%s:$PATH\" TMPDIR=\"%s\";", binDir, tmpDir));
|
|
|
|
|
|
|
|
preamble = builder.toString();
|
2017-08-01 06:21:59 +02:00
|
|
|
}
|
|
|
|
|
2017-08-01 02:00:05 +02:00
|
|
|
/**
|
2017-12-16 06:17:27 +01:00
|
|
|
* Run a command in a root shell.
|
2017-08-01 02:00:05 +02:00
|
|
|
*
|
2018-01-01 09:06:37 +01:00
|
|
|
* @param output Lines read from stdout are appended to this list. Pass null if the
|
|
|
|
* output from the shell is not important.
|
|
|
|
* @param command Command to run as root.
|
2017-08-01 02:00:05 +02:00
|
|
|
* @return The exit value of the last command run, or -1 if there was an internal error.
|
|
|
|
*/
|
2017-12-19 02:42:00 +01:00
|
|
|
public int run(final List<String> output, final String command) {
|
2017-08-01 02:00:05 +02:00
|
|
|
int exitValue = -1;
|
|
|
|
try {
|
2017-12-16 06:17:27 +01:00
|
|
|
final ProcessBuilder builder = new ProcessBuilder();
|
|
|
|
builder.environment().put("LANG", "C");
|
|
|
|
builder.command("su", "-c", preamble + command);
|
|
|
|
final Process process = builder.start();
|
|
|
|
Log.d(TAG, "Running: " + command);
|
2017-08-01 02:00:05 +02:00
|
|
|
final InputStream stdout = process.getInputStream();
|
2017-12-16 06:17:27 +01:00
|
|
|
final InputStream stderr = process.getErrorStream();
|
2017-08-01 02:00:05 +02:00
|
|
|
final BufferedReader stdoutReader =
|
|
|
|
new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
|
2017-12-16 06:17:27 +01:00
|
|
|
final BufferedReader stderrReader =
|
|
|
|
new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
|
2017-08-01 02:00:05 +02:00
|
|
|
String line;
|
|
|
|
while ((line = stdoutReader.readLine()) != null) {
|
|
|
|
if (output != null)
|
|
|
|
output.add(line);
|
2017-12-16 06:17:27 +01:00
|
|
|
Log.v(TAG, "stdout: " + line);
|
2017-08-01 02:00:05 +02:00
|
|
|
}
|
2017-12-16 06:17:27 +01:00
|
|
|
int linesOfStderr = 0;
|
|
|
|
String stderrLast = null;
|
|
|
|
while ((line = stderrReader.readLine()) != null) {
|
|
|
|
++linesOfStderr;
|
|
|
|
stderrLast = line;
|
|
|
|
Log.v(TAG, "stderr: " + line);
|
2017-08-01 02:00:05 +02:00
|
|
|
}
|
2017-12-16 06:17:27 +01:00
|
|
|
exitValue = process.waitFor();
|
|
|
|
process.destroy();
|
|
|
|
if (exitValue == 1 && linesOfStderr == 1 && stderrLast.equals("Permission denied"))
|
|
|
|
exitValue = OsConstants.EACCES;
|
|
|
|
Log.d(TAG, "Exit status: " + exitValue);
|
|
|
|
} catch (IOException | InterruptedException e) {
|
2017-08-01 02:00:05 +02:00
|
|
|
Log.w(TAG, "Session failed with exception", e);
|
2017-11-30 21:46:56 +01:00
|
|
|
final Matcher match = ERRNO_EXTRACTOR.matcher(e.toString());
|
|
|
|
if (match.find())
|
|
|
|
exitValue = Integer.valueOf(match.group(1));
|
2017-08-01 02:00:05 +02:00
|
|
|
}
|
|
|
|
return exitValue;
|
|
|
|
}
|
|
|
|
}
|