package net.geedge.api.controller; import cn.hutool.core.codec.Base32Codec; import cn.hutool.core.io.FileUtil; import cn.hutool.core.thread.ThreadUtil; 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.common.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @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("files") MultipartFile file, @RequestParam("packageName") String packageName, @RequestParam("id") String id) { File apkFile = null; File playbookAirDir = 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 T.ZipUtil.unzip(playbook, playbookDir); playbookAirDir = 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()); Map resultMap = T.MapUtil.builder() .put("status", "error") .build(); Constant.PLAYBOOK_RUN_RESULT.put(id, resultMap); return R.ok(); } finally { T.FileUtil.del(destination); } PlaybookRunnable playbookRunnable = new PlaybookRunnable(apiYml, apkFile, playbookAirDir, id, packageName); 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); } Map result = Constant.PLAYBOOK_RUN_RESULT.get(id); return R.ok().putData(result); } @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")) { 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, 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 zipFile = T.FileUtil.file(Constant.TEMP_PATH, id, T.StrUtil.concat(true, id, ".zip")); File[] files = jobResult.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".log") || name.endsWith(".pcap"); } }); T.ZipUtil.zip(zipFile, true, files); T.ResponseUtil.downloadFile(response,zipFile.getName(), T.FileUtil.readBytes(zipFile)); } public class PlaybookRunnable extends Thread { private AdbUtil adbUtil; private EnvApiYml envApiYml; private String tid; private File apkFile; private String packageName; private File playbookDir; public PlaybookRunnable(EnvApiYml envApiYml, File apkFile, File playbookDir, String tid, String packageName) { this.envApiYml = envApiYml; this.tid = tid; this.apkFile = apkFile; this.packageName = packageName; this.playbookDir = playbookDir; } @Override public void run() { File logFile = FileUtil.file(Constant.TEMP_PATH, tid, "result.log"); try { T.FileUtil.appendString(String.format("Running with %s:%s Android Simulator \n", envApiYml.getAdb().getHost(), envApiYml.getAdb().getPort()), logFile, "UTF-8"); adbUtil = new AdbUtil(envApiYml.getAdb(), new CommandExec(logFile)); Map resultMap = T.MapUtil.builder() .put("status", "running") .build(); Constant.PLAYBOOK_RUN_RESULT.put(tid, resultMap); // 1. install apk AdbUtil.CommandResult install = adbUtil.install(apkFile.getAbsolutePath(), true, true); if (0 != install.exitCode()) { T.FileUtil.appendString(String.format("ERROR: Install apk failed: exit code %s \n", install.exitCode()), logFile, "UTF-8"); throw new APIException(install.output()); } // 2. star tcpdump AdbUtil.CommandResult startTcpdump = adbUtil.startTcpdump(packageName); if (0 != startTcpdump.exitCode()) { T.FileUtil.appendString(String.format("ERROR: Start tcpdump failed: exit code %s \n", startTcpdump.exitCode()), logFile, "UTF-8"); throw new APIException("exec tcpdump error"); } // 3. exec playbook AdbUtil.CommandResult execResult = adbUtil.execPlaybook(playbookDir.getPath(), logFile); if (0 != execResult.exitCode()) { // exec playbook error, stop tcpdump and delete pcap T.FileUtil.appendString(String.format("ERROR: Exec playbook failed: exit code %s \n", execResult.exitCode()), logFile, "UTF-8"); AdbUtil.CommandResult stopTcpdump = adbUtil.stopTcpdump(startTcpdump.output()); adbUtil.execShellCommand(String.format("shell rm -rf %s", stopTcpdump.output())); throw new APIException("exec playbook error"); } // 4. stop tcpdump AdbUtil.CommandResult stopTcpdump = adbUtil.stopTcpdump(startTcpdump.output()); if (0 != stopTcpdump.exitCode()) { T.FileUtil.appendString(String.format("ERROR: Stop tcpdump failed: exit code %s \n", stopTcpdump.exitCode()), logFile, "UTF-8"); throw new APIException(stopTcpdump.output()); } // 5. pull pcap file String filePath = stopTcpdump.output(); File localPcapFile = T.FileUtil.file(Constant.TEMP_PATH, tid, startTcpdump.output() + ".pcap"); if (T.StrUtil.isEmpty(filePath)) { throw new APIException(RCode.NOT_EXISTS); } AdbUtil.CommandResult pull = adbUtil.pull(filePath, localPcapFile.getAbsolutePath()); if (0 != pull.exitCode()) { T.FileUtil.appendString(String.format("ERROR: Pull pcap file failed: exit code %s \n", pull.exitCode()), logFile, "UTF-8"); throw new APIException(pull.output()); } // 6. delete android pcap adbUtil.execShellCommand(String.format("shell rm -rf %s", filePath)); resultMap = T.MapUtil.builder() .put("status", "done") .build(); Constant.PLAYBOOK_RUN_RESULT.put(tid, resultMap); } catch (Exception e) { log.error(e); Map resultMap = T.MapUtil.builder() .put("status", "error") .build(); Constant.PLAYBOOK_RUN_RESULT.put(tid, resultMap); } finally { adbUtil.stopApp(packageName); T.FileUtil.appendString(String.format("Job succeeded"), logFile, "UTF-8"); } } } }