Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
228921a7f9 | ||
|
|
308b30a656 | ||
|
|
fa15fd76ca | ||
|
|
83b17b5f5e | ||
|
|
35eebd7beb | ||
|
|
b3a790db74 | ||
|
|
63f4e6ae4a | ||
|
|
7eb847d7bf | ||
|
|
8b540ba127 | ||
|
|
7ef08fba4a | ||
|
|
88d4638c27 | ||
|
|
3965a7a44d | ||
|
|
f90a6deffb | ||
|
|
ad63b414bf | ||
|
|
97adfa39cd | ||
|
|
1d6b710f63 | ||
|
|
5cf1d09260 | ||
|
|
562ac17fd4 | ||
|
|
05bacb1bf3 | ||
|
|
28e34185c5 | ||
|
|
8d88639655 | ||
|
|
139efeec0b | ||
|
|
6bc0dcea4f | ||
|
|
f15f725cf8 | ||
|
|
d81fbe2145 | ||
|
|
0f4d250328 | ||
|
|
7eb678813f | ||
|
|
87e211987b | ||
|
|
b940421c69 | ||
|
|
c861efd75d | ||
|
|
41ba204ee6 | ||
|
|
9814018149 | ||
|
|
6f6bb2ad90 | ||
|
|
3e306e1a8c |
207
.gitignore
vendored
Normal file
207
.gitignore
vendored
Normal file
@@ -0,0 +1,207 @@
|
||||
/target/
|
||||
/.mvn/
|
||||
#!.mvn/wrapper/maven-wrapper.jar
|
||||
mvnw
|
||||
mvnw.cmd
|
||||
HELP.md
|
||||
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
|
||||
# Created by https://www.gitignore.io/api/git,java,maven,eclipse,windows
|
||||
|
||||
### Eclipse ###
|
||||
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# PyDev specific (Python IDE for Eclipse)
|
||||
*.pydevproject
|
||||
|
||||
# CDT-specific (C/C++ Development Tooling)
|
||||
.cproject
|
||||
|
||||
# CDT- autotools
|
||||
.autotools
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
|
||||
# PDT-specific (PHP Development Tools)
|
||||
.buildpath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
# STS (Spring Tool Suite)
|
||||
.springBeans
|
||||
|
||||
# Code Recommenders
|
||||
.recommenders/
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
|
||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||
.cache-main
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
### Eclipse Patch ###
|
||||
# Eclipse Core
|
||||
.project
|
||||
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
.classpath
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated
|
||||
|
||||
.sts4-cache/
|
||||
|
||||
### Git ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
# $ git config --global mergetool.keepBackup false
|
||||
*.orig
|
||||
|
||||
# Created by git when using merge tools for conflicts
|
||||
*.BACKUP.*
|
||||
*.BASE.*
|
||||
*.LOCAL.*
|
||||
*.REMOTE.*
|
||||
*_BACKUP_*.txt
|
||||
*_BASE_*.txt
|
||||
*_LOCAL_*.txt
|
||||
*_REMOTE_*.txt
|
||||
|
||||
### Java ###
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
/log/
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
### Maven ###
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Some additional ignores (sort later)
|
||||
*.DS_Store
|
||||
*.sw?
|
||||
.#*
|
||||
*#
|
||||
*~
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
bin
|
||||
build
|
||||
target
|
||||
dependency-reduced-pom.xml
|
||||
*.sublime-*
|
||||
/scratch
|
||||
.gradle
|
||||
README.html
|
||||
*.iml
|
||||
.idea
|
||||
.exercism
|
||||
93
README.md
93
README.md
@@ -1,93 +0,0 @@
|
||||
# device-api
|
||||
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
|
||||
## Add your files
|
||||
|
||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://git.mesalab.cn/appsketch-works/device-api.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
|
||||
- [ ] [Set up project integrations](https://git.mesalab.cn/appsketch-works/device-api/-/settings/integrations)
|
||||
|
||||
## Collaborate with your team
|
||||
|
||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
||||
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
||||
|
||||
## Test and Deploy
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
|
||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
||||
|
||||
***
|
||||
|
||||
# Editing this README
|
||||
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
|
||||
## Suggestions for a good README
|
||||
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
143
pom.xml
Normal file
143
pom.xml
Normal file
@@ -0,0 +1,143 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.1.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<groupId>net.geedge</groupId>
|
||||
<artifactId>env-api</artifactId>
|
||||
<version>1.0</version>
|
||||
<name>env-api</name>
|
||||
<description>AppSketch Works Env API</description>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.16</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-cli</groupId>
|
||||
<artifactId>commons-cli</artifactId>
|
||||
<version>1.8.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.java-websocket</groupId>
|
||||
<artifactId>Java-WebSocket</artifactId>
|
||||
<version>1.5.6</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.jetbrains.pty4j/pty4j -->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.pty4j</groupId>
|
||||
<artifactId>pty4j</artifactId>
|
||||
<version>0.12.35</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/net.lingala.zip4j/zip4j -->
|
||||
<dependency>
|
||||
<groupId>net.lingala.zip4j</groupId>
|
||||
<artifactId>zip4j</artifactId>
|
||||
<version>2.11.5</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<excludes>
|
||||
<exclude>**/application-*.yml</exclude>
|
||||
<exclude>**/logback-spring.xml</exclude>
|
||||
<exclude>**/token.auth</exclude>
|
||||
<!--lib 下的依赖不加入 jar,部署时放到 jar 文件同级即可-->
|
||||
<exclude>lib/*.*</exclude>
|
||||
</excludes>
|
||||
<includes>
|
||||
<include>**/*.*</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>
|
||||
net.geedge.EnvApiApplication
|
||||
</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- 跳过单元测试 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
58
src/main/java/net/geedge/EnvApiApplication.java
Normal file
58
src/main/java/net/geedge/EnvApiApplication.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package net.geedge;
|
||||
|
||||
import cn.hutool.extra.spring.EnableSpringUtil;
|
||||
import cn.hutool.log.Log;
|
||||
import net.geedge.api.entity.EnvApiYml;
|
||||
import net.geedge.common.T;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import java.util.TimeZone;
|
||||
|
||||
@EnableSpringUtil
|
||||
@SpringBootApplication
|
||||
public class EnvApiApplication {
|
||||
|
||||
private final static Log log = Log.get();
|
||||
|
||||
public static void main(String[] args) {
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
|
||||
SpringApplication.run(EnvApiApplication.class, args);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
@Bean
|
||||
public EnvApiYml envProperties() {
|
||||
EnvApiYml apiYml = new EnvApiYml();
|
||||
|
||||
EnvApiYml.Env envEntity = apiYml.new Env();
|
||||
envEntity.setRoot(environment.getProperty("env.root"));
|
||||
envEntity.setType(environment.getProperty("env.type"));
|
||||
envEntity.setPlatform(environment.getProperty("env.platform"));
|
||||
|
||||
EnvApiYml.Adb adb = apiYml.new Adb();
|
||||
adb.setSerial(environment.getProperty("adb.serial"));
|
||||
adb.setHost(environment.getProperty("adb.host"));
|
||||
adb.setPort(environment.getProperty("adb.port", Integer.class));
|
||||
|
||||
String droidvncDefaultConfig = T.FileUtil.readUtf8String(T.FileUtil.file(T.WebPathUtil.getRootPath(), "./lib/droidvnc-np-defaults.json"));
|
||||
Integer vncPort = T.JSONUtil.parseObj(droidvncDefaultConfig).getInt("port", 5900);
|
||||
adb.setVncPort(vncPort);
|
||||
|
||||
EnvApiYml.Vnc vnc = apiYml.new Vnc();
|
||||
vnc.setHost(environment.getProperty("vnc.host"));
|
||||
vnc.setPort(environment.getProperty("vnc.port", Integer.class));
|
||||
|
||||
apiYml.setEnv(envEntity);
|
||||
apiYml.setAdb(adb);
|
||||
apiYml.setVnc(vnc);
|
||||
|
||||
log.info("[envProperties] [value: {}]", T.JSONUtil.toJsonStr(apiYml));
|
||||
return apiYml;
|
||||
}
|
||||
}
|
||||
132
src/main/java/net/geedge/api/config/AdbShellProxyHandler.java
Normal file
132
src/main/java/net/geedge/api/config/AdbShellProxyHandler.java
Normal file
@@ -0,0 +1,132 @@
|
||||
package net.geedge.api.config;
|
||||
|
||||
import cn.hutool.log.Log;
|
||||
import com.pty4j.PtyProcessBuilder;
|
||||
import net.geedge.api.entity.EnvApiYml;
|
||||
import net.geedge.api.util.AdbCommandBuilder;
|
||||
import net.geedge.api.util.AdbUtil;
|
||||
import net.geedge.api.util.CommandExec;
|
||||
import net.geedge.common.T;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class AdbShellProxyHandler extends TextWebSocketHandler {
|
||||
|
||||
private static final Log log = Log.get();
|
||||
|
||||
private EnvApiYml.Adb adb;
|
||||
|
||||
private Process process = null;
|
||||
private InputStream inputStream = null;
|
||||
private OutputStream outputStream = null;
|
||||
private ExecutorService executorService = Executors.newFixedThreadPool(2);
|
||||
|
||||
public AdbShellProxyHandler(EnvApiYml.Adb adb) {
|
||||
this.adb = adb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
log.info("[afterConnectionEstablished] [WebSocket connection established] [websocket uri: {}]", session.getUri());
|
||||
super.afterConnectionEstablished(session);
|
||||
|
||||
List<String> cmd = AdbCommandBuilder.builder()
|
||||
.serial(AdbUtil.getInstance(adb, new CommandExec(null)).getSerial())
|
||||
.buildShellCommand("shell")
|
||||
.build();
|
||||
|
||||
Map<String, String> env = new HashMap<>(System.getenv());
|
||||
env.put("TERM", "xterm");
|
||||
|
||||
// start process
|
||||
process = new PtyProcessBuilder()
|
||||
.setCommand(cmd.toArray(new String[]{}))
|
||||
.setEnvironment(env)
|
||||
.setRedirectErrorStream(true)
|
||||
.start();
|
||||
|
||||
// process = new PtyProcessBuilder()
|
||||
// .setCommand(new String[]{"cmd.exe", "/C", "D:\\softwares\\platform-tools\\platform-tools\\adb.exe shell"})
|
||||
// .setEnvironment(env)
|
||||
// .setRedirectErrorStream(true)
|
||||
// .start();
|
||||
|
||||
// stream
|
||||
inputStream = process.getInputStream();
|
||||
outputStream = process.getOutputStream();
|
||||
|
||||
// server to client
|
||||
executorService.submit(() -> {
|
||||
byte[] buffer = new byte[1024 * 4];
|
||||
try {
|
||||
while (session != null && session.isOpen()) {
|
||||
int size = inputStream.read(buffer);
|
||||
if (size == -1) {
|
||||
try {
|
||||
if (session.isOpen()) {
|
||||
session.sendMessage(new TextMessage("Connection closed \r\n"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < size; i++) {
|
||||
char chr = (char) (buffer[i] & 0xff);
|
||||
sb.append(chr);
|
||||
}
|
||||
|
||||
String message = sb.toString();
|
||||
message = T.StrUtil.str(message.getBytes(T.DigestUtils.getEncoding(message)), "UTF-8");
|
||||
|
||||
session.sendMessage(new TextMessage(message));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e, "[serverToClient] [error]");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
super.handleTextMessage(session, message);
|
||||
if (T.ObjectUtil.isNotNull(outputStream)) {
|
||||
// write cmd byte
|
||||
T.IoUtil.write(outputStream, false, message.getPayload().getBytes());
|
||||
T.IoUtil.flush(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||
log.info("[afterConnectionClosed] [WebSocket connection closed] [websocket uri: {}]", session.getUri());
|
||||
super.afterConnectionClosed(session, status);
|
||||
|
||||
// close resources
|
||||
this.closeResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* close resources
|
||||
*/
|
||||
private void closeResources() {
|
||||
try {
|
||||
T.IoUtil.close(outputStream);
|
||||
T.IoUtil.close(inputStream);
|
||||
if (process != null)
|
||||
process.destroy();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
84
src/main/java/net/geedge/api/config/VncProxyHandler.java
Normal file
84
src/main/java/net/geedge/api/config/VncProxyHandler.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package net.geedge.api.config;
|
||||
|
||||
import cn.hutool.log.Log;
|
||||
import net.geedge.api.entity.EnvApiYml;
|
||||
import net.geedge.common.T;
|
||||
import org.springframework.web.socket.BinaryMessage;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
public class VncProxyHandler extends TextWebSocketHandler {
|
||||
|
||||
private static final Log log = Log.get();
|
||||
|
||||
private EnvApiYml.Vnc vnc;
|
||||
|
||||
public VncProxyHandler(EnvApiYml.Vnc vnc) {
|
||||
this.vnc = vnc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
log.info("[afterConnectionEstablished] [WebSocket connection established] [websocket uri: {}]", session.getUri());
|
||||
super.afterConnectionEstablished(session);
|
||||
|
||||
// connect to VNC Server
|
||||
Socket vncSocket = new Socket(vnc.getHost(), vnc.getPort());
|
||||
session.getAttributes().put("vncSocket", vncSocket);
|
||||
log.info("[afterConnectionEstablished] [vnc server: {}] [isConnected: {}]", T.JSONUtil.toJsonStr(vnc), vncSocket.isConnected());
|
||||
|
||||
// vnc server -> web
|
||||
T.ThreadUtil.execute(() -> {
|
||||
this.forwardFromVncToWeb(session, vncSocket);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* vnc server -> web
|
||||
*
|
||||
* @param session
|
||||
* @param vncSocket
|
||||
*/
|
||||
private void forwardFromVncToWeb(WebSocketSession session, Socket vncSocket) {
|
||||
try (InputStream inputStream = vncSocket.getInputStream()) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
session.sendMessage(new BinaryMessage(buffer, 0, bytesRead, true));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error(e, "[forwardFromVncToWeb] [error]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
|
||||
try {
|
||||
// web -> vnc server
|
||||
Socket vncSocket = (Socket) session.getAttributes().get("vncSocket");
|
||||
if (vncSocket != null && !vncSocket.isClosed()) {
|
||||
OutputStream outputStream = vncSocket.getOutputStream();
|
||||
outputStream.write(message.getPayload().array());
|
||||
outputStream.flush();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error(e, "[handleBinaryMessage] [error]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||
log.info("[afterConnectionClosed] [WebSocket connection closed] [websocket uri: {}]", session.getUri());
|
||||
Socket vncSocket = (Socket) session.getAttributes().get("vncSocket");
|
||||
if (vncSocket != null && !vncSocket.isClosed()) {
|
||||
vncSocket.close();
|
||||
}
|
||||
super.afterConnectionClosed(session, status);
|
||||
}
|
||||
}
|
||||
22
src/main/java/net/geedge/api/config/WebSocketConfig.java
Normal file
22
src/main/java/net/geedge/api/config/WebSocketConfig.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package net.geedge.api.config;
|
||||
|
||||
import net.geedge.api.entity.EnvApiYml;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
@Autowired
|
||||
private EnvApiYml envApiYml;
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(new VncProxyHandler(envApiYml.getVnc()), "/api/v1/env/novnc").setAllowedOrigins("*");
|
||||
registry.addHandler(new AdbShellProxyHandler(envApiYml.getAdb()), "/api/v1/env/terminal").setAllowedOrigins("*");
|
||||
}
|
||||
}
|
||||
483
src/main/java/net/geedge/api/controller/APIController.java
Normal file
483
src/main/java/net/geedge/api/controller/APIController.java
Normal file
@@ -0,0 +1,483 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
34
src/main/java/net/geedge/api/entity/EnvApiYml.java
Normal file
34
src/main/java/net/geedge/api/entity/EnvApiYml.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package net.geedge.api.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class EnvApiYml {
|
||||
|
||||
private Env env;
|
||||
private Adb adb;
|
||||
private Vnc vnc;
|
||||
|
||||
@Data
|
||||
public class Env {
|
||||
String type;
|
||||
String platform;
|
||||
String root;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class Adb {
|
||||
String serial;
|
||||
String host;
|
||||
Integer port;
|
||||
|
||||
Integer vncPort;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class Vnc {
|
||||
String host;
|
||||
Integer port;
|
||||
}
|
||||
|
||||
}
|
||||
24
src/main/java/net/geedge/api/interceptor/APIInterceptor.java
Normal file
24
src/main/java/net/geedge/api/interceptor/APIInterceptor.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package net.geedge.api.interceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import net.geedge.api.util.AdbUtil;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
public class APIInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
AdbUtil adbUtil = AdbUtil.getInstance();
|
||||
if (!adbUtil.connect()) {
|
||||
// 记录日志或设置响应状态
|
||||
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "device connect error");
|
||||
return false;
|
||||
}
|
||||
|
||||
// init
|
||||
adbUtil.init(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
18
src/main/java/net/geedge/api/interceptor/WebConfig.java
Normal file
18
src/main/java/net/geedge/api/interceptor/WebConfig.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package net.geedge.api.interceptor;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* WebMvc配置
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new APIInterceptor()).addPathPatterns("/api/v1/env/**");
|
||||
}
|
||||
|
||||
}
|
||||
252
src/main/java/net/geedge/api/util/AdbCommandBuilder.java
Normal file
252
src/main/java/net/geedge/api/util/AdbCommandBuilder.java
Normal file
@@ -0,0 +1,252 @@
|
||||
package net.geedge.api.util;
|
||||
|
||||
import net.geedge.common.T;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class AdbCommandBuilder {
|
||||
|
||||
private final String adbPath;
|
||||
private final List<String> command;
|
||||
|
||||
public AdbCommandBuilder(String adbPath) {
|
||||
this.adbPath = adbPath;
|
||||
this.command = new LinkedList<>();
|
||||
this.command.add(adbPath);
|
||||
}
|
||||
|
||||
public static AdbCommandBuilder builder() {
|
||||
return new AdbCommandBuilder("adb");
|
||||
}
|
||||
|
||||
public static AdbCommandBuilder builder(String adbPath) {
|
||||
return new AdbCommandBuilder(adbPath);
|
||||
}
|
||||
|
||||
public AdbCommandBuilder serial(String serial) {
|
||||
this.command.add("-s");
|
||||
this.command.add(serial);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildConnectCommand(String host, Integer port) {
|
||||
this.command.add("connect");
|
||||
this.command.add(String.format("%s:%s", host, port));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildDevicesCommand() {
|
||||
this.command.add("devices");
|
||||
this.command.add("-l");
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildRootCommand() {
|
||||
this.command.add("root");
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildGetpropCommand() {
|
||||
this.command.add("shell");
|
||||
this.command.add("getprop");
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildWmSizeCommand() {
|
||||
this.command.add("shell");
|
||||
this.command.add("wm");
|
||||
this.command.add("size");
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildCheckRootCommand() {
|
||||
this.command.add("shell");
|
||||
this.command.add("ls");
|
||||
this.command.add("/data");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定 String cmd 执行,解决命令过长阅读性较差问题
|
||||
*/
|
||||
public AdbCommandBuilder buildShellCommand(String shellCmd) {
|
||||
String[] strings = T.CommandLineUtil.translateCommandline(shellCmd);
|
||||
for (String string : strings) {
|
||||
this.command.add(string);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildPushCommand(String local, String remote) {
|
||||
this.command.add("push");
|
||||
this.command.add(local);
|
||||
this.command.add(remote);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildPullCommand(String remote, String local) {
|
||||
this.command.add("pull");
|
||||
this.command.add(remote);
|
||||
this.command.add(local);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildLsDirCommand(String path) {
|
||||
this.command.add("shell");
|
||||
this.command.add("ls");
|
||||
this.command.add("-l");
|
||||
this.command.add(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildPmListPackagesCommand(String arg) {
|
||||
this.command.add("shell");
|
||||
this.command.add("pm");
|
||||
this.command.add("list");
|
||||
this.command.add("packages");
|
||||
if (T.StrUtil.isNotEmpty(arg)) {
|
||||
this.command.add(arg);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildCheckPackage(String packageName) {
|
||||
this.command.add("shell");
|
||||
this.command.add("pm");
|
||||
this.command.add("list");
|
||||
this.command.add("packages");
|
||||
this.command.add("-3");
|
||||
this.command.add(packageName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildFindPackageNameList() {
|
||||
this.command.add("shell");
|
||||
this.command.add("pm");
|
||||
this.command.add("list");
|
||||
this.command.add("packages");
|
||||
this.command.add("-3");
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildStartAPP(String packageName) {
|
||||
this.command.add("shell");
|
||||
this.command.add("monkey");
|
||||
this.command.add("-p");
|
||||
this.command.add(packageName);
|
||||
this.command.add("-c");
|
||||
this.command.add("android.intent.category.LAUNCHER");
|
||||
this.command.add("1");
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildMd5sumCommand(String path) {
|
||||
this.command.add("shell");
|
||||
this.command.add("md5sum");
|
||||
this.command.add(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildInstallCommand(String localFilePath, boolean isDebugApk, boolean isReInstall) {
|
||||
this.command.add("install");
|
||||
if (isDebugApk) {
|
||||
this.command.add("-d");
|
||||
}
|
||||
if (isReInstall) {
|
||||
this.command.add("-r");
|
||||
}
|
||||
this.command.add(localFilePath);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildUnInstallCommand(String packageName) {
|
||||
this.command.add("uninstall");
|
||||
this.command.add(packageName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildIptablesLnRulesCommand(String chainName) {
|
||||
this.command.add("shell");
|
||||
this.command.add("iptables");
|
||||
this.command.add("-nL");
|
||||
this.command.add(chainName);
|
||||
this.command.add("--line-numbers");
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildIptablesAddRuleCommand(String chainName, String protocol, String ip, String port) {
|
||||
this.command.add("shell");
|
||||
this.command.add("iptables");
|
||||
this.command.add("-A");
|
||||
this.command.add(chainName);
|
||||
this.command.add("-p");
|
||||
this.command.add(protocol);
|
||||
|
||||
if (T.StrUtil.isNotEmpty(ip)) {
|
||||
this.command.add("-d");
|
||||
this.command.add(ip);
|
||||
}
|
||||
|
||||
if (T.StrUtil.isNotEmpty(port) && !"all".equals(protocol)) {
|
||||
this.command.add("--dport");
|
||||
this.command.add(port);
|
||||
}
|
||||
|
||||
this.command.add("-j");
|
||||
this.command.add("ACCEPT");
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildIptablesDelRuleCommand(String chainName, String protocol, String ip, String port) {
|
||||
this.command.add("shell");
|
||||
this.command.add("iptables");
|
||||
this.command.add("-D");
|
||||
this.command.add(chainName);
|
||||
this.command.add("-p");
|
||||
this.command.add(protocol);
|
||||
|
||||
if (T.StrUtil.isNotEmpty(ip)) {
|
||||
this.command.add("-d");
|
||||
this.command.add(ip);
|
||||
}
|
||||
|
||||
if (T.StrUtil.isNotEmpty(port) && !"all".equals(protocol)) {
|
||||
this.command.add("--dport");
|
||||
this.command.add(port);
|
||||
}
|
||||
|
||||
this.command.add("-j");
|
||||
this.command.add("ACCEPT");
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildIptablesFlushRuleCommand(String chainName) {
|
||||
this.command.add("shell");
|
||||
this.command.add("iptables");
|
||||
this.command.add("-F");
|
||||
this.command.add(chainName);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public List<String> build() {
|
||||
return this.command;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildStopAppCommand(String packageName) {
|
||||
this.command.add("shell");
|
||||
this.command.add("am");
|
||||
this.command.add("force-stop");
|
||||
this.command.add(packageName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdbCommandBuilder buildClearAppDateCommand(String packageName) {
|
||||
this.command.add("shell");
|
||||
this.command.add("pm");
|
||||
this.command.add("clear");
|
||||
this.command.add(packageName);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
41
src/main/java/net/geedge/api/util/AdbDevice.java
Normal file
41
src/main/java/net/geedge/api/util/AdbDevice.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package net.geedge.api.util;
|
||||
|
||||
import lombok.Data;
|
||||
import net.geedge.common.T;
|
||||
|
||||
@Data
|
||||
public class AdbDevice implements Comparable<AdbDevice> {
|
||||
|
||||
private String serial;
|
||||
private boolean available;
|
||||
|
||||
|
||||
public AdbDevice(String line) {
|
||||
String[] array = line.split(" ");
|
||||
serial = array[0];
|
||||
|
||||
for (int i = 1; i < array.length; i++) {
|
||||
if (!T.StrUtil.isEmpty(array[i])) {
|
||||
available = "device".equals(array[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return available;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (object instanceof AdbDevice)
|
||||
return serial.equals(((AdbDevice) object).serial) && available == ((AdbDevice) object).available;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(AdbDevice device) {
|
||||
return serial.compareTo(device.serial);
|
||||
}
|
||||
|
||||
}
|
||||
947
src/main/java/net/geedge/api/util/AdbUtil.java
Normal file
947
src/main/java/net/geedge/api/util/AdbUtil.java
Normal file
@@ -0,0 +1,947 @@
|
||||
package net.geedge.api.util;
|
||||
|
||||
import cn.hutool.core.codec.Base32Codec;
|
||||
import cn.hutool.core.thread.NamedThreadFactory;
|
||||
import cn.hutool.log.Log;
|
||||
import net.geedge.api.entity.EnvApiYml;
|
||||
import net.geedge.common.APIException;
|
||||
import net.geedge.common.Constant;
|
||||
import net.geedge.common.RCode;
|
||||
import net.geedge.common.T;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AdbUtil {
|
||||
|
||||
private static final Log log = Log.get();
|
||||
|
||||
private static AdbUtil instance;
|
||||
|
||||
private static String DEFAULT_DROIDVNC_NG_PKG_NAME = "net.christianbeier.droidvnc_ng";
|
||||
private static String DEFAULT_DROIDVNC_NG_APK_PATH = "./lib/droidvnc-np-2.6.0.apk";
|
||||
private static String DEFAULT_DROIDVNC_NG_DEFAULTS_JSON_PATH = "./lib/droidvnc-np-defaults.json";
|
||||
private static String ANDROID_LAUNCHER = "./lib/android-launcher.py";
|
||||
|
||||
private String serial;
|
||||
private String host;
|
||||
private Integer port;
|
||||
|
||||
private Integer vncPort;
|
||||
|
||||
private CommandExec commandExec;
|
||||
|
||||
private boolean interrupt;
|
||||
|
||||
private ExecutorService threadPool;
|
||||
|
||||
|
||||
public String getSerial() {
|
||||
return T.StrUtil.isNotEmpty(this.serial) ? serial : String.format("%s:%s", this.host, this.port);
|
||||
}
|
||||
|
||||
public void setInterrupt(boolean interrupt) {
|
||||
this.interrupt = interrupt;
|
||||
}
|
||||
|
||||
public record CommandResult(Integer exitCode, String output) {
|
||||
}
|
||||
|
||||
public AdbUtil(EnvApiYml.Adb adb, CommandExec commandExec) {
|
||||
this.serial = T.StrUtil.emptyToDefault(adb.getSerial(), "");
|
||||
this.host = adb.getHost();
|
||||
this.port = adb.getPort();
|
||||
this.vncPort = adb.getVncPort();
|
||||
this.commandExec = commandExec;
|
||||
// adb connect
|
||||
if (!this.connect()) {
|
||||
log.error("[connect error, program exit]");
|
||||
Runtime.getRuntime().halt(1);
|
||||
}
|
||||
// init
|
||||
this.init(true);
|
||||
}
|
||||
|
||||
public static AdbUtil getInstance() {
|
||||
if (instance == null) {
|
||||
throw new IllegalArgumentException("Object has not been instantiated.");
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static AdbUtil getInstance(EnvApiYml.Adb connInfo, CommandExec commandExec) {
|
||||
if (instance == null) {
|
||||
synchronized (AdbUtil.class) {
|
||||
if (instance == null) {
|
||||
instance = new AdbUtil(connInfo, commandExec);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* connect
|
||||
*/
|
||||
public boolean connect() {
|
||||
if (T.StrUtil.isNotEmpty(this.serial)) {
|
||||
// local
|
||||
AdbDevice adbDevice = this.getAdbDevice();
|
||||
log.info("[connect] [result: {}]", T.JSONUtil.toJsonStr(adbDevice));
|
||||
if (null == adbDevice || !adbDevice.isAvailable()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// remote
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.buildConnectCommand(this.host, this.port)
|
||||
.build());
|
||||
log.info("[connect] [result: {}]", result);
|
||||
if (!T.StrUtil.contains(result, "connected")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* init
|
||||
* su root
|
||||
* install droidVNC NG
|
||||
*/
|
||||
public void init(boolean install) {
|
||||
// adb root
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildRootCommand()
|
||||
.build()
|
||||
);
|
||||
log.info("[init] [adb root] [result: {}]", result);
|
||||
|
||||
if (install) {
|
||||
// install droidVNC NG
|
||||
CommandResult installed = this.install(DEFAULT_DROIDVNC_NG_APK_PATH, true, true);
|
||||
log.info("[init] [install droidVNC NG] [result: {}]", installed);
|
||||
|
||||
// 上传默认配置
|
||||
this.execShellCommand("shell mkdir -p /storage/emulated/0/Android/data/net.christianbeier.droidvnc_ng/files");
|
||||
this.push(DEFAULT_DROIDVNC_NG_DEFAULTS_JSON_PATH, "/storage/emulated/0/Android/data/net.christianbeier.droidvnc_ng/files/defaults.json");
|
||||
|
||||
// 无障碍权限
|
||||
this.execShellCommand("shell settings put secure enabled_accessibility_services net.christianbeier.droidvnc_ng/.InputService:$(settings get secure enabled_accessibility_services)");
|
||||
// 存储空间权限
|
||||
this.execShellCommand("shell pm grant net.christianbeier.droidvnc_ng android.permission.WRITE_EXTERNAL_STORAGE");
|
||||
// 屏幕录制权限
|
||||
this.execShellCommand("shell appops set net.christianbeier.droidvnc_ng PROJECT_MEDIA allow");
|
||||
|
||||
// ACTION_STOP
|
||||
this.execShellCommand("shell am start-foreground-service -n net.christianbeier.droidvnc_ng/.MainService -a net.christianbeier.droidvnc_ng.ACTION_STOP --es net.christianbeier.droidvnc_ng.EXTRA_ACCESS_KEY d042e2b5d5f348588a4e1a243eb7a9a0");
|
||||
}
|
||||
|
||||
// ACTION_START
|
||||
this.execShellCommand("shell am start-foreground-service -n net.christianbeier.droidvnc_ng/.MainService -a net.christianbeier.droidvnc_ng.ACTION_START --es net.christianbeier.droidvnc_ng.EXTRA_ACCESS_KEY d042e2b5d5f348588a4e1a243eb7a9a0");
|
||||
|
||||
// 添加自定义链
|
||||
this.addAswOutputChain();
|
||||
}
|
||||
|
||||
/**
|
||||
* status
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<Object, Object> status() {
|
||||
Map<Object, Object> m = T.MapUtil.builder()
|
||||
.put("platform", "android")
|
||||
.build();
|
||||
AdbDevice device = this.getAdbDevice();
|
||||
m.put("status", device.isAvailable() ? "online" : "offline");
|
||||
|
||||
Map<String, String> prop = this.getProp();
|
||||
m.put("name", T.MapUtil.getStr(prop, "ro.product.name", ""));
|
||||
m.put("brand", T.MapUtil.getStr(prop, "ro.product.brand", ""));
|
||||
m.put("model", T.MapUtil.getStr(prop, "ro.product.model", ""));
|
||||
m.put("version", T.MapUtil.getStr(prop, "ro.build.version.release", ""));
|
||||
m.put("resolution", T.MapUtil.getStr(prop, "wm.size", ""));
|
||||
|
||||
// 默认为真机
|
||||
String type = "device";
|
||||
for (Map.Entry<String, String> entry : prop.entrySet()) {
|
||||
// 根据 ro.build.* 这一组配置值判定是否为模拟器,如果包含 emulator、sdk 则为模拟器
|
||||
if (entry.getKey().contains("ro.build")) {
|
||||
String value = entry.getValue();
|
||||
if (T.StrUtil.containsAnyIgnoreCase(value, "emulator", "sdk")) {
|
||||
type = "emulator";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m.put("type", type);
|
||||
|
||||
// check root
|
||||
String checkRootResult = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildCheckRootCommand()
|
||||
.build()
|
||||
);
|
||||
m.put("root", !T.StrUtil.containsIgnoreCase(checkRootResult, "Permission denied"));
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* getAdbDevice
|
||||
* adb devices -l
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private AdbDevice getAdbDevice() {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.buildDevicesCommand()
|
||||
.build()
|
||||
);
|
||||
List<AdbDevice> list = T.ListUtil.list(true);
|
||||
String[] lines = result.split("\\n");
|
||||
for (String line : lines) {
|
||||
if (line.startsWith("*") || line.startsWith("List") || T.StrUtil.isEmpty(line))
|
||||
continue;
|
||||
list.add(new AdbDevice(line));
|
||||
}
|
||||
AdbDevice adbDevice = list.stream()
|
||||
.filter(pojo -> T.StrUtil.equals(pojo.getSerial(), this.getSerial()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
return adbDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* getProp
|
||||
* adb shell getprop
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private Map<String, String> getProp() {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildGetpropCommand()
|
||||
.build()
|
||||
);
|
||||
Map<String, String> prop = new LinkedHashMap<>();
|
||||
|
||||
Pattern pattern = Pattern.compile("\\[(.*?)\\]: \\[(.*?)\\]");
|
||||
Matcher matcher = pattern.matcher(result);
|
||||
|
||||
while (matcher.find()) {
|
||||
String key = matcher.group(1).trim();
|
||||
String value = matcher.group(2).trim();
|
||||
prop.put(key, value);
|
||||
}
|
||||
|
||||
// 分辨率 Physical size: 1440x3040
|
||||
String wmSize = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildWmSizeCommand()
|
||||
.build()
|
||||
);
|
||||
prop.put("wm.size", T.StrUtil.trim(wmSize.replaceAll("Physical size: ", "")));
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* md5sum
|
||||
*/
|
||||
private CommandResult md5sum(String path) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildMd5sumCommand(path)
|
||||
.build()
|
||||
);
|
||||
log.info("[md5sum] [path: {}] [result: {}]", path, result);
|
||||
if (T.StrUtil.isNotEmpty(result)) {
|
||||
String md5 = result.split("\\s+")[0];
|
||||
return new CommandResult(0, md5);
|
||||
}
|
||||
return new CommandResult(1, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* push
|
||||
* 0 success; !0 failed
|
||||
*/
|
||||
public CommandResult push(String local, String remote) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildPushCommand(local, remote)
|
||||
.build()
|
||||
);
|
||||
log.info("[push] [local: {}] [remote: {}] [result: {}]", local, remote, result);
|
||||
return new CommandResult(T.StrUtil.contains(result, "failed") ? 1 : 0, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* pull
|
||||
* 0 success; !0 failed
|
||||
*/
|
||||
public CommandResult pull(String remote, String local) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildPullCommand(remote, local)
|
||||
.build()
|
||||
);
|
||||
log.info("[pull] [remote: {}] [local: {}] [result: {}]", remote, local, result);
|
||||
return new CommandResult(T.StrUtil.containsAny(result, "file pulled", "files pulled") ? 0 : 1, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* list dir
|
||||
* ls -l
|
||||
* stat filename
|
||||
*/
|
||||
public List<Map> listDir(String path) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildLsDirCommand(path)
|
||||
.build()
|
||||
);
|
||||
if (T.StrUtil.contains(result, "No such file or directory")) {
|
||||
log.warn("[listDir] [path: {}] [result: {}]", path, result);
|
||||
throw new APIException(RCode.NOT_EXISTS);
|
||||
}
|
||||
|
||||
if (T.StrUtil.contains(result, "Permission denied")) {
|
||||
log.warn("[listDir] [path: {}] [result: {}]", path, result);
|
||||
throw new APIException(RCode.NOT_PERMISSION);
|
||||
}
|
||||
|
||||
List<CompletableFuture<Map>> futureList = T.ListUtil.list(false);
|
||||
|
||||
List<Map> listDir = T.ListUtil.list(true);
|
||||
String[] lines = result.split("\\n");
|
||||
boolean isDir = false;
|
||||
for (String line : lines) {
|
||||
if (line.startsWith("total")) {
|
||||
isDir = true;
|
||||
continue;
|
||||
}
|
||||
String[] split = line.split("\\s+");
|
||||
String name;
|
||||
// link file|dir
|
||||
if (10 == split.length) {
|
||||
name = split[7];
|
||||
} else {
|
||||
name = split[split.length - 1];
|
||||
}
|
||||
|
||||
String statFilePath = isDir ? Paths.get(path).resolve(name).toString() : path;
|
||||
String statCommand = "shell stat -c \"'%N %A %g %u %s %a %X %Y'\" " + statFilePath;
|
||||
futureList.add(
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
String statResult = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand(statCommand.replaceAll("\\\\", "/"))
|
||||
.build()
|
||||
);
|
||||
// reverse result
|
||||
List<String> list = Arrays.asList(statResult.split("\\s+"));
|
||||
Collections.reverse(list);
|
||||
|
||||
String fullName = list.get(7).replaceAll("'|`", "");
|
||||
Map<String, Object> relMap = T.MapUtil.newHashMap();
|
||||
relMap.put("name", name);
|
||||
relMap.put("value", T.MapUtil.builder()
|
||||
.put("id", Base32Codec.Base32Encoder.ENCODER.encode(fullName.getBytes()))
|
||||
.put("fullName", fullName)
|
||||
.put("gid", Long.parseLong(list.get(5)))
|
||||
.put("uid", Long.parseLong(list.get(4)))
|
||||
.put("size", Long.parseLong(list.get(3)))
|
||||
.put("permissions", Long.parseLong(list.get(2)))
|
||||
.put("cts", Long.parseLong(list.get(1)))
|
||||
.put("uts", Long.parseLong(list.get(0)))
|
||||
.put("isDir", list.get(6).startsWith("d"))
|
||||
.put("isBlk", list.get(6).startsWith("b"))
|
||||
.put("isFifo", list.get(6).startsWith("p"))
|
||||
.put("isLink", list.get(6).startsWith("l"))
|
||||
.put("isReg", list.get(6).startsWith("-"))
|
||||
.put("isSock", list.get(6).startsWith("s"))
|
||||
.build());
|
||||
return relMap;
|
||||
}, getThreadPool())
|
||||
);
|
||||
|
||||
Map<Object, Object> m = T.MapUtil.builder()
|
||||
.put("name", name)
|
||||
.build();
|
||||
listDir.add(m);
|
||||
}
|
||||
|
||||
try {
|
||||
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get();
|
||||
futureList.forEach(f -> {
|
||||
Map map = f.getNow(null);
|
||||
if (T.MapUtil.isNotEmpty(map)) {
|
||||
String name = T.MapUtil.getStr(map, "name");
|
||||
Map fileAttr = listDir.stream().filter(m -> T.MapUtil.getStr(m, "name").equals(name)).findFirst().get();
|
||||
fileAttr.putAll(T.MapUtil.get(map, "value", Map.class));
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.warn(e);
|
||||
}
|
||||
return listDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* listApp
|
||||
* adb shell pm list packages
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<Map> listApp(String arg) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildPmListPackagesCommand(arg)
|
||||
.build()
|
||||
);
|
||||
|
||||
List<Map> listApp = T.ListUtil.list(true);
|
||||
|
||||
List<CompletableFuture<Map>> futureList = T.ListUtil.list(false);
|
||||
|
||||
String prefix = "package:";
|
||||
String[] lines = result.split("\\n");
|
||||
for (String line : lines) {
|
||||
String packageName = T.StrUtil.trim(line.substring(prefix.length()));
|
||||
if (T.StrUtil.equals(DEFAULT_DROIDVNC_NG_PKG_NAME, packageName)) continue;
|
||||
|
||||
String dumpsysResult = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand("shell dumpsys package " + packageName)
|
||||
.build()
|
||||
);
|
||||
String[] split = dumpsysResult.split("\\n");
|
||||
String version = "", apkPath = "";
|
||||
for (String s : split) {
|
||||
if (s.contains("versionName=")) {
|
||||
version = T.StrUtil.trim(s).replaceAll("versionName=", "");
|
||||
}
|
||||
if (s.contains("path: ")) {
|
||||
apkPath = T.StrUtil.trim(s).replaceAll("path: ", "");
|
||||
}
|
||||
}
|
||||
if (T.StrUtil.isNotEmpty(apkPath)) {
|
||||
String finalApkPath = apkPath;
|
||||
futureList.add(
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
CommandResult md5sumRes = this.md5sum(finalApkPath);
|
||||
String md5Value = md5sumRes.output();
|
||||
File localApk = T.FileUtil.file(Constant.TEMP_PATH, md5Value + ".apk");
|
||||
if (!T.FileUtil.exist(localApk)) {
|
||||
CommandResult pulled = this.pull(finalApkPath, localApk.getAbsolutePath());
|
||||
if (0 != pulled.exitCode()) {
|
||||
log.warn("[listApp] [pull apk error] [pkg: {}]", packageName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
ApkUtil apkUtil = new ApkUtil();
|
||||
ApkInfo apkInfo = apkUtil.parseApk(localApk.getAbsolutePath());
|
||||
String appName = apkInfo.getLabel();
|
||||
// String iconFilename = apkInfo.getIcon();
|
||||
// String base64IconDate = apkUtil.extractFileFromApk(localApk.getAbsolutePath(), iconFilename);
|
||||
// if (T.StrUtil.isNotEmpty(base64IconDate)) {
|
||||
// base64IconDate = "data:image/jpeg;base64," + base64IconDate;
|
||||
// }
|
||||
|
||||
Map<String, Object> relMap = T.MapUtil.newHashMap();
|
||||
relMap.put("pkg", packageName);
|
||||
relMap.put("value", T.MapUtil.builder()
|
||||
.put("name", appName)
|
||||
// .put("icon", base64IconDate)
|
||||
.build());
|
||||
return relMap;
|
||||
} catch (Exception e) {
|
||||
log.error(e, "[listApp] [parse apk] [pkg: {}]", packageName);
|
||||
}
|
||||
return null;
|
||||
}, getThreadPool())
|
||||
);
|
||||
}
|
||||
|
||||
Map<Object, Object> m = T.MapUtil.builder()
|
||||
.put("packageName", packageName)
|
||||
.put("version", version)
|
||||
.build();
|
||||
listApp.add(m);
|
||||
}
|
||||
|
||||
try {
|
||||
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get();
|
||||
futureList.forEach(f -> {
|
||||
Map map = f.getNow(null);
|
||||
if (T.MapUtil.isNotEmpty(map)) {
|
||||
String pkg = T.MapUtil.getStr(map, "pkg");
|
||||
Map appAttr = listApp.stream().filter(m -> T.MapUtil.getStr(m, "packageName").equals(pkg)).findFirst().get();
|
||||
appAttr.putAll(T.MapUtil.get(map, "value", Map.class));
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.warn(e);
|
||||
}
|
||||
return listApp;
|
||||
}
|
||||
|
||||
/**
|
||||
* install app
|
||||
* adb install apk
|
||||
*/
|
||||
public CommandResult install(String localFilePath, boolean isDebugApk, boolean isReInstall) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildInstallCommand(localFilePath, isDebugApk, isReInstall)
|
||||
.build());
|
||||
|
||||
log.info("[install] [localFilePath: {}] [isDebugApk: {}] [isReInstall: {}] [result: {}]", localFilePath, isDebugApk, isReInstall, result);
|
||||
return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* uninstall app
|
||||
* adb uninstall package_name
|
||||
*/
|
||||
public CommandResult uninstall(String packageName) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildUnInstallCommand(packageName)
|
||||
.build()
|
||||
);
|
||||
log.info("[uninstall] [packageName: {}] [result: {}]", packageName, result);
|
||||
return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* stop app
|
||||
* adb shell am force-stop package_name
|
||||
*/
|
||||
public CommandResult stopApp(String packageName) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildStopAppCommand(packageName)
|
||||
.build()
|
||||
);
|
||||
log.info("[stopApp] [packageName: {}] [result: {}]", packageName, result);
|
||||
return new CommandResult(T.StrUtil.isEmpty(result) ? 0 : 1, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* iptables -F
|
||||
* iptables -X
|
||||
*/
|
||||
@Deprecated
|
||||
private void cleanIptables() {
|
||||
// Delete all rules in chain or all chains
|
||||
commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand("shell iptables -F")
|
||||
.build()
|
||||
);
|
||||
// Delete user-defined chain
|
||||
commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand("shell iptables -X")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* list tcpdump
|
||||
*/
|
||||
public List<Map> 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<Map> 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<Object, Object> 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
|
||||
* tcpdump pcap
|
||||
*/
|
||||
public CommandResult startTcpdump(String packageName) {
|
||||
String taskId = T.IdUtil.fastSimpleUUID();
|
||||
if (T.StrUtil.isNotEmpty(packageName)) {
|
||||
log.info("[startTcpdump] [capture app package] [pkg: {}]", packageName);
|
||||
String dumpsysResult = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand("shell dumpsys package " + packageName)
|
||||
.build()
|
||||
);
|
||||
String[] lines = dumpsysResult.split("\\n");
|
||||
String userId = Arrays.stream(lines)
|
||||
.filter(s -> T.StrUtil.contains(s, "userId="))
|
||||
.findFirst()
|
||||
.map(s -> T.StrUtil.trim(s).replaceAll("userId=", ""))
|
||||
.orElseThrow(() -> new APIException("Not found userId by package name. package name: " + packageName));
|
||||
|
||||
commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand(String.format("shell iptables -A 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 -A 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 -A OUTPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId))
|
||||
.build());
|
||||
|
||||
String ruleList = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand("shell iptables -L")
|
||||
.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 '(tcp or udp or icmp) and not port %s' -w %s &\"", this.vncPort, pcapFilePath))
|
||||
.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}' \"", taskId))
|
||||
.build());
|
||||
log.info("[startTcpdump] [taskId: {}] [tcpdump pid: {}]", taskId, result);
|
||||
return new CommandResult(T.StrUtil.isNotEmpty(result) ? 0 : 1, taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* stop tcpdump
|
||||
* 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());
|
||||
|
||||
// 等待 tcpdump 资源释放,避免出现错误:The capture file appears to have been cut short in the middle of a packet.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
T.ThreadUtil.sleep(500);
|
||||
|
||||
String str = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s \"", id))
|
||||
.build());
|
||||
log.info("[stopTcpdump] [id: {}] [is running: {}]", id, str);
|
||||
if (T.StrUtil.isEmpty(str)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[stopTcpdump] [id: {}] [pcapFilePath: {}] [result: {}]", id, pcapFilePath, result);
|
||||
if (T.StrUtil.isEmpty(result)) {
|
||||
return new CommandResult(0, pcapFilePath);
|
||||
}
|
||||
return new CommandResult(1, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* exec shell command
|
||||
*/
|
||||
public void execShellCommand(String shellCmd) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand(shellCmd)
|
||||
.build());
|
||||
log.info("[execShellCommand] [shellCmd: {}] [result: {}]", shellCmd, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* exec shell command
|
||||
*/
|
||||
public String execShellCommand(String cmd, Integer timeout){
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand("shell " + cmd)
|
||||
.build());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 添加自定义链
|
||||
* 2. 自定义链添加到 OUTPUT 链中
|
||||
*/
|
||||
private void addAswOutputChain() {
|
||||
// name=ASW_OUTPUT
|
||||
this.execShellCommand("shell iptables -N ASW_OUTPUT");
|
||||
|
||||
String outputChainResult = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand(String.format("shell \"iptables -L OUTPUT --line-numbers | grep ASW_OUTPUT\""))
|
||||
.build());
|
||||
log.info("[addAswOutputChain] [ASW_OUTPUT in OUTPUT Chain] [exist: {}]", T.StrUtil.isEmpty(outputChainResult));
|
||||
if (T.StrUtil.isEmpty(outputChainResult)) {
|
||||
// ASW_OUTPUT 添加到 OUTPUT 链中
|
||||
this.execShellCommand("shell iptables -A OUTPUT -j ASW_OUTPUT");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ASW_OUTPUT chain rules
|
||||
* iptables -nL ASW_OUTPUT --line-numbers
|
||||
*/
|
||||
public List<Map> listAcl() {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildIptablesLnRulesCommand("ASW_OUTPUT")
|
||||
.build());
|
||||
|
||||
List<Map> chainList = T.ListUtil.list(true);
|
||||
|
||||
String[] lines = result.split("\\n");
|
||||
for (String line : lines) {
|
||||
String[] split = line.split("\\s+");
|
||||
|
||||
String chainIndex = T.StrUtil.trim(split[0]);
|
||||
|
||||
if (T.StrUtil.isNumeric(chainIndex)) {
|
||||
String protocol = T.StrUtil.trim(split[2]);
|
||||
Map<Object, Object> m = T.MapUtil.builder()
|
||||
.put("num", Integer.valueOf(chainIndex))
|
||||
.put("protocol", protocol)
|
||||
.build();
|
||||
|
||||
String destIp = T.StrUtil.trim(split[5]);
|
||||
if (!T.StrUtil.equals("0.0.0.0/0", destIp)) {
|
||||
m.put("ip", destIp);
|
||||
}
|
||||
|
||||
if (split.length == 8) {
|
||||
String dpt = T.StrUtil.trim(split[7]);
|
||||
dpt = dpt.replaceAll("dpt:", "");
|
||||
if (T.StrUtil.isNumeric(chainIndex)) {
|
||||
m.put("port", Integer.valueOf(dpt));
|
||||
}
|
||||
}
|
||||
chainList.add(m);
|
||||
}
|
||||
}
|
||||
return chainList;
|
||||
}
|
||||
|
||||
/**
|
||||
* add chain rule
|
||||
* iptables -A ASW_OUTPUT -p prot -d ip --dport port -j ACCEPT
|
||||
*/
|
||||
public void addAcl(String protocol, String ip, String port) {
|
||||
// add chain
|
||||
this.addAswOutputChain();
|
||||
|
||||
// add chain ruls
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildIptablesAddRuleCommand("ASW_OUTPUT", protocol, ip, port)
|
||||
.build());
|
||||
log.info("[addAcl] [protocol: {}] [ip: {}] [port: {}] [result: {}]", protocol, ip, port, result);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* del chain rule
|
||||
* iptables -D ASW_OUTPUT -p prot -d ip --dport port -j ACCEPT
|
||||
*/
|
||||
public void deleteAcl(String protocol, String ip, String port) {
|
||||
// add chain ruls
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildIptablesDelRuleCommand("ASW_OUTPUT", protocol, ip, port)
|
||||
.build());
|
||||
log.info("[deleteAcl] [protocol: {}] [ip: {}] [port: {}] [result: {}]", protocol, ip, port, result);
|
||||
}
|
||||
|
||||
public CommandResult clearAppData(String packageName) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildClearAppDateCommand(packageName)
|
||||
.build());
|
||||
log.info("[clearAppData] [packageName: {}]", packageName);
|
||||
return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* flushAcl
|
||||
* iptables -F ASW_OUTPUT
|
||||
*/
|
||||
public CommandResult flushAcl() {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildIptablesFlushRuleCommand("ASW_OUTPUT")
|
||||
.build());
|
||||
log.info("[flushAcl] [result: {}]", result);
|
||||
return new CommandResult(T.StrUtil.isNotEmpty(result) ? 1 : 0, result);
|
||||
}
|
||||
|
||||
|
||||
public boolean findPackageInstall(String packageName) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildCheckPackage(packageName)
|
||||
.build()
|
||||
);
|
||||
|
||||
return T.StrUtil.equals(result, packageName);
|
||||
}
|
||||
|
||||
public CommandResult startApp(String packageName) {
|
||||
String result = commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildStartAPP(packageName)
|
||||
.build()
|
||||
);
|
||||
log.info("[startApp] [result: {}]", result);
|
||||
return new CommandResult(T.StrUtil.isNotEmpty(result) ? 1 : 0, result);
|
||||
}
|
||||
|
||||
|
||||
public void closeOtherApps() {
|
||||
commandExec.exec(AdbCommandBuilder.builder()
|
||||
.serial(this.getSerial())
|
||||
.buildShellCommand(String.format("shell 'for app in $(pm list packages -3 | grep -v \"net.christianbeier.droidvnc_ng\" | cut -f 2 -d \":\"); do am force-stop $app; done'"))
|
||||
.build());
|
||||
}
|
||||
|
||||
private synchronized ExecutorService getThreadPool() {
|
||||
if (threadPool == null) {
|
||||
threadPool = new ThreadPoolExecutor(
|
||||
5,
|
||||
10,
|
||||
30,
|
||||
TimeUnit.SECONDS,
|
||||
new ArrayBlockingQueue<Runnable>(10000),
|
||||
new NamedThreadFactory("API-", true));
|
||||
}
|
||||
return threadPool;
|
||||
}
|
||||
|
||||
|
||||
public CommandResult execPlaybook(String scriptPath, String tid, String packageName, String type, File logFile) {
|
||||
log.info("[execPlaybook] [begin!] [serial:{}]", this.getSerial());
|
||||
List<String> command;
|
||||
File environment = null;
|
||||
if (T.StrUtil.equals(type, "python")){
|
||||
// check requirements.txt
|
||||
Path parent = Paths.get(scriptPath).getParent();
|
||||
Path resolve = parent.resolve("requirements.txt");
|
||||
command = new PythonCommandBuilder("python")
|
||||
.buildRunPythonScript(scriptPath, tid, packageName, this.getSerial())
|
||||
.build();
|
||||
if (T.FileUtil.exist(resolve.toString())){
|
||||
// create venv
|
||||
environment = T.FileUtil.file(parent.toString(), "environment");
|
||||
commandExec.exec(new PythonCommandBuilder("python").buildCreateVenv(environment.getAbsolutePath()).build());
|
||||
commandExec.exec(new PythonCommandBuilder(T.StrUtil.concat(true, environment.getAbsolutePath(), "/bin/pip")).buildUpgradePip().build());
|
||||
commandExec.exec(new PythonCommandBuilder(T.StrUtil.concat(true, environment.getAbsolutePath(), "/bin/pip")).buildInstallRequirements(resolve.toString()).build());
|
||||
command = new PythonCommandBuilder(T.StrUtil.concat(true, environment.getAbsolutePath(), "/bin/python"))
|
||||
.buildRunPythonScript(scriptPath, tid, packageName, this.getSerial())
|
||||
.build();
|
||||
}
|
||||
}else {
|
||||
command = new PythonCommandBuilder("python")
|
||||
.buildRunAirScript(ANDROID_LAUNCHER, scriptPath, tid, packageName, this.getSerial())
|
||||
.build();
|
||||
}
|
||||
Process process = commandExec.execForProcess(command);
|
||||
|
||||
T.FileUtil.appendString(T.StrUtil.concat(true, "$ ", command.stream().collect(Collectors.joining(" ")), "\n"), logFile, "UTF-8");
|
||||
InputStreamReader inputStreamReader = null;
|
||||
BufferedReader bufferedReader = null;
|
||||
try {
|
||||
inputStreamReader = new InputStreamReader(process.getInputStream(), "UTF-8");
|
||||
bufferedReader = new BufferedReader(inputStreamReader);
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (T.ObjectUtil.isNotNull(interrupt) && interrupt){
|
||||
log.info("[PlaybookRunnable] [execPlaybook] [stop exec playbook]");
|
||||
process.destroyForcibly();
|
||||
}
|
||||
// 处理每一行输出
|
||||
T.FileUtil.appendString(T.StrUtil.concat(true, line, "\n"), logFile, "UTF-8");
|
||||
}
|
||||
int exitCode = process.waitFor();
|
||||
return new CommandResult(exitCode, T.StrUtil.EMPTY);
|
||||
} catch (Exception e) {
|
||||
process.destroyForcibly();
|
||||
throw new APIException(RCode.ERROR);
|
||||
}finally {
|
||||
T.FileUtil.del(environment);
|
||||
T.IoUtil.close(inputStreamReader);
|
||||
T.IoUtil.close(bufferedReader);
|
||||
}
|
||||
}
|
||||
}
|
||||
169
src/main/java/net/geedge/api/util/ApkInfo.java
Normal file
169
src/main/java/net/geedge/api/util/ApkInfo.java
Normal file
@@ -0,0 +1,169 @@
|
||||
package net.geedge.api.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ApkInfo {
|
||||
|
||||
public static final String APPLICATION_ICON_120 = "application-icon-120";
|
||||
public static final String APPLICATION_ICON_160 = "application-icon-160";
|
||||
public static final String APPLICATION_ICON_240 = "application-icon-240";
|
||||
public static final String APPLICATION_ICON_320 = "application-icon-320";
|
||||
|
||||
// 所需设备属性
|
||||
private List<String> features;
|
||||
// 图标
|
||||
private String icon;
|
||||
// 各分辨率下图标路径
|
||||
private Map<String, String> icons;
|
||||
// 应用程序名
|
||||
private String label;
|
||||
// 入口Activity
|
||||
private String launchableActivity;
|
||||
// 支持的Android平台最低版本号
|
||||
private String minSdkVersion;
|
||||
// 主包名
|
||||
private String packageName;
|
||||
// 支持的SDK版本
|
||||
private String sdkVersion;
|
||||
// Apk文件大小(字节)
|
||||
private long size;
|
||||
// 目标SDK版本
|
||||
private String targetSdkVersion;
|
||||
// 所需权限
|
||||
private List<String> usesPermissions;
|
||||
// 内部版本号
|
||||
private String versionCode;
|
||||
// 外部版本号
|
||||
private String versionName;
|
||||
|
||||
public ApkInfo() {
|
||||
this.features = new ArrayList<>();
|
||||
this.icons = new HashMap<>();
|
||||
this.usesPermissions = new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<String> getFeatures() {
|
||||
return features;
|
||||
}
|
||||
|
||||
public void setFeatures(List<String> features) {
|
||||
this.features = features;
|
||||
}
|
||||
|
||||
public void addToFeatures(String feature) {
|
||||
this.features.add(feature);
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(String icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public Map<String, String> getIcons() {
|
||||
return icons;
|
||||
}
|
||||
|
||||
public void setIcons(Map<String, String> icons) {
|
||||
this.icons = icons;
|
||||
}
|
||||
|
||||
public void addToIcons(String key, String value) {
|
||||
this.icons.put(key, value);
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getLaunchableActivity() {
|
||||
return launchableActivity;
|
||||
}
|
||||
|
||||
public void setLaunchableActivity(String launchableActivity) {
|
||||
this.launchableActivity = launchableActivity;
|
||||
}
|
||||
|
||||
public String getMinSdkVersion() {
|
||||
return minSdkVersion;
|
||||
}
|
||||
|
||||
public void setMinSdkVersion(String minSdkVersion) {
|
||||
this.minSdkVersion = minSdkVersion;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public String getSdkVersion() {
|
||||
return sdkVersion;
|
||||
}
|
||||
|
||||
public void setSdkVersion(String sdkVersion) {
|
||||
this.sdkVersion = sdkVersion;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getTargetSdkVersion() {
|
||||
return targetSdkVersion;
|
||||
}
|
||||
|
||||
public void setTargetSdkVersion(String targetSdkVersion) {
|
||||
this.targetSdkVersion = targetSdkVersion;
|
||||
}
|
||||
|
||||
public List<String> getUsesPermissions() {
|
||||
return usesPermissions;
|
||||
}
|
||||
|
||||
public void setUsesPermissions(List<String> usesPermissions) {
|
||||
this.usesPermissions = usesPermissions;
|
||||
}
|
||||
|
||||
public void addToUsesPermissions(String usesPermission) {
|
||||
this.usesPermissions.add(usesPermission);
|
||||
}
|
||||
|
||||
public String getVersionCode() {
|
||||
return versionCode;
|
||||
}
|
||||
|
||||
public void setVersionCode(String versionCode) {
|
||||
this.versionCode = versionCode;
|
||||
}
|
||||
|
||||
public String getVersionName() {
|
||||
return versionName;
|
||||
}
|
||||
|
||||
public void setVersionName(String versionName) {
|
||||
this.versionName = versionName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ApkInfo [features=" + features + ", icon=" + icon + ", icons=" + icons + ", label=" + label + ", launchableActivity=" + launchableActivity + ", minSdkVersion=" + minSdkVersion + ", packageName=" + packageName + ", sdkVersion=" + sdkVersion + ", size=" + size + ", targetSdkVersion=" + targetSdkVersion + ", usesPermissions=" + usesPermissions + ", versionCode=" + versionCode + ", versionName=" + versionName + "]";
|
||||
}
|
||||
|
||||
}
|
||||
145
src/main/java/net/geedge/api/util/ApkUtil.java
Normal file
145
src/main/java/net/geedge/api/util/ApkUtil.java
Normal file
@@ -0,0 +1,145 @@
|
||||
package net.geedge.api.util;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.log.Log;
|
||||
import net.geedge.common.Constant;
|
||||
import net.geedge.common.T;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Base64;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ApkUtil {
|
||||
|
||||
private static final Log log = Log.get();
|
||||
|
||||
public static final String APPLICATION = "application:";
|
||||
public static final String APPLICATION_ICON = "application-icon";
|
||||
public static final String APPLICATION_LABEL = "application-label";
|
||||
public static final String APPLICATION_LABEL_N = "application: label";
|
||||
public static final String DENSITIES = "densities";
|
||||
public static final String LAUNCHABLE_ACTIVITY = "launchable";
|
||||
public static final String PACKAGE = "package";
|
||||
public static final String SDK_VERSION = "sdkVersion";
|
||||
public static final String SUPPORTS_ANY_DENSITY = "support-any-density";
|
||||
public static final String SUPPORTS_SCREENS = "support-screens";
|
||||
public static final String TARGET_SDK_VERSION = "targetSdkVersion";
|
||||
public static final String VERSION_CODE = "versionCode";
|
||||
public static final String VERSION_NAME = "versionName";
|
||||
public static final String USES_FEATURE = "uses-feature";
|
||||
public static final String USES_IMPLIED_FEATURE = "uses-implied-feature";
|
||||
public static final String USES_PERMISSION = "uses-permission";
|
||||
|
||||
private static final String SPLIT_REGEX = "(: )|(=')|(' )|'";
|
||||
|
||||
private ProcessBuilder builder;
|
||||
// aapt 所在目录
|
||||
private String aaptToolPath = "aapt";
|
||||
|
||||
public ApkUtil() {
|
||||
builder = new ProcessBuilder();
|
||||
builder.redirectErrorStream(true);
|
||||
}
|
||||
|
||||
public String getAaptToolPath() {
|
||||
return aaptToolPath;
|
||||
}
|
||||
|
||||
public void setAaptToolPath(String aaptToolPath) {
|
||||
this.aaptToolPath = aaptToolPath;
|
||||
}
|
||||
|
||||
public ApkInfo parseApk(String apkPath) {
|
||||
String aaptTool = aaptToolPath;
|
||||
Process process = null;
|
||||
InputStream inputStream = null;
|
||||
BufferedReader bufferedReader = null;
|
||||
try {
|
||||
process = builder.command(aaptTool, "d", "badging", apkPath).start();
|
||||
inputStream = process.getInputStream();
|
||||
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
|
||||
ApkInfo apkInfo = new ApkInfo();
|
||||
apkInfo.setSize(new File(apkPath).length());
|
||||
String temp = null;
|
||||
while ((temp = bufferedReader.readLine()) != null) {
|
||||
setApkInfoProperty(apkInfo, temp);
|
||||
}
|
||||
return apkInfo;
|
||||
} catch (IOException e) {
|
||||
log.error(e, "[parseApk] [error]");
|
||||
return null;
|
||||
} finally {
|
||||
if (process != null) {
|
||||
process.destroy();
|
||||
}
|
||||
T.IoUtil.close(inputStream);
|
||||
T.IoUtil.close(bufferedReader);
|
||||
}
|
||||
}
|
||||
|
||||
private void setApkInfoProperty(ApkInfo apkInfo, String source) {
|
||||
if (source.startsWith(APPLICATION)) {
|
||||
String[] rs = source.split("( icon=')|'");
|
||||
apkInfo.setIcon(rs[rs.length - 1]);
|
||||
} else if (source.startsWith(APPLICATION_ICON)) {
|
||||
apkInfo.addToIcons(getKeyBeforeColon(source), getPropertyInQuote(source));
|
||||
} else if (source.startsWith(APPLICATION_LABEL)) {
|
||||
apkInfo.setLabel(getPropertyInQuote(source));
|
||||
} else if (source.startsWith(LAUNCHABLE_ACTIVITY)) {
|
||||
apkInfo.setLaunchableActivity(getPropertyInQuote(source));
|
||||
} else if (source.startsWith(PACKAGE)) {
|
||||
String[] packageInfo = source.split(SPLIT_REGEX);
|
||||
apkInfo.setPackageName(packageInfo[2]);
|
||||
apkInfo.setVersionCode(packageInfo[4]);
|
||||
apkInfo.setVersionName(packageInfo[6]);
|
||||
} else if (source.startsWith(SDK_VERSION)) {
|
||||
apkInfo.setSdkVersion(getPropertyInQuote(source));
|
||||
} else if (source.startsWith(TARGET_SDK_VERSION)) {
|
||||
apkInfo.setTargetSdkVersion(getPropertyInQuote(source));
|
||||
} else if (source.startsWith(USES_PERMISSION)) {
|
||||
apkInfo.addToUsesPermissions(getPropertyInQuote(source));
|
||||
} else if (source.startsWith(USES_FEATURE)) {
|
||||
apkInfo.addToFeatures(getPropertyInQuote(source));
|
||||
}
|
||||
}
|
||||
|
||||
private String getKeyBeforeColon(String source) {
|
||||
return source.substring(0, source.indexOf(':'));
|
||||
}
|
||||
|
||||
private String getPropertyInQuote(String source) {
|
||||
int index = source.indexOf("'") + 1;
|
||||
return source.substring(index, source.indexOf('\'', index));
|
||||
}
|
||||
|
||||
public String extractFileFromApk(String apkPath, String fileName) {
|
||||
ZipFile zipFile = null;
|
||||
File tempIconFile = T.FileUtil.file(Constant.TEMP_PATH, T.IdUtil.fastSimpleUUID());
|
||||
try {
|
||||
zipFile = new ZipFile(apkPath);
|
||||
ZipEntry entry = zipFile.getEntry(fileName);
|
||||
InputStream inputStream = zipFile.getInputStream(entry);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempIconFile), 1024);
|
||||
byte[] b = new byte[1024];
|
||||
BufferedInputStream bis = new BufferedInputStream(inputStream, 1024);
|
||||
while (bis.read(b) != -1) {
|
||||
bos.write(b);
|
||||
}
|
||||
IoUtil.flush(bos);
|
||||
T.IoUtil.close(bos);
|
||||
T.IoUtil.close(bis);
|
||||
T.IoUtil.close(inputStream);
|
||||
T.IoUtil.close(zipFile);
|
||||
|
||||
String base64Str = Base64.getEncoder().encodeToString(T.FileUtil.readBytes(tempIconFile));
|
||||
return base64Str;
|
||||
} catch (IOException e) {
|
||||
log.error(e, "[extractFileFromApk] [error]");
|
||||
} finally {
|
||||
T.FileUtil.del(tempIconFile);
|
||||
T.IoUtil.close(zipFile);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
63
src/main/java/net/geedge/api/util/CommandExec.java
Normal file
63
src/main/java/net/geedge/api/util/CommandExec.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package net.geedge.api.util;
|
||||
|
||||
import net.geedge.common.APIException;
|
||||
import net.geedge.common.RCode;
|
||||
import net.geedge.common.T;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CommandExec {
|
||||
|
||||
private File logFile;
|
||||
private ProcessBuilder processBuilder;
|
||||
|
||||
public String exec(List<String> command) {
|
||||
|
||||
if (logFile != null) {
|
||||
T.FileUtil.appendString(T.StrUtil.concat(true, "$ ", command.stream().collect(Collectors.joining(" ")), "\n"), this.logFile, "UTF-8");
|
||||
}
|
||||
|
||||
InputStreamReader inputStreamReader = null;
|
||||
BufferedReader bufferedReader = null;
|
||||
StringBuilder stringBuilder = null;
|
||||
Process process = null;
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
process = processBuilder.command(command).start();
|
||||
stringBuilder = new StringBuilder();
|
||||
inputStream = process.getInputStream();
|
||||
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (logFile != null) {
|
||||
// 处理每一行输出
|
||||
T.FileUtil.appendString(T.StrUtil.concat(true, line, "\n"), this.logFile, "UTF-8");
|
||||
}
|
||||
stringBuilder.append(line).append(System.lineSeparator());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new APIException(RCode.ERROR);
|
||||
}finally {
|
||||
if (process != null) {
|
||||
process.destroy();
|
||||
}
|
||||
T.IoUtil.close(inputStreamReader);
|
||||
T.IoUtil.close(bufferedReader);
|
||||
T.IoUtil.close(inputStream);
|
||||
}
|
||||
|
||||
return stringBuilder.toString().stripTrailing();
|
||||
}
|
||||
|
||||
public Process execForProcess(List<String> command) {
|
||||
Process process = T.RuntimeUtil.exec(command.stream().toArray(String[]::new));
|
||||
return process;
|
||||
}
|
||||
|
||||
public CommandExec(File logFile ) {
|
||||
this.logFile = logFile;
|
||||
this.processBuilder = new ProcessBuilder();
|
||||
}
|
||||
}
|
||||
193
src/main/java/net/geedge/api/util/PlaybookRunnable.java
Normal file
193
src/main/java/net/geedge/api/util/PlaybookRunnable.java
Normal file
@@ -0,0 +1,193 @@
|
||||
package net.geedge.api.util;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.log.Log;
|
||||
import net.geedge.api.entity.EnvApiYml;
|
||||
import net.geedge.common.APIException;
|
||||
import net.geedge.common.Constant;
|
||||
import net.geedge.common.RCode;
|
||||
import net.geedge.common.T;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
public class PlaybookRunnable implements Runnable {
|
||||
private final static Log log = Log.get();
|
||||
|
||||
private AdbUtil adbUtil;
|
||||
private EnvApiYml envApiYml;
|
||||
private String tid;
|
||||
private File apkFile;
|
||||
private String packageName;
|
||||
private File scriptPath;
|
||||
private String type;
|
||||
private boolean reInstall;
|
||||
private boolean clearCache;
|
||||
private boolean unInstall;
|
||||
private boolean interrupt;
|
||||
|
||||
public PlaybookRunnable(EnvApiYml envApiYml, File apkFile, File scriptPath, String tid, String packageName, String type, Boolean reInstall, Boolean clearCache, Boolean unInstall) {
|
||||
this.envApiYml = envApiYml;
|
||||
this.tid = tid;
|
||||
this.apkFile = apkFile;
|
||||
this.packageName = packageName;
|
||||
this.scriptPath = scriptPath;
|
||||
this.type = type;
|
||||
this.reInstall = reInstall;
|
||||
this.clearCache = clearCache;
|
||||
this.unInstall = unInstall;
|
||||
this.interrupt = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("exec-playbook-thread-" + tid);
|
||||
Constant.ACTIVE_TASKS.add(this);
|
||||
File logFile = FileUtil.file(Constant.TEMP_PATH, tid, "result.log");
|
||||
File statusFile = FileUtil.file(Constant.TEMP_PATH, tid, "result.json");
|
||||
AdbUtil.CommandResult tcpdumpPackage = null;
|
||||
AdbUtil.CommandResult tcpdumpAll = null;
|
||||
try {
|
||||
Map resultMap = T.MapUtil.builder()
|
||||
.put("status", "running")
|
||||
.build();
|
||||
T.FileUtil.writeString(T.JSONUtil.toJsonStr(resultMap), statusFile, "UTF-8");
|
||||
|
||||
if (interrupt) return;
|
||||
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));
|
||||
|
||||
// Check if the package is installed
|
||||
if (interrupt) return;
|
||||
boolean packageIsInstall = adbUtil.findPackageInstall(packageName);
|
||||
if (packageIsInstall) {
|
||||
if (!reInstall) {
|
||||
// install apk
|
||||
if (interrupt) return;
|
||||
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());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// install apk
|
||||
if (interrupt) return;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
//Close other apps
|
||||
if (interrupt) return;
|
||||
adbUtil.closeOtherApps();
|
||||
|
||||
// clear app data
|
||||
if (interrupt) return;
|
||||
if (clearCache) {
|
||||
AdbUtil.CommandResult clearData = adbUtil.clearAppData(packageName);
|
||||
if (0 != clearData.exitCode()) {
|
||||
T.FileUtil.appendString(String.format("ERROR: Clear %s data error: exit code %s \n", packageName, clearData.exitCode()), logFile, "UTF-8");
|
||||
throw new APIException(clearData.output());
|
||||
}
|
||||
}
|
||||
|
||||
// Launch the app
|
||||
if (interrupt) return;
|
||||
adbUtil.startApp(packageName);
|
||||
|
||||
// star tcpdump: package name
|
||||
if (interrupt) return;
|
||||
tcpdumpPackage = adbUtil.startTcpdump(packageName);
|
||||
if (0 != tcpdumpPackage.exitCode()) {
|
||||
T.FileUtil.appendString(String.format("ERROR: Start tcpdump %s failed: exit code %s \n", packageName, tcpdumpPackage.exitCode()), logFile, "UTF-8");
|
||||
throw new APIException(String.format("tcpdump %s error", packageName));
|
||||
}
|
||||
|
||||
// star tcpdump: all
|
||||
if (interrupt) return;
|
||||
tcpdumpAll = adbUtil.startTcpdump(T.StrUtil.EMPTY);
|
||||
if (0 != tcpdumpAll.exitCode()) {
|
||||
T.FileUtil.appendString(String.format("ERROR: Start tcpdump all failed: exit code %s \n", tcpdumpAll.exitCode()), logFile, "UTF-8");
|
||||
throw new APIException("tcpdump all error");
|
||||
}
|
||||
|
||||
// exec playbook
|
||||
if (interrupt) return;
|
||||
AdbUtil.CommandResult airtestResult = adbUtil.execPlaybook(scriptPath.getPath(), tid, packageName, type, logFile);
|
||||
if (0 != airtestResult.exitCode()) {
|
||||
T.FileUtil.appendString(String.format("ERROR: Exec playbook failed: exit code %s \n", airtestResult.exitCode()), logFile, "UTF-8");
|
||||
throw new APIException("playbook exec error");
|
||||
}
|
||||
|
||||
// stop package tcpdump
|
||||
if (interrupt) return;
|
||||
stopTcpdump(tcpdumpPackage, logFile, packageName);
|
||||
|
||||
// stop all tcpdump
|
||||
if (interrupt) return;
|
||||
stopTcpdump(tcpdumpAll, logFile, T.StrUtil.EMPTY);
|
||||
|
||||
resultMap = T.MapUtil.builder()
|
||||
.put("status", "done")
|
||||
.build();
|
||||
T.FileUtil.writeString(T.JSONUtil.toJsonStr(resultMap), statusFile, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
log.error(e);
|
||||
Map resultMap = T.MapUtil.builder()
|
||||
.put("status", "error")
|
||||
.build();
|
||||
T.FileUtil.writeString(T.JSONUtil.toJsonStr(resultMap), statusFile, "UTF-8");
|
||||
} finally {
|
||||
if (T.StrUtil.isNotEmpty(tcpdumpPackage.output())) {
|
||||
AdbUtil.CommandResult packageTcpdump = adbUtil.stopTcpdump(tcpdumpPackage.output());
|
||||
adbUtil.execShellCommand(String.format("shell rm -rf %s", packageTcpdump.output()));
|
||||
}
|
||||
if (T.StrUtil.isNotEmpty(tcpdumpAll.output())) {
|
||||
AdbUtil.CommandResult allTcpdump = adbUtil.stopTcpdump(tcpdumpAll.output());
|
||||
adbUtil.execShellCommand(String.format("shell rm -rf %s", allTcpdump.output()));
|
||||
}
|
||||
|
||||
adbUtil.stopApp(packageName);
|
||||
|
||||
if (unInstall) {
|
||||
adbUtil.uninstall(packageName);
|
||||
}
|
||||
T.FileUtil.appendString(String.format("Job execution ends"), logFile, "UTF-8");
|
||||
Constant.ACTIVE_TASKS.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void interrupt() {
|
||||
this.interrupt = true;
|
||||
adbUtil.setInterrupt(true);
|
||||
}
|
||||
|
||||
private void stopTcpdump(AdbUtil.CommandResult tcpdump, File logFile, String packageName) {
|
||||
// stop tcpdump
|
||||
AdbUtil.CommandResult stopTcpdump = adbUtil.stopTcpdump(tcpdump.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());
|
||||
}
|
||||
|
||||
// pull pcap file
|
||||
String filePath = stopTcpdump.output();
|
||||
packageName = T.StrUtil.isEmpty(packageName) ? "all" : packageName;
|
||||
File localPcapFile = T.FileUtil.file(Constant.TEMP_PATH, tid, String.format("%s-%s%s", tcpdump.output(), packageName, ".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());
|
||||
}
|
||||
|
||||
// delete android pcap
|
||||
adbUtil.execShellCommand(String.format("shell rm -rf %s", filePath));
|
||||
}
|
||||
}
|
||||
80
src/main/java/net/geedge/api/util/PythonCommandBuilder.java
Normal file
80
src/main/java/net/geedge/api/util/PythonCommandBuilder.java
Normal file
@@ -0,0 +1,80 @@
|
||||
package net.geedge.api.util;
|
||||
|
||||
import net.geedge.common.T;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class PythonCommandBuilder {
|
||||
|
||||
private final String pythonPath;
|
||||
private final List<String> command;
|
||||
|
||||
public PythonCommandBuilder(String pythonPath) {
|
||||
this.pythonPath = pythonPath;
|
||||
this.command = new LinkedList<>();
|
||||
this.command.add(pythonPath);
|
||||
}
|
||||
|
||||
public static PythonCommandBuilder builder() {
|
||||
return new PythonCommandBuilder("python");
|
||||
}
|
||||
|
||||
public static PythonCommandBuilder builder(String pythonPath) {
|
||||
return new PythonCommandBuilder(pythonPath);
|
||||
}
|
||||
public PythonCommandBuilder buildRunAirScript(String launcher, String path, String jobId, String packageName, String serial) {
|
||||
this.command.add(launcher);
|
||||
this.command.add(path);
|
||||
this.command.add("--device");
|
||||
this.command.add(T.StrUtil.concat(true,"Android://127.0.0.1:5037/", serial));
|
||||
this.command.add("--job_id");
|
||||
this.command.add(jobId);
|
||||
this.command.add("--job_path");
|
||||
this.command.add(path);
|
||||
this.command.add("--package_name");
|
||||
this.command.add(packageName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PythonCommandBuilder buildRunPythonScript(String path, String jobId, String packageName, String serial) {
|
||||
this.command.add(path);
|
||||
this.command.add("--device");
|
||||
this.command.add(T.StrUtil.concat(true,"Android://127.0.0.1:5037/", serial));
|
||||
this.command.add("--serial");
|
||||
this.command.add(serial);
|
||||
this.command.add("--job_id");
|
||||
this.command.add(jobId);
|
||||
this.command.add("--job_path");
|
||||
this.command.add(path);
|
||||
this.command.add("--package_name");
|
||||
this.command.add(packageName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PythonCommandBuilder buildCreateVenv(String path) {
|
||||
this.command.add("-m");
|
||||
this.command.add("venv");
|
||||
this.command.add(path);
|
||||
this.command.add("--system-site-packages");
|
||||
return this;
|
||||
}
|
||||
|
||||
public PythonCommandBuilder buildUpgradePip() {
|
||||
this.command.add("install");
|
||||
this.command.add("--upgrade");
|
||||
this.command.add("pip");
|
||||
return this;
|
||||
}
|
||||
|
||||
public PythonCommandBuilder buildInstallRequirements(String path) {
|
||||
this.command.add("install");
|
||||
this.command.add("-r");
|
||||
this.command.add(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> build() {
|
||||
return this.command;
|
||||
}
|
||||
}
|
||||
47
src/main/java/net/geedge/common/APIException.java
Normal file
47
src/main/java/net/geedge/common/APIException.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package net.geedge.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 自定义异常
|
||||
*/
|
||||
@Data
|
||||
public class APIException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String msg;
|
||||
private int code = RCode.ERROR.getCode();
|
||||
private Object[] param = new Object[]{};
|
||||
private RCode rCode;
|
||||
|
||||
public APIException(RCode rCode) {
|
||||
super(rCode.getMsg());
|
||||
this.code = rCode.getCode();
|
||||
this.msg = rCode.getMsg();
|
||||
this.param = rCode.getParam();
|
||||
this.rCode = rCode;
|
||||
}
|
||||
|
||||
public APIException(String msg) {
|
||||
super(msg);
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public APIException(String msg, Throwable e) {
|
||||
super(msg, e);
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public APIException(String msg, int code) {
|
||||
super(msg);
|
||||
this.msg = msg;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public APIException(String msg, int code, Throwable e) {
|
||||
super(msg, e);
|
||||
this.msg = msg;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
39
src/main/java/net/geedge/common/APIExceptionHandler.java
Normal file
39
src/main/java/net/geedge/common/APIExceptionHandler.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package net.geedge.common;
|
||||
|
||||
import cn.hutool.log.Log;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 异常处理器
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class APIExceptionHandler {
|
||||
|
||||
private static final Log log = Log.get();
|
||||
|
||||
@ExceptionHandler(APIException.class)
|
||||
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
|
||||
public R handleNZException(APIException e) {
|
||||
R r = new R();
|
||||
r.put("code", e.getCode());
|
||||
r.put("msg", e.getMsg());
|
||||
return r;
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleException(Exception e, HttpServletRequest request) {
|
||||
if (e instanceof ClientAbortException) {
|
||||
return null;
|
||||
}
|
||||
log.error(e, "Request uri: {}", request.getRequestURI());
|
||||
return R.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
31
src/main/java/net/geedge/common/Constant.java
Normal file
31
src/main/java/net/geedge/common/Constant.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package net.geedge.common;
|
||||
|
||||
import net.geedge.api.util.PlaybookRunnable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Constant {
|
||||
/**
|
||||
* 临时目录
|
||||
*/
|
||||
public static final String TEMP_PATH = System.getProperty("user.dir") + File.separator + "tmp";
|
||||
|
||||
public static final File PLAYBOOK_AIR_PATH =T.FileUtil.file(T.WebPathUtil.getRootPath(), "tmp" , "playbook", "main.air");
|
||||
|
||||
public static final Map<String, Map> PLAYBOOK_RUN_RESULT = T.MapUtil.newConcurrentHashMap();
|
||||
|
||||
public static final List<PlaybookRunnable> ACTIVE_TASKS = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
static {
|
||||
File tempPath = T.FileUtil.file(TEMP_PATH);
|
||||
// 程序启动清空临时目录
|
||||
// T.FileUtil.del(tempPath);
|
||||
if (!T.FileUtil.exist(tempPath)) {
|
||||
T.FileUtil.mkdir(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/main/java/net/geedge/common/R.java
Normal file
90
src/main/java/net/geedge/common/R.java
Normal file
@@ -0,0 +1,90 @@
|
||||
package net.geedge.common;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 返回数据
|
||||
* <p>
|
||||
* 错误码、错误内容统一在枚举类RCode中定义, 错误码格式见RCode注释,错误码内容必须用英文,作为国际化的code 自定义的错误类型必须加注释
|
||||
*/
|
||||
public class R extends HashMap<String, Object> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public R() {
|
||||
put("code", RCode.SUCCESS.getCode());
|
||||
put("msg", RCode.SUCCESS.getMsg());
|
||||
put("timestamp", T.DateUtil.current());
|
||||
}
|
||||
|
||||
public static R ok() {
|
||||
return new R();
|
||||
}
|
||||
|
||||
public static R ok(String msg) {
|
||||
R r = new R();
|
||||
r.put("msg", msg);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static R ok(Object data) {
|
||||
R r = new R();
|
||||
r.put("data", data);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static R error() {
|
||||
return error(RCode.ERROR.getCode(), RCode.ERROR.getMsg());
|
||||
}
|
||||
|
||||
public static R error(RCode rCode) {
|
||||
R r = new R();
|
||||
r.put("code", rCode.getCode());
|
||||
r.put("msg", rCode.getMsg());
|
||||
return r;
|
||||
}
|
||||
|
||||
public static R error(String msg) {
|
||||
R r = new R();
|
||||
r.put("code", RCode.ERROR.getCode());
|
||||
r.put("msg", msg);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static R error(Integer code, String msg) {
|
||||
R r = new R();
|
||||
r.put("code", code);
|
||||
r.put("msg", msg);
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R put(String key, Object value) {
|
||||
super.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public R putData(Object value) {
|
||||
this.put("data", value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public R putData(String key, Object value) {
|
||||
Object data = super.getOrDefault("data", new LinkedHashMap<String, Object>());
|
||||
if (!(data instanceof Map)) {
|
||||
throw new APIException("data put error");
|
||||
}
|
||||
((Map<String, Object>) data).put(key, value);
|
||||
super.put("data", data);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public R putAllData(Map m) {
|
||||
super.putAll(m);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
43
src/main/java/net/geedge/common/RCode.java
Normal file
43
src/main/java/net/geedge/common/RCode.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package net.geedge.common;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
|
||||
public enum RCode {
|
||||
|
||||
|
||||
BAD_REQUEST(400, "Bad Request "),
|
||||
|
||||
NOT_EXISTS(404, "No such file or directory"),
|
||||
NOT_PERMISSION(401 , "Permission denied"),
|
||||
TIMEOUT(408, "Request Timeout"),
|
||||
|
||||
ERROR(999, "error"), // 通用错误/未知错误
|
||||
|
||||
SUCCESS(200, "success"); // 成功
|
||||
|
||||
RCode(Integer code, String msg) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
private Integer code;
|
||||
private String msg;
|
||||
private Object[] param;
|
||||
|
||||
public RCode setParam(Object... param) {
|
||||
this.param = param;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object[] getParam() {
|
||||
return param;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return MessageFormat.format(msg, param);
|
||||
}
|
||||
}
|
||||
277
src/main/java/net/geedge/common/T.java
Normal file
277
src/main/java/net/geedge/common/T.java
Normal file
@@ -0,0 +1,277 @@
|
||||
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.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
public class T {
|
||||
/**
|
||||
* 时间工具类
|
||||
*/
|
||||
public static class DateUtil extends cn.hutool.core.date.DateUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串工具类
|
||||
*/
|
||||
public static class StrUtil extends cn.hutool.core.util.StrUtil {
|
||||
}
|
||||
/**
|
||||
* 反射工具类
|
||||
*
|
||||
* @author Looly
|
||||
* @since 3.0.9
|
||||
*/
|
||||
public static class ReflectUtil extends cn.hutool.core.util.ReflectUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Map相关工具类
|
||||
*/
|
||||
public static class MapUtil extends cn.hutool.core.map.MapUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合工具类
|
||||
*/
|
||||
public static class ListUtil extends cn.hutool.core.collection.ListUtil {
|
||||
}
|
||||
/**
|
||||
* 字符集工具类
|
||||
*
|
||||
* @author xiaoleilu
|
||||
*/
|
||||
public static class CharsetUtil extends cn.hutool.core.util.CharsetUtil {
|
||||
}
|
||||
/**
|
||||
* ID生成器工具类,此工具类中主要封装:
|
||||
*
|
||||
* <pre>
|
||||
* 1. 唯一性ID生成器:UUID、ObjectId(MongoDB)、Snowflake
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* ID相关文章见:http://calvin1978.blogcn.com/articles/uuid.html
|
||||
*
|
||||
* @author looly
|
||||
* @since 4.1.13
|
||||
*/
|
||||
public static class IdUtil extends cn.hutool.core.util.IdUtil {
|
||||
}
|
||||
/**
|
||||
* 线程池工具
|
||||
*
|
||||
* @author luxiaolei
|
||||
*/
|
||||
public static class ThreadUtil extends cn.hutool.core.thread.ThreadUtil {
|
||||
}
|
||||
/**
|
||||
* json 工具类
|
||||
*/
|
||||
public static class JSONUtil extends cn.hutool.json.JSONUtil {
|
||||
}
|
||||
/**
|
||||
* 系统运行时工具类,用于执行系统命令的工具
|
||||
*
|
||||
* @author Looly
|
||||
* @since 3.1.1
|
||||
*/
|
||||
public static class RuntimeUtil extends cn.hutool.core.util.RuntimeUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件工具类
|
||||
*
|
||||
* @author looly
|
||||
*/
|
||||
public static class FileUtil extends cn.hutool.core.io.FileUtil {
|
||||
}
|
||||
/**
|
||||
* 压缩工具类
|
||||
*
|
||||
* @author Looly
|
||||
*/
|
||||
public static class ZipUtil extends cn.hutool.core.util.ZipUtil {
|
||||
}
|
||||
/**
|
||||
* IO工具类<br>
|
||||
* IO工具类只是辅助流的读写,并不负责关闭流。原因是流可能被多次读写,读写关闭后容易造成问题。
|
||||
*
|
||||
* @author xiaoleilu
|
||||
*/
|
||||
public static class IoUtil extends cn.hutool.core.io.IoUtil {
|
||||
}
|
||||
/**
|
||||
* URL(Uniform Resource Locator)统一资源定位符相关工具类
|
||||
*
|
||||
* <p>
|
||||
* 统一资源定位符,描述了一台特定服务器上某资源的特定位置。
|
||||
* </p>
|
||||
* URL组成:
|
||||
*
|
||||
* <pre>
|
||||
* 协议://主机名[:端口]/ 路径/[:参数] [?查询]#Fragment
|
||||
* protocol :// hostname[:port] / path / [:parameters][?query]#fragment
|
||||
* </pre>
|
||||
*
|
||||
* @author xiaoleilu
|
||||
*/
|
||||
public static class URLUtil extends cn.hutool.core.util.URLUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象工具类,包括判空、克隆、序列化等操作
|
||||
*
|
||||
* @author Looly
|
||||
*/
|
||||
public static class ObjectUtil extends cn.hutool.core.util.ObjectUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* CommandLineUtil
|
||||
*
|
||||
* @version org.apache.commons.commons-exec:1.3
|
||||
* @apiNote copy from rg.apache.commons.exec.CommandLine.translateCommandline
|
||||
*/
|
||||
public static class CommandLineUtil {
|
||||
/**
|
||||
* translateCommandline
|
||||
*
|
||||
* @param toProcess
|
||||
* @return
|
||||
*/
|
||||
public static String[] translateCommandline(String toProcess) {
|
||||
if (toProcess != null && toProcess.length() != 0) {
|
||||
int state = 0;
|
||||
StringTokenizer tok = new StringTokenizer(toProcess, "\"' ", true);
|
||||
ArrayList<String> list = new ArrayList();
|
||||
StringBuilder current = new StringBuilder();
|
||||
boolean lastTokenHasBeenQuoted = false;
|
||||
|
||||
while (true) {
|
||||
while (tok.hasMoreTokens()) {
|
||||
String nextTok = tok.nextToken();
|
||||
switch (state) {
|
||||
case 1:
|
||||
if ("'".equals(nextTok)) {
|
||||
lastTokenHasBeenQuoted = true;
|
||||
state = 0;
|
||||
} else {
|
||||
current.append(nextTok);
|
||||
}
|
||||
continue;
|
||||
case 2:
|
||||
if ("\"".equals(nextTok)) {
|
||||
lastTokenHasBeenQuoted = true;
|
||||
state = 0;
|
||||
} else {
|
||||
current.append(nextTok);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("'".equals(nextTok)) {
|
||||
state = 1;
|
||||
} else if ("\"".equals(nextTok)) {
|
||||
state = 2;
|
||||
} else if (" ".equals(nextTok)) {
|
||||
if (lastTokenHasBeenQuoted || current.length() != 0) {
|
||||
list.add(current.toString());
|
||||
current = new StringBuilder();
|
||||
}
|
||||
} else {
|
||||
current.append(nextTok);
|
||||
}
|
||||
|
||||
lastTokenHasBeenQuoted = false;
|
||||
}
|
||||
|
||||
if (lastTokenHasBeenQuoted || current.length() != 0) {
|
||||
list.add(current.toString());
|
||||
}
|
||||
|
||||
if (state != 1 && state != 2) {
|
||||
String[] args = new String[list.size()];
|
||||
return (String[]) list.toArray(args);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unbalanced quotes in " + toProcess);
|
||||
}
|
||||
} else {
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResponseUtil {
|
||||
static Log log = Log.get();
|
||||
/**
|
||||
* reponse 下载 byte数据
|
||||
*/
|
||||
public static void downloadFile(HttpServletResponse response, String filename, byte[] data) throws IORuntimeException, IOException {
|
||||
log.info("[downloadFile] [fileName: {}] [size: {}]", filename, data.length);
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
String fileName = T.URLUtil.encode(filename, T.CharsetUtil.CHARSET_UTF_8);
|
||||
cn.hutool.core.util.ReflectUtil.invoke(response, "addHeader", "Content-Disposition", "attachment; filename=" + fileName);
|
||||
cn.hutool.core.util.ReflectUtil.invoke(response, "addHeader", "Content-Length", "" + data.length);
|
||||
cn.hutool.core.util.ReflectUtil.invoke(response, "setHeader", "Access-Control-Expose-Headers", "Content-Disposition");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public class DigestUtils {
|
||||
private static final List<String> ENCODE_LIST = Arrays.asList("ISO-8859-1", "GB2312", "UTF-8", "GBK");
|
||||
|
||||
public static String getEncoding(String text) {
|
||||
for (String enc : ENCODE_LIST) {
|
||||
try {
|
||||
byte[] bytes = text.getBytes(enc);
|
||||
String str = cn.hutool.core.util.StrUtil.str(bytes, enc);
|
||||
Arrays.fill(bytes, (byte) 0);
|
||||
if (cn.hutool.core.util.StrUtil.equals(text, str)) {
|
||||
return enc;
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
54
src/main/java/net/geedge/common/config/TokenInterceptor.java
Normal file
54
src/main/java/net/geedge/common/config/TokenInterceptor.java
Normal file
@@ -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("${env.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);
|
||||
}
|
||||
}
|
||||
12
src/main/resources/application.yml
Normal file
12
src/main/resources/application.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
spring:
|
||||
profiles:
|
||||
active: prod
|
||||
servlet:
|
||||
context-path: /
|
||||
multipart:
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
enabled: true
|
||||
|
||||
env:
|
||||
tokenFile: ./config/token.auth
|
||||
1
src/main/resources/config/token.auth
Normal file
1
src/main/resources/config/token.auth
Normal file
@@ -0,0 +1 @@
|
||||
2fa9a369
|
||||
23
src/main/resources/lib/android-launcher.py
Normal file
23
src/main/resources/lib/android-launcher.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from airtest.cli.runner import AirtestCase, run_script
|
||||
from airtest.cli.parser import runner_parser
|
||||
|
||||
|
||||
class CustomAirtestCase(AirtestCase):
|
||||
def setUp(self):
|
||||
if self.args.job_id:
|
||||
self.scope['job_id']=self.args.job_id
|
||||
if self.args.job_path:
|
||||
self.scope['job_path']=self.args.job_path
|
||||
if self.args.package_name:
|
||||
self.scope['package_name']=self.args.package_name
|
||||
|
||||
if __name__ == '__main__':
|
||||
ap = runner_parser()
|
||||
ap.add_argument(
|
||||
"--job_id", help="job id")
|
||||
ap.add_argument(
|
||||
"--job_path", help="job path")
|
||||
ap.add_argument(
|
||||
"--package_name", help="app installation package name")
|
||||
args = ap.parse_args()
|
||||
run_script(args, CustomAirtestCase)
|
||||
BIN
src/main/resources/lib/droidvnc-np-2.6.0.apk
Normal file
BIN
src/main/resources/lib/droidvnc-np-2.6.0.apk
Normal file
Binary file not shown.
10
src/main/resources/lib/droidvnc-np-defaults.json
Normal file
10
src/main/resources/lib/droidvnc-np-defaults.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"password": "",
|
||||
"port": 5900,
|
||||
"viewOnly": false,
|
||||
"accessKey": "d042e2b5d5f348588a4e1a243eb7a9a0",
|
||||
"showPointers": true,
|
||||
"fileTransfer": true,
|
||||
"startOnBoot": true,
|
||||
"scaling": 1
|
||||
}
|
||||
8
src/test/java/net/geedge/TestJ.java
Normal file
8
src/test/java/net/geedge/TestJ.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package net.geedge;
|
||||
|
||||
public class TestJ {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user