package net.geedge.api.controller; import cn.hutool.core.codec.Base32Codec; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.log.Log; import jakarta.servlet.http.HttpServletResponse; import net.geedge.api.entity.EnvApiYml; import net.geedge.api.util.AdbUtil; import net.geedge.api.util.CommandExec; import net.geedge.api.util.PlaybookRunnable; import net.geedge.common.*; import net.lingala.zip4j.ZipFile; import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.model.enums.CompressionLevel; import net.lingala.zip4j.model.enums.CompressionMethod; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; import java.util.regex.Pattern; @RestController @RequestMapping("/api/v1/env") public class APIController { private final static Log log = Log.get(); private final AdbUtil adbUtil; @Autowired public APIController(EnvApiYml envApiYml) { this.adbUtil = AdbUtil.getInstance(envApiYml.getAdb(), new CommandExec(null)); } @Autowired private EnvApiYml apiYml; @GetMapping("/status") public R status() { return R.ok(adbUtil.status()); } @PostMapping("/file") public R push(@RequestParam(value = "file") MultipartFile file, @RequestParam String path) throws IOException { File tempFile = null; try { tempFile = T.FileUtil.file(Constant.TEMP_PATH, file.getOriginalFilename()); file.transferTo(tempFile); AdbUtil.CommandResult result = adbUtil.push(tempFile.getAbsolutePath(), path); if (0 != result.exitCode()) { return R.error(result.output()); } } finally { T.FileUtil.del(tempFile); } return R.ok(); } @GetMapping("/file/{fileId}") public void pull(@PathVariable String fileId, HttpServletResponse response) throws IOException { byte[] decode = Base32Codec.Base32Decoder.DECODER.decode(fileId); String filePath = T.StrUtil.str(decode, T.CharsetUtil.CHARSET_UTF_8); String fileName = T.FileUtil.getName(filePath); File tempFile = T.FileUtil.file(Constant.TEMP_PATH, fileName); try { AdbUtil.CommandResult result = adbUtil.pull(filePath, tempFile.getAbsolutePath()); if (0 != result.exitCode()) { throw new APIException(result.output()); } if (T.FileUtil.isDirectory(tempFile)) { File zip = T.ZipUtil.zip(tempFile); try { T.ResponseUtil.downloadFile(response, zip.getName(), T.FileUtil.readBytes(zip)); } finally { T.FileUtil.del(zip); } } else { T.ResponseUtil.downloadFile(response, fileName, T.FileUtil.readBytes(tempFile)); } } finally { T.FileUtil.del(tempFile); } } @GetMapping("/file") public R listDir(@RequestParam(defaultValue = "/") String path) { List listDir = adbUtil.listDir(path); Map data = T.MapUtil.builder() .put("path", path) .put("records", listDir) .build(); return R.ok(data); } @GetMapping("/app") public R listApp(@RequestParam(required = false) String arg) { return R.ok().putData("records", adbUtil.listApp(arg)); } @PostMapping("/app") public R install(@RequestParam(value = "file", required = false) MultipartFile file, @RequestParam(required = false) String path) throws IOException { if (file != null) { File tempFile = null; try { tempFile = T.FileUtil.file(Constant.TEMP_PATH, file.getOriginalFilename()); file.transferTo(tempFile); AdbUtil.CommandResult result = adbUtil.install(tempFile.getAbsolutePath(), true, true); if (0 != result.exitCode()) { throw new APIException(result.output()); } return R.ok(); } finally { T.FileUtil.del(tempFile); } } if (T.StrUtil.isNotEmpty(path)) { AdbUtil.CommandResult result = adbUtil.install(path, true, true); if (0 != result.exitCode()) { throw new APIException(result.output()); } return R.ok(); } return R.error(RCode.BAD_REQUEST); } @DeleteMapping("/app") public R uninstall(@RequestParam String packageName) { AdbUtil.CommandResult result = adbUtil.uninstall(packageName); if (0 != result.exitCode()) { throw new APIException(result.output()); } return R.ok(); } @GetMapping("/pcap") public R listTcpdump() { return R.ok().putData("records", adbUtil.listTcpdump()); } @PostMapping("/pcap") public R startTcpdump(@RequestParam(required = false, defaultValue = "") String packageName) { AdbUtil.CommandResult result = adbUtil.startTcpdump(packageName); if (0 != result.exitCode()) { throw new APIException("exec tcpdump error"); } return R.ok().putData("id", result.output()); } @DeleteMapping("/pcap") public synchronized void stopTcpdump(@RequestParam String id, @RequestParam(required = false, defaultValue = "false") Boolean returnFile, HttpServletResponse response) throws IOException { AdbUtil.CommandResult result = adbUtil.stopTcpdump(id); if (0 != result.exitCode()) { throw new APIException(result.output()); } String filePath = result.output(); try { if (returnFile) { // response pcap file File tempFile = T.FileUtil.file(Constant.TEMP_PATH, id + ".pcap"); try { if (T.StrUtil.isEmpty(filePath)) { throw new APIException(RCode.NOT_EXISTS); } AdbUtil.CommandResult pulled = adbUtil.pull(filePath, tempFile.getAbsolutePath()); if (0 != pulled.exitCode()) { throw new APIException(pulled.output()); } T.ResponseUtil.downloadFile(response, tempFile.getName(), T.FileUtil.readBytes(tempFile)); } finally { T.FileUtil.del(tempFile); } } else { // response taskid response.getWriter().write(T.JSONUtil.toJsonStr(R.ok().putData("id", id))); } } finally { if (T.StrUtil.isNotEmpty(filePath)) { // remove pcap file adbUtil.execShellCommand(String.format("shell rm -rf %s", filePath)); } } } @PostMapping("/shell") public R execShellCmd(@RequestBody Map requestBody) { String cmd = T.MapUtil.getStr(requestBody, "cmd", ""); if (T.StrUtil.isEmpty(cmd)) { return R.error(RCode.BAD_REQUEST); } Integer timeout = T.MapUtil.getInt(requestBody, "timeout", 10); return R.ok().putData("result", adbUtil.execShellCommand(cmd, timeout)); } @GetMapping("/acl") public R listAcl() { return R.ok().putData("records", adbUtil.listAcl()); } @PostMapping("/acl") public R addAcl(@RequestBody Map requestBody) { String ip = T.MapUtil.getStr(requestBody, "ip"); String port = T.MapUtil.getStr(requestBody, "port"); if (T.StrUtil.isAllEmpty(ip, port)) { return R.error(RCode.BAD_REQUEST); } String protocol = T.MapUtil.getStr(requestBody, "protocol", "all"); if (!T.StrUtil.equalsAny(protocol, "tcp", "udp", "all")) { return R.error(RCode.BAD_REQUEST); } if ("all".equals(protocol) && T.StrUtil.isEmpty(ip)) { return R.error(RCode.BAD_REQUEST); } adbUtil.addAcl(protocol, ip, port); return R.ok().putData("records", adbUtil.listAcl()); } @DeleteMapping("/acl") public R deleteAcl(@RequestBody Map requestBody) { String ip = T.MapUtil.getStr(requestBody, "ip"); String port = T.MapUtil.getStr(requestBody, "port"); if (T.StrUtil.isAllEmpty(ip, port)) { return R.error(RCode.BAD_REQUEST); } String protocol = T.MapUtil.getStr(requestBody, "protocol", "all"); if (!T.StrUtil.equalsAny(protocol, "tcp", "udp", "all")) { return R.error(RCode.BAD_REQUEST); } if ("all".equals(protocol) && T.StrUtil.isEmpty(ip)) { return R.error(RCode.BAD_REQUEST); } adbUtil.deleteAcl(protocol, ip, port); return R.ok().putData("records", adbUtil.listAcl()); } @DeleteMapping("/acl/flush") public R flushAcl() { AdbUtil.CommandResult result = adbUtil.flushAcl(); if (0 != result.exitCode()) { return R.error(result.output()); } return R.ok(); } @PostMapping("/playbook") public R execPlaybook(@RequestParam("file") MultipartFile file, @RequestParam("packageName") String packageName, @RequestParam("id") String id, @RequestParam("type") String type, @RequestParam("reInstall") Boolean reInstall, @RequestParam("clearCache") Boolean clearCache, @RequestParam("unInstall") Boolean unInstall) { File apkFile = null; File scriptPath = null; File destination = null; try { File playbookDir = T.FileUtil.file(Constant.TEMP_PATH, id); destination = T.FileUtil.file(Constant.TEMP_PATH, id, file.getName()); T.FileUtil.writeBytes(file.getInputStream().readAllBytes(), destination); // unzip file T.ZipUtil.unzip(destination, playbookDir); // apk apkFile = Arrays.stream(playbookDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".apk"); } })).findFirst().get(); // playbook zip File playbook = Arrays.stream(playbookDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".zip") && !name.equals(file.getName()); } })).findFirst().get(); // unzip playbook zip if (T.StrUtil.equals(type, "python")){ playbookDir = T.FileUtil.file(Constant.TEMP_PATH, id, "main"); T.ZipUtil.unzip(playbook, playbookDir); scriptPath = Arrays.stream(playbookDir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.getName().equals("main.py"); } })).findFirst().get(); }else { T.ZipUtil.unzip(playbook, playbookDir); scriptPath = Arrays.stream(playbookDir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.getName().endsWith(".air"); } })).findFirst().get(); } } catch (Exception e) { log.error(e.getMessage()); throw new APIException(RCode.ERROR); } finally { T.FileUtil.del(destination); } PlaybookRunnable playbookRunnable = new PlaybookRunnable(apiYml, apkFile, scriptPath, id, packageName, type, reInstall, clearCache, unInstall); ThreadUtil.execAsync(playbookRunnable); return R.ok(); } @GetMapping("/playbook/{id}") public R checkJobResult(@PathVariable("id") String id){ if (T.StrUtil.isEmpty(id)) { throw new APIException(RCode.BAD_REQUEST); } File statusFile = FileUtil.file(Constant.TEMP_PATH, id, "result.json"); String status = T.FileUtil.readString(statusFile, "UTF-8"); return R.ok().putData(status); } @DeleteMapping("/playbook/{id}") public R cancel(@PathVariable("id") String id){ if (T.StrUtil.isEmpty(id)) { throw new APIException(RCode.BAD_REQUEST); } if (CollUtil.isNotEmpty(Constant.ACTIVE_TASKS)) { Constant.ACTIVE_TASKS.stream().forEach(thread -> { log.info(String.format("playbook thread: %s has been canceled", id)); thread.interrupt(); }); } return R.ok(); } @GetMapping("/playbook/{id}/log") public R getJobResultLog(@PathVariable("id") String id, @RequestParam("offset") Integer offset){ if (T.StrUtil.isEmpty(id)) { throw new APIException(RCode.BAD_REQUEST); } // log file File logFile = T.FileUtil.file(Constant.TEMP_PATH, id, "result.log"); HashMap result = T.MapUtil.newHashMap(false); try (RandomAccessFile raf = new RandomAccessFile(logFile, "r")) { if (offset < raf.length()) { raf.seek(offset); byte[] bytes = new byte[(int)raf.length() - offset]; raf.readFully(bytes); String content = new String(bytes); result.put("content", content); result.put("length", bytes.length); result.put("offset", offset + bytes.length); } } catch (IOException e) { log.error("getJobResultLog error", e); throw new APIException(RCode.ERROR); } return R.ok().putData(result); } @GetMapping("/playbook/{id}/artifact") public void getJobResultArtifact(@PathVariable("id") String id, @RequestParam(value = "artifacts",required = false) String[] artifacts , HttpServletResponse response) throws IOException { if (T.StrUtil.isEmpty(id)) { throw new APIException(RCode.BAD_REQUEST); } // job dir File jobResult = T.FileUtil.file(Constant.TEMP_PATH, id); File[] files = jobResult.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".log") || name.endsWith(".pcap"); } }); // artifact List artifactFiles = ListUtil.list(false); File playbookDir = Arrays.stream(jobResult.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".air") || name.equals("main"); } })).toList().getFirst(); if (ArrayUtil.isNotEmpty(artifacts)) { for (String artifact : artifacts) { if (containsRegex(artifact)) { int lastSeparator = artifact.lastIndexOf(FileSystems.getDefault().getSeparator()); String regex = (lastSeparator >= 0) ? artifact.substring(lastSeparator + 1) : artifact; String parent = (lastSeparator >= 0) ? artifact.substring(0, lastSeparator) : ""; // Resolve parent directory Path parentPath = parent.isEmpty() ? Paths.get(playbookDir.getPath()) : Paths.get(playbookDir.getPath(), parent).normalize(); // Compile regex pattern Pattern pattern = Pattern.compile(regex); // Find matching files artifactFiles = findMatchingFiles(parentPath, pattern); } else { Path resolvedPath = Paths.get(artifact); if (!resolvedPath.isAbsolute()) { resolvedPath = Paths.get(playbookDir.getPath(), artifact).normalize(); } if (Files.exists(resolvedPath)) { artifactFiles.add(resolvedPath.toFile()); } } } } ZipFile zip = null; try { File zipFile = T.FileUtil.file(Constant.TEMP_PATH, id, T.StrUtil.concat(true, id, ".zip")); zip = new ZipFile(zipFile); ZipParameters parameters = new ZipParameters(); parameters.setCompressionMethod(CompressionMethod.DEFLATE); // 压缩方法 parameters.setCompressionLevel(CompressionLevel.FASTEST); // 压缩级别,选项有 FASTEST、ULTRA 等 // 添加文件到 ZIP for (File file : files) { zip.addFile(file, parameters); } for (File artifactFile : artifactFiles) { zip.addFile(artifactFile, parameters); } T.ResponseUtil.downloadFile(response, zipFile.getName(), T.FileUtil.readBytes(zipFile.getPath())); } finally { zip.close(); } } private static boolean containsRegex(String path) { return path.contains("*") || path.contains("?") || path.contains("[") || path.contains("]"); } private static List findMatchingFiles(Path directory, Pattern pattern) throws IOException { List artifactFiles = new ArrayList<>(); Files.walkFileTree(directory, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (pattern.matcher(file.getFileName().toString()).matches()) { artifactFiles.add(file.toFile()); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (pattern.matcher(dir.getFileName().toString()).matches()) { artifactFiles.add(dir.toFile()); } return FileVisitResult.CONTINUE; } }); return artifactFiles; } }