Compare commits

..

34 Commits

Author SHA1 Message Date
李佳航
9e31d820e0 Merge pull request #94 from dromara/dev
Dev
2025-03-03 11:25:03 +08:00
lijiahang
92353d859a 初始化 sql. 2025-03-03 11:13:42 +08:00
lijiahang
bef8d69e59 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	sql/init-4-data.sql
2025-03-03 09:46:24 +08:00
lijiahangmax
b3ab78e063 🔖 升级版本. 2025-03-02 13:28:41 +08:00
lijiahangmax
95d2c6cb65 🔨 修改 sql 脚本. 2025-03-01 23:23:52 +08:00
lijiahang
7017c7502b 修改创建人逻辑. 2025-02-21 17:57:35 +08:00
李佳航
c14055ba8c Merge pull request #91 from dromara/dev
Dev
2025-02-19 10:17:03 +08:00
lijiahangmax
04aa6c9680 ✏️ 修改文档. 2025-02-18 23:35:48 +08:00
lijiahang
397bbb2657 🔖 升级版本. 2025-02-18 10:56:41 +08:00
lijiahang
9a68282127 🐛 修复计划任务日志无权限. 2025-02-18 10:54:10 +08:00
lijiahang
dcd02acc61 删除冗余配置. 2025-02-14 12:07:39 +08:00
李佳航
1025688e9b Merge pull request #88 from dromara/dev
Dev
2025-02-11 09:42:16 +08:00
lijiahangmax
26eeb26a75 ✏️ 修改文档. 2025-02-10 22:59:41 +08:00
lijiahang
7f76325284 🔖 升级版本. 2025-02-10 10:08:55 +08:00
lijiahang
de9a921c49 🔨 修改日志查看逻辑. 2025-02-10 09:51:53 +08:00
lijiahang
a9ac9d0f79 🔨 优化执行日志查看. 2025-02-08 10:03:07 +08:00
lijiahang
a14b28de6a 修改连接记录字段. 2025-02-07 10:07:45 +08:00
lijiahang
ccd7430b8f 🔨 自动清理任务使用系统配置. 2025-02-06 09:56:12 +08:00
lijiahang
6791ea5770 🔨 登录使用系统配置. 2025-02-06 09:55:37 +08:00
lijiahang
2d5835b150 修改文件预览逻辑. 2025-02-05 10:45:16 +08:00
lijiahang
cec11ce8c3 🔨 设置执行参数逻辑. 2025-02-05 10:18:22 +08:00
lijiahang
972103841c 🔨 优化执行日志查看逻辑. 2025-02-05 10:16:12 +08:00
lijiahang
89f6d2cd1c 🔨 优化执行日志查看逻辑. 2025-02-05 10:14:07 +08:00
lijiahang
d13008ce0c 🔨 添加修改密码时间. 2025-02-05 10:08:30 +08:00
lijiahang
abf384dd3c 🔨 修改系统设置逻辑. 2025-01-23 10:13:54 +08:00
李佳航
0abd4a893b Merge pull request #87 from dromara/hotfix
Hotfix
2025-01-22 23:17:36 +08:00
李佳航
d0710fb52b Merge branch 'dev' into hotfix 2025-01-22 23:12:47 +08:00
李佳航
534fe83ac2 Merge pull request #86 from dromara/hotfix
merge
2025-01-22 22:59:57 +08:00
lijiahangmax
d95d958de0 🔖 升级版本. 2025-01-22 22:40:28 +08:00
lijiahangmax
4e5730f31f 🐛 修复加密失败的问题. 2025-01-22 22:32:10 +08:00
lijiahang
81b9bacb96 🔨 添加日志系统设置. 2025-01-21 10:05:36 +08:00
lijiahang
dc42a31711 🔨 修改系统设置逻辑. 2025-01-20 10:24:40 +08:00
lijiahang
27e3e65ea1 🔨 使用系统配置替代配置文件. 2025-01-20 10:21:41 +08:00
lijiahang
a001ab3f16 🔨 优化批量执行模块. 2025-01-20 09:59:45 +08:00
132 changed files with 3130 additions and 2600 deletions

View File

@@ -86,11 +86,11 @@ docker compose up -d
## 技术栈
* SpringBoot 2.7.+
* Mysql 8.0.+
* Redis 6.0.+
* Vue3 3.2.+
* Arco Design 2.55.+
* SpringBoot 2.7+
* Mysql 8+
* Redis 6+
* Vue3 3+
* Arco Design 2+
## 主要功能预览

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.3.0
version=2.3.4
docker build -t orion-visor-adminer:${version} .
docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version}
docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:latest

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.3.0
version=2.3.4
cp -r ../../sql ./sql
docker build -t orion-visor-mysql:${version} .
rm -rf ./sql

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.3.0
version=2.3.4
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version}
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-mysql:${version}
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version}

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.3.0
version=2.3.4
docker build -t orion-visor-redis:${version} .
docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version}
docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:latest

View File

@@ -1,8 +1,8 @@
#/bin/bash
version=2.3.0
version=2.3.4
mv ../../orion-visor-launch/target/orion-visor-launch.jar ./orion-visor-launch.jar
mv ../../orion-visor-ui/dist ./dist
docker build --no-cache -t orion-visor-service:${version} .
docker build -t orion-visor-service:${version} .
rm -rf ./orion-visor-launch.jar
rm -rf ./dist
docker tag orion-visor-service:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-service:${version}

View File

