diff --git a/pom.xml b/pom.xml index c4dd00b..7ac2eb9 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,7 @@ **/application-*.yml **/logback-spring.xml + **/token.auth lib/*.* diff --git a/src/main/java/net/geedge/api/controller/APIController.java b/src/main/java/net/geedge/api/controller/APIController.java index 58e8c73..f1b4be1 100644 --- a/src/main/java/net/geedge/api/controller/APIController.java +++ b/src/main/java/net/geedge/api/controller/APIController.java @@ -127,6 +127,11 @@ public class APIController { 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); @@ -149,7 +154,10 @@ public class APIController { // response pcap file File tempFile = T.FileUtil.file(Constant.TEMP_PATH, id + ".pcap"); try { - String filePath = "/data/local/tmp/" + id + ".pcap"; + String filePath = result.output(); + 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()); @@ -163,4 +171,15 @@ public class APIController { response.getWriter().write(T.JSONUtil.toJsonStr(R.ok().putData("id", id))); } } + + @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)); + } } \ No newline at end of file diff --git a/src/main/java/net/geedge/api/util/AdbUtil.java b/src/main/java/net/geedge/api/util/AdbUtil.java index beba56e..71d4adb 100644 --- a/src/main/java/net/geedge/api/util/AdbUtil.java +++ b/src/main/java/net/geedge/api/util/AdbUtil.java @@ -482,6 +482,7 @@ public class AdbUtil { * iptables -F * iptables -X */ + @Deprecated private void cleanIptables() { // Delete all rules in chain or all chains CommandExec.exec(AdbCommandBuilder.builder() @@ -497,17 +498,47 @@ public class AdbUtil { ); } + /** + * list tcpdump + */ + public List listTcpdump() { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep capture_ | awk '{print $NF}' \"")) + .build()); + + List list = T.ListUtil.list(true); + + String[] lines = result.split("\\n"); + for (String line : lines) { + try { + String fileName = T.FileUtil.mainName(line); + String taskId = "", packageName = ""; + if (fileName.contains("capture_all_")) { + taskId = fileName.replaceAll("capture_all_", ""); + } else { + String[] split = fileName.split("_"); + packageName = split[2]; + taskId = split[split.length - 1]; + } + Map m = T.MapUtil.builder() + .put("id", taskId) + .put("packageName", packageName) + .build(); + list.add(m); + } catch (Exception e) { + log.warn(e, "[listTcpdump] [get task info error] [line: {}]", line); + } + } + return list; + } + /** * start Tcpdump - * iptables option * tcpdump pcap */ public CommandResult startTcpdump(String packageName) { - // clean iptables conf - this.cleanIptables(); - String taskId = T.IdUtil.fastSimpleUUID(); - String pcapFilePath = "/data/local/tmp/" + taskId + ".pcap"; if (T.StrUtil.isNotEmpty(packageName)) { log.info("[startTcpdump] [capture app package] [pkg: {}]", packageName); String dumpsysResult = CommandExec.exec(AdbCommandBuilder.builder() @@ -541,12 +572,16 @@ public class AdbUtil { .build()); log.info("[startTcpdump] [iptables -L] [result: {}]", ruleList); + // pcap 格式:capture_{userId}_{pcakageName}_{taskId}.pcap + String pcapFilePath = "/data/local/tmp/capture_" + userId + "_" + packageName + "_" + taskId + ".pcap"; CommandExec.execForProcess(AdbCommandBuilder.builder() .serial(this.getSerial()) .buildShellCommand(String.format("shell tcpdump -i nflog:%s -w %s &", userId, pcapFilePath)) .build()); } else { log.info("[startTcpdump] [capture all package]"); + // pcap 格式:capture_all_{taskId}.pcap + String pcapFilePath = "/data/local/tmp/capture_all_" + taskId + ".pcap"; CommandExec.execForProcess(AdbCommandBuilder.builder() .serial(this.getSerial()) .buildShellCommand(String.format("shell tcpdump -w %s &", pcapFilePath)) @@ -566,12 +601,39 @@ public class AdbUtil { * kill -INT {pid} */ public CommandResult stopTcpdump(String id) { + String pcapFilePath = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $NF}' \"", id)) + .build()); + if (T.StrUtil.isNotEmpty(pcapFilePath)) { + if (!pcapFilePath.contains("capture_all_")) { + // 删除 iptables rule + String[] split = T.FileUtil.mainName(pcapFilePath).split("_"); + String userId = split[1]; + log.info("[stopTcpdump] [remove iptables rule] [userId: {}]", userId); + CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell iptables -D OUTPUT -m owner --uid-owner %s -j CONNMARK --set-mark %s", userId, userId)) + .build()); + CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell iptables -D INPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId)) + .build()); + CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell iptables -D OUTPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId)) + .build()); + } + } String result = CommandExec.exec(AdbCommandBuilder.builder() .serial(this.getSerial()) .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $2}' | xargs kill -INT \"", id)) .build()); - log.info("[stopTcpdump] [id: {}] [result: {}]", id, result); - return new CommandResult(T.StrUtil.isEmpty(result) ? 0 : 1, result); + log.info("[stopTcpdump] [id: {}] [pcapFilePath: {}] [result: {}]", id, pcapFilePath, result); + if (T.StrUtil.isEmpty(result)) { + return new CommandResult(0, pcapFilePath); + } + return new CommandResult(1, result); } /** @@ -585,6 +647,30 @@ public class AdbUtil { log.info("[execShellCommand] [shellCmd: {}] [result: {}]", shellCmd, result); } + /** + * exec shell command + */ + public String execShellCommand(String cmd, Integer timeout){ + Process process = CommandExec.execForProcess(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand("shell " + cmd) + .build()); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> T.IoUtil.read(process.getInputStream(), T.CharsetUtil.CHARSET_UTF_8)); + try { + String result = future.get(timeout, TimeUnit.SECONDS); + return result; + } catch (TimeoutException e) { + process.destroyForcibly(); + throw new APIException(RCode.TIMEOUT); + } catch (ExecutionException | InterruptedException e) { + throw new APIException(RCode.ERROR); + } finally { + executor.shutdown(); + } + } + private synchronized ExecutorService getThreadPool() { if (threadPool == null) { threadPool = new ThreadPoolExecutor( diff --git a/src/main/java/net/geedge/common/RCode.java b/src/main/java/net/geedge/common/RCode.java index a7e6b97..8880eef 100644 --- a/src/main/java/net/geedge/common/RCode.java +++ b/src/main/java/net/geedge/common/RCode.java @@ -9,6 +9,7 @@ public enum RCode { NOT_EXISTS(404, "No such file or directory"), NOT_PERMISSION(401 , "Permission denied"), + TIMEOUT(408, "Request Timeout"), ERROR(999, "error"), // 通用错误/未知错误 diff --git a/src/main/java/net/geedge/common/T.java b/src/main/java/net/geedge/common/T.java index 2545cfb..8bb3b1a 100644 --- a/src/main/java/net/geedge/common/T.java +++ b/src/main/java/net/geedge/common/T.java @@ -1,10 +1,14 @@ package net.geedge.common; import cn.hutool.core.io.IORuntimeException; +import cn.hutool.log.Log; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; +import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.StringTokenizer; @@ -212,4 +216,30 @@ public class T { T.IoUtil.write(response.getOutputStream(), false, data); } } + + public static class WebPathUtil { + static Log log = Log.get(); + + /** + * 如果已打成jar包,则返回jar包所在目录 + * 如果未打成jar,则返回target所在目录 + * + * @return + */ + public static String getClassPath() { + try { + // 项目的编译文件的根目录 + String path = URLDecoder.decode(System.getProperty("user.dir"), "utf-8"); + log.debug("root path:{}", path); + return path; + } catch (UnsupportedEncodingException e) { + return null; + } + } + + public static String getRootPath() { + File file = T.FileUtil.file(WebPathUtil.getClassPath()); + return file.getAbsolutePath(); + } + } } \ No newline at end of file diff --git a/src/main/java/net/geedge/common/config/TokenInterceptor.java b/src/main/java/net/geedge/common/config/TokenInterceptor.java new file mode 100644 index 0000000..c0c0708 --- /dev/null +++ b/src/main/java/net/geedge/common/config/TokenInterceptor.java @@ -0,0 +1,54 @@ +package net.geedge.common.config; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.log.Log; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import net.geedge.common.T; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.io.File; + +@Configuration(proxyBeanMethods = false) +public class TokenInterceptor implements WebMvcConfigurer { + private final static Log log = Log.get(); + + private static String tokenValue; + + @Value("${device.tokenFile:config/token.auth}") + protected String tokenFile; + + + @PostConstruct + public void init() throws IORuntimeException { + tokenValue = readToken(); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new HandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String token = request.getHeader("Authorization"); + if (token == null || !token.equals(tokenValue)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Unauthorized"); + return false; + } + return true; + } + }).addPathPatterns("/**"); + } + + private String readToken() throws IORuntimeException { + File tf = T.FileUtil.file(T.WebPathUtil.getRootPath(), tokenFile); + log.info("token file path: {}", tf.getAbsolutePath()); + String token = T.FileUtil.readString(tf, T.CharsetUtil.UTF_8); + return T.StrUtil.trim(token); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e86d68f..628890e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,5 +8,5 @@ spring: max-request-size: 500MB enabled: true -logging: - config:./config/logback-spring.xml +device: + tokenFile: ./config/token.auth diff --git a/src/main/resources/config/token.auth b/src/main/resources/config/token.auth new file mode 100644 index 0000000..ead27ac --- /dev/null +++ b/src/main/resources/config/token.auth @@ -0,0 +1 @@ +2fa9a369 \ No newline at end of file