This repository has been archived on 2025-09-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
appsketch-works-device-api/src/main/java/net/geedge/api/controller/APIController.java
2024-11-26 10:08:24 +08:00

483 lines
18 KiB
Java

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<Map> listDir = adbUtil.listDir(path);
Map<Object, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<Object, Object> 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<File> 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<File> findMatchingFiles(Path directory, Pattern pattern) throws IOException {
List<File> 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;
}
}