@@ -36,7 +36,7 @@ public interface AppConst extends OrionConst {
/**
* 同 ${orion.version} 迭代时候需要手动更改
*/
String VERSION = "2.3.0";
String VERSION = "2.3.4";
/**
* 同 ${spring.application.name}

View File

@@ -31,14 +31,119 @@ package org.dromara.visor.common.constant;
*/
public interface ConfigKeys {
/**
* SFTP 文件预览大小
*/
String SFTP_PREVIEW_SIZE = "sftp_previewSize";
/**
* SFTP 重复文件备份
*/
String SFTP_UPLOAD_PRESENT_BACKUP = "sftp_uploadPresentBackup";
/**
* SFTP 备份文件名称
*/
String SFTP_UPLOAD_BACKUP_FILE_NAME = "sftp_uploadBackupFileName";
/**
* 加密公钥
*/
String ENCRYPT_PUBLIC_KEY = "encrypt.publicKey";
String ENCRYPT_PUBLIC_KEY = "encrypt_publicKey";
/**
* 加密私钥
*/
String ENCRYPT_PRIVATE_KEY = "encrypt.privateKey";
String ENCRYPT_PRIVATE_KEY = "encrypt_privateKey";
/**
* 日志前端显示行数
*/
String LOG_WEB_SCROLL_LINES = "log_webScrollLines";
/**
* 日志加载偏移行
*/
String LOG_TRACKER_LOAD_LINES = "log_trackerLoadLines";
/**
* 日志加载间隔毫秒
*/
String LOG_TRACKER_LOAD_INTERVAL = "log_trackerLoadInterval";
/**
* 是否生成详细的执行日志
*/
String LOG_EXEC_DETAIL_LOG = "log_execDetailLog";
/**
* 凭证有效期分
*/
String LOGIN_LOGIN_SESSION_TIME = "login_loginSessionTime";
/**
* 允许多端登录
*/
String LOGIN_ALLOW_MULTI_DEVICE = "login_allowMultiDevice";
/**
* 允许凭证续签
*/
String LOGIN_ALLOW_REFRESH = "login_allowRefresh";
/**
* 凭证续签最大次数
*/
String LOGIN_MAX_REFRESH_COUNT = "login_maxRefreshCount";
/**
* 凭证续签间隔分
*/
String LOGIN_REFRESH_INTERVAL = "login_refreshInterval";
/**
* 登录失败锁定
*/
String LOGIN_LOGIN_FAILED_LOCK = "login_loginFailedLock";
/**
* 登录失败锁定阈值分
*/
String LOGIN_LOGIN_FAILED_LOCK_THRESHOLD = "login_loginFailedLockThreshold";
/**
* 登录失败锁定时间分
*/
String LOGIN_LOGIN_FAILED_LOCK_TIME = "login_loginFailedLockTime";
/**
* 登录失败发信
*/
String LOGIN_LOGIN_FAILED_SEND = "login_loginFailedSend";
/**
* 登录失败发信阈值
*/
String LOGIN_LOGIN_FAILED_SEND_THRESHOLD = "login_loginFailedSendThreshold";
/**
* 是否开启自动清理命令记录
*/
String AUTO_CLEAR_EXEC_LOG_ENABLED = "autoClear_execLogEnabled";
/**
* 自动清理命令记录保留天数
*/
String AUTO_CLEAR_EXEC_LOG_KEEP_DAYS = "autoClear_execLogKeepDays";
/**
* 是否开启自动清理终端连接记录
*/
String AUTO_CLEAR_TERMINAL_LOG_ENABLED = "autoClear_terminalLogEnabled";
/**
* 自动清理终端连接记录保留天数
*/
String AUTO_CLEAR_TERMINAL_LOG_KEEP_DAYS = "autoClear_terminalLogKeepDays";
}

View File

@@ -23,21 +23,13 @@
package org.dromara.visor.common.interfaces;
/**
* rsa 密器
* rsa 密器
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/5 20:58
*/
public interface RsaEncryptor {
/**
* 加密
*
* @param value value
* @return value
*/
String encrypt(String value);
public interface RsaDecryptor {
/**
* 解密

View File

@@ -45,4 +45,11 @@ public interface SecurityHolder {
*/
Long getLoginUserId();
/**
* 获取当前用户名
*
* @return username
*/
String getLoginUsername();
}

View File

@@ -23,30 +23,20 @@
package org.dromara.visor.common.utils;
import cn.orionsec.kit.lang.utils.Exceptions;
import org.dromara.visor.common.interfaces.RsaEncryptor;
import org.dromara.visor.common.interfaces.RsaDecryptor;
/**
* rsa 密工具类
* rsa 参数解密工具类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/5 21:13
*/
public class RsaEncryptUtils {
public class RsaParamDecryptUtils {
private static RsaEncryptor delegate;
private static RsaDecryptor delegate;
private RsaEncryptUtils() {
}
/**
* 加密
*
* @param value value
* @return value
*/
public static String encrypt(String value) {
return delegate.encrypt(value);
private RsaParamDecryptUtils() {
}
/**
@@ -59,12 +49,12 @@ public class RsaEncryptUtils {
return delegate.decrypt(value);
}
public static void setDelegate(RsaEncryptor delegate) {
if (RsaEncryptUtils.delegate != null) {
public static void setDelegate(RsaDecryptor delegate) {
if (RsaParamDecryptUtils.delegate != null) {
// unmodified
throw Exceptions.state();
}
RsaEncryptUtils.delegate = delegate;
RsaParamDecryptUtils.delegate = delegate;
}
}

View File

@@ -14,7 +14,7 @@
<url>https://github.com/dromara/orion-visor</url>
<properties>
<revision>2.3.0</revision>
<revision>2.3.4</revision>
<spring.boot.version>2.7.17</spring.boot.version>
<spring.boot.admin.version>2.7.15</spring.boot.admin.version>
<flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version>

View File

@@ -25,12 +25,12 @@ package org.dromara.visor.framework.encrypt.configuration;
import org.dromara.visor.common.config.ConfigStore;
import org.dromara.visor.common.constant.AutoConfigureOrderConst;
import org.dromara.visor.common.interfaces.AesEncryptor;
import org.dromara.visor.common.interfaces.RsaEncryptor;
import org.dromara.visor.common.interfaces.RsaDecryptor;
import org.dromara.visor.common.utils.AesEncryptUtils;
import org.dromara.visor.common.utils.RsaEncryptUtils;
import org.dromara.visor.common.utils.RsaParamDecryptUtils;
import org.dromara.visor.framework.encrypt.configuration.config.AesEncryptConfig;
import org.dromara.visor.framework.encrypt.core.impl.AesEncryptorImpl;
import org.dromara.visor.framework.encrypt.core.impl.RsaEncryptorImpl;
import org.dromara.visor.framework.encrypt.core.impl.RsaDecryptorImpl;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -63,15 +63,15 @@ public class OrionEncryptAutoConfiguration {
/**
* @param configStore configStore
* @return rsa 密器
* @return rsa 参数解密器
*/
@Bean
public RsaEncryptor rsaEncryptor(ConfigStore configStore) {
// 密器
RsaEncryptor encryptor = new RsaEncryptorImpl(configStore);
public RsaDecryptor rsaParamDecryptor(ConfigStore configStore) {
// 密器
RsaDecryptor decryptor = new RsaDecryptorImpl(configStore);
// 设置工具类
RsaEncryptUtils.setDelegate(encryptor);
return encryptor;
RsaParamDecryptUtils.setDelegate(decryptor);
return decryptor;
}
}

View File

@@ -26,10 +26,11 @@ import cn.orionsec.kit.lang.utils.crypto.RSA;
import org.dromara.visor.common.config.ConfigRef;
import org.dromara.visor.common.config.ConfigStore;
import org.dromara.visor.common.constant.ConfigKeys;
import org.dromara.visor.common.interfaces.RsaEncryptor;
import org.dromara.visor.common.interfaces.RsaDecryptor;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* rsa 加密器
@@ -38,25 +39,21 @@ import java.security.interfaces.RSAPublicKey;
* @version 1.0.0
* @since 2025/1/7 11:32
*/
public class RsaEncryptorImpl implements RsaEncryptor {
public class RsaDecryptorImpl implements RsaDecryptor {
private final ConfigRef<RSAPublicKey> publicKey;
private static final String SPLIT = "\\|";
private final ConfigRef<RSAPrivateKey> privateKey;
public RsaEncryptorImpl(ConfigStore configStore) {
this.publicKey = configStore.ref(ConfigKeys.ENCRYPT_PUBLIC_KEY, RSA::getPublicKey);
public RsaDecryptorImpl(ConfigStore configStore) {
this.privateKey = configStore.ref(ConfigKeys.ENCRYPT_PRIVATE_KEY, RSA::getPrivateKey);
}
@Override
public String encrypt(String value) {
return RSA.encrypt(value, publicKey.value);
}
@Override
public String decrypt(String value) {
return RSA.decrypt(value, privateKey.value);
return Arrays.stream(value.split(SPLIT))
.map(s -> RSA.decrypt(s, privateKey.value))
.collect(Collectors.joining());
}
}

View File

@@ -50,6 +50,7 @@ public class DomainFillUtils {
*/
public static void fillInsert(BaseDO baseDO) {
Date now = new Date();
String username = securityHolder.getLoginUsername();
// 创建时间
if (Objects.isNull(baseDO.getCreateTime())) {
baseDO.setCreateTime(now);
@@ -58,15 +59,13 @@ public class DomainFillUtils {
if (Objects.isNull(baseDO.getUpdateTime())) {
baseDO.setUpdateTime(now);
}
Long userId = securityHolder.getLoginUserId();
// 创建人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
baseDO.setCreator(userId.toString());
if (Objects.nonNull(username) && Objects.isNull(baseDO.getCreator())) {
baseDO.setCreator(username);
}
// 更新人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(userId.toString());
if (Objects.nonNull(username) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(username);
}
// 逻辑删除字段
if (Objects.isNull(baseDO.getDeleted())) {
@@ -85,9 +84,9 @@ public class DomainFillUtils {
baseDO.setUpdateTime(new Date());
}
// 更新人
Long userId = securityHolder.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(userId.toString());
String username = securityHolder.getLoginUsername();
if (Objects.nonNull(username) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(username);
}
}

View File

@@ -45,4 +45,9 @@ public class SecurityHolderDelegate implements SecurityHolder {
return SecurityUtils.getLoginUserId();
}
@Override
public String getLoginUsername() {
return SecurityUtils.getLoginUsername();
}
}

View File

@@ -71,6 +71,11 @@ public class OrionMockBeanTestConfiguration {
public Long getLoginUserId() {
return DEFAULT.getId();
}
@Override
public String getLoginUsername() {
return DEFAULT.getUsername();
}
};
}

View File

@@ -28,7 +28,7 @@ import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.utils.RsaEncryptUtils;
import org.dromara.visor.common.utils.RsaParamDecryptUtils;
import org.dromara.visor.common.utils.Valid;
import java.io.IOException;
@@ -51,7 +51,7 @@ public class ParamDecryptDeserializer extends JsonDeserializer<String> {
return value;
}
// 解密参数
String decrypt = RsaEncryptUtils.decrypt(value);
String decrypt = RsaParamDecryptUtils.decrypt(value);
return Valid.notNull(decrypt, ErrorMessage.DECRYPT_ERROR);
}

View File

@@ -1,18 +1,18 @@
spring:
datasource:
druid:
url: jdbc:mysql://127.0.0.1:3306/orion_visor?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true
username: root
password: Data@123456
url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:orion_visor}?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true
username: ${MYSQL_USER:root}
password: ${MYSQL_PASSWORD:Data@123456}
initial-size: 0
min-idle: 1
max-active: 5
stat-view-servlet:
enabled: false
redis:
host: 127.0.0.1
port: 6379
password: Data@123456
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:Data@123456}
redisson:
threads: 2
netty-threads: 2

View File

@@ -157,51 +157,6 @@ logging:
level:
org.dromara.visor.launch.controller.BootstrapController: INFO
# 应用配置
app:
# 认证配置
authentication:
# 是否允许多端登录
allow-multi-device: true
# 是否允许凭证续签
allow-refresh: true
# 凭证续签最大次数
max-refresh-count: 3
# 登录失败发送站内信阈值
login-failed-send-threshold: 3
# 登录失败锁定次数
login-failed-lock-count: 5
# 登录失败锁定时间 (分)
login-failed-lock-time: 30
# tracker 配置
tracker:
# 加载偏移量 (行)
offset: 300
# 延迟时间 (ms)
delay: 100
# 文件未找到等待次数
wait-times: 100
# sftp 配置
sftp:
# 上传文件时 文件存在是否备份
upload-present-backup: true
# 备份文件名称
backup-file-name: bk_${fileName}_${timestamp}
# 执行日志
exec-log:
# 是否拼接 ansi 执行状态日志
append-ansi: true
# 自动清理配置
auto-clear:
# 批量执行日志
exec-log:
enabled: false
keep-period: 30
# 终端连接日志
terminal-connect-log:
enabled: false
keep-period: 30
# orion framework config
orion:
# 版本

View File

@@ -39,9 +39,9 @@ import java.util.function.Function;
*/
public class ReplaceVersion {
private static final String TARGET_VERSION = "2.2.3";
private static final String TARGET_VERSION = "2.3.3";
private static final String REPLACE_VERSION = "2.3.0";
private static final String REPLACE_VERSION = "2.3.4";
private static final String PATH = new File("").getAbsolutePath();

View File

@@ -38,7 +38,6 @@ import org.dromara.visor.module.asset.define.operator.ExecCommandLogOperatorType
import org.dromara.visor.module.asset.entity.request.exec.ExecInterruptRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogClearRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogQueryRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogTailRequest;
import org.dromara.visor.module.asset.entity.vo.ExecHostLogVO;
import org.dromara.visor.module.asset.entity.vo.ExecLogStatusVO;
import org.dromara.visor.module.asset.entity.vo.ExecLogVO;
@@ -93,6 +92,14 @@ public class ExecCommandLogController {
return execLogService.getExecLog(id, SOURCE);
}
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/get-host")
@Operation(summary = "查询执行主机日志")
@PreAuthorize("@ss.hasPermission('asset:exec-command-log:query')")
public ExecHostLogVO getExecCommandHostLog(@RequestParam("id") Long id) {
return execHostLogService.getExecHostLog(id);
}
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/host-list")
@Operation(summary = "查询全部执行主机日志")
@@ -164,12 +171,12 @@ public class ExecCommandLogController {
return execLogService.clearExecLog(request);
}
@PostMapping("/tail")
@GetMapping("/tail")
@Operation(summary = "查看批量执行日志")
@Parameter(name = "id", description = "id", required = true)
@PreAuthorize("@ss.hasAnyPermission('asset:exec-command-log:query', 'asset:exec-command:exec')")
public String getExecCommandLogTailToken(@Validated @RequestBody ExecLogTailRequest request) {
request.setSource(SOURCE);
return execLogService.getExecLogTailToken(request);
public String getExecCommandLogTailToken(@RequestParam("id") Long id) {
return execLogService.getExecLogTailToken(id);
}
@OperatorLog(ExecCommandLogOperatorType.DOWNLOAD)

View File

@@ -37,7 +37,6 @@ import org.dromara.visor.module.asset.define.operator.ExecJobLogOperatorType;
import org.dromara.visor.module.asset.entity.request.exec.ExecInterruptRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogClearRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogQueryRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogTailRequest;
import org.dromara.visor.module.asset.entity.vo.ExecHostLogVO;
import org.dromara.visor.module.asset.entity.vo.ExecLogStatusVO;
import org.dromara.visor.module.asset.entity.vo.ExecLogVO;
@@ -92,6 +91,14 @@ public class ExecJobLogController {
return execLogService.getExecLog(id, SOURCE);
}
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/get-host")
@Operation(summary = "查询执行主机日志")
@PreAuthorize("@ss.hasPermission('asset:exec-job-log:query')")
public ExecHostLogVO getExecJobHostLog(@RequestParam("id") Long id) {
return execHostLogService.getExecHostLog(id);
}
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/host-list")
@Operation(summary = "查询全部执行主机日志")
@@ -153,12 +160,12 @@ public class ExecJobLogController {
return execLogService.clearExecLog(request);
}
@PostMapping("/tail")
@GetMapping("/tail")
@Operation(summary = "查看计划任务日志")
@Parameter(name = "id", description = "id", required = true)
@PreAuthorize("@ss.hasPermission('asset:exec-job-log:query')")
public String getExecJobLogTailToken(@Validated @RequestBody ExecLogTailRequest request) {
request.setSource(SOURCE);
return execLogService.getExecLogTailToken(request);
public String getExecJobLogTailToken(@RequestParam("id") Long id) {
return execLogService.getExecLogTailToken(id);
}
@OperatorLog(ExecJobLogOperatorType.DOWNLOAD)

View File

@@ -81,7 +81,7 @@ public class TerminalConnectLogController {
}
@IgnoreLog(IgnoreLogMode.RET)
@PostMapping("/session")
@PostMapping("/sessions")
@Operation(summary = "查询全部终端连接会话")
@PreAuthorize("@ss.hasPermission('asset:terminal-connect-session:management:query')")
public List<TerminalConnectLogVO> getTerminalConnectSessions(@Validated @RequestBody TerminalConnectLogQueryRequest request) {

View File

@@ -52,4 +52,20 @@ public interface ExecHostLogDAO extends IMapper<ExecHostLogDO> {
.list();
}
/**
* 通过 logId 查询
*
* @param id id
* @param logId logId
* @return row
*/
default ExecHostLogDO selectByIdAndLogId(Long id, Long logId) {
return this.of()
.createWrapper()
.eq(ExecHostLogDO::getId, id)
.eq(ExecHostLogDO::getLogId, logId)
.then()
.get();
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.define.config;
import org.dromara.visor.common.config.ConfigRef;
import org.dromara.visor.common.config.ConfigStore;
import org.dromara.visor.common.constant.ConfigKeys;
import org.springframework.stereotype.Component;
/**
* 自动清理设置
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/6/24 15:01
*/
@Component
public class AppAutoClearConfig {
/**
* 是否开启自动清理命令记录
*/
private final ConfigRef<Boolean> execLogEnabled;
/**
* 自动清理命令记录保留天数
*/
private final ConfigRef<Integer> execLogKeepDays;
/**
* 是否开启自动清理终端连接记录
*/
private final ConfigRef<Boolean> terminalLogEnabled;
/**
* 自动清理终端连接记录保留天数
*/
private final ConfigRef<Integer> terminalLogKeepDays;
public AppAutoClearConfig(ConfigStore configStore) {
this.execLogEnabled = configStore.bool(ConfigKeys.AUTO_CLEAR_EXEC_LOG_ENABLED);
this.execLogKeepDays = configStore.int32(ConfigKeys.AUTO_CLEAR_EXEC_LOG_KEEP_DAYS);
this.terminalLogEnabled = configStore.bool(ConfigKeys.AUTO_CLEAR_TERMINAL_LOG_ENABLED);
this.terminalLogKeepDays = configStore.int32(ConfigKeys.AUTO_CLEAR_TERMINAL_LOG_KEEP_DAYS);
}
public Boolean getExecLogEnabled() {
return execLogEnabled.value;
}
public Integer getExecLogKeepDays() {
return execLogKeepDays.value;
}
public Boolean getTerminalLogEnabled() {
return terminalLogEnabled.value;
}
public Integer getTerminalLogKeepDays() {
return terminalLogKeepDays.value;
}
}

View File

@@ -1,44 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.define.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.visor.common.entity.AutoClearConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 批量执行日志自动清理配置
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/6/24 15:01
*/
@Data
@Component
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "app.auto-clear.exec-log")
public class AppExecLogAutoClearConfig extends AutoClearConfig {
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.define.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 应用执行日志配置
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/4/25 13:36
*/
@Data
@Component
@ConfigurationProperties(prefix = "app.exec-log")
public class AppExecLogConfig {
/**
* 是否拼接 ansi 执行状态日志
*/
private Boolean appendAnsi;
public AppExecLogConfig() {
this.appendAnsi = true;
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.define.config;
import org.dromara.visor.common.config.ConfigRef;
import org.dromara.visor.common.config.ConfigStore;
import org.dromara.visor.common.constant.ConfigKeys;
import org.springframework.stereotype.Component;
/**
* 应用日志设置
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/4/15 22:00
*/
@Component
public class AppLogConfig {
/**
* 日志加载偏移行
*/
private final ConfigRef<Integer> trackerLoadLines;
/**
* 日志加载间隔毫秒
*/
private final ConfigRef<Integer> trackerLoadInterval;
/**
* 是否生成详细的执行日志
*/
private final ConfigRef<Boolean> execDetailLog;
public AppLogConfig(ConfigStore configStore) {
this.trackerLoadLines = configStore.int32(ConfigKeys.LOG_TRACKER_LOAD_LINES);
this.trackerLoadInterval = configStore.int32(ConfigKeys.LOG_TRACKER_LOAD_INTERVAL);
this.execDetailLog = configStore.bool(ConfigKeys.LOG_EXEC_DETAIL_LOG);
}
public Integer getTrackerLoadLines() {
return trackerLoadLines.value;
}
public Integer getTrackerLoadInterval() {
return trackerLoadInterval.value;
}
public Boolean getExecDetailLog() {
return execDetailLog.value;
}
}

View File

@@ -22,8 +22,9 @@
*/
package org.dromara.visor.module.asset.define.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.dromara.visor.common.config.ConfigRef;
import org.dromara.visor.common.config.ConfigStore;
import org.dromara.visor.common.constant.ConfigKeys;
import org.springframework.stereotype.Component;
/**
@@ -33,24 +34,40 @@ import org.springframework.stereotype.Component;
* @version 1.0.0
* @since 2024/4/15 22:00
*/
@Data
@Component
@ConfigurationProperties(prefix = "app.sftp")
public class AppSftpConfig {
/**
* 上传文件时 文件存在是否备份
* 文件预览大小
*/
private Boolean uploadPresentBackup;
private final ConfigRef<Integer> previewSize;
/**
* 重复文件备份
*/
private final ConfigRef<Boolean> uploadPresentBackup;
/**
* 备份文件名称
*/
private String backupFileName;
private final ConfigRef<String> uploadBackupFileName;
public AppSftpConfig() {
this.uploadPresentBackup = true;
this.backupFileName = "bk_${fileName}_${timestamp}";
public AppSftpConfig(ConfigStore configStore) {
this.previewSize = configStore.int32(ConfigKeys.SFTP_PREVIEW_SIZE);
this.uploadPresentBackup = configStore.bool(ConfigKeys.SFTP_UPLOAD_PRESENT_BACKUP);
this.uploadBackupFileName = configStore.string(ConfigKeys.SFTP_UPLOAD_BACKUP_FILE_NAME);
}
public Integer getPreviewSize() {
return previewSize.value;
}
public Boolean getUploadPresentBackup() {
return uploadPresentBackup.value;
}
public String getUploadBackupFileName() {
return uploadBackupFileName.value;
}
}

View File

@@ -1,44 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.define.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.visor.common.entity.AutoClearConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 终端连接日志自动清理配置
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/6/24 15:01
*/
@Data
@Component
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "app.auto-clear.terminal-connect-log")
public class AppTerminalConnectLogAutoClearConfig extends AutoClearConfig {
}

View File

@@ -1,61 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.define.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 应用 tracker 配置
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/4/15 22:00
*/
@Data
@Component
@ConfigurationProperties(prefix = "app.tracker")
public class AppTrackerConfig {
/**
* 加载偏移量 (行)
*/
private Integer offset;
/**
* 延迟时间 (ms)
*/
private Integer delay;
/**
* 文件未找到等待次数
*/
private Integer waitTimes;
public AppTrackerConfig() {
this.offset = 300;
this.delay = 100;
this.waitTimes = 100;
}
}

View File

@@ -78,9 +78,9 @@ public class TerminalConnectLogDO extends BaseDO {
@TableField("type")
private String type;
@Schema(description = "token")
@TableField("token")
private String token;
@Schema(description = "sessionId")
@TableField("session_id")
private String sessionId;
@Schema(description = "状态")
@TableField("status")

View File

@@ -1,59 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 执行主机日志查看 缓存对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/3/18 16:34
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "ExecHostLogTailDTO", description = "执行主机日志查看 缓存对象")
public class ExecHostLogTailDTO implements Serializable {
@Schema(description = "id")
private Long id;
@Schema(description = "hostId")
private Long hostId;
@Schema(description = "文件路径")
private String path;
@Schema(description = "输出编码")
private String charset;
}

View File

@@ -29,7 +29,6 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 执行日志查看 缓存对象
@@ -45,8 +44,8 @@ import java.util.List;
@Schema(name = "ExecLogTailDTO", description = "执行日志查看 缓存对象")
public class ExecLogTailDTO implements Serializable {
@Schema(description = "id")
private Long id;
@Schema(description = "execId")
private Long execId;
@Schema(description = "用户id")
private Long userId;
@@ -54,7 +53,4 @@ public class ExecLogTailDTO implements Serializable {
@Schema(description = "token")
private String token;
@Schema(description = "执行主机")
private List<ExecHostLogTailDTO> hosts;
}

View File

@@ -57,6 +57,9 @@ public class TerminalConnectDTO {
@Schema(description = "hostName")
private String hostName;
@Schema(description = "主机编码")
private String hostCode;
@Schema(description = "主机地址")
private String hostAddress;

View File

@@ -1,58 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.entity.request.exec;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 执行日志查看 请求对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/3/11 11:46
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "ExecLogTailRequest", description = "执行日志查看 请求对象")
public class ExecLogTailRequest {
@Schema(description = "执行来源")
private String source;
@NotNull
@Schema(description = "执行id")
private Long execId;
@Schema(description = "执行主机id")
private List<Long> hostExecIdList;
}

View File

@@ -63,9 +63,9 @@ public class TerminalConnectLogCreateRequest {
@Schema(description = "状态")
private String status;
@Size(max = 128)
@Schema(description = "token")
private String token;
@Size(max = 64)
@Schema(description = "sessionId")
private String sessionId;
@Schema(description = "拓展信息")
private Map<String, Object> extra;

View File

@@ -69,9 +69,9 @@ public class TerminalConnectLogQueryRequest extends PageRequest {
@Schema(description = "类型")
private String type;
@Size(max = 128)
@Schema(description = "token")
private String token;
@Size(max = 64)
@Schema(description = "sessionId")
private String sessionId;
@Size(max = 16)
@Schema(description = "状态")

View File

@@ -72,6 +72,9 @@ public class TerminalConnectLogVO implements Serializable {
@Schema(description = "状态")
private String status;
@Schema(description = "sessionId")
private String sessionId;
@Schema(description = "开始时间")
private Date startTime;

View File

@@ -29,7 +29,7 @@ import org.dromara.visor.common.constant.Const;
import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.handler.data.strategy.AbstractGenericsDataStrategy;
import org.dromara.visor.common.utils.AesEncryptUtils;
import org.dromara.visor.common.utils.RsaEncryptUtils;
import org.dromara.visor.common.utils.RsaParamDecryptUtils;
import org.dromara.visor.common.utils.Valid;
import org.dromara.visor.module.asset.dao.HostIdentityDAO;
import org.dromara.visor.module.asset.dao.HostKeyDAO;
@@ -134,7 +134,7 @@ public class HostSshConfigStrategy extends AbstractGenericsDataStrategy<HostSshC
// 检查新密码
String newPassword = Valid.notBlank(after.getPassword(), ErrorMessage.PASSWORD_MISSING);
// 解密密码
newPassword = RsaEncryptUtils.decrypt(newPassword);
newPassword = RsaParamDecryptUtils.decrypt(newPassword);
Valid.notBlank(newPassword, ErrorMessage.DECRYPT_ERROR);
// 设置密码
after.setPassword(AesEncryptUtils.encryptAsString(newPassword));

View File

@@ -24,7 +24,8 @@ package org.dromara.visor.module.asset.handler.host.exec.command;
import org.dromara.visor.module.asset.define.AssetThreadPools;
import org.dromara.visor.module.asset.handler.host.exec.command.handler.ExecTaskHandler;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandDTO;
import java.util.List;
/**
* 批量执行命令执行器
@@ -38,10 +39,11 @@ public class ExecTaskExecutors {
/**
* 执行命令
*
* @param command command
* @param id id
* @param execHostIdList execHostIdList
*/
public static void start(ExecCommandDTO command) {
AssetThreadPools.EXEC_TASK.execute(new ExecTaskHandler(command));
public static void start(Long id, List<Long> execHostIdList) {
AssetThreadPools.EXEC_TASK.execute(new ExecTaskHandler(id, execHostIdList));
}
}

View File

@@ -25,12 +25,13 @@ package org.dromara.visor.module.asset.handler.host.exec.command.handler;
import cn.orionsec.kit.lang.exception.AuthenticationException;
import cn.orionsec.kit.lang.exception.ConnectionRuntimeException;
import cn.orionsec.kit.lang.exception.SftpException;
import cn.orionsec.kit.lang.id.UUIds;
import cn.orionsec.kit.lang.support.timeout.TimeoutChecker;
import cn.orionsec.kit.lang.support.timeout.TimeoutEndpoint;
import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.ansi.AnsiAppender;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
@@ -40,22 +41,29 @@ import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.constant.FileConst;
import org.dromara.visor.common.enums.BooleanBit;
import org.dromara.visor.common.enums.EndpointDefine;
import org.dromara.visor.common.interfaces.FileClient;
import org.dromara.visor.common.utils.PathUtils;
import org.dromara.visor.common.utils.Valid;
import org.dromara.visor.module.asset.dao.ExecHostLogDAO;
import org.dromara.visor.module.asset.entity.domain.ExecHostLogDO;
import org.dromara.visor.module.asset.entity.domain.ExecLogDO;
import org.dromara.visor.module.asset.entity.dto.TerminalConnectDTO;
import org.dromara.visor.module.asset.enums.ExecHostStatusEnum;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandDTO;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandHostDTO;
import org.dromara.visor.module.asset.enums.HostOsTypeEnum;
import org.dromara.visor.module.asset.handler.host.exec.log.manager.ExecLogManager;
import org.dromara.visor.module.asset.handler.host.jsch.SessionStores;
import org.dromara.visor.module.asset.service.TerminalService;
import org.dromara.visor.module.asset.utils.ExecUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* 命令执行器 基类
@@ -75,16 +83,21 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
private static final ExecHostLogDAO execHostLogDAO = SpringHolder.getBean(ExecHostLogDAO.class);
protected final ExecCommandDTO execCommand;
@Getter
protected final Long id;
protected final ExecCommandHostDTO execHostCommand;
protected final Map<String, Object> builtParams;
private final TimeoutChecker<TimeoutEndpoint> timeoutChecker;
protected final TimeoutChecker<TimeoutEndpoint> timeoutChecker;
protected final ExecLogDO execLog;
protected ExecHostLogDO execHostLog;
@Getter
protected ExecHostStatusEnum status;
protected ExecHostLogDO updateRecord;
private TerminalConnectDTO connect;
private OutputStream logOutputStream;
@@ -99,24 +112,28 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
private volatile boolean interrupted;
public BaseExecCommandHandler(ExecCommandDTO execCommand,
ExecCommandHostDTO execHostCommand,
public BaseExecCommandHandler(Long id,
ExecLogDO execLog,
Map<String, Object> builtParams,
TimeoutChecker<TimeoutEndpoint> timeoutChecker) {
this.status = ExecHostStatusEnum.WAITING;
this.execCommand = execCommand;
this.execHostCommand = execHostCommand;
this.id = id;
this.execLog = execLog;
this.builtParams = builtParams;
this.timeoutChecker = timeoutChecker;
this.updateRecord = new ExecHostLogDO();
this.status = ExecHostStatusEnum.WAITING;
}
@Override
public void run() {
Long id = execHostCommand.getHostLogId();
Exception ex = null;
log.info("ExecCommandHandler run start id: {}, info: {}", id, JSON.toJSONString(execHostCommand));
// 更新状态
this.updateStatus(ExecHostStatusEnum.RUNNING, null);
log.info("ExecCommandHandler run start id: {}", id);
// 初始化数据以及修改状态
if (!this.initData()) {
return;
}
try {
// 初始化日志
this.initLogOutputStream();
// 执行命令
this.execCommand();
log.info("ExecCommandHandler run complete id: {}", id);
@@ -135,13 +152,41 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
}
/**
* 初始化日志输出流
* 初始化数据
*
* @throws Exception Exception
* @return pass
*/
protected void initLogOutputStream() throws Exception {
// 打开日志流
this.logOutputStream = fileClient.getContentOutputStream(execHostCommand.getLogPath());
private boolean initData() {
Exception ex = null;
try {
// 查询任务
this.execHostLog = execHostLogDAO.selectById(id);
Valid.notNull(this.execHostLog, ErrorMessage.TASK_ABSENT);
// 检查任务状态
this.status = ExecHostStatusEnum.of(execHostLog.getStatus());
Valid.eq(this.status, ExecHostStatusEnum.WAITING, ErrorMessage.TASK_ABSENT, ErrorMessage.ILLEGAL_STATUS);
// 获取主机会话
this.connect = terminalService.getTerminalConnectInfo(execHostLog.getHostId(), execLog.getUserId());
// 设置日志路径
this.setLogPath();
// 设置脚本路径
this.setScriptPath();
// 设置执行的命令以及参数
this.setExecCommand();
} catch (Exception e) {
log.error("BaseExecCommandHandler.initData error id: {}", id, e);
ex = e;
}
boolean passed = ex == null;
// 更新状态
this.updateStatus(passed ? ExecHostStatusEnum.RUNNING : ExecHostStatusEnum.FAILED, ex, (s) -> {
// 修改其他参数
s.setCommand(execHostLog.getCommand());
s.setParameter(execHostLog.getParameter());
s.setLogPath(execHostLog.getLogPath());
s.setScriptPath(execHostLog.getScriptPath());
});
return passed;
}
/**
@@ -150,29 +195,36 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
* @throws IOException IOException
*/
protected void execCommand() throws Exception {
// 初始化日志
this.initLogOutputStream();
// 打开会话
TerminalConnectDTO connect = terminalService.getTerminalConnectInfo(execHostCommand.getHostId(), execCommand.getUserId());
this.sessionStore = SessionStores.openSessionStore(connect);
if (Booleans.isTrue(execCommand.getScriptExec())) {
if (BooleanBit.toBoolean(execLog.getScriptExec())) {
// 上传脚本文件
this.uploadScriptFile();
// 执行脚本文件
this.executor = sessionStore.getCommandExecutor(execHostCommand.getScriptPath());
this.executor = sessionStore.getCommandExecutor(execHostLog.getScriptPath());
} else {
// 执行命令
byte[] command = Strings.replaceCRLF(execHostCommand.getCommand()).getBytes(execHostCommand.getCharset());
byte[] command = execHostLog.getCommand().getBytes(connect.getCharset());
this.executor = sessionStore.getCommandExecutor(command);
}
// 执行命令
executor.timeout(execCommand.getTimeout(), TimeUnit.SECONDS, timeoutChecker);
executor.timeout(execLog.getTimeout(), TimeUnit.SECONDS, timeoutChecker);
executor.merge();
executor.transfer(logOutputStream);
executor.connect();
executor.exec();
}
/**
* 初始化日志输出流
*
* @throws Exception Exception
*/
protected void initLogOutputStream() throws Exception {
// 打开日志流
this.logOutputStream = fileClient.getContentOutputStream(execHostLog.getLogPath());
}
/**
* 上传脚本文件
*/
@@ -180,14 +232,14 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
SftpExecutor sftpExecutor = null;
try {
// 打开 sftp
sftpExecutor = sessionStore.getSftpExecutor(execHostCommand.getFileNameCharset());
sftpExecutor = sessionStore.getSftpExecutor(connect.getFileNameCharset());
sftpExecutor.connect();
// 文件上传必须要以 / 开头
String scriptPath = PathUtils.prependSeparator(execHostCommand.getScriptPath());
String scriptPath = PathUtils.prependSeparator(execHostLog.getScriptPath());
// 创建文件
sftpExecutor.touch(scriptPath);
// 写入命令
byte[] command = Strings.replaceCRLF(execHostCommand.getCommand()).getBytes(execHostCommand.getFileContentCharset());
byte[] command = execHostLog.getCommand().getBytes(connect.getFileContentCharset());
sftpExecutor.write(scriptPath, command);
// 修改权限
sftpExecutor.changeMode(scriptPath, 777);
@@ -207,16 +259,16 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
// 执行回调
if (this.interrupted) {
// 中断执行
this.updateStatus(ExecHostStatusEnum.INTERRUPTED, null);
this.updateStatus(ExecHostStatusEnum.INTERRUPTED, null, null);
} else if (e != null) {
// 执行失败
this.updateStatus(ExecHostStatusEnum.FAILED, e);
this.updateStatus(ExecHostStatusEnum.FAILED, e, null);
} else if (executor.isTimeout()) {
// 更新执行超时
this.updateStatus(ExecHostStatusEnum.TIMEOUT, null);
this.updateStatus(ExecHostStatusEnum.TIMEOUT, null, null);
} else {
// 更新执行完成
this.updateStatus(ExecHostStatusEnum.COMPLETED, null);
this.updateStatus(ExecHostStatusEnum.COMPLETED, null, null);
}
}
@@ -239,33 +291,45 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
*
* @param status status
* @param ex ex
* @param filler filler
*/
private void updateStatus(ExecHostStatusEnum status, Exception ex) {
private void updateStatus(ExecHostStatusEnum status, Exception ex, Consumer<ExecHostLogDO> filler) {
this.status = status;
Long id = execHostCommand.getHostLogId();
String statusName = status.name();
execHostLog.setStatus(statusName);
log.info("BaseExecCommandHandler.updateStatus start id: {}, status: {}", id, statusName);
try {
updateRecord.setId(id);
updateRecord.setStatus(statusName);
if (ExecHostStatusEnum.RUNNING.equals(status)) {
// 运行中
updateRecord.setStartTime(new Date());
execHostLog.setStartTime(new Date());
} else if (ExecHostStatusEnum.COMPLETED.equals(status)) {
// 完成
updateRecord.setFinishTime(new Date());
updateRecord.setExitCode(executor.getExitCode());
execHostLog.setFinishTime(new Date());
execHostLog.setExitCode(executor.getExitCode());
this.exitCode = executor.getExitCode();
} else if (ExecHostStatusEnum.FAILED.equals(status)) {
// 失败
updateRecord.setFinishTime(new Date());
updateRecord.setErrorMessage(this.getErrorMessage(ex));
execHostLog.setFinishTime(new Date());
execHostLog.setErrorMessage(this.getErrorMessage(ex));
} else if (ExecHostStatusEnum.TIMEOUT.equals(status)) {
// 超时
updateRecord.setFinishTime(new Date());
execHostLog.setFinishTime(new Date());
} else if (ExecHostStatusEnum.INTERRUPTED.equals(status)) {
// 中断
updateRecord.setFinishTime(new Date());
execHostLog.setFinishTime(new Date());
}
// 选择性更新
ExecHostLogDO updateRecord = ExecHostLogDO.builder()
.id(execHostLog.getId())
.status(execHostLog.getStatus())
.exitCode(execHostLog.getExitCode())
.startTime(execHostLog.getStartTime())
.finishTime(execHostLog.getFinishTime())
.errorMessage(execHostLog.getErrorMessage())
.build();
// 填充参数
if (filler != null) {
filler.accept(updateRecord);
}
int effect = execHostLogDAO.updateById(updateRecord);
log.info("BaseExecCommandHandler.updateStatus finish id: {}, effect: {}", id, effect);
@@ -281,8 +345,7 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
@Override
public void interrupt() {
log.info("BaseExecCommandHandler.interrupt id: {}, interrupted: {}, closed: {}",
execHostCommand.getHostLogId(), interrupted, closed);
log.info("BaseExecCommandHandler.interrupt id: {}, interrupted: {}, closed: {}", id, interrupted, closed);
if (this.interrupted || this.closed) {
return;
}
@@ -294,8 +357,7 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
@Override
public void close() {
log.info("BaseExecCommandHandler.closed id: {}, closed: {}",
execHostCommand.getHostLogId(), closed);
log.info("BaseExecCommandHandler.closed id: {}, closed: {}", id, closed);
if (this.closed) {
return;
}
@@ -303,7 +365,59 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
Streams.close(logOutputStream);
Streams.close(executor);
Streams.close(sessionStore);
execLogManager.asyncCloseTailFile(execHostCommand.getLogPath());
execLogManager.asyncCloseTailFile(id);
}
/**
* 设置日志路径
*/
private void setLogPath() {
// 构建日志路径
String logPath = fileClient.getReturnPath(EndpointDefine.EXEC_LOG.format(execHostLog.getLogId(), id));
execHostLog.setLogPath(logPath);
}
/**
* 设置脚本路径
*/
private void setScriptPath() {
String scriptPath = null;
// 获取脚本路径
if (BooleanBit.toBoolean(execLog.getScriptExec())) {
HostOsTypeEnum os = HostOsTypeEnum.of(connect.getOsType());
String name = FileConst.EXEC
+ "/" + execHostLog.getLogId()
+ "/" + id
+ os.getScriptSuffix();
scriptPath = PathUtils.buildAppPath(HostOsTypeEnum.WINDOWS.equals(os), connect.getUsername(), FileConst.SCRIPT, name);
}
execHostLog.setScriptPath(scriptPath);
}
/**
* 设置实际执行的命令以及参数
*/
private void setExecCommand() {
String uuid = UUIds.random();
// 参数列表
Map<String, Object> params = Maps.newMap(builtParams);
params.put("execHostId", id);
params.put("hostId", connect.getHostId());
params.put("hostName", connect.getHostName());
params.put("hostCode", connect.getHostCode());
params.put("hostAddress", connect.getHostAddress());
params.put("hostPort", connect.getHostPort());
params.put("hostUsername", connect.getUsername());
params.put("hostUuid", uuid);
params.put("hostUuidShort", uuid.replace("-", Strings.EMPTY));
params.put("osType", connect.getOsType());
params.put("charset", connect.getCharset());
params.put("scriptPath", execHostLog.getScriptPath());
// 获取实际命令
String paramsJson = JSON.toJSONString(params);
String command = ExecUtils.format(execLog.getCommand(), paramsJson);
execHostLog.setCommand(command);
execHostLog.setParameter(paramsJson);
}
/**
@@ -336,9 +450,4 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
return Strings.retain(message, 250);
}
@Override
public Long getHostId() {
return execHostCommand.getHostId();
}
}

View File

@@ -24,15 +24,16 @@ package org.dromara.visor.module.asset.handler.host.exec.command.handler;
import cn.orionsec.kit.lang.support.timeout.TimeoutChecker;
import cn.orionsec.kit.lang.support.timeout.TimeoutEndpoint;
import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.ansi.AnsiAppender;
import cn.orionsec.kit.lang.utils.ansi.style.color.AnsiForeground;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.net.host.ssh.ExitCode;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.common.enums.BooleanBit;
import org.dromara.visor.module.asset.entity.domain.ExecLogDO;
import org.dromara.visor.module.asset.enums.ExecHostStatusEnum;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandDTO;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandHostDTO;
import java.util.Map;
/**
* 命令执行器 ansi 日志输出
@@ -41,10 +42,13 @@ import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecComman
* @version 1.0.0
* @since 2024/4/25 18:19
*/
public class ExecCommandAnsiHandler extends BaseExecCommandHandler {
public class ExecCommandDetailHandler extends BaseExecCommandHandler {
public ExecCommandAnsiHandler(ExecCommandDTO execCommand, ExecCommandHostDTO execHostCommand, TimeoutChecker<TimeoutEndpoint> timeoutChecker) {
super(execCommand, execHostCommand, timeoutChecker);
public ExecCommandDetailHandler(Long id,
ExecLogDO execLog,
Map<String, Object> builtParams,
TimeoutChecker<TimeoutEndpoint> timeoutChecker) {
super(id, execLog, builtParams, timeoutChecker);
}
@Override
@@ -56,52 +60,52 @@ public class ExecCommandAnsiHandler extends BaseExecCommandHandler {
.append(this.getCurrentTime())
.newLine()
.append(AnsiForeground.BRIGHT_BLUE, "执行记录: ")
.append(execCommand.getLogId())
.append(execLog.getId())
.newLine()
.append(AnsiForeground.BRIGHT_BLUE, "执行描述: ")
.append(execCommand.getDescription())
.append(execLog.getDescription())
.newLine()
.append(AnsiForeground.BRIGHT_BLUE, "执行用户: ")
.append(execCommand.getUsername());
.append(execLog.getUsername());
// 非系统用户执行添加 userId
if (Const.SYSTEM_USER_ID.equals(execCommand.getUserId())) {
if (Const.SYSTEM_USER_ID.equals(execLog.getUserId())) {
appender.newLine();
} else {
appender.append(" (")
.append(execCommand.getUserId())
.append(execLog.getUserId())
.append(")")
.newLine();
}
// 执行序列
if (execCommand.getExecSeq() != null) {
if (execLog.getExecSeq() != null) {
appender.append(AnsiForeground.BRIGHT_BLUE, "执行序列: ")
.append('#')
.append(execCommand.getExecSeq())
.append(execLog.getExecSeq())
.newLine();
}
appender.append(AnsiForeground.BRIGHT_BLUE, "执行主机: ")
.append(execHostCommand.getHostName())
.append(execHostLog.getHostName())
.append(" (")
.append(execHostCommand.getHostId())
.append(execHostLog.getHostId())
.append(")")
.newLine()
.append(AnsiForeground.BRIGHT_BLUE, "主机地址: ")
.append(execHostCommand.getHostAddress())
.append(execHostLog.getHostAddress())
.newLine()
.append(AnsiForeground.BRIGHT_BLUE, "超时时间: ")
.append(execCommand.getTimeout())
.append(execLog.getTimeout())
.newLine()
.append(AnsiForeground.BRIGHT_BLUE, "脚本执行: ")
.append(execCommand.getScriptExec())
.append(execLog.getScriptExec())
.newLine()
.newLine()
.append(AnsiForeground.BRIGHT_GREEN, "> 执行命令 ")
.newLine()
.append(execHostCommand.getCommand())
.append(execHostLog.getCommand())
.newLine()
.newLine();
// 非脚本执行拼接开始执行日志
if (!Booleans.isTrue(execCommand.getScriptExec())) {
if (!BooleanBit.toBoolean(execLog.getScriptExec())) {
appender.append(AnsiForeground.BRIGHT_GREEN, "> 开始执行命令 ")
.append(this.getCurrentTime())
.newLine();
@@ -119,7 +123,7 @@ public class ExecCommandAnsiHandler extends BaseExecCommandHandler {
.append(this.getCurrentTime())
.newLine()
.append(AnsiForeground.BRIGHT_BLUE, "文件路径: ")
.append(execHostCommand.getScriptPath())
.append(execHostLog.getScriptPath())
.newLine();
this.appendLog(startAppender);
// 上传脚本文件
@@ -170,9 +174,9 @@ public class ExecCommandAnsiHandler extends BaseExecCommandHandler {
appender.append(AnsiForeground.BRIGHT_YELLOW, "< 命令执行超时 ")
.append(this.getCurrentTime())
.newLine();
} else {
long ms = updateRecord.getFinishTime().getTime() - updateRecord.getStartTime().getTime();
Integer exitCode = updateRecord.getExitCode();
} else if (this.status == ExecHostStatusEnum.COMPLETED) {
long ms = execHostLog.getFinishTime().getTime() - execHostLog.getStartTime().getTime();
Integer exitCode = execHostLog.getExitCode();
// 执行完成
appender.append(AnsiForeground.BRIGHT_GREEN, "< 命令执行完成 ")
.append(this.getCurrentTime())

View File

@@ -25,8 +25,9 @@ package org.dromara.visor.module.asset.handler.host.exec.command.handler;
import cn.orionsec.kit.lang.support.timeout.TimeoutChecker;
import cn.orionsec.kit.lang.support.timeout.TimeoutEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandDTO;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandHostDTO;
import org.dromara.visor.module.asset.entity.domain.ExecLogDO;
import java.util.Map;
/**
* 命令执行器 原始日志输出
@@ -38,8 +39,11 @@ import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecComman
@Slf4j
public class ExecCommandOriginHandler extends BaseExecCommandHandler {
public ExecCommandOriginHandler(ExecCommandDTO execCommand, ExecCommandHostDTO execHostCommand, TimeoutChecker<TimeoutEndpoint> timeoutChecker) {
super(execCommand, execHostCommand, timeoutChecker);
public ExecCommandOriginHandler(Long id,
ExecLogDO execLog,
Map<String, Object> builtParams,
TimeoutChecker<TimeoutEndpoint> timeoutChecker) {
super(id, execLog, builtParams, timeoutChecker);
}
}

View File

@@ -22,11 +22,14 @@
*/
package org.dromara.visor.module.asset.handler.host.exec.command.handler;
import cn.orionsec.kit.lang.id.UUIds;
import cn.orionsec.kit.lang.support.timeout.TimeoutChecker;
import cn.orionsec.kit.lang.support.timeout.TimeoutCheckers;
import cn.orionsec.kit.lang.support.timeout.TimeoutEndpoint;
import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.Valid;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.time.Dates;
@@ -34,17 +37,17 @@ import cn.orionsec.kit.net.host.ssh.ExitCode;
import cn.orionsec.kit.spring.SpringHolder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.constant.ExtraFieldConst;
import org.dromara.visor.module.asset.dao.ExecLogDAO;
import org.dromara.visor.module.asset.define.AssetThreadPools;
import org.dromara.visor.module.asset.define.config.AppExecLogConfig;
import org.dromara.visor.module.asset.define.config.AppLogConfig;
import org.dromara.visor.module.asset.define.message.ExecMessageDefine;
import org.dromara.visor.module.asset.entity.domain.ExecLogDO;
import org.dromara.visor.module.asset.enums.ExecHostStatusEnum;
import org.dromara.visor.module.asset.enums.ExecStatusEnum;
import org.dromara.visor.module.asset.handler.host.exec.command.manager.ExecTaskManager;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandDTO;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandHostDTO;
import org.dromara.visor.module.asset.utils.ExecUtils;
import org.dromara.visor.module.infra.api.SystemMessageApi;
import org.dromara.visor.module.infra.entity.dto.message.SystemMessageDTO;
@@ -63,15 +66,21 @@ import java.util.Map;
@Slf4j
public class ExecTaskHandler implements IExecTaskHandler {
private static final AppLogConfig appLogConfig = SpringHolder.getBean(AppLogConfig.class);
private static final ExecLogDAO execLogDAO = SpringHolder.getBean(ExecLogDAO.class);
private static final ExecTaskManager execTaskManager = SpringHolder.getBean(ExecTaskManager.class);
private static final AppExecLogConfig appExecLogConfig = SpringHolder.getBean(AppExecLogConfig.class);
private static final SystemMessageApi systemMessageApi = SpringHolder.getBean(SystemMessageApi.class);
private final ExecCommandDTO execCommand;
private final Long id;
private final List<Long> execHostIdList;
private ExecLogDO execLog;
private Map<String, Object> builtParams;
private TimeoutChecker<TimeoutEndpoint> timeoutChecker;
@@ -80,14 +89,19 @@ public class ExecTaskHandler implements IExecTaskHandler {
private Date startTime;
public ExecTaskHandler(ExecCommandDTO execCommand) {
this.execCommand = execCommand;
public ExecTaskHandler(Long id, List<Long> execHostIdList) {
this.id = id;
this.execHostIdList = execHostIdList;
this.handlers = Lists.newList();
}
@Override
public void run() {
Long id = execCommand.getLogId();
log.info("ExecTaskHandler start id: {}", id);
// 初始化数据
if (!this.initData()) {
return;
}
// 添加任务
execTaskManager.addTask(id, this);
log.info("ExecTaskHandler.run start id: {}", id);
@@ -115,17 +129,37 @@ public class ExecTaskHandler implements IExecTaskHandler {
@Override
public void interrupt() {
log.info("ExecTaskHandler-interrupt id: {}", execCommand.getLogId());
log.info("ExecTaskHandler-interrupt id: {}", id);
handlers.forEach(IExecCommandHandler::interrupt);
}
@Override
public void close() {
log.info("ExecTaskHandler-close id: {}", execCommand.getLogId());
log.info("ExecTaskHandler-close id: {}", id);
Streams.close(timeoutChecker);
this.handlers.forEach(Streams::close);
}
/**
* 初始化数据
*
* @return pass
*/
private boolean initData() {
try {
// 查询任务
this.execLog = execLogDAO.selectById(id);
Valid.notNull(execLog, ErrorMessage.TASK_ABSENT);
Valid.eq(execLog.getStatus(), ExecStatusEnum.WAITING.name(), ErrorMessage.ILLEGAL_STATUS);
// 获取内置变量
this.builtParams = this.getBaseBuiltinParams();
return true;
} catch (Exception e) {
log.error("ExecTaskHandler.init error id: {}", id, e);
return false;
}
}
/**
* 执行主机命令
*
@@ -133,19 +167,18 @@ public class ExecTaskHandler implements IExecTaskHandler {
*/
private void runHostCommand() throws Exception {
// 超时检查
if (execCommand.getTimeout() != 0) {
if (execLog.getTimeout() != 0) {
this.timeoutChecker = TimeoutCheckers.create();
AssetThreadPools.TIMEOUT_CHECK.execute(this.timeoutChecker);
}
// 执行命令
List<ExecCommandHostDTO> hosts = execCommand.getHosts();
if (hosts.size() == 1) {
if (execHostIdList.size() == 1) {
// 单个主机直接执行
IExecCommandHandler handler = this.createCommandHandler(hosts.get(0));
IExecCommandHandler handler = this.createCommandHandler(execHostIdList.get(0));
handlers.add(handler);
handler.run();
} else {
hosts.stream()
execHostIdList.stream()
.map(this::createCommandHandler)
.forEach(handlers::add);
// 多个主机异步阻塞执行
@@ -156,16 +189,16 @@ public class ExecTaskHandler implements IExecTaskHandler {
/**
* 创建命令执行器
*
* @param host host
* @param execHostId execHostId
* @return handler
*/
private IExecCommandHandler createCommandHandler(ExecCommandHostDTO host) {
if (Booleans.isTrue(appExecLogConfig.getAppendAnsi())) {
// ansi 日志
return new ExecCommandAnsiHandler(execCommand, host, timeoutChecker);
private IExecCommandHandler createCommandHandler(Long execHostId) {
if (Booleans.isTrue(appLogConfig.getExecDetailLog())) {
// 详细日志
return new ExecCommandDetailHandler(execHostId, execLog, builtParams, timeoutChecker);
} else {
// 原始日志
return new ExecCommandOriginHandler(execCommand, host, timeoutChecker);
return new ExecCommandOriginHandler(execHostId, execLog, builtParams, timeoutChecker);
}
}
@@ -175,7 +208,6 @@ public class ExecTaskHandler implements IExecTaskHandler {
* @param status status
*/
private void updateStatus(ExecStatusEnum status) {
Long id = execCommand.getLogId();
try {
String statusName = status.name();
log.info("ExecTaskHandler-updateStatus start id: {}, status: {}", id, statusName);
@@ -214,16 +246,43 @@ public class ExecTaskHandler implements IExecTaskHandler {
}
// 参数
Map<String, Object> params = new HashMap<>();
params.put(ExtraFieldConst.ID, execCommand.getLogId());
params.put(ExtraFieldConst.ID, id);
params.put(ExtraFieldConst.TIME, Dates.format(this.startTime, Dates.MD_HM));
SystemMessageDTO message = SystemMessageDTO.builder()
.receiverId(execCommand.getUserId())
.receiverUsername(execCommand.getUsername())
.relKey(String.valueOf(execCommand.getLogId()))
.receiverId(execLog.getUserId())
.receiverUsername(execLog.getUsername())
.relKey(String.valueOf(id))
.params(params)
.build();
// 发送
systemMessageApi.create(ExecMessageDefine.EXEC_FAILED, message);
}
/**
* 获取基础内置参数
*
* @return params
*/
private Map<String, Object> getBaseBuiltinParams() {
String uuid = UUIds.random();
Date date = new Date();
// 输入参数
Map<String, Object> params = ExecUtils.extraSchemaParams(execLog.getParameterSchema());
// 添加内置参数
params.put("userId", execLog.getUserId());
params.put("username", execLog.getUsername());
params.put("source", execLog.getSource());
params.put("sourceId", execLog.getSourceId());
params.put("seq", execLog.getExecSeq());
params.put("execId", id);
params.put("scriptExec", execLog.getScriptExec());
params.put("uuid", uuid);
params.put("uuidShort", uuid.replace("-", Strings.EMPTY));
params.put("timestampMillis", date.getTime());
params.put("timestamp", date.getTime() / Dates.SECOND_STAMP);
params.put("date", Dates.format(date, Dates.YMD));
params.put("datetime", Dates.format(date, Dates.YMD_HMS));
return params;
}
}

View File

@@ -61,10 +61,10 @@ public interface IExecCommandHandler extends Runnable, SafeCloseable {
Integer getExitCode();
/**
* 获取主机 id
* 获取任务 id
*
* @return hostId
*/
Long getHostId();
Long getId();
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.handler.host.exec.command.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 批量执行启动对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/3/11 15:46
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExecCommandDTO {
/**
* logId
*/
private Long logId;
/**
* 用户id
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 执行描述
*/
private String description;
/**
* 执行序列
*/
private Integer execSeq;
/**
* 超时时间
*/
private Integer timeout;
/**
* 是否使用脚本执行
*/
private Boolean scriptExec;
/**
* 执行主机
*/
private List<ExecCommandHostDTO> hosts;
}

View File

@@ -1,98 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.handler.host.exec.command.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 批量执行启动主机对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/3/11 15:46
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExecCommandHostDTO {
/**
* hostLogId
*/
private Long hostLogId;
/**
* hostId
*/
private Long hostId;
/**
* 主机名称
*/
private String hostName;
/**
* 主机地址
*/
private String hostAddress;
/**
* 日志文件路径
*/
private String logPath;
/**
* 脚本路径
*/
private String scriptPath;
/**
* 执行命令
*/
private String command;
/**
* 主机用户
*/
private String username;
/**
* 命令编码
*/
private String charset;
/**
* 文件名称编码
*/
private String fileNameCharset;
/**
* 文件内容编码
*/
private String fileContentCharset;
}

View File

@@ -22,13 +22,11 @@
*/
package org.dromara.visor.module.asset.handler.host.exec.log;
import cn.orionsec.kit.lang.annotation.Keep;
import cn.orionsec.kit.lang.utils.Strings;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.ExtraFieldConst;
import org.dromara.visor.common.interfaces.FileClient;
import org.dromara.visor.framework.websocket.core.utils.WebSockets;
import org.dromara.visor.module.asset.define.AssetThreadPools;
import org.dromara.visor.module.asset.entity.dto.ExecHostLogTailDTO;
import org.dromara.visor.module.asset.entity.dto.ExecLogTailDTO;
import org.dromara.visor.module.asset.handler.host.exec.log.constant.LogConst;
import org.dromara.visor.module.asset.handler.host.exec.log.manager.ExecLogManager;
@@ -52,41 +50,27 @@ import javax.annotation.Resource;
@Component
public class ExecLogTailHandler extends AbstractWebSocketHandler {
@Keep
@Resource
private FileClient logsFileClient;
@Resource
private ExecLogManager execLogManager;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String id = session.getId();
log.info("ExecLogTailHandler-afterConnectionEstablished id: {}", id);
// 获取参数
ExecLogTailDTO info = WebSockets.getAttr(session, ExtraFieldConst.INFO);
// 打开会话
for (ExecHostLogTailDTO host : info.getHosts()) {
String trackerId = this.getTrackerId(id, info, host);
String absolutePath = logsFileClient.getAbsolutePath(host.getPath());
// 追踪器
ExecLogTracker tracker = new ExecLogTracker(trackerId,
absolutePath,
WebSockets.createSyncSession(session),
host);
// 执行
String payload = message.getPayload();
if (LogConst.PING_PAYLOAD.equals(payload)) {
// ping
WebSockets.sendText(session, LogConst.PONG_PAYLOAD);
} else if (Strings.isInteger(payload)) {
// 获取日志
ExecLogTailDTO info = WebSockets.getAttr(session, ExtraFieldConst.INFO);
Long execHostId = Long.valueOf(payload);
ExecLogTracker tracker = new ExecLogTracker(info.getExecId(),
execHostId,
WebSockets.createSyncSession(session));
// 执行追踪器
AssetThreadPools.EXEC_LOG.execute(tracker);
// 添加追踪器
execLogManager.addTracker(tracker);
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload();
// ping
if (LogConst.PING_PAYLOAD.equals(payload)) {
WebSockets.sendText(session, LogConst.PONG_PAYLOAD);
execLogManager.addTracker(id, tracker);
}
}
@@ -99,24 +83,8 @@ public class ExecLogTailHandler extends AbstractWebSocketHandler {
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String id = session.getId();
log.info("ExecLogTailHandler-afterConnectionClosed id: {}, code: {}, reason: {}", id, status.getCode(), status.getReason());
// 关闭会话
ExecLogTailDTO info = WebSockets.getAttr(session, ExtraFieldConst.INFO);
// 移除追踪器
for (ExecHostLogTailDTO host : info.getHosts()) {
execLogManager.removeTracker(this.getTrackerId(id, info, host));
}
}
/**
* 获取追踪器 id
*
* @param id id
* @param info info
* @param host host
* @return trackerId
*/
private String getTrackerId(String id, ExecLogTailDTO info, ExecHostLogTailDTO host) {
return id + "_" + info.getId() + "_" + host.getId();
execLogManager.removeTrackers(id);
}
}

View File

@@ -28,6 +28,8 @@ import org.dromara.visor.common.constant.Const;
import org.dromara.visor.module.asset.handler.host.exec.log.tracker.IExecLogTracker;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@@ -43,58 +45,58 @@ import java.util.stream.Collectors;
@Component
public class ExecLogManager {
private final ConcurrentHashMap<String, IExecLogTracker> execTrackers = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, List<IExecLogTracker>> execTrackers = new ConcurrentHashMap<>();
/**
* 添加执行日志追踪器
*
* @param id id
* @param tracker tracker
*/
public void addTracker(IExecLogTracker tracker) {
execTrackers.put(tracker.getTrackerId(), tracker);
}
/**
* 获取日志追踪器
*
* @param trackerId trackerId
* @return tracker
*/
public IExecLogTracker getTracker(String trackerId) {
return execTrackers.get(trackerId);
public void addTracker(String id, IExecLogTracker tracker) {
execTrackers.computeIfAbsent(id, k -> new ArrayList<>()).add(tracker);
}
/**
* 移除日志追踪器
*
* @param trackerId trackerId
* @param id id
*/
public void removeTracker(String trackerId) {
IExecLogTracker tracker = execTrackers.remove(trackerId);
if (tracker != null) {
tracker.close();
public void removeTrackers(String id) {
// 移除并且关闭
List<IExecLogTracker> trackers = execTrackers.remove(id);
if (trackers != null) {
trackers.forEach(IExecLogTracker::close);
}
}
/**
* 异步关闭进行中的追踪器
*
* @param path path
* @param execHostId execHostId
*/
public void asyncCloseTailFile(String path) {
public void asyncCloseTailFile(Long execHostId) {
if (execHostId == null) {
return;
}
// 获取当前路径的全部追踪器
List<IExecLogTracker> trackers = execTrackers.values()
.stream()
.flatMap(Collection::stream)
.filter(s -> s.getExecHostId().equals(execHostId))
.collect(Collectors.toList());
if (trackers.isEmpty()) {
return;
}
// 异步设置更新并且关闭
Threads.start(() -> {
try {
// 获取当前路径的全部追踪器
List<IExecLogTracker> trackers = execTrackers.values()
.stream()
.filter(s -> s.getPath().equals(path))
.collect(Collectors.toList());
Threads.sleep(Const.MS_S_1);
trackers.forEach(IExecLogTracker::setLastModify);
Threads.sleep(Const.MS_S_5);
trackers.forEach(IExecLogTracker::close);
} catch (Exception e) {
log.error("ExecLogManager.asyncCloseTailFile error path: {}", path, e);
log.error("ExecLogManager.asyncCloseTailFile error execHostId: {}", execHostId, e);
}
});
}

View File

@@ -26,15 +26,32 @@ import cn.orionsec.kit.ext.tail.Tracker;
import cn.orionsec.kit.ext.tail.delay.DelayTrackerListener;
import cn.orionsec.kit.ext.tail.mode.FileNotFoundMode;
import cn.orionsec.kit.ext.tail.mode.FileOffsetMode;
import cn.orionsec.kit.lang.exception.argument.InvalidArgumentException;
import cn.orionsec.kit.lang.utils.Charsets;
import cn.orionsec.kit.lang.utils.io.FileReaders;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.spring.SpringHolder;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.interfaces.FileClient;
import org.dromara.visor.common.utils.Valid;
import org.dromara.visor.framework.websocket.core.utils.WebSockets;
import org.dromara.visor.module.asset.define.config.AppTrackerConfig;
import org.dromara.visor.module.asset.entity.dto.ExecHostLogTailDTO;
import org.dromara.visor.module.asset.dao.ExecHostLogDAO;
import org.dromara.visor.module.asset.define.config.AppLogConfig;
import org.dromara.visor.module.asset.entity.domain.ExecHostLogDO;
import org.dromara.visor.module.asset.enums.ExecHostStatusEnum;
import org.dromara.visor.module.asset.handler.host.exec.log.constant.LogConst;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
/**
* log tracker 实现类
*
@@ -45,78 +62,168 @@ import org.springframework.web.socket.WebSocketSession;
@Slf4j
public class ExecLogTracker implements IExecLogTracker {
private static final AppTrackerConfig trackerConfig = SpringHolder.getBean(AppTrackerConfig.class);
private static final AppLogConfig appLogConfig = SpringHolder.getBean(AppLogConfig.class);
private static final FileClient localFileClient = SpringHolder.getBean("localFileClient");
private static final ExecHostLogDAO execHostLogDAO = SpringHolder.getBean(ExecHostLogDAO.class);
private final WebSocketSession session;
private final ExecHostLogTailDTO config;
@Getter
private final Long execId;
@Getter
private final String trackerId;
private final Long execHostId;
@Getter
private final String absolutePath;
private Charset charset;
private String absolutePath;
private ExecHostLogDO execHostLog;
private RandomAccessFile file;
private DelayTrackerListener tracker;
private volatile boolean close;
public ExecLogTracker(String trackerId,
String absolutePath,
WebSocketSession session,
ExecHostLogTailDTO config) {
this.trackerId = trackerId;
this.absolutePath = absolutePath;
public ExecLogTracker(Long execId,
Long execHostId,
WebSocketSession session) {
this.execId = execId;
this.execHostId = execHostId;
this.session = session;
this.config = config;
}
@Override
public void run() {
try {
this.tracker = new DelayTrackerListener(absolutePath, this);
tracker.charset(config.getCharset());
tracker.delayMillis(trackerConfig.getDelay());
tracker.offset(FileOffsetMode.LINE, trackerConfig.getOffset());
tracker.notFoundMode(FileNotFoundMode.WAIT_COUNT, trackerConfig.getWaitTimes());
// 开始监听文件
tracker.run();
// 初始化数据
this.initData();
// 查看日志
if (ExecHostStatusEnum.RUNNING.name().equals(execHostLog.getStatus())) {
// 追踪文件
this.tailFile();
} else {
// 直接读取文件
this.readFile();
}
} catch (InvalidArgumentException e) {
// 业务异常
log.error("exec log tracker init error id: {}", execHostId, e);
// 发送消息
this.sendMessage(e.getMessage());
} catch (Exception e) {
log.error("exec log tracker error path: {}", absolutePath, e);
log.error("exec log tracker exec error id: {}", execHostId, e);
} finally {
// 释放资源
this.close();
}
}
@Override
public void setLastModify() {
tracker.setFileLastModifyTime();
/**
* 初始化数据
*/
private void initData() {
// 读取数据
this.execHostLog = execHostLogDAO.selectByIdAndLogId(execHostId, execId);
Valid.notNull(execHostLog, ErrorMessage.DATA_ABSENT);
// 检查任务状态
Valid.neq(execHostLog.getStatus(), ExecHostStatusEnum.WAITING.name(), ErrorMessage.ILLEGAL_STATUS);
// 获取文件路径
this.absolutePath = localFileClient.getAbsolutePath(execHostLog.getLogPath());
Valid.isTrue(Files1.isFile(absolutePath), ErrorMessage.FILE_ABSENT);
// 获取编码集
this.charset = Charsets.of(this.getCharset());
}
@Override
public String getPath() {
return config.getPath();
/**
* 追踪文件
*/
private void tailFile() {
// 创建追踪器
this.tracker = new DelayTrackerListener(absolutePath, this);
tracker.charset(charset.name());
tracker.delayMillis(appLogConfig.getTrackerLoadInterval());
tracker.offset(FileOffsetMode.LINE, appLogConfig.getTrackerLoadLines());
tracker.notFoundMode(FileNotFoundMode.WAIT_COUNT, Const.N_10);
// 开始追踪
tracker.run();
}
@Override
public void read(byte[] bytes, int len, Tracker tracker) {
// 发送消息
String message = config.getId() + LogConst.SEPARATOR + new String(bytes, 0, len);
/**
* 读取文件
*
* @throws IOException IOException
*/
private void readFile() throws IOException {
this.file = Files1.openRandomAccess(absolutePath, Const.ACCESS_R);
// 获取文件位置
long pos = FileReaders.readTailLinesSeek(file, appLogConfig.getTrackerLoadLines());
// 设置文件位置
file.seek(pos);
// 读取到尾部
byte[] buffer = new byte[Const.BUFFER_KB_8];
int len;
while ((len = file.read(buffer)) != -1) {
// 发送消息
this.sendMessage(new String(buffer, 0, len, charset));
}
}
/**
* 读取参数中的编码集
*
* @return charset
*/
private String getCharset() {
JSONObject params = JSON.parseObject(execHostLog.getParameter());
if (params != null) {
String charset = params.getString(Const.CHARSET);
if (charset != null) {
return charset;
}
}
return Const.UTF_8;
}
/**
* 发送消息
*
* @param message message
*/
private void sendMessage(String message) {
try {
WebSockets.sendText(session, message);
WebSockets.sendText(session, execHostId + LogConst.SEPARATOR + message);
} catch (Exception e) {
log.error("ExecLogTracker.send error", e);
}
}
@Override
public void setLastModify() {
if (tracker != null) {
tracker.setFileLastModifyTime();
}
}
@Override
public void read(byte[] bytes, int len, Tracker tracker) {
// 发送消息
this.sendMessage(new String(bytes, 0, len, charset));
}
@Override
public void close() {
log.info("ExecLogTracker.close path: {}, closed: {}", absolutePath, close);
log.info("ExecLogTracker.close execHostId: {}, closed: {}", execHostId, close);
if (close) {
return;
}
this.close = true;
if (file != null) {
Streams.close(file);
}
if (tracker != null) {
tracker.stop();
}

View File

@@ -34,30 +34,23 @@ import cn.orionsec.kit.lang.able.SafeCloseable;
*/
public interface IExecLogTracker extends Runnable, DataHandler, SafeCloseable {
/**
* 获取 execId
*
* @return execId
*/
Long getExecId();
/**
* 获取 execHostId
*
* @return execHostId
*/
Long getExecHostId();
/**
* 设置最后修改时间
*/
void setLastModify();
/**
* 获取 id
*
* @return id
*/
String getTrackerId();
/**
* 获取路径
*
* @return path
*/
String getPath();
/**
* 获取绝对路径
*
* @return 绝对路径
*/
String getAbsolutePath();
}

View File

@@ -225,7 +225,7 @@ public class TerminalCheckHandler extends AbstractTerminalHandler<TerminalCheckR
.hostName(hostName)
.hostAddress(host.getAddress())
.status(ex == null ? TerminalConnectStatusEnum.CONNECTING.name() : TerminalConnectStatusEnum.FAILED.name())
.token(sessionId)
.sessionId(sessionId)
.extra(extra)
.build();
// 填充其他信息

View File

@@ -34,6 +34,7 @@ import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.constant.ExtraFieldConst;
import org.dromara.visor.common.enums.BooleanBit;
import org.dromara.visor.framework.websocket.core.utils.WebSockets;
import org.dromara.visor.module.asset.define.config.AppSftpConfig;
import org.dromara.visor.module.asset.entity.dto.TerminalConnectDTO;
import org.dromara.visor.module.asset.enums.TerminalConnectStatusEnum;
import org.dromara.visor.module.asset.enums.TerminalConnectTypeEnum;
@@ -64,6 +65,9 @@ import java.util.Map;
@Component
public class TerminalConnectHandler extends AbstractTerminalHandler<TerminalConnectRequest> {
@Resource
private AppSftpConfig appSftpConfig;
@Resource
private TerminalConnectLogService terminalConnectLogService;
@@ -136,6 +140,7 @@ public class TerminalConnectHandler extends AbstractTerminalHandler<TerminalConn
.charset(connect.getCharset())
.fileNameCharset(connect.getFileNameCharset())
.fileContentCharset(connect.getFileContentCharset())
.filePreviewSize(appSftpConfig.getPreviewSize())
.build();
// 建立连接
SessionStore sessionStore = SessionStores.openSessionStore(connect);

View File

@@ -85,4 +85,9 @@ public class TerminalConfig {
*/
private String fileContentCharset;
/**
* 文件预览大小
*/
private Integer filePreviewSize;
}

View File

@@ -92,4 +92,9 @@ public class SftpFileVO {
*/
private Date modifyTime;
/**
* 是否可预览
*/
private Boolean canPreview;
}

View File

@@ -23,6 +23,7 @@
package org.dromara.visor.module.asset.handler.host.terminal.session;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Objects1;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.io.FileType;
import cn.orionsec.kit.lang.utils.io.Files1;
@@ -85,7 +86,7 @@ public class SftpSession extends TerminalSession implements ISftpSession {
false,
true);
return files.stream()
.map(SftpSession::fileMapping)
.map(this::fileMapping)
.sorted(Comparator.comparing(SftpFileVO::getName))
.collect(Collectors.toList());
}
@@ -133,7 +134,7 @@ public class SftpSession extends TerminalSession implements ISftpSession {
return Arrays.stream(paths)
.map(s -> executor.listFiles(s, true, false))
.flatMap(Collection::stream)
.map(SftpSession::fileMapping)
.map(this::fileMapping)
.collect(Collectors.toList());
}
@@ -197,7 +198,7 @@ public class SftpSession extends TerminalSession implements ISftpSession {
* @param sftpFile sftpFile
* @return file
*/
private static SftpFileVO fileMapping(SftpFile sftpFile) {
private SftpFileVO fileMapping(SftpFile sftpFile) {
SftpFileVO file = new SftpFileVO();
file.setName(sftpFile.getName());
file.setPath(sftpFile.getPath());
@@ -212,7 +213,19 @@ public class SftpSession extends TerminalSession implements ISftpSession {
.map(FileType.DIRECTORY::equals)
.orElse(false);
file.setIsDir(isDir);
file.setCanPreview(this.calcCanPreview(sftpFile));
return file;
}
/**
* 检查是否可预览
*
* @param file file
* @return canPreview
*/
private boolean calcCanPreview(SftpFile file) {
// 检查文件类型及大小
return file.isRegularFile() && file.getSize() <= Objects1.def(config.getFilePreviewSize(), Const.N_2) * 1024 * 1024;
}
}

View File

@@ -34,7 +34,6 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.framework.biz.operator.log.core.model.OperatorLogModel;
import org.dromara.visor.framework.biz.operator.log.core.service.OperatorLogFrameworkService;
import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs;
import org.dromara.visor.module.asset.define.config.AppSftpConfig;
import org.dromara.visor.module.asset.entity.dto.TerminalConnectDTO;
import org.dromara.visor.module.asset.handler.host.terminal.utils.TerminalUtils;
import org.dromara.visor.module.asset.handler.host.transfer.enums.TransferReceiver;
@@ -54,8 +53,6 @@ import java.util.Map;
@Slf4j
public abstract class TransferSession implements ITransferSession {
protected static final AppSftpConfig sftpConfig = SpringHolder.getBean(AppSftpConfig.class);
protected final TerminalConnectDTO connectInfo;
protected final SessionStore sessionStore;

View File

@@ -62,7 +62,7 @@ public class UploadSession extends TransferSession {
// 检查连接
this.init();
// 检查文件是否存在
SftpUtils.checkUploadFilePresent(sftpConfig, executor, path);
SftpUtils.checkUploadFilePresent(executor, path);
// 打开输出流
this.outputStream = executor.openOutputStream(path);
// 响应结果

View File

@@ -38,7 +38,6 @@ import org.dromara.visor.common.enums.EndpointDefine;
import org.dromara.visor.common.interfaces.FileClient;
import org.dromara.visor.common.utils.PathUtils;
import org.dromara.visor.module.asset.dao.UploadTaskFileDAO;
import org.dromara.visor.module.asset.define.config.AppSftpConfig;
import org.dromara.visor.module.asset.entity.domain.UploadTaskFileDO;
import org.dromara.visor.module.asset.entity.dto.TerminalConnectDTO;
import org.dromara.visor.module.asset.enums.HostOsTypeEnum;
@@ -70,8 +69,6 @@ public class FileUploader implements IFileUploader {
private static final UploadTaskFileDAO uploadTaskFileDAO = SpringHolder.getBean(UploadTaskFileDAO.class);
private static final AppSftpConfig sftpConfig = SpringHolder.getBean(AppSftpConfig.class);
private static final FileClient localFileClient = SpringHolder.getBean("localFileClient");
private TerminalConnectDTO connectInfo;
@@ -189,7 +186,7 @@ public class FileUploader implements IFileUploader {
String endpoint = EndpointDefine.UPLOAD_SWAP.format(taskId);
String localPath = localFileClient.getReturnPath(endpoint + Const.SLASH + file.getFileId());
// 检查文件是否存在
SftpUtils.checkUploadFilePresent(sftpConfig, executor, path);
SftpUtils.checkUploadFilePresent(executor, path);
// 打开输出流
this.inputStream = localFileClient.getContentInputStream(localPath);
this.outputStream = executor.openOutputStream(path);

View File

@@ -36,21 +36,13 @@ import org.dromara.visor.module.asset.entity.vo.ExecLogVO;
public interface ExecCommandService {
/**
* 批量执行命令
* 执行命令
*
* @param request request
* @return result
*/
ExecLogVO execCommand(ExecCommandRequest request);
/**
* 批量执行命令
*
* @param request request
* @return result
*/
ExecLogVO execCommandWithSource(ExecCommandExecDTO request);
/**
* 重新执行命令
*
@@ -59,4 +51,20 @@ public interface ExecCommandService {
*/
ExecLogVO reExecCommand(Long logId);
/**
* 执行命令
*
* @param request request
* @return result
*/
ExecLogVO execCommandWithSource(ExecCommandExecDTO request);
/**
* 创建执行命令
*
* @param request request
* @return result
*/
ExecLogVO createCommandWithSource(ExecCommandExecDTO request);
}

View File

@@ -35,6 +35,14 @@ import java.util.List;
*/
public interface ExecHostLogService {
/**
* 查询批量执行主机日志
*
* @param id id
* @return row
*/
ExecHostLogVO getExecHostLog(Long id);
/**
* 查询全部批量执行主机日志
*

View File

@@ -28,7 +28,6 @@ import org.dromara.visor.module.asset.entity.domain.ExecLogDO;
import org.dromara.visor.module.asset.entity.dto.ExecLogTailDTO;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogClearRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogQueryRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogTailRequest;
import org.dromara.visor.module.asset.entity.vo.ExecLogStatusVO;
import org.dromara.visor.module.asset.entity.vo.ExecLogVO;
@@ -139,10 +138,10 @@ public interface ExecLogService {
/**
* 查看执行日志
*
* @param request request
* @param id id
* @return token
*/
String getExecLogTailToken(ExecLogTailRequest request);
String getExecLogTailToken(Long id);
/**
* 获取查看执行日志参数

View File

@@ -24,10 +24,6 @@ package org.dromara.visor.module.asset.service;
import org.dromara.visor.common.handler.data.model.GenericsDataModel;
import org.dromara.visor.module.asset.entity.domain.HostDO;
import org.dromara.visor.module.asset.enums.HostTypeEnum;
import java.util.List;
import java.util.Map;
/**
* 主机配置 服务类
@@ -36,47 +32,24 @@ import java.util.Map;
* @version 1.0.0
* @since 2023-9-11 14:16
*/
// TODO 待优化
public interface HostConfigService {
/**
* 获取主机配置
*
* @param id id
* @param type type
* @param <T> T
* @param id id
* @param <T> T
* @return host
*/
<T extends GenericsDataModel> T getHostConfig(Long id, HostTypeEnum type);
/**
* 构建主机配置
*
* @param host host
* @param type type
* @param <T> T
* @return host
*/
<T extends GenericsDataModel> T buildHostConfig(HostDO host, HostTypeEnum type);
<T extends GenericsDataModel> T getHostConfig(Long id);
/**
* 获取主机配置
*
* @param idList idList
* @param type type
* @param <T> T
* @return config
* @param host host
* @param <T> T
* @return host
*/
<T extends GenericsDataModel> Map<Long, T> getHostConfigMap(List<Long> idList, HostTypeEnum type);
/**
* 构建主机配置
*
* @param hostList hostList
* @param type type
* @param <T> T
* @return config
*/
<T extends GenericsDataModel> Map<Long, T> buildHostConfigMap(List<HostDO> hostList, HostTypeEnum type);
<T extends GenericsDataModel> T getHostConfig(HostDO host);
}

View File

@@ -22,26 +22,15 @@
*/
package org.dromara.visor.module.asset.service.impl;
import cn.orionsec.kit.lang.annotation.Keep;
import cn.orionsec.kit.lang.function.Functions;
import cn.orionsec.kit.lang.id.UUIds;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.Valid;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.json.matcher.NoMatchStrategy;
import cn.orionsec.kit.lang.utils.json.matcher.ReplacementFormatter;
import cn.orionsec.kit.lang.utils.json.matcher.ReplacementFormatters;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.spring.SpringHolder;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.constant.FileConst;
import org.dromara.visor.common.enums.EndpointDefine;
import org.dromara.visor.common.interfaces.FileClient;
import org.dromara.visor.common.security.LoginUser;
import org.dromara.visor.common.utils.PathUtils;
import org.dromara.visor.common.utils.Valid;
import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs;
import org.dromara.visor.framework.security.core.utils.SecurityUtils;
import org.dromara.visor.module.asset.convert.ExecConvert;
@@ -54,25 +43,19 @@ import org.dromara.visor.module.asset.entity.domain.ExecHostLogDO;
import org.dromara.visor.module.asset.entity.domain.ExecLogDO;
import org.dromara.visor.module.asset.entity.domain.HostDO;
import org.dromara.visor.module.asset.entity.dto.ExecCommandExecDTO;
import org.dromara.visor.module.asset.entity.dto.ExecParameterSchemaDTO;
import org.dromara.visor.module.asset.entity.request.exec.ExecCommandRequest;
import org.dromara.visor.module.asset.entity.vo.ExecHostLogVO;
import org.dromara.visor.module.asset.entity.vo.ExecLogVO;
import org.dromara.visor.module.asset.enums.*;
import org.dromara.visor.module.asset.handler.host.config.model.HostSshConfigModel;
import org.dromara.visor.module.asset.handler.host.exec.command.ExecTaskExecutors;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandDTO;
import org.dromara.visor.module.asset.handler.host.exec.command.model.ExecCommandHostDTO;
import org.dromara.visor.module.asset.service.AssetAuthorizedDataService;
import org.dromara.visor.module.asset.service.ExecCommandService;
import org.dromara.visor.module.asset.service.HostConfigService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -86,15 +69,8 @@ import java.util.stream.Collectors;
@Service
public class ExecCommandServiceImpl implements ExecCommandService {
private static final ReplacementFormatter FORMATTER = ReplacementFormatters.create("@{{ ", " }}")
.noMatchStrategy(NoMatchStrategy.KEEP);
private static final int DESC_OMIT = 60;
@Keep
@Resource
private FileClient logsFileClient;
@Resource
private ExecLogDAO execLogDAO;
@@ -104,17 +80,12 @@ public class ExecCommandServiceImpl implements ExecCommandService {
@Resource
private HostDAO hostDAO;
@Resource
private HostConfigService hostConfigService;
@Resource
private AssetAuthorizedDataService assetAuthorizedDataService;
@Override
@Transactional(rollbackFor = Exception.class)
public ExecLogVO execCommand(ExecCommandRequest request) {
log.info("ExecService.startExecCommand start params: {}", JSON.toJSONString(request));
Valid.valid(ScriptExecEnum::of, request.getScriptExec());
log.info("ExecService.execCommand start params: {}", JSON.toJSONString(request));
LoginUser user = Valid.notNull(SecurityUtils.getLoginUser());
Long userId = user.getId();
List<Long> hostIdList = request.getHostIdList();
@@ -123,26 +94,58 @@ public class ExecCommandServiceImpl implements ExecCommandService {
hostIdList.removeIf(s -> !authorizedHostIdList.contains(s));
log.info("ExecService.startExecCommand host hostList: {}", hostIdList);
Valid.notEmpty(hostIdList, ErrorMessage.CHECK_AUTHORIZED_HOST);
// 执行命令
// 创建命令
ExecCommandExecDTO execRequest = ExecConvert.MAPPER.to(request);
execRequest.setUserId(userId);
execRequest.setUsername(user.getUsername());
execRequest.setSource(ExecSourceEnum.BATCH.name());
execRequest.setExecMode(ExecModeEnum.MANUAL.name());
// 调用执行
return this.execCommandWithSource(execRequest);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ExecLogVO reExecCommand(Long logId) {
log.info("ExecService.reExecCommand start logId: {}", logId);
// 获取执行记录
ExecLogDO execLog = execLogDAO.selectByIdSource(logId, ExecSourceEnum.BATCH.name());
Valid.notNull(execLog, ErrorMessage.DATA_ABSENT);
// 获取执行主机
List<ExecHostLogDO> hostLogs = execHostLogDAO.selectByLogId(logId);
Valid.notEmpty(hostLogs, ErrorMessage.DATA_ABSENT);
List<Long> hostIdList = hostLogs.stream()
.map(ExecHostLogDO::getHostId)
.collect(Collectors.toList());
// 调用创建任务
ExecCommandRequest request = ExecCommandRequest.builder()
.description(execLog.getDescription())
.timeout(execLog.getTimeout())
.scriptExec(execLog.getScriptExec())
.command(execLog.getCommand())
.parameterSchema(execLog.getParameterSchema())
.hostIdList(hostIdList)
.build();
// 调用执行
return SpringHolder.getBean(ExecCommandService.class).execCommand(request);
}
@Override
public ExecLogVO execCommandWithSource(ExecCommandExecDTO request) {
log.info("ExecService.execCommandWithSource start params: {}", JSON.toJSONString(request));
// 上下文调用执行
ExecLogVO result = SpringHolder.getBean(ExecCommandService.class).createCommandWithSource(request);
// 执行命令
ExecTaskExecutors.start(result.getId(), Lists.map(result.getHosts(), ExecHostLogVO::getId));
return result;
}
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public ExecLogVO createCommandWithSource(ExecCommandExecDTO request) {
log.info("ExecService.createCommandWithSource start params: {}", JSON.toJSONString(request));
String command = request.getCommand();
List<Long> hostIdList = request.getHostIdList();
// 查询主机信息
List<HostDO> hosts = hostDAO.selectBatchIds(hostIdList);
// 查询主机配置
// TODO 待优化
Map<Long, HostSshConfigModel> hostConfigMap = hostConfigService.buildHostConfigMap(hosts, HostTypeEnum.SSH);
// 插入日志
ExecLogDO execLog = ExecLogDO.builder()
.userId(request.getUserId())
@@ -166,17 +169,19 @@ public class ExecCommandServiceImpl implements ExecCommandService {
.build();
execLogDAO.insert(execLog);
Long execId = execLog.getId();
// 获取内置参数
Map<String, Object> builtinParams = this.getBaseBuiltinParams(execId, request);
// 设置主机日志
List<ExecHostLogDO> execHostLogs = hosts.stream()
.map(s -> this.convertExecHostLog(s, execLog, hostConfigMap.get(s.getId()), builtinParams))
.map(s -> ExecHostLogDO.builder()
.logId(execLog.getId())
.hostId(s.getId())
.hostName(s.getName())
.hostAddress(s.getAddress())
.status(ExecHostStatusEnum.WAITING.name())
.build())
.collect(Collectors.toList());
execHostLogDAO.insertBatch(execHostLogs);
// 操作日志
OperatorLogs.add(OperatorLogs.LOG_ID, execId);
// 开始执行
this.startExec(execLog, execHostLogs, hostConfigMap);
// 返回
ExecLogVO result = ExecLogConvert.MAPPER.to(execLog);
List<ExecHostLogVO> resultHosts = ExecHostLogConvert.MAPPER.to(execHostLogs);
@@ -184,218 +189,4 @@ public class ExecCommandServiceImpl implements ExecCommandService {
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ExecLogVO reExecCommand(Long logId) {
log.info("ExecService.reExecCommand start logId: {}", logId);
// 获取执行记录
ExecLogDO execLog = execLogDAO.selectByIdSource(logId, ExecSourceEnum.BATCH.name());
Valid.notNull(execLog, ErrorMessage.DATA_ABSENT);
// 获取执行主机
List<ExecHostLogDO> hostLogs = execHostLogDAO.selectByLogId(logId);
Valid.notEmpty(hostLogs, ErrorMessage.DATA_ABSENT);
List<Long> hostIdList = hostLogs.stream()
.map(ExecHostLogDO::getHostId)
.collect(Collectors.toList());
// 调用执行方法
ExecCommandRequest request = ExecCommandRequest.builder()
.description(execLog.getDescription())
.timeout(execLog.getTimeout())
.scriptExec(execLog.getScriptExec())
.command(execLog.getCommand())
.parameterSchema(execLog.getParameterSchema())
.hostIdList(hostIdList)
.build();
return this.execCommand(request);
}
/**
* 开始执行命令
*
* @param execLog execLog
* @param execHostLogs hostLogs
* @param hostConfigMap hostConfigMap
*/
private void startExec(ExecLogDO execLog,
List<ExecHostLogDO> execHostLogs,
Map<Long, HostSshConfigModel> hostConfigMap) {
// 执行主机
List<ExecCommandHostDTO> hosts = execHostLogs.stream()
.map(s -> {
HostSshConfigModel config = hostConfigMap.get(s.getHostId());
return ExecCommandHostDTO.builder()
.hostId(s.getHostId())
.hostLogId(s.getId())
.hostName(s.getHostName())
.hostAddress(s.getHostAddress())
.command(s.getCommand())
.logPath(s.getLogPath())
.scriptPath(s.getScriptPath())
.username(config.getUsername())
.charset(config.getCharset())
.fileNameCharset(config.getFileNameCharset())
.fileContentCharset(config.getFileContentCharset())
.build();
}).collect(Collectors.toList());
// 执行信息
ExecCommandDTO exec = ExecCommandDTO.builder()
.logId(execLog.getId())
.userId(execLog.getUserId())
.username(execLog.getUsername())
.description(execLog.getDescription())
.execSeq(execLog.getExecSeq())
.timeout(execLog.getTimeout())
.scriptExec(ScriptExecEnum.isEnabled(execLog.getScriptExec()))
.hosts(hosts)
.build();
// 开始执行
ExecTaskExecutors.start(exec);
}
/**
* 转换为 execHostLog
*
* @param host host
* @param execLog execLog
* @param config config
* @param builtinParams builtinParams
* @return execHostLog
*/
private ExecHostLogDO convertExecHostLog(HostDO host,
ExecLogDO execLog,
HostSshConfigModel config,
Map<String, Object> builtinParams) {
Long execId = execLog.getId();
Long hostId = host.getId();
// 脚本路径
String scriptPath = null;
if (ScriptExecEnum.isEnabled(execLog.getScriptExec())) {
scriptPath = this.buildScriptPath(config.getUsername(), host.getOsType(), execId, hostId);
}
// 获取参数
String parameter = JSON.toJSONString(this.getHostParams(builtinParams, host, config, scriptPath));
return ExecHostLogDO.builder()
.logId(execId)
.hostId(hostId)
.hostName(host.getName())
.hostAddress(host.getAddress())
.status(ExecHostStatusEnum.WAITING.name())
.command(FORMATTER.format(execLog.getCommand(), parameter))
.parameter(parameter)
.logPath(this.buildLogPath(execId, hostId))
.scriptPath(scriptPath)
.build();
}
/**
* 获取基础内置参数
*
* @param execId execId
* @param request request
* @return params
*/
private Map<String, Object> getBaseBuiltinParams(Long execId, ExecCommandExecDTO request) {
String uuid = UUIds.random();
Date date = new Date();
// 输入参数
Map<String, Object> params = this.extraSchemaParams(request.getParameterSchema());
// 添加内置参数
params.put("userId", request.getUserId());
params.put("username", request.getUsername());
params.put("source", request.getSource());
params.put("sourceId", request.getSourceId());
params.put("seq", request.getExecSeq());
params.put("execId", execId);
params.put("scriptExec", request.getScriptExec());
params.put("uuid", uuid);
params.put("uuidShort", uuid.replace("-", Strings.EMPTY));
params.put("timestampMillis", date.getTime());
params.put("timestamp", date.getTime() / Dates.SECOND_STAMP);
params.put("date", Dates.format(date, Dates.YMD));
params.put("datetime", Dates.format(date, Dates.YMD_HMS));
return params;
}
/**
* 获取主机参数
*
* @param baseParams baseParams
* @param host host
* @param config config
* @param scriptPath scriptPath
* @return params
*/
private Map<String, Object> getHostParams(Map<String, Object> baseParams,
HostDO host,
HostSshConfigModel config,
String scriptPath) {
String uuid = UUIds.random();
Map<String, Object> params = Maps.newMap(baseParams);
params.put("hostId", host.getId());
params.put("hostName", host.getName());
params.put("hostCode", host.getCode());
params.put("hostAddress", host.getAddress());
params.put("hostPort", host.getPort());
params.put("hostUuid", uuid);
params.put("hostUuidShort", uuid.replace("-", Strings.EMPTY));
params.put("hostUsername", config.getUsername());
params.put("osType", host.getOsType());
params.put("charset", config.getCharset());
params.put("scriptPath", scriptPath);
return params;
}
/**
* 提取参数
*
* @param parameterSchema parameterSchema
* @return params
*/
private Map<String, Object> extraSchemaParams(String parameterSchema) {
List<ExecParameterSchemaDTO> schemaList = JSON.parseArray(parameterSchema, ExecParameterSchemaDTO.class);
if (Lists.isEmpty(schemaList)) {
return Maps.newMap();
}
// 解析参数
return schemaList.stream()
.collect(Collectors.toMap(ExecParameterSchemaDTO::getName,
s -> {
Object value = s.getValue();
if (value == null) {
value = Const.EMPTY;
}
return value;
},
Functions.right()));
}
/**
* 构建日志路径
*
* @param logId logId
* @param hostId hostId
* @return logPath
*/
private String buildLogPath(Long logId, Long hostId) {
return logsFileClient.getReturnPath(EndpointDefine.EXEC_LOG.format(logId, hostId));
}
/**
* 侯建脚本路径
*
* @param username username
* @param osType osType
* @param logId logId
* @param hostId hostId
* @return scriptPath
*/
private String buildScriptPath(String username, String osType, Long logId, Long hostId) {
HostOsTypeEnum os = HostOsTypeEnum.of(osType);
String name = FileConst.EXEC
+ "/" + logId
+ "/" + hostId
+ os.getScriptSuffix();
return PathUtils.buildAppPath(HostOsTypeEnum.WINDOWS.equals(os), username, FileConst.SCRIPT, name);
}
}

View File

@@ -60,6 +60,13 @@ public class ExecHostLogServiceImpl implements ExecHostLogService {
@Resource
private ExecTaskManager execTaskManager;
@Override
public ExecHostLogVO getExecHostLog(Long id) {
ExecHostLogDO record = execHostLogDAO.selectById(id);
Valid.notNull(record, ErrorMessage.DATA_ABSENT);
return ExecHostLogConvert.MAPPER.to(record);
}
@Override
public List<ExecHostLogVO> getExecHostLogList(Long logId) {
return execHostLogDAO.of()
@@ -99,7 +106,7 @@ public class ExecHostLogServiceImpl implements ExecHostLogService {
.map(execTaskManager::getTask)
.map(IExecTaskHandler::getHandlers)
.flatMap(s -> s.stream()
.filter(h -> h.getHostId().equals(record.getHostId()))
.filter(h -> id.equals(h.getId()))
.findFirst())
.ifPresent(IExecCommandHandler::interrupt);
// 删除

View File

@@ -27,6 +27,7 @@ import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.time.cron.Cron;
import cn.orionsec.kit.spring.SpringHolder;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
@@ -49,7 +50,10 @@ import org.dromara.visor.module.asset.entity.request.exec.*;
import org.dromara.visor.module.asset.entity.vo.ExecJobVO;
import org.dromara.visor.module.asset.entity.vo.ExecLogVO;
import org.dromara.visor.module.asset.entity.vo.HostBaseVO;
import org.dromara.visor.module.asset.enums.*;
import org.dromara.visor.module.asset.enums.ExecJobStatusEnum;
import org.dromara.visor.module.asset.enums.ExecModeEnum;
import org.dromara.visor.module.asset.enums.ExecSourceEnum;
import org.dromara.visor.module.asset.enums.HostTypeEnum;
import org.dromara.visor.module.asset.handler.host.exec.job.ExecCommandJob;
import org.dromara.visor.module.asset.service.AssetAuthorizedDataService;
import org.dromara.visor.module.asset.service.ExecCommandService;
@@ -57,6 +61,7 @@ import org.dromara.visor.module.asset.service.ExecJobHostService;
import org.dromara.visor.module.asset.service.ExecJobService;
import org.dromara.visor.module.infra.api.SystemUserApi;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@@ -107,7 +112,6 @@ public class ExecJobServiceImpl implements ExecJobService {
LoginUser loginUser = SecurityUtils.getLoginUser();
// 验证表达式是否正确
Cron.of(request.getExpression());
Valid.valid(ScriptExecEnum::of, request.getScriptExec());
// 转换
ExecJobDO record = ExecJobConvert.MAPPER.to(request);
// 查询数据是否冲突
@@ -137,7 +141,6 @@ public class ExecJobServiceImpl implements ExecJobService {
log.info("ExecJobService-updateExecJobById id: {}, request: {}", id, JSON.toJSONString(request));
// 验证表达式是否正确
Cron.of(request.getExpression());
Valid.valid(ScriptExecEnum::of, request.getScriptExec());
// 查询
ExecJobDO record = execJobDAO.selectById(id);
Valid.notNull(record, ErrorMessage.DATA_ABSENT);
@@ -164,7 +167,6 @@ public class ExecJobServiceImpl implements ExecJobService {
public Integer updateExecJobStatus(ExecJobUpdateStatusRequest request) {
Long id = request.getId();
ExecJobStatusEnum status = ExecJobStatusEnum.of(request.getStatus());
Valid.notNull(status, ErrorMessage.PARAM_ERROR);
log.info("ExecJobService-updateExecJobStatus id: {}, status: {}", id, status);
// 查询任务
ExecJobDO record = execJobDAO.selectById(id);
@@ -203,7 +205,7 @@ public class ExecJobServiceImpl implements ExecJobService {
// 设置日志参数
OperatorLogs.add(OperatorLogs.NAME, job.getName());
OperatorLogs.add(OperatorLogs.USERNAME, username);
log.info("ExecJobService-setExecJobExecUser effect: {}", effect);
log.info("ExecJobService-updateExecJobExecUser effect: {}", effect);
return effect;
}
@@ -320,7 +322,6 @@ public class ExecJobServiceImpl implements ExecJobService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void manualTriggerExecJob(Long id) {
log.info("ExecJobService.manualTriggerExecJob start id: {}", id);
// 查询任务
@@ -336,12 +337,12 @@ public class ExecJobServiceImpl implements ExecJobService {
request.setUserId(user.getId());
request.setUsername(user.getUsername());
}
// 触发任务
this.triggerExecJob(request, job);
// 上下文触发任务
SpringHolder.getBean(ExecJobService.class).triggerExecJob(request, job);
}
@Override
@Transactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void triggerExecJob(ExecJobTriggerRequest request, ExecJobDO job) {
Long id = request.getId();
// 查询任务主机

View File

@@ -26,7 +26,6 @@ import cn.orionsec.kit.lang.annotation.Keep;
import cn.orionsec.kit.lang.define.wrapper.DataGrid;
import cn.orionsec.kit.lang.id.UUIds;
import cn.orionsec.kit.lang.utils.Arrays1;
import cn.orionsec.kit.lang.utils.Objects1;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.io.Files1;
@@ -53,24 +52,19 @@ import org.dromara.visor.module.asset.dao.ExecLogDAO;
import org.dromara.visor.module.asset.define.cache.ExecCacheKeyDefine;
import org.dromara.visor.module.asset.entity.domain.ExecHostLogDO;
import org.dromara.visor.module.asset.entity.domain.ExecLogDO;
import org.dromara.visor.module.asset.entity.dto.ExecHostLogTailDTO;
import org.dromara.visor.module.asset.entity.dto.ExecLogTailDTO;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogClearRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogQueryRequest;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogTailRequest;
import org.dromara.visor.module.asset.entity.vo.ExecHostLogVO;
import org.dromara.visor.module.asset.entity.vo.ExecLogStatusVO;
import org.dromara.visor.module.asset.entity.vo.ExecLogVO;
import org.dromara.visor.module.asset.enums.ExecHostStatusEnum;
import org.dromara.visor.module.asset.enums.ExecStatusEnum;
import org.dromara.visor.module.asset.enums.HostTypeEnum;
import org.dromara.visor.module.asset.handler.host.config.model.HostSshConfigModel;
import org.dromara.visor.module.asset.handler.host.exec.command.handler.IExecCommandHandler;
import org.dromara.visor.module.asset.handler.host.exec.command.handler.IExecTaskHandler;
import org.dromara.visor.module.asset.handler.host.exec.command.manager.ExecTaskManager;
import org.dromara.visor.module.asset.service.ExecHostLogService;
import org.dromara.visor.module.asset.service.ExecLogService;
import org.dromara.visor.module.asset.service.HostConfigService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -79,7 +73,10 @@ import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.InputStream;
import java.util.*;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -105,9 +102,6 @@ public class ExecLogServiceImpl implements ExecLogService {
@Resource
private ExecTaskManager execTaskManager;
@Resource
private HostConfigService hostConfigService;
@Keep
@Resource
private FileClient logsFileClient;
@@ -328,7 +322,7 @@ public class ExecLogServiceImpl implements ExecLogService {
log.info("ExecLogService.interruptHostExec interrupt logId: {}, hostLogId: {}", logId, hostLogId);
IExecCommandHandler handler = task.getHandlers()
.stream()
.filter(s -> s.getHostId().equals(hostLog.getHostId()))
.filter(s -> hostLogId.equals(s.getId()))
.findFirst()
.orElse(null);
// 中断
@@ -366,55 +360,17 @@ public class ExecLogServiceImpl implements ExecLogService {
}
@Override
public String getExecLogTailToken(ExecLogTailRequest request) {
String source = request.getSource();
Long execId = request.getExecId();
List<Long> hostExecIdList = request.getHostExecIdList();
log.info("ExecLogService.getExecLogTailToken start execId: {}, hostExecIdList: {}", execId, hostExecIdList);
// 查询执行日志
ExecLogDO execLog = execLogDAO.selectByIdSource(execId, source);
Valid.notNull(execLog, ErrorMessage.LOG_ABSENT);
// 查询主机日志
List<ExecHostLogDO> hostLogs;
if (hostExecIdList == null) {
hostLogs = execHostLogDAO.selectByLogId(execId);
} else {
hostLogs = execHostLogDAO.of()
.createWrapper()
.eq(ExecHostLogDO::getLogId, execId)
.in(ExecHostLogDO::getId, hostExecIdList)
.then()
.list();
}
Valid.notEmpty(hostLogs, ErrorMessage.LOG_ABSENT);
// 获取编码集
// TODO 待优化
List<Long> hostIdList = hostLogs.stream()
.map(ExecHostLogDO::getHostId)
.collect(Collectors.toList());
Map<Long, HostSshConfigModel> configMap = hostConfigService.getHostConfigMap(hostIdList, HostTypeEnum.SSH);
public String getExecLogTailToken(Long id) {
// 生成缓存
String token = UUIds.random19();
String cacheKey = ExecCacheKeyDefine.EXEC_TAIL.format(token);
ExecLogTailDTO cache = ExecLogTailDTO.builder()
.execId(id)
.token(token)
.id(execId)
.userId(SecurityUtils.getLoginUserId())
.hosts(hostLogs.stream()
.map(s -> ExecHostLogTailDTO.builder()
.id(s.getId())
.hostId(s.getHostId())
.path(s.getLogPath())
.charset(Optional.ofNullable(configMap.get(s.getHostId()))
.map(HostSshConfigModel::getCharset)
.map(Objects1::toString)
.orElse(Const.UTF_8))
.build())
.collect(Collectors.toList()))
.build();
// 设置缓存
RedisStrings.setJson(cacheKey, ExecCacheKeyDefine.EXEC_TAIL, cache);
log.info("ExecLogService.getExecLogTailToken finish token: {}, execId: {}, hostExecIdList: {}", token, execId, hostExecIdList);
return token;
}

View File

@@ -22,7 +22,6 @@
*/
package org.dromara.visor.module.asset.service.impl;
import cn.orionsec.kit.lang.function.Functions;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.handler.data.model.GenericsDataModel;
@@ -31,16 +30,10 @@ import org.dromara.visor.module.asset.dao.HostDAO;
import org.dromara.visor.module.asset.entity.domain.HostDO;
import org.dromara.visor.module.asset.enums.HostStatusEnum;
import org.dromara.visor.module.asset.enums.HostTypeEnum;
import org.dromara.visor.module.asset.handler.host.config.model.HostSshConfigModel;
import org.dromara.visor.module.asset.service.HostConfigService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 主机配置 服务实现类
@@ -57,48 +50,23 @@ public class HostConfigServiceImpl implements HostConfigService {
private HostDAO hostDAO;
@Override
public <T extends GenericsDataModel> T getHostConfig(Long id, HostTypeEnum type) {
public <T extends GenericsDataModel> T getHostConfig(Long id) {
// 查询主机
HostDO host = hostDAO.selectById(id);
// 转换为配置
return this.buildHostConfig(host, type);
return this.getHostConfig(host);
}
@Override
@SuppressWarnings("unchecked")
public <T extends GenericsDataModel> T buildHostConfig(HostDO host, HostTypeEnum type) {
public <T extends GenericsDataModel> T getHostConfig(HostDO host) {
Valid.notNull(host, ErrorMessage.HOST_ABSENT);
// 检查主机类型
Valid.isTrue(type.name().equals(host.getType()), ErrorMessage.HOST_TYPE_ERROR);
HostTypeEnum type = HostTypeEnum.of(host.getType());
// 检查主机状态
Valid.isTrue(HostStatusEnum.ENABLED.name().equals(host.getStatus()), ErrorMessage.HOST_NOT_ENABLED);
// 查询主机配置
HostSshConfigModel model = type.parse(host.getConfig());
Valid.notNull(model, ErrorMessage.CONFIG_ABSENT);
return (T) model;
}
@Override
public <T extends GenericsDataModel> Map<Long, T> getHostConfigMap(List<Long> idList, HostTypeEnum type) {
// 查询主机
Map<Long, HostDO> hostMap = hostDAO.selectBatchIds(idList)
.stream()
.collect(Collectors.toMap(HostDO::getId, Function.identity(), Functions.right()));
// 转换为配置
Map<Long, T> result = new HashMap<>();
for (Long id : idList) {
result.put(id, this.buildHostConfig(hostMap.get(id), type));
}
return result;
}
@Override
public <T extends GenericsDataModel> Map<Long, T> buildHostConfigMap(List<HostDO> hostList, HostTypeEnum type) {
Map<Long, T> result = new HashMap<>();
for (HostDO host : hostList) {
result.put(host.getId(), this.buildHostConfig(host, type));
}
return result;
T config = type.parse(host.getConfig());
Valid.notNull(config, ErrorMessage.CONFIG_ABSENT);
return (T) config;
}
}

View File

@@ -263,7 +263,7 @@ public class TerminalConnectLogServiceImpl implements TerminalConnectLogService
.eq(TerminalConnectLogDO::getHostId, request.getHostId())
.like(TerminalConnectLogDO::getHostAddress, request.getHostAddress())
.eq(TerminalConnectLogDO::getType, request.getType())
.like(TerminalConnectLogDO::getToken, request.getToken())
.like(TerminalConnectLogDO::getSessionId, request.getSessionId())
.eq(TerminalConnectLogDO::getStatus, request.getStatus())
.in(TerminalConnectLogDO::getStatus, request.getStatusList())
.ge(TerminalConnectLogDO::getStartTime, Arrays1.getIfPresent(request.getStartTimeRange(), 0))

View File

@@ -42,7 +42,10 @@ import org.dromara.visor.module.asset.entity.dto.TerminalAccessDTO;
import org.dromara.visor.module.asset.entity.dto.TerminalConnectDTO;
import org.dromara.visor.module.asset.entity.dto.TerminalTransferDTO;
import org.dromara.visor.module.asset.entity.vo.TerminalThemeVO;
import org.dromara.visor.module.asset.enums.*;
import org.dromara.visor.module.asset.enums.HostExtraItemEnum;
import org.dromara.visor.module.asset.enums.HostExtraSshAuthTypeEnum;
import org.dromara.visor.module.asset.enums.HostIdentityTypeEnum;
import org.dromara.visor.module.asset.enums.HostSshAuthTypeEnum;
import org.dromara.visor.module.asset.handler.host.config.model.HostSshConfigModel;
import org.dromara.visor.module.asset.handler.host.extra.model.HostSshExtraModel;
import org.dromara.visor.module.asset.service.HostConfigService;
@@ -171,7 +174,7 @@ public class TerminalServiceImpl implements TerminalService {
// 查询主机
HostDO host = hostDAO.selectById(hostId);
// 查询主机配置
HostSshConfigModel config = hostConfigService.buildHostConfig(host, HostTypeEnum.SSH);
HostSshConfigModel config = hostConfigService.getHostConfig(host);
// 获取配置
return this.getHostConnectInfo(host, config, null);
}
@@ -195,7 +198,7 @@ public class TerminalServiceImpl implements TerminalService {
ErrorMessage.ANY_NO_PERMISSION,
DataPermissionTypeEnum.HOST_GROUP.getPermissionName());
// 获取主机配置
HostSshConfigModel config = hostConfigService.buildHostConfig(host, HostTypeEnum.SSH);
HostSshConfigModel config = hostConfigService.getHostConfig(host);
Valid.notNull(config, ErrorMessage.CONFIG_ABSENT);
// 查询主机额外配置
HostSshExtraModel extra = hostExtraService.getHostExtra(userId, hostId, HostExtraItemEnum.SSH);
@@ -234,6 +237,7 @@ public class TerminalServiceImpl implements TerminalService {
TerminalConnectDTO conn = new TerminalConnectDTO();
conn.setHostId(host.getId());
conn.setHostName(host.getName());
conn.setHostCode(host.getCode());
conn.setHostAddress(host.getAddress());
conn.setHostPort(host.getPort());
conn.setOsType(host.getOsType());

View File

@@ -22,14 +22,15 @@
*/
package org.dromara.visor.module.asset.task;
import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.time.Dates;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.common.utils.LockerUtils;
import org.dromara.visor.module.asset.define.config.AppExecLogAutoClearConfig;
import org.dromara.visor.module.asset.define.config.AppAutoClearConfig;
import org.dromara.visor.module.asset.entity.request.exec.ExecLogClearRequest;
import org.dromara.visor.module.asset.enums.ExecStatusEnum;
import org.dromara.visor.module.asset.service.ExecLogService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -45,16 +46,14 @@ import java.util.Date;
*/
@Slf4j
@Component
@ConditionalOnProperty(value = "app.auto-clear.exec-log.enabled", havingValue = "true")
public class ExecLogFileAutoClearTask {
/**
* 分布式锁名称
*/
private static final Integer BATCH_SIZE = Const.N_1000;
private static final String LOCK_KEY = "clear:exl:lock";
@Resource
private AppExecLogAutoClearConfig appExecLogAutoClearConfig;
private AppAutoClearConfig appAutoClearConfig;
@Resource
private ExecLogService execLogService;
@@ -65,6 +64,11 @@ public class ExecLogFileAutoClearTask {
@Scheduled(cron = "0 0 3 * * ?")
public void clear() {
log.info("ExecLogFileAutoClearTask.clear start");
// 检查是否开启
if (!Booleans.isTrue(appAutoClearConfig.getExecLogEnabled())) {
log.info("ExecLogFileAutoClearTask.clear disabled");
return;
}
// 获取锁并执行
LockerUtils.tryLock(LOCK_KEY, this::doClear);
log.info("ExecLogFileAutoClearTask.clear finish");
@@ -76,13 +80,22 @@ public class ExecLogFileAutoClearTask {
private void doClear() {
// 删除的时间区间
Date createLessEq = Dates.stream()
.subDay(appExecLogAutoClearConfig.getKeepPeriod())
.subDay(appAutoClearConfig.getExecLogKeepDays())
.date();
// 清理
ExecLogClearRequest request = new ExecLogClearRequest();
request.setCreateTimeLe(createLessEq);
request.setStatusList(ExecStatusEnum.FINISH_STATUS_LIST);
execLogService.clearExecLog(request);
for (int i = 0; ; i++) {
log.info("ExecLogFileAutoClearTask.doClear start batch: {}", i + 1);
ExecLogClearRequest request = new ExecLogClearRequest();
request.setCreateTimeLe(createLessEq);
request.setStatusList(ExecStatusEnum.FINISH_STATUS_LIST);
request.setLimit(BATCH_SIZE);
Integer count = execLogService.clearExecLog(request);
log.info("ExecLogFileAutoClearTask.doClear end batch: {}, count: {}", i + 1, count);
// 最后一批
if (count < BATCH_SIZE) {
return;
}
}
}
}

View File

@@ -22,14 +22,15 @@
*/
package org.dromara.visor.module.asset.task;
import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.time.Dates;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.common.utils.LockerUtils;
import org.dromara.visor.module.asset.define.config.AppTerminalConnectLogAutoClearConfig;
import org.dromara.visor.module.asset.define.config.AppAutoClearConfig;
import org.dromara.visor.module.asset.entity.request.host.TerminalConnectLogClearRequest;
import org.dromara.visor.module.asset.enums.TerminalConnectStatusEnum;
import org.dromara.visor.module.asset.service.TerminalConnectLogService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -45,16 +46,14 @@ import java.util.Date;
*/
@Slf4j
@Component
@ConditionalOnProperty(value = "app.auto-clear.terminal-connect-log.enabled", havingValue = "true")
public class TerminalConnectLogAutoClearTask {
/**
* 分布式锁名称
*/
private static final Integer BATCH_SIZE = Const.N_1000;
private static final String LOCK_KEY = "clear:tcl:lock";
@Resource
private AppTerminalConnectLogAutoClearConfig appTerminalConnectLogAutoClearConfig;
private AppAutoClearConfig appAutoClearConfig;
@Resource
private TerminalConnectLogService terminalConnectLogService;
@@ -65,6 +64,11 @@ public class TerminalConnectLogAutoClearTask {
@Scheduled(cron = "0 10 3 * * ?")
public void clear() {
log.info("TerminalConnectLogAutoClearTask.clear start");
// 检查是否开启
if (!Booleans.isTrue(appAutoClearConfig.getTerminalLogEnabled())) {
log.info("TerminalConnectLogAutoClearTask.clear disabled");
return;
}
// 获取锁并执行
LockerUtils.tryLock(LOCK_KEY, this::doClear);
log.info("TerminalConnectLogAutoClearTask.clear finish");
@@ -76,13 +80,22 @@ public class TerminalConnectLogAutoClearTask {
private void doClear() {
// 删除的时间区间
Date createLessEq = Dates.stream()
.subDay(appTerminalConnectLogAutoClearConfig.getKeepPeriod())
.subDay(appAutoClearConfig.getTerminalLogKeepDays())
.date();
// 清理
TerminalConnectLogClearRequest request = new TerminalConnectLogClearRequest();
request.setCreateTimeLe(createLessEq);
request.setStatusList(TerminalConnectStatusEnum.FINISH_STATUS_LIST);
terminalConnectLogService.clearTerminalConnectLog(request);
for (int i = 0; ; i++) {
log.info("TerminalConnectLogAutoClearTask.doClear start batch: {}", i + 1);
TerminalConnectLogClearRequest request = new TerminalConnectLogClearRequest();
request.setCreateTimeLe(createLessEq);
request.setStatusList(TerminalConnectStatusEnum.FINISH_STATUS_LIST);
request.setLimit(BATCH_SIZE);
Integer count = terminalConnectLogService.clearTerminalConnectLog(request);
log.info("TerminalConnectLogAutoClearTask.doClear end batch: {}, count: {}", i + 1, count);
// 最后一批
if (count < BATCH_SIZE) {
return;
}
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.asset.utils;
import cn.orionsec.kit.lang.function.Functions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.json.matcher.NoMatchStrategy;
import cn.orionsec.kit.lang.utils.json.matcher.ReplacementFormatter;
import cn.orionsec.kit.lang.utils.json.matcher.ReplacementFormatters;
import com.alibaba.fastjson.JSON;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.module.asset.entity.dto.ExecParameterSchemaDTO;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 执行工具类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/16 17:28
*/
public class ExecUtils {
private static final ReplacementFormatter FORMATTER = ReplacementFormatters.create("@{{ ", " }}")
.noMatchStrategy(NoMatchStrategy.EMPTY);
private ExecUtils() {
}
/**
* 替换命令
*
* @param command command
* @param params params
* @return command
*/
public static String format(String command, Map<String, Object> params) {
return Strings.replaceCRLF(FORMATTER.format(command, params));
}
/**
* 替换命令
*
* @param command command
* @param json json
* @return command
*/
public static String format(String command, String json) {
return Strings.replaceCRLF(FORMATTER.format(command, json));
}
/**
* 提取参数
*
* @param parameterSchema parameterSchema
* @return params
*/
public static Map<String, Object> extraSchemaParams(String parameterSchema) {
List<ExecParameterSchemaDTO> schemaList = JSON.parseArray(parameterSchema, ExecParameterSchemaDTO.class);
if (Lists.isEmpty(schemaList)) {
return Maps.newMap();
}
// 解析参数
return schemaList.stream()
.collect(Collectors.toMap(ExecParameterSchemaDTO::getName,
s -> {
Object value = s.getValue();
if (value == null) {
value = Const.EMPTY;
}
return value;
},
Functions.right()));
}
}

View File

@@ -26,6 +26,7 @@ import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
import cn.orionsec.kit.net.host.sftp.SftpFile;
import cn.orionsec.kit.spring.SpringHolder;
import com.alibaba.fastjson.JSON;
import org.dromara.visor.module.asset.define.config.AppSftpConfig;
import org.dromara.visor.module.asset.handler.host.transfer.model.SftpFileBackupParams;
@@ -39,19 +40,20 @@ import org.dromara.visor.module.asset.handler.host.transfer.model.SftpFileBackup
*/
public class SftpUtils {
private static final AppSftpConfig appSftpConfig = SpringHolder.getBean(AppSftpConfig.class);
private SftpUtils() {
}
/**
* 检查上传文件是否存在 并且执行响应策略
*
* @param config config
* @param executor executor
* @param path path
*/
public static void checkUploadFilePresent(AppSftpConfig config, SftpExecutor executor, String path) {
public static void checkUploadFilePresent(SftpExecutor executor, String path) {
// 重复不备份
if (!Booleans.isTrue(config.getUploadPresentBackup())) {
if (!Booleans.isTrue(appSftpConfig.getUploadPresentBackup())) {
return;
}
// 检查文件是否存在
@@ -59,7 +61,7 @@ public class SftpUtils {
if (file != null) {
// 文件存在则备份
SftpFileBackupParams backupParams = new SftpFileBackupParams(file.getName());
String target = Strings.format(config.getBackupFileName(), JSON.parseObject(JSON.toJSONString(backupParams)));
String target = Strings.format(appSftpConfig.getUploadBackupFileName(), JSON.parseObject(JSON.toJSONString(backupParams)));
// 移动
executor.move(path, target);
}

View File

@@ -1,87 +0,0 @@
{
"groups": [
{
"name": "app.tracker",
"type": "org.dromara.visor.module.asset.define.config.AppTrackerConfig",
"sourceType": "org.dromara.visor.module.asset.define.config.AppTrackerConfig"
},
{
"name": "app.sftp",
"type": "org.dromara.visor.module.asset.define.config.AppSftpConfig",
"sourceType": "org.dromara.visor.module.asset.define.config.AppSftpConfig"
},
{
"name": "app.exec-log",
"type": "org.dromara.visor.module.asset.define.config.AppExecLogConfig",
"sourceType": "org.dromara.visor.module.asset.define.config.AppExecLogConfig"
},
{
"name": "app.auto-clear.exec-log",
"type": "org.dromara.visor.module.asset.define.config.AppExecLogAutoClearConfig",
"sourceType": "org.dromara.visor.module.asset.define.config.AppExecLogAutoClearConfig"
},
{
"name": "app.auto-clear.terminal-connect-log",
"type": "org.dromara.visor.module.asset.define.config.AppTerminalConnectLogAutoClearConfig",
"sourceType": "org.dromara.visor.module.asset.define.config.AppTerminalConnectLogAutoClearConfig"
}
],
"properties": [
{
"name": "app.tracker.offset",
"type": "java.lang.Integer",
"description": "加载偏移量 (行).",
"defaultValue": "300"
},
{
"name": "app.tracker.delay",
"type": "java.lang.Integer",
"description": "延迟时间 (ms).",
"defaultValue": "100"
},
{
"name": "app.tracker.wait-times",
"type": "java.lang.Integer",
"description": "文件未找到等待次数.",
"defaultValue": "100"
},
{
"name": "app.sftp.upload-present-backup",
"type": "java.lang.Boolean",
"description": "上传文件时 文件存在是否备份.",
"defaultValue": "true"
},
{
"name": "app.sftp.backup-file-name",
"type": "java.lang.String",
"description": "备份文件名称.",
"defaultValue": "bk_${fileName}_${timestamp}"
},
{
"name": "app.exec-log.append-ansi",
"type": "java.lang.Boolean",
"description": "是否拼接 ansi 执行状态日志.",
"defaultValue": "true"
},
{
"name": "app.auto-clear.exec-log.enabled",
"type": "java.lang.Boolean",
"description": "开启 批量执行日志自动清理."
},
{
"name": "app.auto-clear.exec-log.keep-period",
"type": "java.lang.Integer",
"description": "批量执行日志自动清理 保留周期 (天)."
},
{
"name": "app.auto-clear.terminal-connect-log.enabled",
"type": "java.lang.Boolean",
"description": "开启 终端连接日志自动清理."
},
{
"name": "app.auto-clear.terminal-connect-log.keep-period",
"type": "java.lang.Integer",
"description": "终端连接日志自动清理 保留周期 (天)."
}
]
}

View File

@@ -11,7 +11,7 @@
<result column="host_name" property="hostName"/>
<result column="host_address" property="hostAddress"/>
<result column="type" property="type"/>
<result column="token" property="token"/>
<result column="session_id" property="sessionId"/>
<result column="status" property="status"/>
<result column="start_time" property="startTime"/>
<result column="end_time" property="endTime"/>
@@ -30,7 +30,7 @@
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, user_id, username, host_id, host_name, host_address, type, token, status, start_time, end_time, extra_info, create_time, update_time, deleted
id, user_id, username, host_id, host_name, host_address, type, session_id, status, start_time, end_time, extra_info, create_time, update_time, deleted
</sql>
<select id="selectLatestConnectHostId" resultType="java.lang.Long">

View File

@@ -68,9 +68,6 @@ public class SystemUserDTO implements Serializable {
@Schema(description = "用户状态 0停用 1启用")
private Integer status;
@Schema(description = "最后登录时间")
private Date lastLoginTime;
@Schema(description = "描述")
private String description;

View File

@@ -35,8 +35,7 @@ import org.dromara.visor.module.infra.define.operator.SystemSettingOperatorType;
import org.dromara.visor.module.infra.entity.request.system.SystemSettingUpdateBatchRequest;
import org.dromara.visor.module.infra.entity.request.system.SystemSettingUpdateRequest;
import org.dromara.visor.module.infra.entity.vo.AppInfoVO;
import org.dromara.visor.module.infra.entity.vo.SystemSettingAggregateVO;
import org.dromara.visor.module.infra.handler.setting.model.EncryptSystemSettingModel;
import org.dromara.visor.module.infra.entity.vo.RsaKeyPairVO;
import org.dromara.visor.module.infra.service.SystemSettingService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@@ -44,6 +43,7 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import java.util.Map;
/**
* 系统设置服务
@@ -74,7 +74,7 @@ public class SystemSettingController {
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/setting")
@Operation(summary = "获取系统聚合设置")
public SystemSettingAggregateVO getSystemAggregateSetting() {
public Map<String, String> getSystemAggregateSetting() {
return systemSettingService.getSystemAggregateSetting();
}
@@ -83,7 +83,7 @@ public class SystemSettingController {
@GetMapping("/generator-keypair")
@Operation(summary = "生成密钥对")
@PreAuthorize("@ss.hasPermission('infra:system-setting:update')")
public EncryptSystemSettingModel generatorKeypair() {
public RsaKeyPairVO generatorKeypair() {
return systemSettingService.generatorKeypair();
}
@@ -92,7 +92,7 @@ public class SystemSettingController {
@Operation(summary = "查询系统设置")
@Parameter(name = "type", description = "type", required = true)
@PreAuthorize("@ss.hasPermission('infra:system-setting:update')")
public Object getSystemSettingByType(@RequestParam("type") String type) {
public Map<String, String> getSystemSettingByType(@RequestParam("type") String type) {
return systemSettingService.getSystemSettingByType(type);
}

View File

@@ -25,7 +25,6 @@ package org.dromara.visor.module.infra.define.cache;
import cn.orionsec.kit.lang.define.cache.key.CacheKeyBuilder;
import cn.orionsec.kit.lang.define.cache.key.CacheKeyDefine;
import cn.orionsec.kit.lang.define.cache.key.struct.RedisCacheStruct;
import org.dromara.visor.module.infra.entity.vo.SystemSettingAggregateVO;
import java.util.concurrent.TimeUnit;
@@ -39,10 +38,10 @@ import java.util.concurrent.TimeUnit;
public interface SystemSettingKeyDefine {
CacheKeyDefine SETTING = new CacheKeyBuilder()
.key("system:setting:agg")
.key("system:setting:view")
.desc("系统聚合设置")
.type(SystemSettingAggregateVO.class)
.struct(RedisCacheStruct.STRING)
.type(String.class)
.struct(RedisCacheStruct.HASH)
.timeout(8, TimeUnit.HOURS)
.build();

View File

@@ -1,71 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.infra.define.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 应用认证配置
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/3/5 18:26
*/
@Data
@Component
@ConfigurationProperties("app.authentication")
public class AppAuthenticationConfig {
/**
* 是否允许多端登录
*/
private Boolean allowMultiDevice;
/**
* 是否允许凭证续签
*/
private Boolean allowRefresh;
/**
* 凭证续签最大次数
*/
private Integer maxRefreshCount;
/**
* 登录失败发送站内信阈值
*/
private Integer loginFailedSendThreshold;
/**
* 登录失败锁定次数
*/
private Integer loginFailedLockCount;
/**
* 登录失败锁定时间 (分)
*/
private Integer loginFailedLockTime;
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.infra.define.config;
import org.dromara.visor.common.config.ConfigRef;
import org.dromara.visor.common.config.ConfigStore;
import org.dromara.visor.common.constant.ConfigKeys;
import org.springframework.stereotype.Component;
/**
* 应用登录配置
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/3/5 18:26
*/
@Component
public class AppLoginConfig {
/**
* 凭证有效期(分)
*/
private final ConfigRef<Integer> loginSessionTime;
/**
* 是否允许多端登录
*/
private final ConfigRef<Boolean> allowMultiDevice;
/**
* 是否允许凭证续签
*/
private final ConfigRef<Boolean> allowRefresh;
/**
* 凭证续签最大次数
*/
private final ConfigRef<Integer> maxRefreshCount;
/**
* 凭证续签间隔分
*/
private final ConfigRef<Integer> refreshInterval;
/**
* 登录失败是否锁定
*/
private final ConfigRef<Boolean> loginFailedLock;
/**
* 登录失败锁定阈值
*/
private final ConfigRef<Integer> loginFailedLockThreshold;
/**
* 登录失败锁定时间 (分)
*/
private final ConfigRef<Integer> loginFailedLockTime;
/**
* 登录失败发信
*/
private final ConfigRef<Boolean> loginFailedSend;
/**
* 登录失败发送站内信阈值
*/
private final ConfigRef<Integer> loginFailedSendThreshold;
public AppLoginConfig(ConfigStore configStore) {
this.loginSessionTime = configStore.int32(ConfigKeys.LOGIN_LOGIN_SESSION_TIME);
this.allowMultiDevice = configStore.bool(ConfigKeys.LOGIN_ALLOW_MULTI_DEVICE);
this.allowRefresh = configStore.bool(ConfigKeys.LOGIN_ALLOW_REFRESH);
this.maxRefreshCount = configStore.int32(ConfigKeys.LOGIN_MAX_REFRESH_COUNT);
this.refreshInterval = configStore.int32(ConfigKeys.LOGIN_REFRESH_INTERVAL);
this.loginFailedLock = configStore.bool(ConfigKeys.LOGIN_LOGIN_FAILED_LOCK);
this.loginFailedLockThreshold = configStore.int32(ConfigKeys.LOGIN_LOGIN_FAILED_LOCK_THRESHOLD);
this.loginFailedLockTime = configStore.int32(ConfigKeys.LOGIN_LOGIN_FAILED_LOCK_TIME);
this.loginFailedSend = configStore.bool(ConfigKeys.LOGIN_LOGIN_FAILED_SEND);
this.loginFailedSendThreshold = configStore.int32(ConfigKeys.LOGIN_LOGIN_FAILED_SEND_THRESHOLD);
}
public Integer getLoginSessionTime() {
return loginSessionTime.value;
}
public Boolean getAllowMultiDevice() {
return allowMultiDevice.value;
}
public Boolean getAllowRefresh() {
return allowRefresh.value;
}
public Integer getMaxRefreshCount() {
return maxRefreshCount.value;
}
public Integer getRefreshInterval() {
return refreshInterval.value;
}
public Boolean getLoginFailedLock() {
return loginFailedLock.value;
}
public Integer getLoginFailedLockThreshold() {
return loginFailedLockThreshold.value;
}
public Integer getLoginFailedLockTime() {
return loginFailedLockTime.value;
}
public Boolean getLoginFailedSend() {
return loginFailedSend.value;
}
public Integer getLoginFailedSendThreshold() {
return loginFailedSendThreshold.value;
}
}

View File

@@ -38,16 +38,12 @@ import static org.dromara.visor.framework.biz.operator.log.core.enums.OperatorRi
@Module("infra:system-setting")
public class SystemSettingOperatorType extends InitializingOperatorTypes {
public static final String UPDATE_TEXT = "<sb>{}</sb> - <sb>{}</sb> - <sb>{}</sb>";
public static final String UPDATE_BATCH_TEXT = "<sb>{}</sb>";
public static final String UPDATE = "system-setting:update";
@Override
public OperatorType[] types() {
return new OperatorType[]{
new OperatorType(M, UPDATE, "更新系统设置 ${text}"),
new OperatorType(M, UPDATE, "更新系统设置 <sb>${type}</sb>"),
};
}

View File

@@ -52,17 +52,13 @@ public class SystemSettingDO extends BaseDO {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "配置key")
@TableField("config_key")
private String configKey;
@Schema(description = "配置类型")
@TableField("type")
private String type;
@Schema(description = "配置")
@TableField("item")
private String item;
@Schema(description = "配置key")
@TableField("config_key")
private String configKey;
@Schema(description = "配置值")
@TableField("value")

View File

@@ -90,6 +90,10 @@ public class SystemUserDO extends BaseDO {
@TableField("update_password_reason")
private String updatePasswordReason;
@Schema(description = "修改密码时间")
@TableField("update_password_time")
private Date updatePasswordTime;
@Schema(description = "最后登录时间")
@TableField("last_login_time")
private Date lastLoginTime;

View File

@@ -54,9 +54,9 @@ public class SystemSettingUpdateRequest implements Serializable {
private String type;
@NotBlank
@Size(max = 32)
@Size(max = 64)
@Schema(description = "配置项")
private String item;
private String configKey;
@Schema(description = "配置值")
private String value;

View File

@@ -58,7 +58,7 @@ public class InfraWorkplaceStatisticsVO {
@Schema(description = "未读消息数量")
private Integer unreadMessageCount;
@Schema(description = "上次登录时间")
@Schema(description = "最后登录时间")
private Date lastLoginTime;
@Schema(description = "当前登录会话数量")

View File

@@ -20,34 +20,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.infra.handler.setting.model;
package org.dromara.visor.module.infra.entity.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 加密系统设置模型
* RSA 密钥对响应对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/10/9 11:45
* @since 2025/1/3 17:46
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EncryptSystemSettingModel {
@Schema(name = "RsaKeyPairVO", description = "RSA 密钥对响应对象")
public class RsaKeyPairVO {
/**
* 加密公钥
*/
@Schema(description = "公钥")
private String publicKey;
/**
* 加密私钥
*/
@Schema(description = "私钥")
private String privateKey;
}

View File

@@ -1,53 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.infra.entity.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.dromara.visor.module.infra.handler.setting.model.EncryptSystemSettingModel;
import org.dromara.visor.module.infra.handler.setting.model.SftpSystemSettingModel;
/**
* 系统设置 聚合响应对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/3 17:46
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "SystemSettingAggregateVO", description = "系统设置 聚合响应对象")
public class SystemSettingAggregateVO {
@Schema(description = "SFTP 设置")
private SftpSystemSettingModel sftp;
@Schema(description = "加密设置")
private EncryptSystemSettingModel encrypt;
}

View File

@@ -72,6 +72,9 @@ public class SystemUserVO implements Serializable {
@Schema(description = "最后登录时间")
private Date lastLoginTime;
@Schema(description = "描述")
private String description;
@Schema(description = "创建时间")
private Date createTime;
@@ -84,9 +87,6 @@ public class SystemUserVO implements Serializable {
@Schema(description = "修改人")
private String updater;
@Schema(description = "描述")
private String description;
@Schema(description = "用户角色")
private List<SystemRoleVO> roles;

View File

@@ -22,14 +22,8 @@
*/
package org.dromara.visor.module.infra.enums;
import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.module.infra.handler.setting.model.EncryptSystemSettingModel;
import org.dromara.visor.module.infra.handler.setting.model.SftpSystemSettingModel;
import java.util.Map;
/**
* 系统设置类型枚举
@@ -43,52 +37,32 @@ import java.util.Map;
public enum SystemSettingTypeEnum {
/**
* SFTP
* SFTP
*/
SFTP("sftp", SftpSystemSettingModel.class),
SFTP("sftp"),
/**
* 加密
* 加密
*/
ENCRYPT("encrypt", EncryptSystemSettingModel.class),
ENCRYPT("encrypt"),
/**
* 登录设置
*/
LOGIN("login"),
/**
* 日志设置
*/
LOG("log"),
/**
* 自动清理设置
*/
AUTO_CLEAR("autoClear"),
;
private final String prefix;
private final Class<?> modelClass;
/**
* 解析
*
* @param settings settings
* @return model
*/
@SuppressWarnings("unchecked")
public <T> T parseModel(Map<String, String> settings) {
return (T) JSON.parseObject(JSON.toJSONString(settings)).toJavaObject(modelClass);
}
public static SystemSettingTypeEnum of(String type) {
if (type == null) {
return null;
}
for (SystemSettingTypeEnum value : values()) {
if (value.name().equals(type)) {
return value;
}
}
return null;
}
/**
* 获取 key
*
* @param item item
* @return key
*/
public String getConfigKey(String item) {
return prefix + Const.DOT + item;
}
private final String type;
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.infra.handler.setting.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* SFTP 系统设置模型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/10/9 11:45
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SftpSystemSettingModel {
/**
* 预览大小
*/
private Integer previewSize;
}

View File

@@ -25,8 +25,9 @@ package org.dromara.visor.module.infra.service;
import org.dromara.visor.module.infra.entity.request.system.SystemSettingUpdateBatchRequest;
import org.dromara.visor.module.infra.entity.request.system.SystemSettingUpdateRequest;
import org.dromara.visor.module.infra.entity.vo.AppInfoVO;
import org.dromara.visor.module.infra.entity.vo.SystemSettingAggregateVO;
import org.dromara.visor.module.infra.handler.setting.model.EncryptSystemSettingModel;
import org.dromara.visor.module.infra.entity.vo.RsaKeyPairVO;
import java.util.Map;
/**
* 系统设置服务
@@ -49,33 +50,32 @@ public interface SystemSettingService {
*
* @return setting
*/
SystemSettingAggregateVO getSystemAggregateSetting();
Map<String, String> getSystemAggregateSetting();
/**
* 生成密钥对
*
* @return keypair
*/
EncryptSystemSettingModel generatorKeypair();
RsaKeyPairVO generatorKeypair();
/**
* 通过类型查询系统设置
*
* @param type type
* @param <T> T
* @return row
* @return rows
*/
<T> T getSystemSettingByType(String type);
Map<String, String> getSystemSettingByType(String type);
/**
* 更新部分系统设置
* 更新系统设置-单个
*
* @param request request
*/
void updateSystemSetting(SystemSettingUpdateRequest request);
/**
* 更新系统设置
* 更新系统设置-多个
*
* @param request request
*/

View File

@@ -24,6 +24,7 @@ package org.dromara.visor.module.infra.service.impl;
import cn.orionsec.kit.lang.annotation.Keep;
import cn.orionsec.kit.lang.define.wrapper.Pair;
import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Lists;
@@ -31,6 +32,8 @@ import cn.orionsec.kit.lang.utils.crypto.Signatures;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.web.servlet.web.Servlets;
import com.alibaba.fastjson.JSON;
import org.dromara.visor.common.config.ConfigStore;
import org.dromara.visor.common.constant.ConfigKeys;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.constant.ExtraFieldConst;
@@ -48,7 +51,7 @@ import org.dromara.visor.module.infra.convert.SystemUserConvert;
import org.dromara.visor.module.infra.dao.SystemUserDAO;
import org.dromara.visor.module.infra.dao.SystemUserRoleDAO;
import org.dromara.visor.module.infra.define.cache.UserCacheKeyDefine;
import org.dromara.visor.module.infra.define.config.AppAuthenticationConfig;
import org.dromara.visor.module.infra.define.config.AppLoginConfig;
import org.dromara.visor.module.infra.define.message.SystemUserMessageDefine;
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
import org.dromara.visor.module.infra.entity.dto.LoginTokenDTO;
@@ -63,6 +66,7 @@ import org.dromara.visor.module.infra.service.UserPermissionService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@@ -80,7 +84,7 @@ import java.util.stream.Collectors;
public class AuthenticationServiceImpl implements AuthenticationService {
@Resource
private AppAuthenticationConfig appAuthenticationConfig;
private AppLoginConfig appLoginConfig;
@Resource
private SystemUserDAO systemUserDAO;
@@ -98,6 +102,17 @@ public class AuthenticationServiceImpl implements AuthenticationService {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private ConfigStore configStore;
@PostConstruct
public void initCacheExpireTime() {
// 监听并且设置缓存过期时间
configStore.int32(ConfigKeys.LOGIN_LOGIN_SESSION_TIME).onChange((v, b) -> this.setCacheExpireTime());
configStore.int32(ConfigKeys.LOGIN_REFRESH_INTERVAL).onChange((v, b) -> this.setCacheExpireTime());
this.setCacheExpireTime();
}
@Override
public UserLoginVO login(UserLoginRequest request, HttpServletRequest servletRequest) {
// 获取登录信息
@@ -118,7 +133,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
// 用户状态校验
this.checkUserStatus(user);
Long id = user.getId();
// 设置上次登录时间
// 设置最后登录时间
this.setLastLoginTime(id);
// 删除用户缓存
this.deleteUserCache(user);
@@ -126,7 +141,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
this.setUserCache(user);
long current = System.currentTimeMillis();
// 不允许多端登录
if (!appAuthenticationConfig.getAllowMultiDevice()) {
if (Booleans.isFalse(appLoginConfig.getAllowMultiDevice())) {
// 无效化其他缓存
this.invalidOtherDeviceToken(id, current, remoteAddr, location, userAgent);
}
@@ -188,7 +203,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
return JSON.parseObject(loginCache, LoginTokenDTO.class);
}
// loginToken 不存在 需要查询 refreshToken
if (!appAuthenticationConfig.getAllowRefresh()) {
if (Booleans.isFalse(appLoginConfig.getAllowRefresh())) {
return null;
}
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(pair.getKey(), pair.getValue());
@@ -203,7 +218,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
refresh.setRefreshCount(refreshCount);
// 设置登录缓存
RedisStrings.setJson(loginKey, UserCacheKeyDefine.LOGIN_TOKEN, refresh);
if (refreshCount < appAuthenticationConfig.getMaxRefreshCount()) {
if (refreshCount < appLoginConfig.getMaxRefreshCount()) {
// 小于续签最大次数 则再次设置 refreshToken
RedisStrings.setJson(refreshKey, UserCacheKeyDefine.LOGIN_REFRESH, refresh);
} else {
@@ -219,12 +234,14 @@ public class AuthenticationServiceImpl implements AuthenticationService {
if (password.length() != Const.MD5_LEN) {
throw Exceptions.argument(ErrorMessage.USERNAME_PASSWORD_ERROR);
}
// 检查登录失败次数
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(username);
String failedCount = redisTemplate.opsForValue().get(failedCountKey);
if (failedCount != null
&& Integer.parseInt(failedCount) >= appAuthenticationConfig.getLoginFailedLockCount()) {
throw Exceptions.argument(ErrorMessage.MAX_LOGIN_FAILED);
// 检查登录失败次数锁定
if (Booleans.isTrue(appLoginConfig.getLoginFailedLock())) {
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(username);
String failedCount = redisTemplate.opsForValue().get(failedCountKey);
if (failedCount != null
&& Integer.parseInt(failedCount) >= appLoginConfig.getLoginFailedLockThreshold()) {
throw Exceptions.argument(ErrorMessage.MAX_LOGIN_FAILED);
}
}
// 获取登录用户
SystemUserDO user = systemUserDAO.of()
@@ -244,7 +261,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
// 刷新登录失败缓存
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(user.getUsername());
redisTemplate.opsForValue().increment(failedCountKey);
RedisUtils.setExpire(failedCountKey, appAuthenticationConfig.getLoginFailedLockTime(), TimeUnit.MINUTES);
RedisUtils.setExpire(failedCountKey, appLoginConfig.getLoginFailedLockTime(), TimeUnit.MINUTES);
}
return passRight;
}
@@ -268,13 +285,17 @@ public class AuthenticationServiceImpl implements AuthenticationService {
if (passRight) {
return;
}
// 检查是否开启登录失败发信
if (!Booleans.isTrue(appLoginConfig.getLoginFailedSend())) {
return;
}
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(user.getUsername());
String failedCountStr = redisTemplate.opsForValue().get(failedCountKey);
if (failedCountStr == null || !Strings.isInteger(failedCountStr)) {
return;
}
// 直接用相等 因为只触发一次
if (!appAuthenticationConfig.getLoginFailedSendThreshold().equals(Integer.valueOf(failedCountStr))) {
if (!Integer.valueOf(failedCountStr).equals(appLoginConfig.getLoginFailedSendThreshold())) {
return;
}
// 发送站内信
@@ -393,7 +414,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
}
}
// 删除续签信息
if (appAuthenticationConfig.getAllowRefresh()) {
if (Booleans.isTrue(appLoginConfig.getAllowRefresh())) {
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_REFRESH.format(id, "*"));
}
}
@@ -421,7 +442,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
.build();
RedisStrings.setJson(loginKey, UserCacheKeyDefine.LOGIN_TOKEN, loginValue);
// 生成 refreshToken
if (appAuthenticationConfig.getAllowRefresh()) {
if (Booleans.isTrue(appLoginConfig.getAllowRefresh())) {
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(id, loginTime);
RedisStrings.setJson(refreshKey, UserCacheKeyDefine.LOGIN_REFRESH, loginValue);
}
@@ -429,4 +450,15 @@ public class AuthenticationServiceImpl implements AuthenticationService {
return AesEncryptUtils.encryptBase62(id + ":" + loginTime);
}
/**
* 重新设置缓存时间
*/
private void setCacheExpireTime() {
Integer loginSessionTime = appLoginConfig.getLoginSessionTime();
if (loginSessionTime != null) {
UserCacheKeyDefine.LOGIN_TOKEN.setTimeout(loginSessionTime);
UserCacheKeyDefine.LOGIN_REFRESH.setTimeout(loginSessionTime + appLoginConfig.getRefreshInterval());
}
}
}

View File

@@ -22,43 +22,32 @@
*/
package org.dromara.visor.module.infra.service.impl;
import cn.orionsec.kit.ext.process.ProcessAwaitExecutor;
import cn.orionsec.kit.lang.define.wrapper.Pair;
import cn.orionsec.kit.lang.function.Functions;
import cn.orionsec.kit.lang.support.Attempt;
import cn.orionsec.kit.lang.utils.Arrays1;
import cn.orionsec.kit.lang.utils.Objects1;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.crypto.Keys;
import cn.orionsec.kit.lang.utils.crypto.RSA;
import cn.orionsec.kit.lang.utils.crypto.Signatures;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.spring.SpringHolder;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.dromara.visor.common.constant.AppConst;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.common.constant.ErrorMessage;
import org.dromara.visor.common.utils.Valid;
import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs;
import org.dromara.visor.common.constant.ConfigKeys;
import org.dromara.visor.framework.config.core.event.ConfigUpdateEvent;
import org.dromara.visor.framework.redis.core.utils.RedisStrings;
import org.dromara.visor.framework.mybatis.core.query.Conditions;
import org.dromara.visor.framework.redis.core.utils.RedisMaps;
import org.dromara.visor.framework.redis.core.utils.RedisUtils;
import org.dromara.visor.module.infra.dao.SystemSettingDAO;
import org.dromara.visor.module.infra.define.cache.SystemSettingKeyDefine;
import org.dromara.visor.module.infra.define.operator.SystemSettingOperatorType;
import org.dromara.visor.module.infra.entity.domain.SystemSettingDO;
import org.dromara.visor.module.infra.entity.request.system.SystemSettingUpdateBatchRequest;
import org.dromara.visor.module.infra.entity.request.system.SystemSettingUpdateRequest;
import org.dromara.visor.module.infra.entity.vo.AppInfoVO;
import org.dromara.visor.module.infra.entity.vo.SystemSettingAggregateVO;
import org.dromara.visor.module.infra.enums.SystemSettingTypeEnum;
import org.dromara.visor.module.infra.handler.setting.model.EncryptSystemSettingModel;
import org.dromara.visor.module.infra.handler.setting.model.SftpSystemSettingModel;
import org.dromara.visor.module.infra.entity.vo.RsaKeyPairVO;
import org.dromara.visor.module.infra.service.SystemSettingService;
import org.dromara.visor.module.infra.utils.SystemUuidUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
@@ -75,8 +64,6 @@ import java.util.stream.Collectors;
@Service
public class SystemSettingServiceImpl implements SystemSettingService {
private String uuid;
@Resource
private SystemSettingDAO systemSettingDAO;
@@ -84,129 +71,102 @@ public class SystemSettingServiceImpl implements SystemSettingService {
public AppInfoVO getAppInfo() {
return AppInfoVO.builder()
.version(AppConst.VERSION)
.uuid(this.getSystemUuid())
.uuid(SystemUuidUtils.getSystemUuid())
.build();
}
@Override
public SystemSettingAggregateVO getSystemAggregateSetting() {
public Map<String, String> getSystemAggregateSetting() {
// 查询缓存
SystemSettingAggregateVO cache = RedisStrings.getJson(SystemSettingKeyDefine.SETTING);
SystemSettingAggregateVO result = Objects1.def(cache, SystemSettingAggregateVO::new);
if (cache == null) {
Map<String, String> cache = RedisMaps.entities(SystemSettingKeyDefine.SETTING);
if (Maps.isEmpty(cache)) {
// 查询数据库
Map<String, List<SystemSettingDO>> typeGroup = systemSettingDAO.of()
cache = systemSettingDAO.of()
.createWrapper()
.select(SystemSettingDO::getType,
SystemSettingDO::getItem,
.select(SystemSettingDO::getConfigKey,
SystemSettingDO::getValue)
.in(SystemSettingDO::getType,
SystemSettingTypeEnum.SFTP.name(),
SystemSettingTypeEnum.ENCRYPT.name())
.in(SystemSettingDO::getConfigKey,
ConfigKeys.ENCRYPT_PUBLIC_KEY,
ConfigKeys.LOG_WEB_SCROLL_LINES)
.then()
.stream()
.collect(Collectors.groupingBy(SystemSettingDO::getType));
// 数据组合
typeGroup.forEach((k, v) -> {
// 类型数据
SystemSettingTypeEnum settingType = SystemSettingTypeEnum.of(k);
Map<String, String> typeSettings = v.stream()
.collect(Collectors.toMap(
SystemSettingDO::getItem,
SystemSettingDO::getValue,
Functions.right()));
Object setting = settingType.parseModel(typeSettings);
if (SystemSettingTypeEnum.SFTP.equals(settingType)) {
// SFTP 设置
result.setSftp((SftpSystemSettingModel) setting);
} else if (SystemSettingTypeEnum.ENCRYPT.equals(settingType)) {
// 加密设置
EncryptSystemSettingModel encryptSetting = (EncryptSystemSettingModel) setting;
encryptSetting.setPrivateKey(null);
result.setEncrypt(encryptSetting);
}
});
.collect(Collectors.toMap(SystemSettingDO::getConfigKey,
SystemSettingDO::getValue,
Functions.right()));
// 设置缓存
RedisStrings.setJson(SystemSettingKeyDefine.SETTING, result);
RedisMaps.putAll(SystemSettingKeyDefine.SETTING, cache);
}
return result;
return cache;
}
@Override
public EncryptSystemSettingModel generatorKeypair() {
public RsaKeyPairVO generatorKeypair() {
// 生成密钥对
Pair<RSAPublicKey, RSAPrivateKey> pair = RSA.generatorKeys();
return EncryptSystemSettingModel.builder()
return RsaKeyPairVO.builder()
.publicKey(Keys.getPublicKey(pair.getKey()))
.privateKey(Keys.getPrivateKey(pair.getValue()))
.build();
}
@Override
public <T> T getSystemSettingByType(String type) {
SystemSettingTypeEnum settingType = SystemSettingTypeEnum.of(type);
Valid.notNull(settingType, ErrorMessage.ERROR_TYPE);
// 查询数据库
Map<String, String> settings = systemSettingDAO.of()
public Map<String, String> getSystemSettingByType(String type) {
return systemSettingDAO.of()
.createWrapper()
.eq(SystemSettingDO::getType, type)
.then()
.stream()
.collect(Collectors.toMap(
SystemSettingDO::getItem,
SystemSettingDO::getConfigKey,
SystemSettingDO::getValue,
Functions.right()));
// 解析
return settingType.parseModel(settings);
}
@Override
public void updateSystemSetting(SystemSettingUpdateRequest request) {
String type = request.getType();
SystemSettingTypeEnum settingType = Valid.valid(SystemSettingTypeEnum::of, type);
String item = request.getItem();
String configKey = request.getConfigKey();
String value = request.getValue();
// 删除
systemSettingDAO.delete(Conditions.eq(SystemSettingDO::getConfigKey, configKey));
// 插入
SystemSettingDO insert = SystemSettingDO.builder()
.type(type)
.configKey(configKey)
.value(Strings.def(value))
.build();
systemSettingDAO.insert(insert);
// 更新
SystemSettingDO update = new SystemSettingDO();
update.setValue(value);
LambdaQueryWrapper<SystemSettingDO> wrapper = systemSettingDAO.lambda()
.eq(SystemSettingDO::getType, type)
.eq(SystemSettingDO::getItem, item);
.eq(SystemSettingDO::getConfigKey, configKey);
systemSettingDAO.update(update, wrapper);
// 删除缓存
RedisUtils.delete(SystemSettingKeyDefine.SETTING);
// 设置日志参数
OperatorLogs.add(OperatorLogs.TEXT, Strings.format(SystemSettingOperatorType.UPDATE_TEXT, type, item, value));
// 触发修改事件
SpringHolder.publishEvent(ConfigUpdateEvent.of(settingType.getConfigKey(item), value));
SpringHolder.publishEvent(ConfigUpdateEvent.of(configKey, value));
}
@Override
public void updateSystemSettingBatch(SystemSettingUpdateBatchRequest request) {
String type = request.getType();
SystemSettingTypeEnum settingType = Valid.valid(SystemSettingTypeEnum::of, type);
Map<String, String> settings = request.getSettings();
// 删除
LambdaQueryWrapper<SystemSettingDO> deleteWrapper = systemSettingDAO.lambda()
.eq(SystemSettingDO::getType, type)
.in(SystemSettingDO::getItem, settings.keySet());
systemSettingDAO.delete(deleteWrapper);
systemSettingDAO.delete(Conditions.in(SystemSettingDO::getConfigKey, settings.keySet()));
// 插入
List<SystemSettingDO> rows = settings.entrySet()
.stream()
.map(s -> SystemSettingDO.builder()
.configKey(settingType.getConfigKey(s.getKey()))
.type(type)
.item(s.getKey())
.value(s.getValue())
.configKey(s.getKey())
.value(Strings.def(s.getValue()))
.build())
.collect(Collectors.toList());
// 插入
systemSettingDAO.insertBatch(rows);
// 删除缓存
RedisUtils.delete(SystemSettingKeyDefine.SETTING);
// 设置日志参数
OperatorLogs.add(OperatorLogs.TEXT, Strings.format(SystemSettingOperatorType.UPDATE_BATCH_TEXT, type));
// 触发修改事件
Map<String, String> eventConfig = rows.stream()
.collect(Collectors.toMap(
@@ -216,69 +176,4 @@ public class SystemSettingServiceImpl implements SystemSettingService {
SpringHolder.publishEvent(ConfigUpdateEvent.of(eventConfig));
}
/**
* 获取系统 uuid
*
* @return uuid
*/
private String getSystemUuid() {
if (this.uuid != null) {
return this.uuid;
}
String[][] cmd = new String[][]{
new String[]{"/bin/sh", "-c", "cat /sys/class/dmi/id/product_serial"},
new String[]{"/bin/bash", "-c", "cat /sys/class/dmi/id/product_serial"},
new String[]{"/bin/sh", "-c", "dmidecode -s system-uuid"},
new String[]{"/bin/bash", "-c", "dmidecode -s system-uuid"},
new String[]{"cmd", "/c", "wmic csproduct get uuid"}
};
for (String[] s : cmd) {
try {
String uuid = this.getCommandOutput(s);
if (Strings.isBlank(uuid)) {
continue;
}
// 去除符号并且转为大写
uuid = uuid.replaceAll(Const.DASHED, Const.EMPTY)
.toUpperCase()
.trim();
// 去除 \n
String extraUuid = Arrays1.last(uuid.trim().split(Const.LF));
if (!Strings.isBlank(extraUuid)) {
uuid = extraUuid.trim();
}
// 去除 :
extraUuid = Arrays1.last(uuid.trim().split(Const.COLON));
if (!Strings.isBlank(extraUuid)) {
uuid = extraUuid.trim();
}
return this.uuid = Signatures.md5(uuid);
} catch (Exception e) {
// IGNORED
}
}
return this.uuid = Const.UNKNOWN;
}
/**
* 获取输出结果
*
* @param command command
* @return result
*/
private String getCommandOutput(String[] command) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ProcessAwaitExecutor executor = new ProcessAwaitExecutor(command);
try {
executor.streamHandler(i -> Attempt.uncheck(Streams::transfer, i, out))
.waitFor()
.sync()
.exec();
return out.toString();
} finally {
Streams.close(out);
Streams.close(executor);
}
}
}

View File

@@ -50,7 +50,6 @@ import org.dromara.visor.module.infra.dao.SystemUserDAO;
import org.dromara.visor.module.infra.dao.SystemUserRoleDAO;
import org.dromara.visor.module.infra.define.cache.TipsCacheKeyDefine;
import org.dromara.visor.module.infra.define.cache.UserCacheKeyDefine;
import org.dromara.visor.module.infra.define.config.AppAuthenticationConfig;
import org.dromara.visor.module.infra.entity.domain.SystemRoleDO;
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
import org.dromara.visor.module.infra.entity.dto.UserInfoDTO;
@@ -64,10 +63,7 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -81,9 +77,6 @@ import java.util.stream.Collectors;
@Service
public class SystemUserServiceImpl implements SystemUserService {
@Resource
private AppAuthenticationConfig appAuthenticationConfig;
@Resource
private SystemUserDAO systemUserDAO;
@@ -322,6 +315,7 @@ public class SystemUserServiceImpl implements SystemUserService {
update.setPassword(Signatures.md5(request.getPassword()));
update.setUpdatePasswordStatus(UpdatePasswordStatusEnum.NO_REQUIRE.getStatus());
update.setUpdatePasswordReason(Const.EMPTY);
update.setUpdatePasswordTime(new Date());
int effect = systemUserDAO.updateById(update);
log.info("SystemUserService-resetPassword record: {}, effect: {}", JSON.toJSONString(update), effect);
// 删除登录失败次数缓存
@@ -329,9 +323,7 @@ public class SystemUserServiceImpl implements SystemUserService {
// 删除登录缓存
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_TOKEN.format(id, "*"));
// 删除续签信息
if (appAuthenticationConfig.getAllowRefresh()) {
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_REFRESH.format(id, "*"));
}
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_REFRESH.format(id, "*"));
}
/**

View File

@@ -0,0 +1,114 @@
/*
* Copyright (c) 2023 - present Dromara, All rights reserved.
*
* https://visor.dromara.org
* https://visor.dromara.org.cn
* https://visor.orionsec.cn
*
* Members:
* Jiahang Li - ljh1553488six@139.com - author
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.infra.utils;
import cn.orionsec.kit.ext.process.ProcessAwaitExecutor;
import cn.orionsec.kit.lang.support.Attempt;
import cn.orionsec.kit.lang.utils.Arrays1;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.crypto.Signatures;
import cn.orionsec.kit.lang.utils.io.Streams;
import org.dromara.visor.common.constant.Const;
import java.io.ByteArrayOutputStream;
/**
* 系统 UUID 工具类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/16 11:07
*/
public class SystemUuidUtils {
private static String uuid;
private SystemUuidUtils() {
}
/**
* 获取系统 uuid
*
* @return uuid
*/
public static String getSystemUuid() {
if (SystemUuidUtils.uuid != null) {
return SystemUuidUtils.uuid;
}
String[][] cmd = new String[][]{
new String[]{"/bin/sh", "-c", "cat /sys/class/dmi/id/product_serial"},
new String[]{"/bin/bash", "-c", "cat /sys/class/dmi/id/product_serial"},
new String[]{"/bin/sh", "-c", "dmidecode -s system-uuid"},
new String[]{"/bin/bash", "-c", "dmidecode -s system-uuid"},
new String[]{"cmd", "/c", "wmic csproduct get uuid"}
};
for (String[] s : cmd) {
try {
String uuid = SystemUuidUtils.getCommandOutput(s);
if (Strings.isBlank(uuid)) {
continue;
}
// 去除符号并且转为大写
uuid = uuid.replaceAll(Const.DASHED, Const.EMPTY)
.toUpperCase()
.trim();
// 去除 \n
String extraUuid = Arrays1.last(uuid.trim().split(Const.LF));
if (!Strings.isBlank(extraUuid)) {
uuid = extraUuid.trim();
}
// 去除 :
extraUuid = Arrays1.last(uuid.trim().split(Const.COLON));
if (!Strings.isBlank(extraUuid)) {
uuid = extraUuid.trim();
}
return SystemUuidUtils.uuid = Signatures.md5(uuid);
} catch (Exception e) {
// IGNORED
}
}
return SystemUuidUtils.uuid = Const.UNKNOWN;
}
/**
* 获取输出结果
*
* @param command command
* @return result
*/
public static String getCommandOutput(String[] command) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ProcessAwaitExecutor executor = new ProcessAwaitExecutor(command);
try {
executor.streamHandler(i -> Attempt.uncheck(Streams::transfer, i, out))
.waitFor()
.sync()
.exec();
return out.toString();
} finally {
Streams.close(out);
Streams.close(executor);
}
}
}

View File

@@ -1,41 +0,0 @@
{
"groups": [
{
"name": "app.authentication",
"type": "org.dromara.visor.module.infra.define.config.AppAuthenticationConfig",
"sourceType": "org.dromara.visor.module.infra.define.config.AppAuthenticationConfig"
}
],
"properties": [
{
"name": "app.authentication.allowMultiDevice",
"type": "java.lang.Boolean",
"description": "是否允许多端登录."
},
{
"name": "app.authentication.allowRefresh",
"type": "java.lang.Boolean",
"description": "是否允许凭证续签."
},
{
"name": "app.authentication.maxRefreshCount",
"type": "java.lang.Integer",
"description": "凭证续签最大次数."
},
{
"name": "app.authentication.loginFailedSendThreshold",
"type": "java.lang.Integer",
"description": "登录失败发送站内信阈值."
},
{
"name": "app.authentication.loginFailedLockCount",
"type": "java.lang.Integer",
"description": "登录失败锁定次数."
},
{
"name": "app.authentication.loginFailedLockTime",
"type": "java.lang.Integer",
"description": "登录失败锁定时间 (分)."
}
]
}

View File

@@ -5,9 +5,8 @@
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="org.dromara.visor.module.infra.entity.domain.SystemSettingDO">
<id column="id" property="id"/>
<result column="config_key" property="configKey"/>
<result column="type" property="type"/>
<result column="item" property="item"/>
<result column="config_key" property="configKey"/>
<result column="value" property="value"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
@@ -18,7 +17,7 @@
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, config_key, type, item, value, create_time, update_time, creator, updater, deleted
id, config_key, type, value, create_time, update_time, creator, updater, deleted
</sql>
</mapper>

Some files were not shown because too many files have changed in this diff Show More