├── docs ├── sftp.png ├── alipay.png ├── login.png ├── shell.png ├── wechatpay.png └── wechat-zmzhou-star.png ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── src ├── main │ ├── resources │ │ ├── static │ │ │ ├── img │ │ │ │ ├── b.png │ │ │ │ ├── c.png │ │ │ │ ├── l.png │ │ │ │ ├── p.png │ │ │ │ ├── s.png │ │ │ │ └── favicon.ico │ │ │ ├── css │ │ │ │ ├── jstree │ │ │ │ │ ├── 32px.png │ │ │ │ │ ├── 40px.png │ │ │ │ │ ├── throbber.gif │ │ │ │ │ └── style.min.css │ │ │ │ ├── xterm.css │ │ │ │ └── webShell.css │ │ │ └── js │ │ │ │ ├── web-socket.js │ │ │ │ └── web-shell.js │ │ ├── META-INF │ │ │ ├── spring.factories │ │ │ └── additional-spring-configuration-metadata.json │ │ ├── banner.txt │ │ ├── application.yml │ │ ├── ehcache.xml │ │ ├── logback-spring.xml │ │ ├── log4j2.xml │ │ └── templates │ │ │ ├── index.html │ │ │ └── sftp.html │ └── java │ │ └── com │ │ └── github │ │ └── zmzhoustar │ │ └── webshell │ │ ├── lombok.config │ │ ├── vo │ │ ├── ShellConnectInfo.java │ │ ├── WebShellData.java │ │ ├── SftpFileTreeVo.java │ │ └── ApiResult.java │ │ ├── handler │ │ ├── ApplicationStartup.java │ │ └── WebShellWebSocketHandler.java │ │ ├── config │ │ ├── CorsConfig.java │ │ ├── WebSocketConfig.java │ │ └── WebMvcConfig.java │ │ ├── WebShellApplication.java │ │ ├── Constants.java │ │ ├── interceptor │ │ └── WebSocketInterceptor.java │ │ ├── controller │ │ ├── ElectronController.java │ │ ├── RouterController.java │ │ └── SftpController.java │ │ ├── utils │ │ ├── WebShellUtils.java │ │ ├── EhCacheUtils.java │ │ ├── FileType.java │ │ ├── SpringUtils.java │ │ ├── SftpFileUtils.java │ │ ├── ThreadPoolUtils.java │ │ ├── SecretUtils.java │ │ └── SftpUtils.java │ │ ├── listener │ │ └── ApplicationEventListener.java │ │ └── service │ │ └── WebShellService.java └── test │ └── java │ └── com │ └── github │ └── zmzhoustar │ └── webshell │ ├── WebShellApplicationTests.java │ └── utils │ └── SecretUtilsTest.java ├── .editorconfig ├── .github └── workflows │ ├── maven.yml │ └── maven-publish.yml ├── .gitignore ├── HELP.md ├── README.md ├── README.en.md ├── pom.xml ├── mvnw.cmd ├── mvnw └── LICENSE /docs/sftp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/docs/sftp.png -------------------------------------------------------------------------------- /docs/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/docs/alipay.png -------------------------------------------------------------------------------- /docs/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/docs/login.png -------------------------------------------------------------------------------- /docs/shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/docs/shell.png -------------------------------------------------------------------------------- /docs/wechatpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/docs/wechatpay.png -------------------------------------------------------------------------------- /docs/wechat-zmzhou-star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/docs/wechat-zmzhou-star.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/static/img/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/src/main/resources/static/img/b.png -------------------------------------------------------------------------------- /src/main/resources/static/img/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/src/main/resources/static/img/c.png -------------------------------------------------------------------------------- /src/main/resources/static/img/l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/src/main/resources/static/img/l.png -------------------------------------------------------------------------------- /src/main/resources/static/img/p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/src/main/resources/static/img/p.png -------------------------------------------------------------------------------- /src/main/resources/static/img/s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/src/main/resources/static/img/s.png -------------------------------------------------------------------------------- /src/main/resources/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/src/main/resources/static/img/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/css/jstree/32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/src/main/resources/static/css/jstree/32px.png -------------------------------------------------------------------------------- /src/main/resources/static/css/jstree/40px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/src/main/resources/static/css/jstree/40px.png -------------------------------------------------------------------------------- /src/main/resources/static/css/jstree/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhou-star/web-shell/HEAD/src/main/resources/static/css/jstree/throbber.gif -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.context.ApplicationListener=com.github.zmzhoustar.webshell.listener.ApplicationEventListener 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "logstash.address", 5 | "type": "java.lang.String", 6 | "description": "logstash服务地址." 7 | } 8 | ] } 9 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /src/test/java/com/github/zmzhoustar/webshell/WebShellApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.github.zmzhoustar.webshell; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class WebShellApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/lombok.config: -------------------------------------------------------------------------------- 1 | # lombok 配置文件支持分层,在根目录配置的的配置文件对全局生效,如果某个子包中也有配置文件,则子包的类优先以子包中的配置为准 2 | # 该配置声明这个配置文件是一个根配置文件,他会从该配置文件所在的目录开始扫描 3 | config.stopBubbling=true 4 | # 全局配置 equalsAndHashCode 的 callSuper 属性为true,这样就不用每个类都要去写了 5 | lombok.equalsAndHashCode.callSuper=call 6 | # 全局配置 tostring 的 callSuper 属性为true,这样就不用每个类都要去写了 7 | lombok.tostring.callsuper=call -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | # 空格替代Tab缩进在各种编辑工具下效果一致 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | charset = utf-8 9 | end_of_line = crlf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.{java,xml}] 14 | indent_size = 4 15 | 16 | [*.md] 17 | insert_final_newline = false 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${AnsiColor.BLUE} 2 | __ __ __ __ 3 | _ __ ___ / /_ _____ / /_ ___ / // / 4 | | | /| / // _ \ / __ \ ______ / ___// __ \ / _ \ / // / 5 | | |/ |/ // __// /_/ //_____/(__ )/ / / // __// // / 6 | |__/|__/ \___//_.___/ /____//_/ /_/ \___//_//_/ 7 | application.formatted-version:${application.formatted-version} 8 | spring-boot.formatted-version:${spring-boot.formatted-version} 9 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 8 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '8' 23 | distribution: 'adopt' 24 | - name: Build with Maven 25 | run: mvn -B package -Dmaven.test.skip=true --file pom.xml 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | target/ 26 | !.mvn/wrapper/maven-wrapper.jar 27 | !**/src/main/**/target/ 28 | !**/src/test/**/target/ 29 | 30 | ### STS ### 31 | .apt_generated 32 | .classpath 33 | .factorypath 34 | .project 35 | .settings 36 | .springBeans 37 | .sts4-cache 38 | 39 | ### IntelliJ IDEA ### 40 | .idea 41 | *.iws 42 | *.iml 43 | *.ipr 44 | 45 | ### VS Code ### 46 | .vscode/ 47 | -------------------------------------------------------------------------------- /src/test/java/com/github/zmzhoustar/webshell/utils/SecretUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.zmzhoustar.webshell.utils; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | /** 9 | * SecretUtilsTest 10 | * 11 | * @author zmzhou 12 | * @version 1.0 13 | * @date 2021/2/24 16:15 14 | */ 15 | @Slf4j 16 | class SecretUtilsTest { 17 | 18 | private static final String a = "12345_67890-abc"; 19 | 20 | /** 21 | * Encrypt. 22 | */ 23 | @Test 24 | void encrypt() { 25 | String res = SecretUtils.encrypt(a, SecretUtils.AES_KEY); 26 | log.info("加密结果:{}", res); 27 | Assertions.assertNotNull(res); 28 | res = SecretUtils.decrypt(res, SecretUtils.AES_KEY); 29 | log.info("解密结果:{}", res); 30 | Assertions.assertNotNull(res); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/vo/ShellConnectInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.vo; 6 | 7 | import java.io.Serializable; 8 | 9 | import org.springframework.web.socket.WebSocketSession; 10 | 11 | import com.jcraft.jsch.Channel; 12 | import com.jcraft.jsch.JSch; 13 | 14 | import lombok.Data; 15 | 16 | /** 17 | * ssh连接信息 18 | * @title SSHConnectInfo 19 | * @author zmzhou 20 | * @version 1.0 21 | * @date 2021/2/23 21:05 22 | */ 23 | @Data 24 | public class ShellConnectInfo implements Serializable { 25 | /** serialVersionUID */ 26 | private static final long serialVersionUID = 1555506471798748444L; 27 | /** WebSocketSession */ 28 | private WebSocketSession webSocketSession; 29 | /** JSch是SSH2的一个纯Java实现 */ 30 | private JSch jsch; 31 | /** shell通道 */ 32 | private Channel channel; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/vo/WebShellData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.vo; 6 | 7 | import java.io.Serializable; 8 | 9 | import lombok.Data; 10 | 11 | /** 12 | * web shell数据传输 13 | * @title WebSSHData 14 | * @author zmzhou 15 | * @version 1.0 16 | * @date 2021/2/23 20:57 17 | */ 18 | @Data 19 | public class WebShellData implements Serializable { 20 | /** serialVersionUID */ 21 | private static final long serialVersionUID = -2326528171211907216L; 22 | /** 操作类型 */ 23 | private String operate; 24 | /** 主机IP */ 25 | private String host; 26 | /** 端口号 默认22 */ 27 | private Integer port = 22; 28 | /** 用户名 */ 29 | private String username; 30 | /** 密码 */ 31 | private String password; 32 | /** 命令 */ 33 | private String command = ""; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/handler/ApplicationStartup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.handler; 6 | 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.boot.CommandLineRunner; 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.github.zmzhou.utils.ServerUtils; 12 | 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | /** 16 | * 项目启动成功后执行 17 | * @title ApplicationStartup 18 | * @author zmzhou 19 | * @version 1.0 20 | * @date 2021/2/23 22:05 21 | */ 22 | @Slf4j 23 | @Component 24 | public class ApplicationStartup implements CommandLineRunner { 25 | /** 应用的访问端口 */ 26 | @Value("${server.port}") 27 | private int port; 28 | 29 | /** 应用的访问路径上下文 */ 30 | @Value("${server.servlet.context-path}") 31 | private String contextPath; 32 | 33 | @Override 34 | public void run(String... args) { 35 | log.info("项目启动成功!访问地址:{}", "http://" + ServerUtils.getHostIp() + ":" + port + contextPath); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/maven-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Maven Package 5 | 6 | on: 7 | release: 8 | types: [created,edited] 9 | jobs: 10 | publish: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 8 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '8' 20 | distribution: 'adopt' 21 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 22 | settings-path: ${{ github.workspace }} # location for the settings.xml file 23 | 24 | - name: Build with Maven 25 | run: mvn -B package -DskipTests --file pom.xml 26 | 27 | - name: Publish to GitHub Packages Apache Maven 28 | run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml -Dmaven.test.skip=true 29 | env: 30 | GITHUB_TOKEN: ${{ github.token }} 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/config/CorsConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.config; 6 | 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import lombok.Data; 11 | 12 | /** 13 | * CORS(Cross Origin Resourse-Sharing) - 跨站资源共享 14 | * CSRF(Cross-Site Request Forgery) - 跨站请求伪造 15 | * 16 | * @author zmzhou 17 | * @version 1.0 18 | * @since 2021/5/11 16:28 19 | */ 20 | @Data 21 | @Configuration 22 | @ConfigurationProperties(prefix = "cors") 23 | public class CorsConfig { 24 | /** 25 | * 支持跨域请求的请求头信息字段 26 | */ 27 | private String[] allowedHeaders = {"*"}; 28 | /** 29 | * 支持跨域请求的方法 30 | */ 31 | private String[] allowedMethods = {"POST", "GET", "PUT", "DELETE", "OPTIONS", "HEAD"}; 32 | /** 33 | * 允许跨域请求的域名 34 | */ 35 | private String[] allowedOriginPatterns = {"*"}; 36 | /** 37 | * 是否允许发送Cookie 38 | */ 39 | private boolean allowCredentials = true; 40 | /** 41 | * 本次请求的有效期 42 | */ 43 | private long maxAge = 1800L; 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/WebShellApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell; 6 | 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.cache.annotation.EnableCaching; 10 | 11 | import com.github.zmzhoustar.webshell.utils.ThreadPoolUtils; 12 | 13 | /** 14 | * 程序入口 15 | * @title WebShellApplication 16 | * @author zmzhou 17 | * @version 1.0 18 | * @date 2021/1/30 23:00 19 | */ 20 | @SpringBootApplication 21 | @EnableCaching 22 | public class WebShellApplication { 23 | public static void main(String[] args) { 24 | // log4j2全局异步日志配置 http://logging.apache.org/log4j/2.x/manual/async.html#AllAsync 25 | System.setProperty("Log4jContextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector"); 26 | SpringApplication.run(WebShellApplication.class, args); 27 | 28 | // 停止应用时,关闭线程池钩子,或者使用 @PreDestroy 注解执行一系列操作 29 | Runtime.getRuntime().addShutdownHook(new Thread(ThreadPoolUtils::shutdown, "ShutdownThreadPoolHook")); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.config; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 11 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 12 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 13 | 14 | import com.github.zmzhoustar.webshell.handler.WebShellWebSocketHandler; 15 | import com.github.zmzhoustar.webshell.interceptor.WebSocketInterceptor; 16 | 17 | /** 18 | * websocket配置 19 | * @title WebSocketConfig 20 | * @author zmzhou 21 | * @version 1.0 22 | * @date 2021/1/31 13:13 23 | */ 24 | @Configuration 25 | @EnableWebSocket 26 | public class WebSocketConfig implements WebSocketConfigurer { 27 | @Resource 28 | private WebShellWebSocketHandler webSocketHandler; 29 | 30 | @Override 31 | public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { 32 | //socket通道 33 | //指定处理器和路径 34 | webSocketHandlerRegistry.addHandler(webSocketHandler, "/shell") 35 | .addInterceptors(new WebSocketInterceptor()) 36 | .setAllowedOrigins("*"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell; 6 | 7 | /** 8 | * 常量 9 | * @title ConstantPool 10 | * @author zmzhou 11 | * @version 1.0 12 | * @date 2021/2/23 20:39 13 | */ 14 | public final class Constants { 15 | /** 16 | * 分隔符 17 | */ 18 | public static final String SEPARATOR = "/"; 19 | /** 20 | * 减号 21 | */ 22 | public static final String MINUS = "-"; 23 | /** 24 | * 点 25 | */ 26 | public static final String DOT = "."; 27 | /** 28 | * 上级目录 29 | */ 30 | public static final String PARENT_DIRECTORY = ".."; 31 | /** root用户 */ 32 | public static final String USER_ROOT = "root"; 33 | /** 34 | * 缓存字符长度 35 | */ 36 | public static final int BUFFER_SIZE = 2048; 37 | /** 38 | * 随机生成uuid的key名 39 | */ 40 | public static final String USER_UUID_KEY = "user_uuid"; 41 | /** 42 | * 发送指令:连接 43 | */ 44 | public static final String OPERATE_CONNECT = "connect"; 45 | /** 46 | * 发送指令:命令 47 | */ 48 | public static final String OPERATE_COMMAND = "command"; 49 | /** 50 | * 发送指令:安全文件传送 51 | */ 52 | public static final String OPERATE_SFTP = "sftp"; 53 | 54 | /** 1kb */ 55 | public static final long KB = 1024L; 56 | 57 | private Constants() { 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | # 开发环境配置 2 | server: 3 | # 服务器的HTTP端口,默认为8080 4 | port: 9598 5 | servlet: 6 | # 应用的访问路径 7 | context-path: / 8 | # 优雅停机,默认是IMMEDIATE立即停机 9 | shutdown: graceful 10 | tomcat: 11 | # tomcat的URI编码 12 | uri-encoding: UTF-8 13 | connection-timeout: 60S 14 | # tomcat最大线程数,默认为200 15 | # Tomcat启动初始化的线程数,默认值10 16 | threads: 17 | max: 800 18 | min-spare: 30 19 | # Spring配置 20 | spring: 21 | thymeleaf: 22 | cache: false 23 | application: 24 | name: @project.name@ 25 | # 服务模块 26 | devtools: 27 | restart: 28 | # 热部署重启开关,不重启实现快速热部署 29 | enabled: false 30 | web: 31 | resources: 32 | # 静态资源的配置位置,可以写成一个数组配置多个目录 33 | static-locations: classpath:/static/ 34 | mvc: 35 | static-path-pattern: /static/** 36 | view: 37 | # 给返回的页面添加后缀名 38 | suffix: .html 39 | # 定位模板的目录 40 | prefix: classpath:/templates/ 41 | jackson: 42 | date-format: yyyy-MM-dd HH:mm:ss 43 | time-zone: GMT+8 44 | cache: 45 | type: ehcache 46 | ehcache: 47 | config: classpath:ehcache.xml 48 | servlet: 49 | multipart: 50 | max-file-size: 1000MB 51 | max-request-size: 5000MB 52 | # 日志配置 53 | logging: 54 | config: classpath:log4j2.xml 55 | # config: classpath:logback-spring.xml 56 | level: 57 | web: INFO 58 | com.github.zmzhoustar: INFO 59 | org.springframework: INFO 60 | logstash: 61 | address: 192.168.163.132:4567 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/vo/SftpFileTreeVo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.vo; 6 | 7 | import lombok.Builder; 8 | import lombok.Data; 9 | 10 | /** 11 | * 文件树图vo 12 | * @title SftpFileTreeVo 13 | * @author zmzhou 14 | * @version 1.0 15 | * @date 2021/2/28 21:49 16 | */ 17 | @Data 18 | @Builder 19 | public class SftpFileTreeVo implements Comparable { 20 | /** id,文件路径 */ 21 | private String id; 22 | /** 父路径 */ 23 | private String parent; 24 | /** 文件名 */ 25 | private String text; 26 | /** jstree图标 */ 27 | private String icon; 28 | 29 | /** 文件详情:文件类型 */ 30 | private String fileType; 31 | /** 文件详情:文件属性 */ 32 | private String fileAttr; 33 | /** 文件详情:目录/链接个数 */ 34 | private String numberOfDir; 35 | /** 文件详情:所有者 */ 36 | private String owner; 37 | /** 文件详情:组 */ 38 | private String group; 39 | /** 文件详情:文件大小 */ 40 | private String size; 41 | /** 文件详情:修改日期 */ 42 | private String modifiedDate; 43 | 44 | /** 45 | * Compares this object with the specified object for order. Returns a 46 | * negative integer, zero, or a positive integer as this object is less 47 | * than, equal to, or greater than the specified object. 48 | */ 49 | @Override 50 | public int compareTo(SftpFileTreeVo vo) { 51 | // 先根据文件类型排序,再根据文件名排序 52 | int ic = vo.getIcon().compareTo(icon); 53 | if (ic == 0) { 54 | return text.compareTo(vo.getText()); 55 | } 56 | return ic; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.config; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 11 | 12 | /** 13 | * 解决前后端分离跨域请求 14 | * 15 | * @author zmzhou 16 | * @version 1.0 17 | * @title WebMvcConfig 18 | * @date 2021/3/6 19:49 19 | */ 20 | @Configuration 21 | public class WebMvcConfig implements WebMvcConfigurer { 22 | @Autowired 23 | private CorsConfig corsConfig; 24 | 25 | /** 26 | * 跨站资源共享配置 27 | * Cross Origin Resourse-Sharing - 跨站资源共享 28 | * 29 | * @param registry CorsRegistry 30 | * @author zmzhou 31 | * @since 2021/8/22 18:46 32 | */ 33 | @Override 34 | public void addCorsMappings(CorsRegistry registry) { 35 | // Add more mappings... 可以添加多个mapping 36 | registry.addMapping("/**") 37 | // 服务器支持的所有头信息字段 38 | .allowedHeaders(corsConfig.getAllowedHeaders()) 39 | // 服务器支持的所有跨域请求的方法 40 | .allowedMethods(corsConfig.getAllowedMethods()) 41 | // 是否允许发送Cookie 42 | .allowCredentials(corsConfig.isAllowCredentials()) 43 | // 指定本次请求的有效期 44 | .maxAge(corsConfig.getMaxAge()) 45 | // 设置允许跨域请求的域名 46 | .allowedOriginPatterns(corsConfig.getAllowedOriginPatterns()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/static/js/web-socket.js: -------------------------------------------------------------------------------- 1 | function WebSocketClient() { 2 | } 3 | 4 | /** 5 | * 根据location路径生成WebSocket地址 6 | * @returns {string} WebSocket地址 7 | */ 8 | WebSocketClient.prototype.getWebSocketUrl = function () { 9 | let protocol = 'ws://'; 10 | if (window.location.protocol === 'https:') { 11 | protocol = 'wss://'; 12 | } 13 | return protocol + window.location.host + '/shell'; 14 | } 15 | /** 16 | * 连接WebSocket 17 | * @param params 18 | */ 19 | WebSocketClient.prototype.connect = function (params) { 20 | if (window.WebSocket) { 21 | //如果支持websocket 22 | this._connection = new WebSocket(this.getWebSocketUrl()); 23 | }else { 24 | //否则报错 25 | params.onError('WebSocket Not Supported'); 26 | return; 27 | } 28 | 29 | this._connection.onopen = function () { 30 | params.onConnect(); 31 | }; 32 | 33 | this._connection.onmessage = function (evt) { 34 | let data = evt.data.toString(); 35 | params.onData(data); 36 | }; 37 | 38 | this._connection.onclose = function (evt) { 39 | params.onClose(); 40 | }; 41 | } 42 | /** 43 | * 发送指令 44 | * @param {Object} params 指令参数(必须含有operate参数) 45 | */ 46 | WebSocketClient.prototype.send = function (params) { 47 | this._connection.send(JSON.stringify(params)); 48 | } 49 | /** 50 | * 发送普通操作指令 51 | * @param {String} data 操作指令 52 | */ 53 | WebSocketClient.prototype.sendClientData = function (data) { 54 | //发送指令 55 | this._connection.send(JSON.stringify({"operate": "command", "command": data})) 56 | } 57 | -------------------------------------------------------------------------------- /HELP.md: -------------------------------------------------------------------------------- 1 | # Read Me First 2 | The following was discovered as part of building this project: 3 | 4 | * The original package name 'com.github.zmzhou-star.web-shell' is invalid and this project uses 'com.github.zmzhoustar.webshell' instead. 5 | 6 | # Getting Started 7 | 8 | ### Reference Documentation 9 | For further reference, please consider the following sections: 10 | 11 | * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) 12 | * [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.4.1/maven-plugin/reference/html/) 13 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.4.1/maven-plugin/reference/html/#build-image) 14 | * [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#using-boot-devtools) 15 | * [Spring Web](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-developing-web-applications) 16 | * [Thymeleaf](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-spring-mvc-template-engines) 17 | * [WebSocket](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-websockets) 18 | 19 | ### Guides 20 | The following guides illustrate how to use some features concretely: 21 | 22 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) 23 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 24 | * [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/) 25 | * [Handling Form Submission](https://spring.io/guides/gs/handling-form-submission/) 26 | * [Using WebSocket to build an interactive web application](https://spring.io/guides/gs/messaging-stomp-websocket/) 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/interceptor/WebSocketInterceptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.interceptor; 6 | 7 | import java.util.Map; 8 | import java.util.UUID; 9 | 10 | import org.springframework.http.server.ServerHttpRequest; 11 | import org.springframework.http.server.ServerHttpResponse; 12 | import org.springframework.http.server.ServletServerHttpRequest; 13 | import org.springframework.web.socket.WebSocketHandler; 14 | import org.springframework.web.socket.server.HandshakeInterceptor; 15 | 16 | import com.github.zmzhoustar.webshell.Constants; 17 | 18 | import lombok.extern.slf4j.Slf4j; 19 | 20 | /** 21 | * WebSocket拦截器 22 | * 23 | * @author zmzhou 24 | * @version 1.0 25 | * @title WebSocketInterceptor 26 | * @date 2021/1/31 13:18 27 | */ 28 | @Slf4j 29 | public class WebSocketInterceptor implements HandshakeInterceptor { 30 | /** 31 | * 处理前beforeHandshake 32 | * @author zmzhou 33 | * @date 2021/1/31 13:21 34 | */ 35 | @Override 36 | public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, 37 | WebSocketHandler webSocketHandler, Map map) { 38 | if (serverHttpRequest instanceof ServletServerHttpRequest) { 39 | //生成一个UUID 40 | String uuid = UUID.randomUUID().toString().replace("-", ""); 41 | //将uuid放到websocketsession中 42 | map.put(Constants.USER_UUID_KEY, uuid); 43 | return true; 44 | } 45 | return false; 46 | } 47 | 48 | @Override 49 | public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, 50 | WebSocketHandler webSocketHandler, Exception e) { 51 | log.info("afterHandshake"); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/controller/ElectronController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.controller; 6 | 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import com.alibaba.fastjson.JSONObject; 13 | import com.github.zmzhoustar.webshell.utils.WebShellUtils; 14 | import com.github.zmzhoustar.webshell.vo.ApiResult; 15 | 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | /** 19 | * 桌面版控制层 20 | * 21 | * @author zmzhou 22 | * @version 1.0 23 | * @date 2021/3/6 19:41 24 | */ 25 | @Slf4j 26 | @RequestMapping("/") 27 | @RestController 28 | public class ElectronController { 29 | /** 30 | * 登录 31 | * 32 | * @param username 用户名 33 | * @param password 密码 34 | * @return com.github.zmzhoustar.webshell.vo.ApiResult 35 | * @author zmzhou 36 | * @since 2021/8/22 19:06 37 | */ 38 | @PostMapping("/user/login") 39 | public ApiResult login(String username, String password){ 40 | String token = WebShellUtils.getSessionId(); 41 | return ApiResult.builder().data(token); 42 | } 43 | 44 | /** 45 | * 获取用户信息 46 | * 47 | * @param token token 48 | * @return com.github.zmzhoustar.webshell.vo.ApiResult 49 | * @author zmzhou 50 | * @since 2021/8/22 19:07 51 | */ 52 | @GetMapping("/user/info") 53 | public ApiResult userInfo(String token){ 54 | ApiResult res = ApiResult.builder(); 55 | JSONObject json = new JSONObject(); 56 | json.put("roles", "admin"); 57 | json.put("name", "zmzhou"); 58 | return res.data(json); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/controller/RouterController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.controller; 6 | 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | 11 | import com.alibaba.fastjson.JSON; 12 | import com.github.zmzhoustar.webshell.utils.EhCacheUtils; 13 | import com.github.zmzhoustar.webshell.utils.SftpUtils; 14 | import com.github.zmzhoustar.webshell.utils.WebShellUtils; 15 | import com.github.zmzhoustar.webshell.vo.WebShellData; 16 | 17 | import lombok.extern.slf4j.Slf4j; 18 | 19 | /** 20 | * 路由控制类 21 | * 22 | * @author zmzhou 23 | * @version 1.0 24 | * @title RouterController 25 | * @date 2021/1/30 23:32 26 | */ 27 | @Slf4j 28 | @Controller 29 | public class RouterController { 30 | /** 31 | * index 32 | * @author zmzhou 33 | * @date 2021/1/30 23:33 34 | */ 35 | @GetMapping({"/", "/index"}) 36 | public String index() { 37 | return "index"; 38 | } 39 | 40 | /** 41 | * sftp 42 | * @author zmzhou 43 | * @date 2021/2/26 16:40 44 | */ 45 | @GetMapping("/sftp") 46 | public String sftp(String params, Model model) { 47 | String sessionId = WebShellUtils.getSessionId(); 48 | log.info("sessionId:{}", sessionId); 49 | WebShellData sshData = JSON.parseObject(params, WebShellData.class); 50 | // 存放ssh连接信息 51 | if (sshData != null) { 52 | EhCacheUtils.put(sessionId, sshData); 53 | } else { 54 | sshData = EhCacheUtils.get(sessionId); 55 | } 56 | if (sshData != null) { 57 | SftpUtils sftpUtils = new SftpUtils(sshData); 58 | boolean login = sftpUtils.login(); 59 | // 登录成功状态 60 | model.addAttribute("login", login); 61 | model.addAttribute("host", sshData.getHost()); 62 | sftpUtils.logout(); 63 | } 64 | return "sftp"; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/utils/WebShellUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.utils; 6 | 7 | import org.springframework.web.socket.WebSocketSession; 8 | 9 | import com.github.zmzhoustar.webshell.Constants; 10 | 11 | /** 12 | * 常用工具类 13 | * @title WebShellUtils 14 | * @author zmzhou 15 | * @version 1.0 16 | * @date 2021/2/23 20:45 17 | */ 18 | public final class WebShellUtils { 19 | private WebShellUtils() { 20 | } 21 | 22 | /** 23 | * 从WebSocketSession获取用户名 24 | * @param webSocketSession WebSocketSession 25 | * @author zmzhou 26 | * @date 2021/2/23 20:47 27 | */ 28 | public static String getUuid(WebSocketSession webSocketSession){ 29 | return String.valueOf(webSocketSession.getAttributes().get(Constants.USER_UUID_KEY)); 30 | } 31 | 32 | /** 33 | * Gets session id. 34 | * 35 | * @return the session id 36 | * @author zmzhou 37 | * @date 2021/3/1 14:36 38 | */ 39 | public static String getSessionId() { 40 | return SpringUtils.getSession().getId(); 41 | } 42 | 43 | /** 44 | * 文件大小转换单位 45 | * @param size 文件大小 46 | * @author zmzhou 47 | * @date 2021/3/1 17:53 48 | */ 49 | public static String convertFileSize(long size) { 50 | long kb = Constants.KB; 51 | long mb = kb * Constants.KB; 52 | long gb = mb * Constants.KB; 53 | String fileSize; 54 | if (size >= gb) { 55 | fileSize = String.format("%.1fGB", (float)size / (float)gb); 56 | } else { 57 | float f; 58 | if (size >= mb) { 59 | f = (float)size / (float)mb; 60 | fileSize = String.format(f > 100.0F ? "%.0fMB" : "%.1fMB", f); 61 | } else if (size >= kb) { 62 | f = (float)size / (float)kb; 63 | fileSize = String.format(f > 100.0F ? "%.0fKB" : "%.1fKB", f); 64 | } else { 65 | fileSize = String.format("%dB", size); 66 | } 67 | } 68 | return fileSize; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/resources/static/js/web-shell.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by zmzhou on 2021/2/24 16:50 3 | */ 4 | 5 | /** 6 | * 加密 7 | * @param content 加密内容 8 | * @returns {*} 9 | */ 10 | let encrypt = function (content) { 11 | let aseKey = "ws9ybUMn4F81t5oPKqJrqLKxERaYAS12" 12 | return CryptoJS.AES.encrypt(content, CryptoJS.enc.Utf8.parse(aseKey), { 13 | mode: CryptoJS.mode.ECB, 14 | padding: CryptoJS.pad.Pkcs7 15 | }).toString(); 16 | } 17 | 18 | function showTips(msg) { 19 | $("#message_console").html(new Date().toLocaleTimeString() + ":" + msg); 20 | } 21 | function uploadFile() { 22 | let val = $('#current_path').val(); 23 | // 设置上传路径 24 | $("#path").val(val); 25 | // 拼接所有上传文件名 26 | let files = $("#file").get(0).files; 27 | let fileNames = []; 28 | for (let i = 0; i < files.length; i++) { 29 | fileNames.push(files[i].name) 30 | } 31 | //获取form数据 32 | let formData = new FormData(document.querySelector("#upload_form")); 33 | $.ajax({ 34 | url: "/sftp/upload", 35 | type: "POST", 36 | data: formData, 37 | processData: false, // 不处理数据 38 | contentType: false, // 不设置内容类型 39 | success: function (res) { 40 | let tips = fileNames.join(",") + res.data; 41 | showTips(tips); 42 | let $tree = $('#file_tree').jstree(true); 43 | $tree.select_node(val); 44 | $tree.open_node($tree.get_node(val)); 45 | alert(tips); 46 | } 47 | }); 48 | } 49 | function checkErr(data) { 50 | if (data.code === 200) { 51 | showTips("操作成功!"); 52 | return true; 53 | } 54 | showTips("操作失败!" + data.msg); 55 | return false; 56 | } 57 | 58 | /** 59 | * 刷新数据 60 | */ 61 | function refreshTree() { 62 | let val = $('#current_path').val(); 63 | let $tree = $('#file_tree').jstree(true); 64 | $tree.select_node(val); 65 | $tree.open_node($tree.get_node(val)); 66 | } -------------------------------------------------------------------------------- /src/main/resources/ehcache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 19 | 20 | 22 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/utils/EhCacheUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.utils; 6 | 7 | import org.springframework.cache.Cache; 8 | import org.springframework.cache.CacheManager; 9 | 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | /** 13 | * ehcache缓存操作工具类 14 | * 15 | * @author zmzhou 16 | * @version 1.0 17 | * @title EhCacheUtils 18 | * @date 2021/3/1 11:56 19 | */ 20 | @Slf4j 21 | public final class EhCacheUtils { 22 | /** CacheManager */ 23 | private static final CacheManager CACHE_MANAGER = SpringUtils.getBean(CacheManager.class); 24 | 25 | /** 26 | * 获取Cache 27 | * 28 | * @author zmzhou 29 | * @date 2021/3/1 12:49 30 | */ 31 | public static Cache getCache() { 32 | return CACHE_MANAGER.getCache("myCache"); 33 | } 34 | 35 | /** 36 | * 添加缓存数据 37 | * 38 | * @param key 键 39 | * @param value 值 40 | * @author zmzhou 41 | * @date 2021/3/1 12:50 42 | */ 43 | public static void put(String key, Object value) { 44 | try { 45 | Cache cache = getCache(); 46 | cache.put(key, value); 47 | } catch (Exception e) { 48 | log.error("添加缓存失败:{}", e.getMessage()); 49 | } 50 | } 51 | 52 | /** 53 | * 获取缓存数据 54 | * 55 | * @param key 键 56 | * @return 缓存数据 57 | * @author zmzhou 58 | * @date 2021/3/1 12:53 59 | */ 60 | public static T get(String key) { 61 | try { 62 | Cache cache = getCache(); 63 | return (T) cache.get(key).get(); 64 | } catch (Exception e) { 65 | log.error("获取缓存数据失败:", e); 66 | return null; 67 | } 68 | } 69 | 70 | /** 71 | * 删除缓存数据 72 | * 73 | * @param key 键 74 | * @author zmzhou 75 | * @date 2021/3/1 12:53 76 | */ 77 | public static void delete(String key) { 78 | try { 79 | Cache cache = getCache(); 80 | cache.evict(key); 81 | } catch (Exception e) { 82 | log.error("删除缓存数据失败:", e); 83 | } 84 | } 85 | 86 | /** 87 | * @author zmzhou 88 | * @date 2021/3/1 12:02 89 | */ 90 | private EhCacheUtils() { 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-shell [国内站点](https://gitee.com/zmzhou-star/web-shell) 2 | [English version](README.en.md) 3 | 4 | > 作者的个人微信公众号:[Java程序员ZZM](https://gitee.com/zmzhou-star/learnotes/raw/master/docs/wechat-zmzhou-star.png) 关注我不迷路 5 | 6 | > 个人网站:[https://www.zmzhou-star.cn](https://www.zmzhou-star.cn) 7 | 8 | > 学习笔记:[https://zmzhou-star.github.io/learnotes](https://zmzhou-star.github.io/learnotes) 9 | 10 | ## 介绍 11 | 纯Java实现一个web shell登录Linux远程主机,技术选型 SpringBoot + WebSocket + jsch + xterm.js 12 | 13 | #### 软件架构说明 14 | * [Spring Boot](https://start.spring.io/) 15 | * [Thymeleaf](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-spring-mvc-template-engines) 16 | * [WebSocket](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-websockets) 17 | * [jsch](https://github.com/is/jsch) 18 | * [xterm.js](https://github.com/xtermjs/xterm.js/) 19 | * 缓存使用 [spring cache](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-caching-provider-ehcache2) + [ehcache](https://www.ehcache.org/) 20 | 21 | #### 安装教程 22 | 1. `打包` 23 | ``` 24 | mvn clean install -X -DskipTests 25 | ``` 26 | 2. `运行(Windows或Linux)` 27 | ``` 28 | java -jar -server web-shell-1.0.jar 29 | ``` 30 | 当前ssh窗口被锁定,可按CTRL + C打断程序运行,或直接关闭窗口,程序退出。 31 | 32 | 3. `运行(Linux后台运行)` 33 | ``` 34 | nohup java -jar -server web-shell-1.0.jar > logs/web-shell.out 2>&1 & 35 | ``` 36 | nohup 意思是不挂断运行命令,当账户退出或终端关闭时,程序仍然运行。 37 | &代表在后台运行 38 | 39 | 4. `访问地址:`http://127.0.0.1:9598/ 40 | 41 | 42 | 5. `使用说明` 43 | * sftp页面文件详情列表窗口双击文件可下载 44 | * sftp页面点击`选择文件上传`按钮选择文件上传(可以多选) 45 | 46 | #### 运行效果图 47 | ![登录页面](docs/login.png) 48 | ![shell页面](docs/shell.png) 49 | ![sftp页面](docs/sftp.png) 50 | 51 | #### License 52 | [The Apache-2.0 License](./LICENSE) 53 | 54 | 请自由地享受和参与开源 55 | 56 | #### 捐赠 57 | 开源不易,请多鼓励!(注:如果该项目对您有帮助,请捐赠以表示支持,谢谢!捐赠请备注web-shell捐赠和称呼哦,谢谢!) 58 | 59 | | 支付宝 | 微信 | 60 | | :------------: | :------------: | 61 | | ![Alipay](docs/alipay.png) | ![Wechat](docs/wechatpay.png) | 62 | 63 | #### 联系作者 64 | email:Contact zmzhou-star 65 | 66 | 微信公众号:![微信公众号](docs/wechat-zmzhou-star.png) 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/utils/FileType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.utils; 6 | 7 | /** 8 | * 文件类型枚举 9 | * @title FileType 10 | * @author zmzhou 11 | * @version 1.0 12 | * @date 2021/3/2 10:14 13 | */ 14 | public enum FileType { 15 | /** 普通文件 */ 16 | NORMAL_FILE("-", "普通文件"), 17 | /** 目录 */ 18 | DIRECTORY("d", "目录"), 19 | /** 链接文件 */ 20 | LINK_FILE("l", "链接文件"), 21 | /** 管理文件 */ 22 | MANAGE_FILE("p", "管理文件"), 23 | /** 块设备文件 */ 24 | BLOCK_DEVICE_FILE("b", "块设备文件"), 25 | /** 字符设备文件 */ 26 | CHARACTER_DEVICE_FILE("c", "字符设备文件"), 27 | /** 套接字文件 */ 28 | SOCKET_FILE("s", "套接字文件"); 29 | 30 | /** 文件类型字符 */ 31 | private final String sign; 32 | /** 文件类型中文名 */ 33 | private final String zhName; 34 | 35 | FileType(String sign, String zhName) { 36 | this.sign = sign; 37 | this.zhName = zhName; 38 | } 39 | 40 | /** 41 | * Gets zh name. 42 | * 43 | * @param sign the sign 44 | * @return the zh name 45 | */ 46 | public static String getZhName(String sign) { 47 | FileType[] types = FileType.values(); 48 | for (FileType type : types) { 49 | if (type.getSign().equals(sign)) { 50 | return type.getZhName(); 51 | } 52 | } 53 | return NORMAL_FILE.zhName; 54 | } 55 | 56 | /** 57 | * 获取文件类型图标 58 | * @param fileType 文件类型 59 | * @return 图片地址或css class Name 60 | * @author zmzhou 61 | * @date 2021/3/2 11:29 62 | */ 63 | public static String getFileTypeIcon(String fileType) { 64 | String icon; 65 | if (fileType.equals(NORMAL_FILE.getSign())) { 66 | // 文件图标 67 | icon = "jstree-file"; 68 | } else if (fileType.equals(DIRECTORY.getSign())) { 69 | // 文件夹图标 70 | icon = "jstree-folder"; 71 | } else { 72 | icon = "/static/img/" + fileType + ".png"; 73 | } 74 | return icon; 75 | } 76 | 77 | /** 78 | * Gets sign. 79 | * 80 | * @return the sign 81 | */ 82 | public String getSign() { 83 | return sign; 84 | } 85 | 86 | /** 87 | * Gets zh name. 88 | * 89 | * @return the zh name 90 | */ 91 | public String getZhName() { 92 | return zhName; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/vo/ApiResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.vo; 6 | 7 | import java.io.Serializable; 8 | 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.http.HttpStatus; 11 | 12 | import lombok.Data; 13 | 14 | /** 15 | * 返回值对象 16 | * @author zmzhou 17 | * @date 2020/07/02 17:11 18 | */ 19 | @Data 20 | public class ApiResult implements Serializable { 21 | /** serialVersionUID */ 22 | private static final long serialVersionUID = 1L; 23 | /** 消息状态码 */ 24 | private int code = HttpStatus.OK.value(); 25 | /** 消息内容 */ 26 | private String msg = HttpStatus.OK.getReasonPhrase(); 27 | /** 列表数据 */ 28 | private transient T data; 29 | /** 30 | * 构造器 31 | * @author zmzhou 32 | * @date 2020/9/12 11:33 33 | */ 34 | public ApiResult() { 35 | // 构造器 36 | } 37 | 38 | /** 39 | * 错误请求 40 | * @author zmzhou 41 | * @date 2020/07/03 14:12 42 | */ 43 | public static ApiResult badRequest() { 44 | ApiResult result = new ApiResult<>(); 45 | result.setCode(HttpStatus.BAD_REQUEST.value()); 46 | result.setMsg(HttpStatus.BAD_REQUEST.getReasonPhrase()); 47 | return result; 48 | } 49 | public static ApiResult builder() { 50 | return new ApiResult<>(); 51 | } 52 | 53 | /** 54 | * 请求错误设置失败信息 55 | * @param code 消息状态码 56 | * @param errorMsg 错误信息 57 | * @return ApiResult 58 | * @author zmzhou 59 | * @date 2020/07/08 14:05 60 | */ 61 | public ApiResult error(int code, String errorMsg) { 62 | setCode(code); 63 | if (StringUtils.isNotBlank(errorMsg)) { 64 | setMsg(errorMsg); 65 | } 66 | return this; 67 | } 68 | /** 69 | * 请求成功设置返回信息 70 | * @param msg 返回信息 71 | * @return ApiResult 72 | * @author zmzhou 73 | * @date 2020/07/08 14:05 74 | */ 75 | public ApiResult info(String msg) { 76 | if (StringUtils.isNotBlank(msg)) { 77 | setMsg(msg); 78 | } 79 | return this; 80 | } 81 | /** 82 | * 设置返回数据 83 | * @param data 返回数据 84 | * @return ApiResult 85 | * @author zmzhou 86 | * @date 2020/9/9 22:17 87 | */ 88 | public ApiResult data(T data) { 89 | if (null != data) { 90 | setData(data); 91 | } 92 | return this; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # web-shell [Github](https://github.com/zmzhou-star/web-shell) 2 | [中文版](README.md) 3 | 4 | #### Introduce 5 | Pure Java implements a web shell login Linux remote host, technology selection SpringBoot + WebSocket + jsch + xterm.js 6 | 7 | #### Software architecture description 8 | * [Spring Boot](https://start.spring.io/) 9 | * [Thymeleaf](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-spring-mvc-template-engines) 10 | * [WebSocket](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-websockets) 11 | * [jsch](https://github.com/is/jsch) 12 | * [xterm.js](https://github.com/xtermjs/xterm.js/) 13 | * Cache usage [spring cache](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-caching-provider-ehcache2) + [ehcache](https://www.ehcache.org/) 14 | 15 | #### Installation tutorial 16 | 1. `package` 17 | ``` 18 | mvn clean install -X -DskipTests 19 | ``` 20 | 2. `run(Windows or Linux)` 21 | ``` 22 | java -jar -server web-shell-1.0.jar 23 | ``` 24 | The current ssh window is locked, you can press CTRL + C to interrupt the program running, or directly close the window, the program exits. 25 | 26 | 3. `run(Linux running in the background)` 27 | ``` 28 | nohup java -jar -server web-shell-1.0.jar > logs/web-shell.out 2>&1 & 29 | ``` 30 | nohup It means to run the command without hanging up, and the program will still run when the account is exited or the terminal is closed 31 | &represents running in the background 32 | 33 | 4. `Address:`http://127.0.0.1:9598/ 34 | 35 | 36 | 5. `instructions for use` 37 | * sftp page file details list window double click the file to download 38 | * On the sftp page, click the `Select File Upload` button to select file upload (multiple selections are possible) 39 | 40 | #### Running effect chart 41 | ![login](docs/login.png) 42 | ![shell](docs/shell.png) 43 | ![sftp](docs/sftp.png) 44 | 45 | #### License 46 | [The Apache-2.0 License](LICENSE) 47 | 48 | Please feel free to enjoy and participate in open source 49 | 50 | #### Donate 51 | Open source is not easy, please encourage! (Note: If this project is helpful to you, please donate to show your support, thank you! Please note the web-shell donation and title for donation, thank you!) 52 | 53 | | Alipay | Wechat | 54 | | :------------: | :------------: | 55 | | ![Alipay](docs/alipay.png) | ![Wechat](docs/wechatpay.png) | 56 | 57 | #### Contact 58 | email:Contact zmzhou-star 59 | 60 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/listener/ApplicationEventListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.listener; 6 | 7 | import org.springframework.boot.availability.AvailabilityChangeEvent; 8 | import org.springframework.boot.context.event.ApplicationContextInitializedEvent; 9 | import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; 10 | import org.springframework.boot.context.event.ApplicationFailedEvent; 11 | import org.springframework.boot.context.event.ApplicationPreparedEvent; 12 | import org.springframework.boot.context.event.ApplicationReadyEvent; 13 | import org.springframework.boot.context.event.ApplicationStartedEvent; 14 | import org.springframework.boot.context.event.ApplicationStartingEvent; 15 | import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent; 16 | import org.springframework.context.ApplicationEvent; 17 | import org.springframework.context.ApplicationListener; 18 | import org.springframework.context.event.ContextClosedEvent; 19 | import org.springframework.context.event.ContextRefreshedEvent; 20 | import org.springframework.context.event.ContextStoppedEvent; 21 | 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | /** 25 | * springboot生命周期 26 | * @author zmzhou 27 | * @version 1.0 28 | * date 2021/4/20 13:54 29 | */ 30 | @Slf4j 31 | public class ApplicationEventListener implements ApplicationListener { 32 | 33 | @Override 34 | public void onApplicationEvent(ApplicationEvent event) { 35 | // 在这里可以监听到Spring Boot的生命周期 36 | if (event instanceof ApplicationStartingEvent) { 37 | log.info("应用程序启动中"); 38 | } else if (event instanceof ApplicationEnvironmentPreparedEvent) { 39 | log.info("初始化环境变量"); 40 | } else if (event instanceof ApplicationContextInitializedEvent) { 41 | log.info("ApplicationContext初始化完成"); 42 | } else if (event instanceof ApplicationPreparedEvent) { 43 | log.info("Spring容器执行refresh前触发"); 44 | } else if (event instanceof ServletWebServerInitializedEvent) { 45 | log.info("Servlet Web服务器初始化"); 46 | } else if (event instanceof ContextRefreshedEvent) { 47 | log.info("应用Context刷新"); 48 | } else if (event instanceof ApplicationStartedEvent) { 49 | log.info("应用程序启动好了"); 50 | } else if (event instanceof ApplicationReadyEvent) { 51 | log.info("应用已准备完成"); 52 | } else if (event instanceof AvailabilityChangeEvent) { 53 | log.info("应用已处于活动状态"); 54 | } else if (event instanceof ApplicationFailedEvent) { 55 | log.info("应用启动失败"); 56 | } else if (event instanceof ContextStoppedEvent) { 57 | log.info("应用停止"); 58 | } else if (event instanceof ContextClosedEvent) { 59 | log.info("应用关闭"); 60 | } else { 61 | log.info("其他事件:{}", event); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/utils/SpringUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.utils; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpSession; 9 | 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.ApplicationContextAware; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.web.context.request.RequestAttributes; 14 | import org.springframework.web.context.request.RequestContextHolder; 15 | import org.springframework.web.context.request.ServletRequestAttributes; 16 | 17 | import lombok.extern.slf4j.Slf4j; 18 | 19 | /** 20 | * spring上下文工具类 21 | * @title SpringUtils 22 | * @author zmzhou 23 | * Date 2020/07/03 15:36 24 | */ 25 | @Slf4j 26 | @Component 27 | public final class SpringUtils implements ApplicationContextAware { 28 | /** spring上下文 */ 29 | private static ApplicationContext context; 30 | 31 | /** 32 | * @param applicationContext 上下文 33 | * 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量 34 | * @author zmzhou 35 | * @date 2020/9/2 22:36 36 | */ 37 | @Override 38 | public void setApplicationContext(ApplicationContext applicationContext) { 39 | log.info("初始化上下文:{}", applicationContext); 40 | SpringUtils.context = applicationContext; 41 | } 42 | /** 43 | * 获取ApplicationContext. 44 | * @return ApplicationContext 45 | * @author zmzhou 46 | * @date 2020/9/2 22:41 47 | */ 48 | public static ApplicationContext getContext() { 49 | return context; 50 | } 51 | /** 52 | * 从ApplicationContext中取得Bean 53 | * @param clazz bean 54 | * @return 对象 55 | * @author zmzhou 56 | * @date 2020/9/2 22:40 57 | */ 58 | public static T getBean(Class clazz) { 59 | return getContext().getBean(clazz); 60 | } 61 | 62 | /** 63 | * 获取request 64 | */ 65 | public static HttpServletRequest getRequest() { 66 | return getRequestAttributes().getRequest(); 67 | } 68 | 69 | /** 70 | * 获取session 71 | */ 72 | public static HttpSession getSession() { 73 | return getRequest().getSession(); 74 | } 75 | 76 | /** 77 | * 获取ServletRequestAttributes 78 | * @return ServletRequestAttributes 79 | * @author zmzhou 80 | * @date 2020/9/6 12:07 81 | */ 82 | public static ServletRequestAttributes getRequestAttributes() { 83 | RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); 84 | return (ServletRequestAttributes) attributes; 85 | } 86 | /** 87 | * 私有构造器 88 | * 89 | * @author zmzhou 90 | * @date 2020/08/29 14:18 91 | */ 92 | private SpringUtils() { 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/utils/SftpFileUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.utils; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.Vector; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | import com.github.zmzhoustar.webshell.Constants; 15 | import com.github.zmzhoustar.webshell.vo.SftpFileTreeVo; 16 | import com.jcraft.jsch.ChannelSftp; 17 | 18 | /** 19 | * sftp文件工具类 20 | * @title SftpFileUtils 21 | * @author zmzhou 22 | * @version 1.0 23 | * @date 2021/2/28 21:52 24 | */ 25 | public final class SftpFileUtils { 26 | /** 长文件名正则表达式 */ 27 | private static final Pattern FILE_PATTERN = Pattern.compile( 28 | "^([-dlpbcsrwx]{10})\\s+([0-9]+)\\s+([0-9a-zA-Z]+)\\s+([0-9a-zA-Z]+)\\s+([0-9]+)\\s+([0-9a-zA-Z:\\s]+)\\s+"); 29 | /** 30 | * 获取文件列表 31 | * @param sftpUtils SftpUtils 32 | * @param path 路径 33 | * @return 文件列表 34 | * @author zmzhou 35 | * @date 2021/2/28 22:04 36 | */ 37 | public static List getFileTree(SftpUtils sftpUtils, String path) { 38 | List fileTree = new ArrayList<>(); 39 | String parentPath = path; 40 | if (!parentPath.endsWith(Constants.SEPARATOR)) { 41 | parentPath = path + Constants.SEPARATOR; 42 | } 43 | Vector files = sftpUtils.listFiles(parentPath); 44 | String finalParentPath = parentPath; 45 | files.forEach(file -> { 46 | ChannelSftp.LsEntry lsEntry = (ChannelSftp.LsEntry) file; 47 | String fileType = lsEntry.getLongname().substring(0, 1); 48 | // 文件类型图标 49 | String icon = FileType.getFileTypeIcon(fileType); 50 | SftpFileTreeVo vo = SftpFileTreeVo.builder() 51 | .id(finalParentPath + lsEntry.getFilename()) 52 | .parent(finalParentPath) 53 | .text(lsEntry.getFilename()) 54 | .icon(icon) 55 | .build(); 56 | // 匹配文件详情 57 | Matcher m = FILE_PATTERN.matcher(lsEntry.getLongname()); 58 | if (m.find()) { 59 | vo.setFileType(FileType.getZhName(m.group(1).substring(0, 1))); 60 | vo.setFileAttr(m.group(1).substring(1)); 61 | vo.setNumberOfDir(m.group(2)); 62 | vo.setOwner(m.group(3)); 63 | vo.setGroup(m.group(4)); 64 | vo.setSize(WebShellUtils.convertFileSize(Long.parseLong(m.group(5)))); 65 | vo.setModifiedDate(m.group(6)); 66 | } 67 | fileTree.add(vo); 68 | }); 69 | // 排序 70 | Collections.sort(fileTree); 71 | return fileTree; 72 | } 73 | /** 74 | * 获取文件所属用户 75 | * @param longName 文件详情 76 | * @return 文件所属用户 77 | * @author zmzhou 78 | * @date 2021/3/7 19:37 79 | */ 80 | public static String getOwner(String longName){ 81 | // 正则匹配长文件详情 82 | Matcher m = FILE_PATTERN.matcher(longName); 83 | if (m.find()) { 84 | return m.group(3); 85 | } 86 | return ""; 87 | } 88 | /** 89 | * 私有构造器 90 | * @author zmzhou 91 | * @date 2021/2/28 21:53 92 | */ 93 | private SftpFileUtils() { 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/utils/ThreadPoolUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.utils; 6 | 7 | import java.util.concurrent.Callable; 8 | import java.util.concurrent.Future; 9 | import java.util.concurrent.ThreadPoolExecutor; 10 | 11 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 12 | 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | /** 16 | * 线程池工具类 17 | * 18 | * @author zmzhou 19 | * @version 1.0 20 | * @title ThreadPoolUtils 21 | * @date 2020/9/13 18:08 22 | */ 23 | @Slf4j 24 | public final class ThreadPoolUtils { 25 | /** 26 | * ThreadPoolTaskExecutor 27 | */ 28 | private static ThreadPoolTaskExecutor threadPool = null; 29 | 30 | /** 31 | * 工具类私有化构造器 32 | * @author zmzhou 33 | * @date 2020/9/13 18:20 34 | */ 35 | private ThreadPoolUtils() { 36 | } 37 | 38 | /** 39 | * 无返回值直接执行 40 | * 41 | * @param runnable Runnable 42 | * @author zmzhou 43 | * @date 2020/9/13 18:09 44 | */ 45 | public static void execute(Runnable runnable) { 46 | get().execute(runnable); 47 | } 48 | 49 | /** 50 | * 有返回值直接执行 51 | * 52 | * @param callable Callable 53 | * @return Future future.get()获取返回值 54 | * @author zmzhou 55 | * @date 2020/9/13 18:10 56 | */ 57 | public static Future submit(Callable callable) { 58 | return get().submit(callable); 59 | } 60 | 61 | /** 62 | * 获取线程池对象 63 | * @return ThreadPoolTaskExecutor 64 | * @author zmzhou 65 | * @date 2020/9/13 18:21 66 | */ 67 | private static ThreadPoolTaskExecutor get() { 68 | if (threadPool != null) { 69 | log.debug("线程池已创建"); 70 | return threadPool; 71 | } else { 72 | synchronized (ThreadPoolUtils.class) { 73 | //二次检查 74 | if (threadPool == null) { 75 | // 获取处理器数量 Ncpu = CPU核心数 76 | int cpuNum = Runtime.getRuntime().availableProcessors(); 77 | // 根据cpu数量,计算出合理的线程并发数 78 | // Nthreads = Ncpu x Ucpu x (1 + W/C),其中 Ucpu = CPU使用率,0~1;W/C = 等待时间与计算时间的比率 79 | int threadNum = cpuNum * 2; 80 | // 创建线程池 81 | threadPool = new ThreadPoolTaskExecutor(); 82 | threadPool.setThreadNamePrefix("web-shell-thread"); 83 | // 核心线程数 84 | threadPool.setCorePoolSize(threadNum); 85 | // 最大线程数 86 | threadPool.setMaxPoolSize(threadNum + 1); 87 | // 队列已满,而且当前线程数已经超过最大线程数时的异常处理策略 来电运行政策 88 | threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); 89 | //用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁。 90 | threadPool.setWaitForTasksToCompleteOnShutdown(true); 91 | //该方法用来设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。 92 | threadPool.setAwaitTerminationSeconds(30); 93 | // 初始化 94 | threadPool.initialize(); 95 | log.info("创建线程池完成:{}", threadPool.toString()); 96 | } 97 | } 98 | } 99 | return threadPool; 100 | } 101 | 102 | /** 103 | * 停止线程池任务 104 | * @author zmzhou 105 | * @since 2021/5/7 17:12 106 | */ 107 | public static void shutdown() { 108 | if (threadPool != null) { 109 | threadPool.shutdown(); 110 | log.info("关闭线程池"); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/handler/WebShellWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.handler; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.socket.BinaryMessage; 11 | import org.springframework.web.socket.CloseStatus; 12 | import org.springframework.web.socket.PongMessage; 13 | import org.springframework.web.socket.TextMessage; 14 | import org.springframework.web.socket.WebSocketHandler; 15 | import org.springframework.web.socket.WebSocketMessage; 16 | import org.springframework.web.socket.WebSocketSession; 17 | 18 | import com.github.zmzhoustar.webshell.service.WebShellService; 19 | import com.github.zmzhoustar.webshell.utils.WebShellUtils; 20 | 21 | import lombok.extern.slf4j.Slf4j; 22 | 23 | 24 | /** 25 | * WebSocket处理器 26 | * 27 | * @author zmzhou 28 | * @version 1.0 29 | * @title WebShellWebSocketHandler 30 | * @date 2021/2/22 20:58 31 | */ 32 | @Slf4j 33 | @Component 34 | public class WebShellWebSocketHandler implements WebSocketHandler { 35 | @Resource 36 | private WebShellService webShellService; 37 | 38 | /** 39 | * 用户连接上WebSocket回调 40 | * @param webSocketSession WebSocketSession 41 | * @author zmzhou 42 | * @date 2021/2/23 20:35 43 | */ 44 | @Override 45 | public void afterConnectionEstablished(WebSocketSession webSocketSession) { 46 | log.info("用户:{},连接Web Shell", WebShellUtils.getUuid(webSocketSession)); 47 | //调用初始化连接 48 | webShellService.initConnection(webSocketSession); 49 | } 50 | 51 | /** 52 | * 收到消息回调 53 | * @param webSocketSession WebSocketSession 54 | * @param webSocketMessage WebSocketMessage 55 | * @author zmzhou 56 | * @date 2021/2/23 20:41 57 | */ 58 | @Override 59 | public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage webSocketMessage) { 60 | if (webSocketMessage instanceof TextMessage) { 61 | log.info("用户:{},发送命令:{}", WebShellUtils.getUuid(webSocketSession), webSocketMessage.getPayload()); 62 | //调用service接收消息 63 | webShellService.recvHandle(((TextMessage) webSocketMessage).getPayload(), webSocketSession); 64 | } else if (webSocketMessage instanceof BinaryMessage) { 65 | log.info("BinaryMessage:{}", webSocketMessage); 66 | } else if (webSocketMessage instanceof PongMessage) { 67 | log.info("PongMessage:{}", webSocketMessage); 68 | } else { 69 | log.error("Unexpected WebSocket message type: " + webSocketMessage); 70 | } 71 | } 72 | 73 | /** 74 | * 错误的回调 75 | * @param webSocketSession WebSocketSession 76 | * @author zmzhou 77 | * @date 2021/2/23 20:41 78 | */ 79 | @Override 80 | public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) { 81 | log.error("用户:{},数据传输错误:{}", WebShellUtils.getUuid(webSocketSession), throwable); 82 | } 83 | 84 | /** 85 | * 连接关闭的回调 86 | * @param webSocketSession WebSocketSession 87 | * @author zmzhou 88 | * @date 2021/2/23 20:43 89 | */ 90 | @Override 91 | public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) { 92 | log.info("用户:{},断开webSocket连接:{}", WebShellUtils.getUuid(webSocketSession), closeStatus); 93 | //调用service关闭连接 94 | webShellService.close(webSocketSession); 95 | } 96 | 97 | @Override 98 | public boolean supportsPartialMessages() { 99 | return false; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | INFO 22 | 23 | 24 | 25 | ${CONSOLE_LOG_PATTERN} 26 | utf8 27 | 28 | 29 | 30 | 31 | 32 | 33 | false 34 | 35 | 36 | ${LOG_FILE}.%d{yyyy-MM-dd}.log 37 | 38 | 39 | 10 40 | 41 | 42 | 43 | %d{yyyy-MM-dd HH:mm:ss} %-5level logger{39} -%msg%n 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ${LOGSTASH_ADDRESS} 52 | 53 | 54 | 55 | Asia/Shanghai 56 | 57 | 58 | 59 | { 60 | "app": "${APPLICATION_NAME}", 61 | "level": "%level", 62 | "thread": "%thread", 63 | "logger": "%logger{50} %M %L ", 64 | "message": "%msg" 65 | } 66 | 67 | 68 | 69 | 70 | 100 71 | true 72 | true 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/main/resources/static/css/xterm.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014 The xterm.js authors. All rights reserved. 3 | * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) 4 | * https://github.com/chjj/term.js 5 | * @license MIT 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | * Originally forked from (with the author's permission): 26 | * Fabrice Bellard's javascript vt100 for jslinux: 27 | * http://bellard.org/jslinux/ 28 | * Copyright (c) 2011 Fabrice Bellard 29 | * The original design remains. The terminal itself 30 | * has been extended to include xterm CSI codes, among 31 | * other features. 32 | */ 33 | 34 | /** 35 | * Default styles for xterm.js 36 | */ 37 | 38 | .xterm { 39 | font-feature-settings: "liga" 0; 40 | position: relative; 41 | user-select: none; 42 | -ms-user-select: none; 43 | -webkit-user-select: none; 44 | } 45 | 46 | .xterm.focus, 47 | .xterm:focus { 48 | outline: none; 49 | } 50 | 51 | .xterm .xterm-helpers { 52 | position: absolute; 53 | top: 0; 54 | /** 55 | * The z-index of the helpers must be higher than the canvases in order for 56 | * IMEs to appear on top. 57 | */ 58 | z-index: 5; 59 | } 60 | 61 | .xterm .xterm-helper-textarea { 62 | padding: 0; 63 | border: 0; 64 | margin: 0; 65 | /* Move textarea out of the screen to the far left, so that the cursor is not visible */ 66 | position: absolute; 67 | opacity: 0; 68 | left: -9999em; 69 | top: 0; 70 | width: 0; 71 | height: 0; 72 | z-index: -5; 73 | /** Prevent wrapping so the IME appears against the textarea at the correct position */ 74 | white-space: nowrap; 75 | overflow: hidden; 76 | resize: none; 77 | } 78 | 79 | .xterm .composition-view { 80 | /* TODO: Composition position got messed up somewhere */ 81 | background: #000; 82 | color: #FFF; 83 | display: none; 84 | position: absolute; 85 | white-space: nowrap; 86 | z-index: 1; 87 | } 88 | 89 | .xterm .composition-view.active { 90 | display: block; 91 | } 92 | 93 | .xterm .xterm-viewport { 94 | /* On OS X this is required in order for the scroll bar to appear fully opaque */ 95 | background-color: #000; 96 | overflow-y: scroll; 97 | cursor: default; 98 | position: absolute; 99 | right: 0; 100 | left: 0; 101 | top: 0; 102 | bottom: 0; 103 | } 104 | 105 | .xterm .xterm-screen { 106 | position: relative; 107 | } 108 | 109 | .xterm .xterm-screen canvas { 110 | position: absolute; 111 | left: 0; 112 | top: 0; 113 | } 114 | 115 | .xterm .xterm-scroll-area { 116 | visibility: hidden; 117 | } 118 | 119 | .xterm-char-measure-element { 120 | display: inline-block; 121 | visibility: hidden; 122 | position: absolute; 123 | top: 0; 124 | left: -9999em; 125 | line-height: normal; 126 | } 127 | 128 | .xterm { 129 | cursor: text; 130 | } 131 | 132 | .xterm.enable-mouse-events { 133 | /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ 134 | cursor: default; 135 | } 136 | 137 | .xterm.xterm-cursor-pointer { 138 | cursor: pointer; 139 | } 140 | 141 | .xterm.column-select.focus { 142 | /* Column selection mode */ 143 | cursor: crosshair; 144 | } 145 | 146 | .xterm .xterm-accessibility, 147 | .xterm .xterm-message { 148 | position: absolute; 149 | left: 0; 150 | top: 0; 151 | bottom: 0; 152 | right: 0; 153 | z-index: 10; 154 | color: transparent; 155 | } 156 | 157 | .xterm .live-region { 158 | position: absolute; 159 | left: -9999px; 160 | width: 1px; 161 | height: 1px; 162 | overflow: hidden; 163 | } 164 | 165 | .xterm-dim { 166 | opacity: 0.5; 167 | } 168 | 169 | .xterm-underline { 170 | text-decoration: underline; 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/utils/SecretUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.utils; 6 | 7 | import java.nio.charset.StandardCharsets; 8 | import java.security.AlgorithmParameters; 9 | import java.security.InvalidKeyException; 10 | import java.security.NoSuchAlgorithmException; 11 | import java.security.Security; 12 | import java.util.Arrays; 13 | import java.util.Base64; 14 | 15 | import javax.crypto.Cipher; 16 | import javax.crypto.NoSuchPaddingException; 17 | import javax.crypto.spec.IvParameterSpec; 18 | import javax.crypto.spec.SecretKeySpec; 19 | 20 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 21 | 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | /** 25 | * AES对称加密算法 26 | * - 算法种类 27 | * - 对称算法(分组密码算法):AES/DES/SM4 28 | * - 非对称算法(公钥密码算法):RSA/SM2 29 | * - 摘要算法(杂凑算法):MD5/SHA-I/SM3 30 | * - 国密算法 SMx (SM2/SM3/SM4/SM9/ZUC等国密(国家商用密码)) 31 | * @author zmzhou 32 | * @version 1.0 33 | * @date 2021/2/24 15:46 34 | */ 35 | @Slf4j 36 | public final class SecretUtils { 37 | /** 38 | * 默认加密密钥 39 | */ 40 | public static final String AES_KEY = "ws9ybUMn4F81t5oPKqJrqLKxERaYAS12"; 41 | /** 算法 "算法/模式/补码方式" */ 42 | private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; 43 | private static final String KEY_ALGORITHM = "AES"; 44 | /** 45 | * AES加密 46 | * 47 | * @param content 需要加密的内容 48 | * @param aesKey 加密密钥 49 | * @return 密文 50 | * @author zmzhou 51 | * @date 2021/2/24 15:47 52 | */ 53 | public static String encrypt(String content, String aesKey) { 54 | try { 55 | // 创建密码器 56 | Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, aesKey); 57 | byte[] byteContent = content.getBytes(StandardCharsets.UTF_8); 58 | //加密 59 | byte[] encryptResult = cipher.doFinal(byteContent); 60 | // 将加密后的数据转为BASE64字符串 61 | return Base64.getEncoder().encodeToString(encryptResult); 62 | } catch (Exception e) { 63 | log.error("AES加密时出现异常:[content:{};AESPwd:{}]", content, aesKey, e); 64 | } 65 | return null; 66 | } 67 | 68 | /** 69 | * AES解密 70 | * 71 | * @param content 待解密内容 72 | * @param aesKey 解密密钥 73 | * @return 明文 74 | * @author zmzhou 75 | * @date 2021/2/24 15:57 76 | */ 77 | public static String decrypt(String content, String aesKey) { 78 | try { 79 | // 先用base64解密 80 | byte[] contentByte = Base64.getDecoder().decode(content); 81 | // 创建密码器 82 | Cipher cipher = getCipher(Cipher.DECRYPT_MODE, aesKey); 83 | byte[] result = cipher.doFinal(contentByte); 84 | // 解密 85 | return new String(result, StandardCharsets.UTF_8); 86 | } catch (Exception e) { 87 | log.error("AES解密时出现异常:[content:{};AESPwd:{}]", content, aesKey, e); 88 | } 89 | return null; 90 | } 91 | 92 | /** 93 | * 初始化密码器 94 | * 95 | * @param decryptMode 模式 96 | * @param aesKey 加密密钥 97 | * @return 密码器 98 | * @author zmzhou 99 | * @date 2021/2/24 16:04 100 | */ 101 | private static Cipher getCipher(int decryptMode, String aesKey) 102 | throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException { 103 | Security.addProvider(new BouncyCastleProvider()); 104 | SecretKeySpec keySpec = new SecretKeySpec(aesKey.getBytes(), KEY_ALGORITHM); 105 | // 创建密码器 106 | Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); 107 | // 初始化 108 | cipher.init(decryptMode, keySpec); 109 | return cipher; 110 | } 111 | /** 112 | * 微信小程序加密数据解密算法 113 | * @param data 对称解密的目标密文 114 | * @param sessionKey session_key login接口获取 115 | * @param iv 对称解密算法初始向量 116 | * @return 明文 117 | * @author zmzhou 118 | * @date 2021/2/25 15:51 119 | */ 120 | public static String decrypt(String data, String sessionKey, String iv) { 121 | byte[] dataByte = Base64.getDecoder().decode(data); 122 | byte[] keyByte = Base64.getDecoder().decode(sessionKey); 123 | byte[] ivByte = Base64.getDecoder().decode(iv); 124 | int base = 16; 125 | if (keyByte.length % base != 0) { 126 | int groups = keyByte.length / base + 1; 127 | byte[] temp = new byte[groups * base]; 128 | Arrays.fill(temp, (byte)0); 129 | System.arraycopy(keyByte, 0, temp, 0, keyByte.length); 130 | keyByte = temp; 131 | } 132 | try { 133 | Security.addProvider(new BouncyCastleProvider()); 134 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC"); 135 | SecretKeySpec spec = new SecretKeySpec(keyByte, KEY_ALGORITHM); 136 | AlgorithmParameters parameters = AlgorithmParameters.getInstance(KEY_ALGORITHM); 137 | parameters.init(new IvParameterSpec(ivByte)); 138 | cipher.init(2, spec, parameters); 139 | byte[] resultByte = cipher.doFinal(dataByte); 140 | if (null != resultByte && resultByte.length > 0) { 141 | return new String(resultByte, StandardCharsets.UTF_8); 142 | } 143 | } catch (Exception e) { 144 | log.error("对称解密错误:", e); 145 | } 146 | return null; 147 | } 148 | 149 | private SecretUtils() { 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/main/resources/static/css/webShell.css: -------------------------------------------------------------------------------- 1 | html, body{ 2 | width: 100%; 3 | height: 100%; 4 | } 5 | body{ 6 | font-family: 'Open Sans',sans-serif; 7 | margin: 0; 8 | background-color: slategray; 9 | } 10 | .indexContainer{ 11 | width: 100%; 12 | height: calc(100% - 30px); 13 | } 14 | .menu { 15 | color: #34495e; 16 | width: 100%; 17 | height: 30px; 18 | line-height: 30px; 19 | background-color: rgba(243, 242, 238, 1) !important; 20 | } 21 | .menu div { 22 | padding: 0 10px; 23 | display: inline-block; 24 | cursor: pointer; 25 | } 26 | .menu div:hover { 27 | background-color: #4a77d4; 28 | } 29 | .menu div:active { 30 | background-color: #3762bc; 31 | } 32 | #login{ 33 | position: absolute; 34 | top: 50%; 35 | left: 50%; 36 | transform: translate(-50%,-50%); 37 | margin: 0 auto; 38 | width: 300px; 39 | } 40 | .login h1{ 41 | color: #34495e; 42 | text-shadow: 0 0 5px; 43 | letter-spacing: 1px; 44 | text-align: center; 45 | } 46 | h1{ 47 | font-size: 2em; 48 | margin: 0.67em 0; 49 | } 50 | .login input{ 51 | width: 278px; 52 | height: 18px; 53 | margin-bottom: 10px; 54 | outline: none; 55 | padding: 10px; 56 | font-size: 13px; 57 | color: #fff; 58 | border: 1px solid #312E3D; 59 | border-bottom: 1px solid #56536A; 60 | border-radius: 4px; 61 | background-color: #2D2D3F; 62 | } 63 | .button{ 64 | width: 300px; 65 | min-height: 20px; 66 | display: block; 67 | background-color: #58a6ff; 68 | border: 1px solid #37b6ff; 69 | color: #fff; 70 | padding: 9px 14px; 71 | font-size: 15px; 72 | line-height: normal; 73 | border-radius: 5px; 74 | margin: 0; 75 | } 76 | .sftp-container > div{ 77 | float: left; 78 | border: 2px solid #3762bc; 79 | } 80 | .sftp-container{ 81 | width: 100%; 82 | height: 100%; 83 | } 84 | .sftp-file-tree-div{ 85 | width: calc(30% - 8px); 86 | height: 80%; 87 | } 88 | .sftp-file-tree-div > input{ 89 | padding: 0 15px; 90 | height: 30px; 91 | width: calc(100% - 180px); 92 | background-color: slategray; 93 | color: #fff; 94 | border: 1px solid #99D3F5; 95 | outline: none; 96 | border-radius: 5px; 97 | } 98 | .sftp-file-tree-div .sftp-file-tree{ 99 | width: 100%; 100 | height: calc(100% - 40px); 101 | overflow-y: scroll; 102 | } 103 | .sftp-file-detail{ 104 | width: 70%; 105 | height: 80%; 106 | overflow-y: scroll; 107 | } 108 | .sftp-message-console { 109 | width: calc(100% - 4px); 110 | height: calc(20% - 8px); 111 | color: whitesmoke; 112 | font-size: 20px; 113 | } 114 | input::-webkit-input-placeholder{ 115 | color: whitesmoke; 116 | } 117 | /* Mozilla Firefox 19+ */ 118 | input::-moz-placeholder{ 119 | color: whitesmoke; 120 | } 121 | 122 | .file-detail-table { 123 | width: 100%; 124 | } 125 | .file-detail-table tr{ 126 | width: 100%; 127 | height: 24px; 128 | line-height: 24px; 129 | } 130 | table.file-detail-table { 131 | font-family: verdana,arial,sans-serif; 132 | font-size:11px; 133 | color:#333333; 134 | border-width: 1px; 135 | border-color: #a9c6c9; 136 | border-collapse: collapse; 137 | } 138 | table.file-detail-table th { 139 | background-color: #3F3F3F; 140 | color: white; 141 | } 142 | table.file-detail-table th:first-child, table.file-detail-table td:first-child { 143 | padding-left: 10px; 144 | } 145 | table.file-detail-table th, table.file-detail-table td { 146 | border-bottom: 1px solid #a9c6c9; 147 | padding: 6px 0; 148 | text-align: left; 149 | } 150 | table.file-detail-table tr:nth-child(even) { background-color:#c3dde0; } 151 | table.file-detail-table tr:nth-child(odd) { background-color:#d4e3e5; } 152 | table.file-detail-table tr:hover{ 153 | background-color: #dcddc0; 154 | } 155 | .data-cell .file-name { 156 | cursor: pointer; 157 | } 158 | .sftp-file-tree-div form { 159 | padding-left: 10px; 160 | width: 130px; 161 | display: inline-block; 162 | } 163 | .select-file { 164 | position: relative; 165 | display: inline-block; 166 | background: #D0EEFF; 167 | border: 1px solid #99D3F5; 168 | border-radius: 5px; 169 | padding: 5px 15px; 170 | color: #1E88C7; 171 | text-decoration: none; 172 | line-height: 20px; 173 | } 174 | .select-file input { 175 | position: absolute; 176 | width: 120px; 177 | height: 30px; 178 | font-size: 100px; 179 | right: 0; 180 | top: 0; 181 | opacity: 0; 182 | } 183 | .select-file:hover { 184 | background: #AADFFD; 185 | border-color: #78C3F3; 186 | color: #004974; 187 | text-decoration: none; 188 | } 189 | .delete-data { 190 | background-color: #EA633E; 191 | cursor: pointer; 192 | color: white; 193 | border: 0; 194 | } 195 | .github-star a { 196 | padding: 10px 10px 0; 197 | } 198 | .github-star { 199 | display: flex; 200 | flex-wrap: nowrap; 201 | flex-direction: row; 202 | align-content: center; 203 | justify-content: center; 204 | } 205 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | /opt/web-shell/logs 8 | 7 9 | 10 | %d{yyyy-MM-dd HH:mm:ss z} [%thread] %-5level %class{36} [%M:%L] - %msg%xEx%n 11 | %d{HH:mm:ss.SSS} [%thread] %-5level %class{36} [%M:%L] - %msg%xEx%n 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.18 9 | 10 | 11 | com.github.zmzhou-star 12 | web-shell 13 | 1.0.1 14 | web-shell 15 | web-shell project for Spring Boot 16 | 17 | 1.8 18 | 2.7.18 19 | ./dist 20 | 21 | 22 | 23 | 24 | 25 | org.yaml 26 | snakeyaml 27 | 2.2 28 | 29 | 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-thymeleaf 35 | 36 | 37 | spring-boot-starter-logging 38 | org.springframework.boot 39 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-starter-web 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-websocket 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-log4j2 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-cache 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-configuration-processor 69 | true 70 | 71 | 72 | net.sf.ehcache 73 | ehcache 74 | 75 | 76 | com.lmax 77 | disruptor 78 | 3.4.4 79 | 80 | 81 | 82 | com.jcraft 83 | jsch 84 | 0.1.55 85 | 86 | 87 | commons-io 88 | commons-io 89 | 2.15.1 90 | 91 | 92 | org.apache.commons 93 | commons-lang3 94 | 95 | 96 | 97 | com.github.zmzhou-star 98 | server-info 99 | 1.0 100 | 101 | 102 | 103 | org.springframework.boot 104 | spring-boot-devtools 105 | runtime 106 | true 107 | 108 | 109 | org.projectlombok 110 | lombok 111 | true 112 | 113 | 114 | org.springframework.boot 115 | spring-boot-starter-test 116 | test 117 | 118 | 119 | org.bouncycastle 120 | bcprov-jdk18on 121 | 1.75 122 | 123 | 124 | 125 | 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-compiler-plugin 130 | 131 | ${java.version} 132 | ${java.version} 133 | ${project.build.sourceEncoding} 134 | 135 | 136 | 137 | org.springframework.boot 138 | spring-boot-maven-plugin 139 | ${spring-boot.version} 140 | 141 | true 142 | 143 | 144 | 145 | 146 | repackage 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | aliyun spring plugins 156 | aliyun Spring plugins 157 | https://maven.aliyun.com/repository/spring-plugin 158 | 159 | 160 | 161 | 162 | aliyunmaven 163 | 阿里云公共仓库 164 | https://maven.aliyun.com/repository/public 165 | 166 | 167 | 168 | 169 | github 170 | GitHub zmzhou-star Apache Maven Packages 171 | https://maven.pkg.github.com/zmzhou-star/web-shell 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/controller/SftpController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.controller; 6 | 7 | import java.io.BufferedInputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.util.List; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | import javax.activation.MimetypesFileTypeMap; 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | 18 | import org.apache.commons.lang3.StringUtils; 19 | import org.springframework.web.bind.annotation.DeleteMapping; 20 | import org.springframework.web.bind.annotation.GetMapping; 21 | import org.springframework.web.bind.annotation.PostMapping; 22 | import org.springframework.web.bind.annotation.RequestMapping; 23 | import org.springframework.web.bind.annotation.RestController; 24 | import org.springframework.web.multipart.MultipartFile; 25 | import org.springframework.web.multipart.MultipartHttpServletRequest; 26 | 27 | import com.github.zmzhoustar.webshell.Constants; 28 | import com.github.zmzhoustar.webshell.utils.EhCacheUtils; 29 | import com.github.zmzhoustar.webshell.utils.SftpFileUtils; 30 | import com.github.zmzhoustar.webshell.utils.SftpUtils; 31 | import com.github.zmzhoustar.webshell.utils.WebShellUtils; 32 | import com.github.zmzhoustar.webshell.vo.ApiResult; 33 | import com.github.zmzhoustar.webshell.vo.SftpFileTreeVo; 34 | import com.github.zmzhoustar.webshell.vo.WebShellData; 35 | import com.jcraft.jsch.SftpException; 36 | 37 | import lombok.extern.slf4j.Slf4j; 38 | 39 | /** 40 | * SFTP控制层 41 | * SFTP是Secure File Transfer Protocol的缩写,安全文件传送协议 42 | * @title SftpController 43 | * @author zmzhou 44 | * @version 1.0 45 | * @date 2021/3/1 13:54 46 | */ 47 | @Slf4j 48 | @RequestMapping("/sftp") 49 | @RestController 50 | public class SftpController { 51 | /** 52 | * 获取文件列表 53 | * @param path 路径 54 | * @return 文件列表 55 | * @author zmzhou 56 | * @date 2021/3/1 14:10 57 | */ 58 | @GetMapping("getFileTree") 59 | public ApiResult> getFileTree(String path) { 60 | String sessionId = WebShellUtils.getSessionId(); 61 | log.info("sessionId:{}", sessionId); 62 | // 存放ssh连接信息 63 | WebShellData sshData = EhCacheUtils.get(sessionId); 64 | ApiResult> result = new ApiResult<>(); 65 | if (sshData != null) { 66 | SftpUtils sftpUtils = new SftpUtils(sshData); 67 | if (sftpUtils.login()) { 68 | List fileTree = SftpFileUtils.getFileTree(sftpUtils, path); 69 | result.setData(fileTree); 70 | sftpUtils.logout(); 71 | } 72 | } 73 | return result; 74 | } 75 | 76 | /** 77 | * 上传文件到服务器 78 | * @param request HttpServletRequest 79 | * @return 80 | * @author zmzhou 81 | * @date 2021/3/2 23:07 82 | */ 83 | @PostMapping("/upload") 84 | public ApiResult upload(HttpServletRequest request) { 85 | List files = ((MultipartHttpServletRequest) request).getFiles("file"); 86 | String sessionId = WebShellUtils.getSessionId(); 87 | log.info("sessionId:{}", sessionId); 88 | // 存放ssh连接信息 89 | WebShellData sshData = EhCacheUtils.get(sessionId); 90 | // 上传目标文件夹 91 | String directory = request.getParameter("path"); 92 | // 返回值 93 | AtomicReference res = new AtomicReference<>("上传成功!"); 94 | if (sshData != null) { 95 | SftpUtils sftpUtils = new SftpUtils(sshData); 96 | if (sftpUtils.login()) { 97 | files.forEach(file -> { 98 | String fileName = file.getOriginalFilename(); 99 | try { 100 | sftpUtils.upload(directory, fileName, file.getInputStream()); 101 | } catch (SftpException | IOException e) { 102 | log.error("上传文件失败:{}", fileName, e); 103 | res.set("上传失败!"); 104 | } 105 | }); 106 | sftpUtils.logout(); 107 | } 108 | } 109 | ApiResult result = ApiResult.builder(); 110 | return result.data(res.get()); 111 | } 112 | 113 | /** 114 | * 从服务器下载文件 115 | * @param path 服务器文件路径 116 | * @author zmzhou 117 | * @date 2021/3/2 20:46 118 | */ 119 | @GetMapping("/download") 120 | public void download(String path, HttpServletResponse response) { 121 | if (StringUtils.isBlank(path)) { 122 | return; 123 | } 124 | String sessionId = WebShellUtils.getSessionId(); 125 | log.info("sessionId:{}", sessionId); 126 | // 文件名 127 | String fileName = path.substring(path.lastIndexOf(Constants.SEPARATOR) + 1); 128 | // 存放ssh连接信息 129 | WebShellData sshData = EhCacheUtils.get(sessionId); 130 | if (sshData != null) { 131 | SftpUtils sftpUtils = new SftpUtils(sshData); 132 | if (sftpUtils.login()) { 133 | // 设置信息给客户端不解析 134 | String type = new MimetypesFileTypeMap().getContentType(path); 135 | // 设置content-type,即告诉客户端所发送的数据属于什么类型 136 | response.setHeader("Content-type", type); 137 | // 设置强制下载不打开 138 | response.setContentType("application/force-download"); 139 | // 设置文件名 140 | response.addHeader("Content-Disposition", "attachment;fileName=" + fileName); 141 | byte[] buffer = new byte[Constants.BUFFER_SIZE]; 142 | try (InputStream fis = sftpUtils.download(path); 143 | BufferedInputStream bis = new BufferedInputStream(fis)) { 144 | OutputStream os = response.getOutputStream(); 145 | int i = bis.read(buffer); 146 | while (i != -1) { 147 | os.write(buffer, 0, i); 148 | i = bis.read(buffer); 149 | } 150 | } catch (Exception e) { 151 | log.error("下载文件:{}失败", path, e); 152 | } 153 | sftpUtils.logout(); 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * 删除文件 160 | * @param path 文件路径 161 | * @return 删除结果 162 | * @author zmzhou 163 | * @date 2021/3/4 21:05 164 | */ 165 | @DeleteMapping 166 | public ApiResult deleteFile(String path){ 167 | ApiResult result = ApiResult.builder(); 168 | if (StringUtils.isBlank(path)) { 169 | return result.error(404, "文件路径为空!"); 170 | } 171 | String sessionId = WebShellUtils.getSessionId(); 172 | log.info("sessionId:{},删除文件path:{}", sessionId, path); 173 | // 存放ssh连接信息 174 | WebShellData sshData = EhCacheUtils.get(sessionId); 175 | if (sshData != null) { 176 | SftpUtils sftpUtils = new SftpUtils(sshData); 177 | if (sftpUtils.login()) { 178 | // 删除文件 179 | if (!sftpUtils.delete(path)) { 180 | return result.error(500, "删除文件失败!"); 181 | } 182 | sftpUtils.logout(); 183 | } 184 | } 185 | return result.data("删除成功!"); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/service/WebShellService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.service; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.util.Arrays; 11 | import java.util.Map; 12 | import java.util.Properties; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.web.socket.TextMessage; 17 | import org.springframework.web.socket.WebSocketSession; 18 | 19 | import com.fasterxml.jackson.databind.ObjectMapper; 20 | import com.github.zmzhoustar.webshell.Constants; 21 | import com.github.zmzhoustar.webshell.utils.SecretUtils; 22 | import com.github.zmzhoustar.webshell.utils.ThreadPoolUtils; 23 | import com.github.zmzhoustar.webshell.utils.WebShellUtils; 24 | import com.github.zmzhoustar.webshell.vo.ShellConnectInfo; 25 | import com.github.zmzhoustar.webshell.vo.WebShellData; 26 | import com.jcraft.jsch.Channel; 27 | import com.jcraft.jsch.JSch; 28 | import com.jcraft.jsch.JSchException; 29 | import com.jcraft.jsch.Session; 30 | 31 | import lombok.extern.slf4j.Slf4j; 32 | 33 | /** 34 | * Web Shell业务逻辑实现 35 | * @title WebShellService 36 | * @author zmzhou 37 | * @version 1.0 38 | * @date 2021/2/23 21:58 39 | */ 40 | @Slf4j 41 | @Service 42 | public class WebShellService { 43 | /** 存放ssh连接信息的map */ 44 | private static final Map SSH_MAP = new ConcurrentHashMap<>(); 45 | 46 | /** 47 | * 初始化连接 48 | * @param session WebSocketSession 49 | * @author zmzhou 50 | * @date 2021/2/23 21:22 51 | */ 52 | public void initConnection(WebSocketSession session) { 53 | JSch jSch = new JSch(); 54 | ShellConnectInfo shellConnectInfo = new ShellConnectInfo(); 55 | shellConnectInfo.setJsch(jSch); 56 | shellConnectInfo.setWebSocketSession(session); 57 | String uuid = WebShellUtils.getUuid(session); 58 | //将这个ssh连接信息放入缓存中 59 | SSH_MAP.put(uuid, shellConnectInfo); 60 | } 61 | 62 | /** 63 | * 处理客户端发送的数据 64 | * @author zmzhou 65 | * @date 2021/2/23 21:21 66 | */ 67 | public void recvHandle(String buffer, WebSocketSession session) { 68 | ObjectMapper objectMapper = new ObjectMapper(); 69 | WebShellData shellData; 70 | try { 71 | shellData = objectMapper.readValue(buffer, WebShellData.class); 72 | } catch (IOException e) { 73 | log.error("Json转换异常:{}", e.getMessage()); 74 | return; 75 | } 76 | String userId = WebShellUtils.getUuid(session); 77 | //找到刚才存储的ssh连接对象 78 | ShellConnectInfo shellConnectInfo = (ShellConnectInfo) SSH_MAP.get(userId); 79 | if (shellConnectInfo != null) { 80 | if (Constants.OPERATE_CONNECT.equals(shellData.getOperate())) { 81 | //启动线程异步处理 82 | ThreadPoolUtils.execute(() -> { 83 | try { 84 | connectToSsh(shellConnectInfo, shellData, session); 85 | } catch (JSchException e) { 86 | log.error("web shell连接异常:{}", e.getMessage()); 87 | sendMessage(session, e.getMessage().getBytes()); 88 | close(session); 89 | } 90 | }); 91 | } else if (Constants.OPERATE_COMMAND.equals(shellData.getOperate())) { 92 | String command = shellData.getCommand(); 93 | sendToTerminal(shellConnectInfo.getChannel(), command); 94 | } else { 95 | log.error("不支持的操作"); 96 | close(session); 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * 关闭连接 103 | * @param session WebSocketSession 104 | * @author zmzhou 105 | * @date 2021/2/23 21:16 106 | */ 107 | public void close(WebSocketSession session) { 108 | String userId = WebShellUtils.getUuid(session); 109 | ShellConnectInfo shellConnectInfo = (ShellConnectInfo) SSH_MAP.get(userId); 110 | if (shellConnectInfo != null) { 111 | //断开连接 112 | if (shellConnectInfo.getChannel() != null) { 113 | shellConnectInfo.getChannel().disconnect(); 114 | } 115 | //map中移除 116 | SSH_MAP.remove(userId); 117 | } 118 | } 119 | 120 | /** 121 | * 使用jsch连接终端 122 | * @param shellConnectInfo ShellConnectInfo 123 | * @param sshData WebShellData 124 | * @param webSocketSession WebSocketSession 125 | * @author zmzhou 126 | * @date 2021/2/23 21:15 127 | */ 128 | private void connectToSsh(ShellConnectInfo shellConnectInfo, WebShellData sshData, WebSocketSession webSocketSession) 129 | throws JSchException { 130 | Properties config = new Properties(); 131 | // SSH 连接远程主机时,会检查主机的公钥。如果是第一次该主机,会显示该主机的公钥摘要,提示用户是否信任该主机 132 | config.put("StrictHostKeyChecking", "no"); 133 | //获取jsch的会话 134 | Session session = shellConnectInfo.getJsch().getSession(sshData.getUsername(), sshData.getHost(), 135 | sshData.getPort()); 136 | session.setConfig(config); 137 | //设置密码 138 | session.setPassword(SecretUtils.decrypt(sshData.getPassword(), SecretUtils.AES_KEY)); 139 | //连接超时时间30s 140 | session.connect(30000); 141 | 142 | //开启shell通道 143 | Channel channel = session.openChannel("shell"); 144 | //通道连接超时时间3s 145 | channel.connect(3000); 146 | //设置channel 147 | shellConnectInfo.setChannel(channel); 148 | 149 | //查询上次登录时间 150 | // sendToTerminal(channel, "lastlog -u " + sshData.getUsername() + "\r"); 151 | 152 | //读取终端返回的信息流 153 | try (InputStream inputStream = channel.getInputStream()) { 154 | //循环读取 155 | byte[] buffer = new byte[Constants.BUFFER_SIZE]; 156 | int i; 157 | //如果没有数据来,线程会一直阻塞在这个地方等待数据。 158 | while ((i = inputStream.read(buffer)) != -1) { 159 | sendMessage(webSocketSession, Arrays.copyOfRange(buffer, 0, i)); 160 | } 161 | } catch (IOException e) { 162 | log.error("读取终端返回的信息流异常:", e); 163 | } finally { 164 | //断开连接后关闭会话 165 | session.disconnect(); 166 | channel.disconnect(); 167 | } 168 | } 169 | 170 | /** 171 | * 数据写回前端 172 | * @param session WebSocketSession 173 | * @author zmzhou 174 | * @date 2021/2/23 21:18 175 | */ 176 | public void sendMessage(WebSocketSession session, byte[] buffer) { 177 | try { 178 | session.sendMessage(new TextMessage(buffer)); 179 | } catch (IOException e) { 180 | log.error("数据写回前端异常:", e); 181 | } 182 | } 183 | /** 184 | * 将消息转发到终端 185 | * @param channel Channel 186 | * @author zmzhou 187 | * @date 2021/2/23 21:13 188 | */ 189 | private void sendToTerminal(Channel channel, String command) { 190 | if (channel != null) { 191 | try { 192 | OutputStream outputStream = channel.getOutputStream(); 193 | outputStream.write(command.getBytes()); 194 | outputStream.flush(); 195 | } catch (IOException e) { 196 | log.error("web shell将消息转发到终端异常:{}", e.getMessage()); 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | web shell 登录Linux远程主机 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Fork me on Gitee 20 | 21 | 45 | 49 |
50 | 51 | 192 | 193 | -------------------------------------------------------------------------------- /src/main/resources/templates/sftp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | web sftp 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 |
21 | 选择文件上传 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
文件名文件类型文件属性目录/链接个数所有者文件大小修改时间操作
42 |
43 |
44 |
45 | 46 | 242 | 243 | -------------------------------------------------------------------------------- /src/main/java/com/github/zmzhoustar/webshell/utils/SftpUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020-present zmzhou-star. All Rights Reserved. 3 | */ 4 | 5 | package com.github.zmzhoustar.webshell.utils; 6 | 7 | import java.io.InputStream; 8 | import java.util.Iterator; 9 | import java.util.Vector; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | import com.github.zmzhoustar.webshell.Constants; 15 | import com.github.zmzhoustar.webshell.vo.WebShellData; 16 | import com.jcraft.jsch.Channel; 17 | import com.jcraft.jsch.ChannelSftp; 18 | import com.jcraft.jsch.JSch; 19 | import com.jcraft.jsch.JSchException; 20 | import com.jcraft.jsch.Session; 21 | import com.jcraft.jsch.SftpATTRS; 22 | import com.jcraft.jsch.SftpException; 23 | 24 | import lombok.extern.slf4j.Slf4j; 25 | 26 | /** 27 | * SFTP服务器工具类 28 | * SftpUtils 29 | * 30 | * @author zmzhou 31 | * @version 1.0 32 | * @date 2021/2/25 11:46 33 | */ 34 | @Slf4j 35 | public final class SftpUtils { 36 | 37 | private ChannelSftp channelSftp; 38 | private Session session; 39 | 40 | /** 41 | * 用户名 42 | */ 43 | private final String username; 44 | 45 | /** 46 | * 密码 47 | */ 48 | private String password; 49 | 50 | /** 51 | * 秘钥 52 | */ 53 | private String privateKey; 54 | 55 | /** 56 | * FTP服务器Ip 57 | */ 58 | private final String host; 59 | 60 | /** 61 | * FTP服务器端口号 62 | */ 63 | private final int port; 64 | 65 | /** 66 | * 构造器 基于密码认证 67 | * 68 | * @param sshData 用户名,密码,主机,端口 69 | * @author zmzhou 70 | * @date 2021/3/3 15:16 71 | */ 72 | public SftpUtils(WebShellData sshData) { 73 | this.username = sshData.getUsername(); 74 | this.password = SecretUtils.decrypt(sshData.getPassword(), SecretUtils.AES_KEY); 75 | this.host = sshData.getHost(); 76 | this.port = sshData.getPort(); 77 | } 78 | 79 | /** 80 | * 构造器:基于秘钥认证sftp对象 81 | * 82 | * @param username 用户名 83 | * @param privateKey 秘钥 84 | * @param host 服务器ip 85 | * @param port 服务器端口号 86 | */ 87 | public SftpUtils(String username, String privateKey, int port, String host) { 88 | this.username = username; 89 | this.privateKey = privateKey; 90 | this.host = host; 91 | this.port = port; 92 | } 93 | 94 | /** 95 | * 连接SFTP服务器 96 | * 97 | * @return 连接成功 98 | * @author zmzhou 99 | * @date 2021/3/1 14:01 100 | */ 101 | public boolean login() { 102 | JSch jsch = new JSch(); 103 | try { 104 | if (StringUtils.isNotBlank(privateKey)) { 105 | //设置登陆主机的秘钥 106 | jsch.addIdentity(privateKey); 107 | } 108 | //采用指定的端口连接服务器 109 | session = jsch.getSession(username, host, port); 110 | if (StringUtils.isNotBlank(password)) { 111 | //设置登陆主机的密码 112 | session.setPassword(password); 113 | } 114 | //优先使用 password 验证 注:session.connect()性能低,使用password验证可跳过gssapi认证,提升连接服务器速度 115 | session.setConfig("PreferredAuthentications", "password"); 116 | //设置第一次登陆的时候提示,可选值:(ask | yes | no) 117 | session.setConfig("StrictHostKeyChecking", "no"); 118 | session.connect(); 119 | //创建sftp通信通道 120 | Channel channel = session.openChannel("sftp"); 121 | channel.connect(); 122 | channelSftp = (ChannelSftp) channel; 123 | log.info("sftp server connect success !!"); 124 | } catch (JSchException e) { 125 | log.error("SFTP服务器连接异常!!", e); 126 | return false; 127 | } 128 | return true; 129 | } 130 | 131 | /** 132 | * 关闭SFTP连接 133 | */ 134 | public void logout() { 135 | if (channelSftp != null && channelSftp.isConnected()) { 136 | channelSftp.disconnect(); 137 | log.debug("sftp closed"); 138 | } 139 | if (session != null && session.isConnected()) { 140 | session.disconnect(); 141 | log.debug("session closed"); 142 | } 143 | } 144 | 145 | /** 146 | * 将输入流上传到SFTP服务器,作为文件 147 | * 148 | * @param directory 上传到SFTP服务器的路径 149 | * @param sftpFileName 上传到SFTP服务器后的文件名 150 | * @param input 输入流 151 | * @throws SftpException SftpException 152 | * @author zmzhou 153 | * @date 2021/3/2 23:36 154 | */ 155 | public void upload(String directory, String sftpFileName, InputStream input) throws SftpException { 156 | long start = System.currentTimeMillis(); 157 | // 创建不存在的文件夹,并切换到文件夹 158 | createDir(directory); 159 | // 上传文件 160 | channelSftp.put(input, sftpFileName); 161 | log.info("文件上传成功!! 耗时:{}ms", (System.currentTimeMillis() - start)); 162 | } 163 | 164 | /** 165 | * 下载文件 166 | * 167 | * @param path SFTP服务器的文件路径 168 | * @return 输入流 169 | * @author zmzhou 170 | * @date 2021/3/2 21:20 171 | */ 172 | public InputStream download(String path) { 173 | // 文件所在目录 174 | String directory = path.substring(0, path.lastIndexOf(Constants.SEPARATOR)); 175 | // 文件名 176 | String fileName = path.substring(path.lastIndexOf(Constants.SEPARATOR) + 1); 177 | return download(directory, fileName); 178 | } 179 | 180 | /** 181 | * 下载文件 182 | * 183 | * @param directory SFTP服务器的文件路径 184 | * @param fileName SFTP服务器上的文件名 185 | * @return 输入流 186 | * @author zmzhou 187 | * @date 2021/3/2 21:20 188 | */ 189 | public InputStream download(String directory, String fileName) { 190 | try { 191 | if (StringUtils.isNotBlank(directory)) { 192 | channelSftp.cd(directory); 193 | } 194 | log.info("下载文件:{}/{}", directory, fileName); 195 | return channelSftp.get(fileName); 196 | } catch (SftpException e) { 197 | log.error("下载文件:{}/{}异常!", directory, fileName, e); 198 | } 199 | return null; 200 | } 201 | 202 | /** 203 | * 删除文件或者空文件夹 204 | * 205 | * @param directory SFTP服务器的文件路径 206 | * @param fileName 删除的文件名称 207 | * @return 删除结果 208 | * @author zmzhou 209 | * @date 2021/3/4 21:47 210 | */ 211 | private boolean delete(String directory, String fileName) { 212 | String file = directory + Constants.SEPARATOR + fileName; 213 | try { 214 | ChannelSftp.LsEntry lsEntry = (ChannelSftp.LsEntry) listFiles(file).get(0); 215 | // 用户权限处理 216 | if (!(Constants.USER_ROOT.equals(username) 217 | || username.equals(SftpFileUtils.getOwner(lsEntry.getLongname())))) { 218 | log.warn("用户{}没有权限删除文件:{}", username, file); 219 | return false; 220 | } 221 | channelSftp.cd(directory); 222 | if (isDirExists(file)) { 223 | // 删除空文件夹 224 | channelSftp.rmdir(fileName); 225 | } else { 226 | channelSftp.rm(fileName); 227 | } 228 | log.info("删除文件:{}成功", file); 229 | } catch (SftpException e) { 230 | log.error("删除文件异常:{}", file, e); 231 | return false; 232 | } 233 | return true; 234 | } 235 | 236 | /** 237 | * 删除文件或者文件夹 238 | * 239 | * @param path SFTP服务器的文件或者文件夹路径 240 | * @return 删除结果 241 | * @author zmzhou 242 | * @date 2021/3/4 21:47 243 | */ 244 | public boolean delete(String path) { 245 | AtomicBoolean delFlag = new AtomicBoolean(true); 246 | Vector vector = listFiles(path); 247 | // 是文件或者空文件夹 248 | if (isFileExists(path) || vector.isEmpty()) { 249 | // 文件所在目录 250 | String directory = path.substring(0, path.lastIndexOf(Constants.SEPARATOR)); 251 | // 文件名 252 | String fileName = path.substring(path.lastIndexOf(Constants.SEPARATOR) + 1); 253 | return delete(directory, fileName); 254 | } else if (isDirExists(path)) { 255 | // 1.先循环删除子文件 256 | vector.forEach(v -> { 257 | ChannelSftp.LsEntry lsEntry = (ChannelSftp.LsEntry) v; 258 | // 如果是文件夹,递归删除 259 | if (FileType.DIRECTORY.getSign().equals(lsEntry.getLongname().substring(0, 1))) { 260 | delFlag.set(delete(path + Constants.SEPARATOR + lsEntry.getFilename())); 261 | } else { 262 | // 删除文件 263 | delFlag.set(delete(path, lsEntry.getFilename())); 264 | } 265 | }); 266 | // 2.再删除空文件夹 267 | delFlag.set(delete(path)); 268 | } 269 | return delFlag.get(); 270 | } 271 | 272 | /** 273 | * 获取文件夹下的文件列表 274 | * 275 | * @param directory 路径 276 | * @return 文件列表 277 | * @author zmzhou 278 | * @date 2021/3/1 9:56 279 | */ 280 | public Vector listFiles(String directory) { 281 | try { 282 | if (isDirExists(directory) || isFileExists(directory)) { 283 | Vector vector = channelSftp.ls(directory); 284 | //移除上级目录和根目录:"." ".." 285 | Iterator it = vector.iterator(); 286 | while (it.hasNext()) { 287 | ChannelSftp.LsEntry lsEntry = (ChannelSftp.LsEntry) it.next(); 288 | if (Constants.DOT.equals(lsEntry.getFilename()) 289 | || Constants.PARENT_DIRECTORY.equals(lsEntry.getFilename())) { 290 | it.remove(); 291 | } 292 | } 293 | return vector; 294 | } 295 | } catch (SftpException e) { 296 | log.error("获取文件夹信息异常!", e); 297 | } 298 | return new Vector<>(); 299 | } 300 | 301 | /** 302 | * 判断目录是否存在,不存在则创建,并进入目录 303 | * 304 | * @param createPath 路径 305 | * @return 创建成功并进入目录 306 | * @author zmzhou 307 | * @date 2021/3/3 10:53 308 | */ 309 | public boolean createDir(String createPath) { 310 | try { 311 | if (isDirExists(createPath)) { 312 | this.channelSftp.cd(createPath); 313 | return true; 314 | } 315 | String[] pathArray = createPath.split(Constants.SEPARATOR); 316 | StringBuilder filePath = new StringBuilder(Constants.SEPARATOR); 317 | for (String path : pathArray) { 318 | if ("".equals(path)) { 319 | continue; 320 | } 321 | filePath.append(path); 322 | // 路径如果是文件,跳过,保存到同级目录 323 | if (isFileExists(filePath.toString())) { 324 | continue; 325 | } 326 | filePath.append(Constants.SEPARATOR); 327 | if (!isDirExists(filePath.toString())) { 328 | // 建立目录 329 | channelSftp.mkdir(filePath.toString()); 330 | } 331 | // 并进入目录 332 | channelSftp.cd(filePath.toString()); 333 | } 334 | } catch (SftpException e) { 335 | log.error("目录创建异常!", e); 336 | return false; 337 | } 338 | return true; 339 | } 340 | 341 | /** 342 | * 判断目录是否存在 343 | * 344 | * @param directory 路径 345 | * @return 目录是否存在 346 | * @author zmzhou 347 | * @date 2021/3/3 11:04 348 | */ 349 | public boolean isDirExists(String directory) { 350 | try { 351 | SftpATTRS attrs = this.channelSftp.lstat(directory); 352 | return null != attrs && attrs.isDir(); 353 | } catch (Exception e) { 354 | log.error("判断目录是否存在异常:{}", directory, e); 355 | } 356 | return false; 357 | } 358 | 359 | /** 360 | * 判断文件是否存在 361 | * 362 | * @param filePath 文件路径 363 | * @return 文件是否存在 364 | * @author zmzhou 365 | * @date 2021/3/3 11:04 366 | */ 367 | public boolean isFileExists(String filePath) { 368 | try { 369 | SftpATTRS attrs = this.channelSftp.lstat(filePath); 370 | // 存在并且不是文件夹 371 | return null != attrs && !attrs.isDir(); 372 | } catch (Exception e) { 373 | log.error("判断文件是否存在异常:{}", filePath, e); 374 | } 375 | return false; 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright © 2021-present zmzhou-star 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/resources/static/css/jstree/style.min.css: -------------------------------------------------------------------------------- 1 | .jstree-node,.jstree-children,.jstree-container-ul{display:block;margin:0;padding:0;list-style-type:none;list-style-image:none}.jstree-node{white-space:nowrap}.jstree-anchor{display:inline-block;color:black;white-space:nowrap;padding:0 4px 0 1px;margin:0;vertical-align:top}.jstree-anchor:focus{outline:0}.jstree-anchor,.jstree-anchor:link,.jstree-anchor:visited,.jstree-anchor:hover,.jstree-anchor:active{text-decoration:none;color:inherit}.jstree-icon{display:inline-block;text-decoration:none;margin:0;padding:0;vertical-align:top;text-align:center}.jstree-icon:empty{display:inline-block;text-decoration:none;margin:0;padding:0;vertical-align:top;text-align:center}.jstree-ocl{cursor:pointer}.jstree-leaf>.jstree-ocl{cursor:default}.jstree .jstree-open>.jstree-children{display:block}.jstree .jstree-closed>.jstree-children,.jstree .jstree-leaf>.jstree-children{display:none}.jstree-anchor>.jstree-themeicon{margin-right:2px}.jstree-no-icons .jstree-themeicon,.jstree-anchor>.jstree-themeicon-hidden{display:none}.jstree-hidden,.jstree-node.jstree-hidden{display:none}.jstree-rtl .jstree-anchor{padding:0 1px 0 4px}.jstree-rtl .jstree-anchor>.jstree-themeicon{margin-left:2px;margin-right:0}.jstree-rtl .jstree-node{margin-left:0}.jstree-rtl .jstree-container-ul>.jstree-node{margin-right:0}.jstree-wholerow-ul{position:relative;display:inline-block;min-width:100%}.jstree-wholerow-ul .jstree-leaf>.jstree-ocl{cursor:pointer}.jstree-wholerow-ul .jstree-anchor,.jstree-wholerow-ul .jstree-icon{position:relative}.jstree-wholerow-ul .jstree-wholerow{width:100%;cursor:pointer;position:absolute;left:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.jstree-contextmenu .jstree-anchor{-webkit-user-select:none;-webkit-touch-callout:none}.vakata-context{display:none}.vakata-context,.vakata-context ul{margin:0;padding:2px;position:absolute;background:#f5f5f5;border:1px solid #979797;box-shadow:2px 2px 2px #999999}.vakata-context ul{list-style:none;left:100%;margin-top:-2.7em;margin-left:-4px}.vakata-context .vakata-context-right ul{left:auto;right:100%;margin-left:auto;margin-right:-4px}.vakata-context li{list-style:none}.vakata-context li>a{display:block;padding:0 2em 0 2em;text-decoration:none;width:auto;color:black;white-space:nowrap;line-height:2.4em;text-shadow:1px 1px 0 white;border-radius:1px}.vakata-context li>a:hover{position:relative;background-color:#e8eff7;box-shadow:0 0 2px #0a6aa1}.vakata-context li>a.vakata-context-parent{background-image:url("data:image/gif;base64,R0lGODlhCwAHAIAAACgoKP///yH5BAEAAAEALAAAAAALAAcAAAIORI4JlrqN1oMSnmmZDQUAOw==");background-position:right center;background-repeat:no-repeat}.vakata-context li>a:focus{outline:0}.vakata-context .vakata-context-hover>a{position:relative;background-color:#e8eff7;box-shadow:0 0 2px #0a6aa1}.vakata-context .vakata-context-separator>a,.vakata-context .vakata-context-separator>a:hover{background:white;border:0;border-top:1px solid #e2e3e3;height:1px;min-height:1px;max-height:1px;padding:0;margin:0 0 0 2.4em;border-left:1px solid #e0e0e0;text-shadow:0 0 0 transparent;box-shadow:0 0 0 transparent;border-radius:0}.vakata-context .vakata-contextmenu-disabled a,.vakata-context .vakata-contextmenu-disabled a:hover{color:silver;background-color:transparent;border:0;box-shadow:0 0 0}.vakata-context .vakata-contextmenu-disabled>a>i{filter:grayscale(100%)}.vakata-context li>a>i{text-decoration:none;display:inline-block;width:2.4em;height:2.4em;background:transparent;margin:0 0 0 -2em;vertical-align:top;text-align:center;line-height:2.4em}.vakata-context li>a>i:empty{width:2.4em;line-height:2.4em}.vakata-context li>a .vakata-contextmenu-sep{display:inline-block;width:1px;height:2.4em;background:white;margin:0 .5em 0 0;border-left:1px solid #e2e3e3}.vakata-context .vakata-contextmenu-shortcut{font-size:.8em;color:silver;opacity:.5;display:none}.vakata-context-rtl ul{left:auto;right:100%;margin-left:auto;margin-right:-4px}.vakata-context-rtl li>a.vakata-context-parent{background-image:url("data:image/gif;base64,R0lGODlhCwAHAIAAACgoKP///yH5BAEAAAEALAAAAAALAAcAAAINjI+AC7rWHIsPtmoxLAA7");background-position:left center;background-repeat:no-repeat}.vakata-context-rtl .vakata-context-separator>a{margin:0 2.4em 0 0;border-left:0;border-right:1px solid #e2e3e3}.vakata-context-rtl .vakata-context-left ul{right:auto;left:100%;margin-left:-4px;margin-right:auto}.vakata-context-rtl li>a>i{margin:0 -2em 0 0}.vakata-context-rtl li>a .vakata-contextmenu-sep{margin:0 0 0 .5em;border-left-color:white;background:#e2e3e3}#jstree-marker{position:absolute;top:0;left:0;margin:-5px 0 0 0;padding:0;border-right:0;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid;width:0;height:0;font-size:0;line-height:0}#jstree-dnd{line-height:16px;margin:0;padding:4px}#jstree-dnd .jstree-icon,#jstree-dnd .jstree-copy{display:inline-block;text-decoration:none;margin:0 2px 0 0;padding:0;width:16px;height:16px}#jstree-dnd .jstree-ok{background:green}#jstree-dnd .jstree-er{background:red}#jstree-dnd .jstree-copy{margin:0 2px 0 2px}.jstree-default .jstree-node,.jstree-default .jstree-icon{background-repeat:no-repeat;background-color:transparent}.jstree-default .jstree-anchor,.jstree-default .jstree-animated,.jstree-default .jstree-wholerow{transition:background-color .15s,box-shadow .15s}.jstree-default .jstree-hovered{background:#e7f4f9;border-radius:2px;box-shadow:inset 0 0 1px #cccccc}.jstree-default .jstree-context{background:#e7f4f9;border-radius:2px;box-shadow:inset 0 0 1px #cccccc}.jstree-default .jstree-clicked{background:#beebff;border-radius:2px;box-shadow:inset 0 0 1px #999999}.jstree-default .jstree-no-icons .jstree-anchor>.jstree-themeicon{display:none}.jstree-default .jstree-disabled{background:transparent;color:#666666}.jstree-default .jstree-disabled.jstree-hovered{background:transparent;box-shadow:none}.jstree-default .jstree-disabled.jstree-clicked{background:#efefef}.jstree-default .jstree-disabled>.jstree-icon{opacity:.8;filter:url("data:image/svg+xml;utf8,#jstree-grayscale");filter:gray;-webkit-filter:grayscale(100%)}.jstree-default .jstree-search{font-style:italic;color:#8b0000;font-weight:bold}.jstree-default .jstree-no-checkboxes .jstree-checkbox{display:none !important}.jstree-default.jstree-checkbox-no-clicked .jstree-clicked{background:transparent;box-shadow:none}.jstree-default.jstree-checkbox-no-clicked .jstree-clicked.jstree-hovered{background:#e7f4f9}.jstree-default.jstree-checkbox-no-clicked>.jstree-wholerow-ul .jstree-wholerow-clicked{background:transparent}.jstree-default.jstree-checkbox-no-clicked>.jstree-wholerow-ul .jstree-wholerow-clicked.jstree-wholerow-hovered{background:#e7f4f9}.jstree-default>.jstree-striped{min-width:100%;display:inline-block;background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB/qqA+AAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMNAMM9s3UAAAAXSURBVHjajcEBAQAAAIKg/H/aCQZ70AUBjAATb6YPDgAAAABJRU5ErkJggg==") left top repeat}.jstree-default>.jstree-wholerow-ul .jstree-hovered,.jstree-default>.jstree-wholerow-ul .jstree-clicked{background:transparent;box-shadow:none;border-radius:0}.jstree-default .jstree-wholerow{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.jstree-default .jstree-wholerow-hovered{background:#e7f4f9}.jstree-default .jstree-wholerow-clicked{background:#beebff;background:-webkit-linear-gradient(top, #beebff 0, #a8e4ff 100%);background:linear-gradient(to bottom, #beebff 0, #a8e4ff 100%)}.jstree-default .jstree-node{min-height:24px;line-height:24px;margin-left:24px;min-width:24px}.jstree-default .jstree-anchor{line-height:24px;height:24px}.jstree-default .jstree-icon{width:24px;height:24px;line-height:24px}.jstree-default .jstree-icon:empty{width:24px;height:24px;line-height:24px}.jstree-default.jstree-rtl .jstree-node{margin-right:24px}.jstree-default .jstree-wholerow{height:24px}.jstree-default .jstree-node,.jstree-default .jstree-icon{background-image:url("32px.png")}.jstree-default .jstree-node{background-position:-292px -4px;background-repeat:repeat-y}.jstree-default .jstree-last{background:transparent}.jstree-default .jstree-open>.jstree-ocl{background-position:-132px -4px}.jstree-default .jstree-closed>.jstree-ocl{background-position:-100px -4px}.jstree-default .jstree-leaf>.jstree-ocl{background-position:-68px -4px}.jstree-default .jstree-themeicon{background-position:-260px -4px}.jstree-default>.jstree-no-dots .jstree-node,.jstree-default>.jstree-no-dots .jstree-leaf>.jstree-ocl{background:transparent}.jstree-default>.jstree-no-dots .jstree-open>.jstree-ocl{background-position:-36px -4px}.jstree-default>.jstree-no-dots .jstree-closed>.jstree-ocl{background-position:-4px -4px}.jstree-default .jstree-disabled{background:transparent}.jstree-default .jstree-disabled.jstree-hovered{background:transparent}.jstree-default .jstree-disabled.jstree-clicked{background:#efefef}.jstree-default .jstree-checkbox{background-position:-164px -4px}.jstree-default .jstree-checkbox:hover{background-position:-164px -36px}.jstree-default.jstree-checkbox-selection .jstree-clicked>.jstree-checkbox,.jstree-default .jstree-checked>.jstree-checkbox{background-position:-228px -4px}.jstree-default.jstree-checkbox-selection .jstree-clicked>.jstree-checkbox:hover,.jstree-default .jstree-checked>.jstree-checkbox:hover{background-position:-228px -36px}.jstree-default .jstree-anchor>.jstree-undetermined{background-position:-196px -4px}.jstree-default .jstree-anchor>.jstree-undetermined:hover{background-position:-196px -36px}.jstree-default .jstree-checkbox-disabled{opacity:.8;filter:url("data:image/svg+xml;utf8,#jstree-grayscale");filter:gray;-webkit-filter:grayscale(100%)}.jstree-default>.jstree-striped{background-size:auto 48px}.jstree-default.jstree-rtl .jstree-node{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg==");background-position:100% 1px;background-repeat:repeat-y}.jstree-default.jstree-rtl .jstree-last{background:transparent}.jstree-default.jstree-rtl .jstree-open>.jstree-ocl{background-position:-132px -36px}.jstree-default.jstree-rtl .jstree-closed>.jstree-ocl{background-position:-100px -36px}.jstree-default.jstree-rtl .jstree-leaf>.jstree-ocl{background-position:-68px -36px}.jstree-default.jstree-rtl>.jstree-no-dots .jstree-node,.jstree-default.jstree-rtl>.jstree-no-dots .jstree-leaf>.jstree-ocl{background:transparent}.jstree-default.jstree-rtl>.jstree-no-dots .jstree-open>.jstree-ocl{background-position:-36px -36px}.jstree-default.jstree-rtl>.jstree-no-dots .jstree-closed>.jstree-ocl{background-position:-4px -36px}.jstree-default .jstree-themeicon-custom{background-color:transparent;background-image:none;background-position:0 0}.jstree-default>.jstree-container-ul .jstree-loading>.jstree-ocl{background:url("throbber.gif") center center no-repeat}.jstree-default .jstree-file{background:url("32px.png") -100px -68px no-repeat}.jstree-default .jstree-folder{background:url("32px.png") -260px -4px no-repeat}.jstree-default>.jstree-container-ul>.jstree-node{margin-left:0;margin-right:0}#jstree-dnd.jstree-default{line-height:24px;padding:0 4px}#jstree-dnd.jstree-default .jstree-ok,#jstree-dnd.jstree-default .jstree-er{background-image:url("32px.png");background-repeat:no-repeat;background-color:transparent}#jstree-dnd.jstree-default i{background:transparent;width:24px;height:24px;line-height:24px}#jstree-dnd.jstree-default .jstree-ok{background-position:-4px -68px}#jstree-dnd.jstree-default .jstree-er{background-position:-36px -68px}.jstree-default .jstree-ellipsis{overflow:hidden}.jstree-default .jstree-ellipsis .jstree-anchor{width:calc(100% - 29px);text-overflow:ellipsis;overflow:hidden}.jstree-default.jstree-rtl .jstree-node{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg==")}.jstree-default.jstree-rtl .jstree-last{background:transparent}.jstree-default-small .jstree-node{min-height:18px;line-height:18px;margin-left:18px;min-width:18px}.jstree-default-small .jstree-anchor{line-height:18px;height:18px}.jstree-default-small .jstree-icon{width:18px;height:18px;line-height:18px}.jstree-default-small .jstree-icon:empty{width:18px;height:18px;line-height:18px}.jstree-default-small.jstree-rtl .jstree-node{margin-right:18px}.jstree-default-small .jstree-wholerow{height:18px}.jstree-default-small .jstree-node,.jstree-default-small .jstree-icon{background-image:url("32px.png")}.jstree-default-small .jstree-node{background-position:-295px -7px;background-repeat:repeat-y}.jstree-default-small .jstree-last{background:transparent}.jstree-default-small .jstree-open>.jstree-ocl{background-position:-135px -7px}.jstree-default-small .jstree-closed>.jstree-ocl{background-position:-103px -7px}.jstree-default-small .jstree-leaf>.jstree-ocl{background-position:-71px -7px}.jstree-default-small .jstree-themeicon{background-position:-263px -7px}.jstree-default-small>.jstree-no-dots .jstree-node,.jstree-default-small>.jstree-no-dots .jstree-leaf>.jstree-ocl{background:transparent}.jstree-default-small>.jstree-no-dots .jstree-open>.jstree-ocl{background-position:-39px -7px}.jstree-default-small>.jstree-no-dots .jstree-closed>.jstree-ocl{background-position:-7px -7px}.jstree-default-small .jstree-disabled{background:transparent}.jstree-default-small .jstree-disabled.jstree-hovered{background:transparent}.jstree-default-small .jstree-disabled.jstree-clicked{background:#efefef}.jstree-default-small .jstree-checkbox{background-position:-167px -7px}.jstree-default-small .jstree-checkbox:hover{background-position:-167px -39px}.jstree-default-small.jstree-checkbox-selection .jstree-clicked>.jstree-checkbox,.jstree-default-small .jstree-checked>.jstree-checkbox{background-position:-231px -7px}.jstree-default-small.jstree-checkbox-selection .jstree-clicked>.jstree-checkbox:hover,.jstree-default-small .jstree-checked>.jstree-checkbox:hover{background-position:-231px -39px}.jstree-default-small .jstree-anchor>.jstree-undetermined{background-position:-199px -7px}.jstree-default-small .jstree-anchor>.jstree-undetermined:hover{background-position:-199px -39px}.jstree-default-small .jstree-checkbox-disabled{opacity:.8;filter:url("data:image/svg+xml;utf8,#jstree-grayscale");filter:gray;-webkit-filter:grayscale(100%)}.jstree-default-small>.jstree-striped{background-size:auto 36px}.jstree-default-small.jstree-rtl .jstree-node{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg==");background-position:100% 1px;background-repeat:repeat-y}.jstree-default-small.jstree-rtl .jstree-last{background:transparent}.jstree-default-small.jstree-rtl .jstree-open>.jstree-ocl{background-position:-135px -39px}.jstree-default-small.jstree-rtl .jstree-closed>.jstree-ocl{background-position:-103px -39px}.jstree-default-small.jstree-rtl .jstree-leaf>.jstree-ocl{background-position:-71px -39px}.jstree-default-small.jstree-rtl>.jstree-no-dots .jstree-node,.jstree-default-small.jstree-rtl>.jstree-no-dots .jstree-leaf>.jstree-ocl{background:transparent}.jstree-default-small.jstree-rtl>.jstree-no-dots .jstree-open>.jstree-ocl{background-position:-39px -39px}.jstree-default-small.jstree-rtl>.jstree-no-dots .jstree-closed>.jstree-ocl{background-position:-7px -39px}.jstree-default-small .jstree-themeicon-custom{background-color:transparent;background-image:none;background-position:0 0}.jstree-default-small>.jstree-container-ul .jstree-loading>.jstree-ocl{background:url("throbber.gif") center center no-repeat}.jstree-default-small .jstree-file{background:url("32px.png") -103px -71px no-repeat}.jstree-default-small .jstree-folder{background:url("32px.png") -263px -7px no-repeat}.jstree-default-small>.jstree-container-ul>.jstree-node{margin-left:0;margin-right:0}#jstree-dnd.jstree-default-small{line-height:18px;padding:0 4px}#jstree-dnd.jstree-default-small .jstree-ok,#jstree-dnd.jstree-default-small .jstree-er{background-image:url("32px.png");background-repeat:no-repeat;background-color:transparent}#jstree-dnd.jstree-default-small i{background:transparent;width:18px;height:18px;line-height:18px}#jstree-dnd.jstree-default-small .jstree-ok{background-position:-7px -71px}#jstree-dnd.jstree-default-small .jstree-er{background-position:-39px -71px}.jstree-default-small .jstree-ellipsis{overflow:hidden}.jstree-default-small .jstree-ellipsis .jstree-anchor{width:calc(100% - 23px);text-overflow:ellipsis;overflow:hidden}.jstree-default-small.jstree-rtl .jstree-node{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAACAQMAAABv1h6PAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMHBgAAiABBI4gz9AAAAABJRU5ErkJggg==")}.jstree-default-small.jstree-rtl .jstree-last{background:transparent}.jstree-default-large .jstree-node{min-height:32px;line-height:32px;margin-left:32px;min-width:32px}.jstree-default-large .jstree-anchor{line-height:32px;height:32px}.jstree-default-large .jstree-icon{width:32px;height:32px;line-height:32px}.jstree-default-large .jstree-icon:empty{width:32px;height:32px;line-height:32px}.jstree-default-large.jstree-rtl .jstree-node{margin-right:32px}.jstree-default-large .jstree-wholerow{height:32px}.jstree-default-large .jstree-node,.jstree-default-large .jstree-icon{background-image:url("32px.png")}.jstree-default-large .jstree-node{background-position:-288px 0;background-repeat:repeat-y}.jstree-default-large .jstree-last{background:transparent}.jstree-default-large .jstree-open>.jstree-ocl{background-position:-128px 0}.jstree-default-large .jstree-closed>.jstree-ocl{background-position:-96px 0}.jstree-default-large .jstree-leaf>.jstree-ocl{background-position:-64px 0}.jstree-default-large .jstree-themeicon{background-position:-256px 0}.jstree-default-large>.jstree-no-dots .jstree-node,.jstree-default-large>.jstree-no-dots .jstree-leaf>.jstree-ocl{background:transparent}.jstree-default-large>.jstree-no-dots .jstree-open>.jstree-ocl{background-position:-32px 0}.jstree-default-large>.jstree-no-dots .jstree-closed>.jstree-ocl{background-position:0 0}.jstree-default-large .jstree-disabled{background:transparent}.jstree-default-large .jstree-disabled.jstree-hovered{background:transparent}.jstree-default-large .jstree-disabled.jstree-clicked{background:#efefef}.jstree-default-large .jstree-checkbox{background-position:-160px 0}.jstree-default-large .jstree-checkbox:hover{background-position:-160px -32px}.jstree-default-large.jstree-checkbox-selection .jstree-clicked>.jstree-checkbox,.jstree-default-large .jstree-checked>.jstree-checkbox{background-position:-224px 0}.jstree-default-large.jstree-checkbox-selection .jstree-clicked>.jstree-checkbox:hover,.jstree-default-large .jstree-checked>.jstree-checkbox:hover{background-position:-224px -32px}.jstree-default-large .jstree-anchor>.jstree-undetermined{background-position:-192px 0}.jstree-default-large .jstree-anchor>.jstree-undetermined:hover{background-position:-192px -32px}.jstree-default-large .jstree-checkbox-disabled{opacity:.8;filter:url("data:image/svg+xml;utf8,#jstree-grayscale");filter:gray;-webkit-filter:grayscale(100%)}.jstree-default-large>.jstree-striped{background-size:auto 64px}.jstree-default-large.jstree-rtl .jstree-node{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg==");background-position:100% 1px;background-repeat:repeat-y}.jstree-default-large.jstree-rtl .jstree-last{background:transparent}.jstree-default-large.jstree-rtl .jstree-open>.jstree-ocl{background-position:-128px -32px}.jstree-default-large.jstree-rtl .jstree-closed>.jstree-ocl{background-position:-96px -32px}.jstree-default-large.jstree-rtl .jstree-leaf>.jstree-ocl{background-position:-64px -32px}.jstree-default-large.jstree-rtl>.jstree-no-dots .jstree-node,.jstree-default-large.jstree-rtl>.jstree-no-dots .jstree-leaf>.jstree-ocl{background:transparent}.jstree-default-large.jstree-rtl>.jstree-no-dots .jstree-open>.jstree-ocl{background-position:-32px -32px}.jstree-default-large.jstree-rtl>.jstree-no-dots .jstree-closed>.jstree-ocl{background-position:0 -32px}.jstree-default-large .jstree-themeicon-custom{background-color:transparent;background-image:none;background-position:0 0}.jstree-default-large>.jstree-container-ul .jstree-loading>.jstree-ocl{background:url("throbber.gif") center center no-repeat}.jstree-default-large .jstree-file{background:url("32px.png") -96px -64px no-repeat}.jstree-default-large .jstree-folder{background:url("32px.png") -256px 0 no-repeat}.jstree-default-large>.jstree-container-ul>.jstree-node{margin-left:0;margin-right:0}#jstree-dnd.jstree-default-large{line-height:32px;padding:0 4px}#jstree-dnd.jstree-default-large .jstree-ok,#jstree-dnd.jstree-default-large .jstree-er{background-image:url("32px.png");background-repeat:no-repeat;background-color:transparent}#jstree-dnd.jstree-default-large i{background:transparent;width:32px;height:32px;line-height:32px}#jstree-dnd.jstree-default-large .jstree-ok{background-position:0 -64px}#jstree-dnd.jstree-default-large .jstree-er{background-position:-32px -64px}.jstree-default-large .jstree-ellipsis{overflow:hidden}.jstree-default-large .jstree-ellipsis .jstree-anchor{width:calc(100% - 37px);text-overflow:ellipsis;overflow:hidden}.jstree-default-large.jstree-rtl .jstree-node{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAACAQMAAAAD0EyKAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjgIIGBgABCgCBvVLXcAAAAABJRU5ErkJggg==")}.jstree-default-large.jstree-rtl .jstree-last{background:transparent}@media (max-width:768px){#jstree-dnd.jstree-dnd-responsive{line-height:40px;font-weight:bold;font-size:1.1em;text-shadow:1px 1px white}#jstree-dnd.jstree-dnd-responsive>i{background:transparent;width:40px;height:40px}#jstree-dnd.jstree-dnd-responsive>.jstree-ok{background-image:url("40px.png");background-position:0 -200px;background-size:120px 240px}#jstree-dnd.jstree-dnd-responsive>.jstree-er{background-image:url("40px.png");background-position:-40px -200px;background-size:120px 240px}#jstree-marker.jstree-dnd-responsive{border-left-width:10px;border-top-width:10px;border-bottom-width:10px;margin-top:-10px}}@media (max-width:768px){.jstree-default-responsive .jstree-icon{background-image:url("40px.png")}.jstree-default-responsive .jstree-node,.jstree-default-responsive .jstree-leaf>.jstree-ocl{background:transparent}.jstree-default-responsive .jstree-node{min-height:40px;line-height:40px;margin-left:40px;min-width:40px;white-space:nowrap}.jstree-default-responsive .jstree-anchor{line-height:40px;height:40px}.jstree-default-responsive .jstree-icon,.jstree-default-responsive .jstree-icon:empty{width:40px;height:40px;line-height:40px}.jstree-default-responsive>.jstree-container-ul>.jstree-node{margin-left:0}.jstree-default-responsive.jstree-rtl .jstree-node{margin-left:0;margin-right:40px;background:transparent}.jstree-default-responsive.jstree-rtl .jstree-container-ul>.jstree-node{margin-right:0}.jstree-default-responsive .jstree-ocl,.jstree-default-responsive .jstree-themeicon,.jstree-default-responsive .jstree-checkbox{background-size:120px 240px}.jstree-default-responsive .jstree-leaf>.jstree-ocl,.jstree-default-responsive.jstree-rtl .jstree-leaf>.jstree-ocl{background:transparent}.jstree-default-responsive .jstree-open>.jstree-ocl{background-position:0 0 !important}.jstree-default-responsive .jstree-closed>.jstree-ocl{background-position:0 -40px !important}.jstree-default-responsive.jstree-rtl .jstree-closed>.jstree-ocl{background-position:-40px 0 !important}.jstree-default-responsive .jstree-themeicon{background-position:-40px -40px}.jstree-default-responsive .jstree-checkbox,.jstree-default-responsive .jstree-checkbox:hover{background-position:-40px -80px}.jstree-default-responsive.jstree-checkbox-selection .jstree-clicked>.jstree-checkbox,.jstree-default-responsive.jstree-checkbox-selection .jstree-clicked>.jstree-checkbox:hover,.jstree-default-responsive .jstree-checked>.jstree-checkbox,.jstree-default-responsive .jstree-checked>.jstree-checkbox:hover{background-position:0 -80px}.jstree-default-responsive .jstree-anchor>.jstree-undetermined,.jstree-default-responsive .jstree-anchor>.jstree-undetermined:hover{background-position:0 -120px}.jstree-default-responsive .jstree-anchor{font-weight:bold;font-size:1.1em;text-shadow:1px 1px white}.jstree-default-responsive>.jstree-striped{background:transparent}.jstree-default-responsive .jstree-wholerow{border-top:1px solid rgba(255,255,255,0.7);border-bottom:1px solid rgba(64,64,64,0.2);background:#ebebeb;height:40px}.jstree-default-responsive .jstree-wholerow-hovered{background:#e7f4f9}.jstree-default-responsive .jstree-wholerow-clicked{background:#beebff}.jstree-default-responsive .jstree-children .jstree-last>.jstree-wholerow{box-shadow:inset 0 -6px 3px -5px #666666}.jstree-default-responsive .jstree-children .jstree-open>.jstree-wholerow{box-shadow:inset 0 6px 3px -5px #666666;border-top:0}.jstree-default-responsive .jstree-children .jstree-open+.jstree-open{box-shadow:none}.jstree-default-responsive .jstree-node,.jstree-default-responsive .jstree-icon,.jstree-default-responsive .jstree-node>.jstree-ocl,.jstree-default-responsive .jstree-themeicon,.jstree-default-responsive .jstree-checkbox{background-image:url("40px.png");background-size:120px 240px}.jstree-default-responsive .jstree-node{background-position:-80px 0;background-repeat:repeat-y}.jstree-default-responsive .jstree-last{background:transparent}.jstree-default-responsive .jstree-leaf>.jstree-ocl{background-position:-40px -120px}.jstree-default-responsive .jstree-last>.jstree-ocl{background-position:-40px -160px}.jstree-default-responsive .jstree-themeicon-custom{background-color:transparent;background-image:none;background-position:0 0}.jstree-default-responsive .jstree-file{background:url("40px.png") 0 -160px no-repeat;background-size:120px 240px}.jstree-default-responsive .jstree-folder{background:url("40px.png") -40px -40px no-repeat;background-size:120px 240px}.jstree-default-responsive>.jstree-container-ul>.jstree-node{margin-left:0;margin-right:0}} --------------------------------------------------------------------------------