Merge branch 'dev' into hotfix

This commit is contained in:
李佳航
2025-01-22 23:12:47 +08:00
committed by GitHub
40 changed files with 1637 additions and 1105 deletions

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

@@ -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

@@ -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,47 @@ 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());
// 获取内置参数
Map<String, Object> commandParams = this.getCommandParams();
// 获取实际命令
String command = ExecUtils.format(execLog.getCommand(), commandParams);
// 获取日志路径
String logPath = fileClient.getReturnPath(EndpointDefine.EXEC_LOG.format(execHostLog.getLogId(), execHostLog.getHostId()));
// 获取脚本路径
String scriptPath = null;
if (BooleanBit.toBoolean(execLog.getScriptExec())) {
scriptPath = this.buildScriptPath();
}
execHostLog.setCommand(command);
execHostLog.setParameter(JSON.toJSONString(commandParams));
execHostLog.setLogPath(logPath);
execHostLog.setScriptPath(scriptPath);
} 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());
});
return passed;
}
/**
@@ -150,29 +201,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 +238,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 +265,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 +297,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 +351,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 +363,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 +371,7 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
Streams.close(logOutputStream);
Streams.close(executor);
Streams.close(sessionStore);
execLogManager.asyncCloseTailFile(execHostCommand.getLogPath());
execLogManager.asyncCloseTailFile(execHostLog.getLogPath());
}
/**
@@ -336,9 +404,40 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
return Strings.retain(message, 250);
}
@Override
public Long getHostId() {
return execHostCommand.getHostId();
/**
* 获取命令实际参数
*
* @return params
*/
private Map<String, Object> getCommandParams() {
String uuid = UUIds.random();
Map<String, Object> params = Maps.newMap(builtParams);
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());
return params;
}
/**
* 构建脚本路径
*
* @return scriptPath
*/
private String buildScriptPath() {
HostOsTypeEnum os = HostOsTypeEnum.of(connect.getOsType());
String name = FileConst.EXEC
+ "/" + execHostLog.getLogId()
+ "/" + id
+ os.getScriptSuffix();
return PathUtils.buildAppPath(HostOsTypeEnum.WINDOWS.equals(os), connect.getUsername(), FileConst.SCRIPT, name);
}
}

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.getExecAppendAnsi())) {
// 详细日志
return new ExecCommandDetailHandler(id, execLog, builtParams, timeoutChecker);
} else {
// 原始日志
return new ExecCommandOriginHandler(execCommand, host, timeoutChecker);
return new ExecCommandOriginHandler(id, 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

@@ -82,6 +82,9 @@ public class ExecLogManager {
* @param path path
*/
public void asyncCloseTailFile(String path) {
if (path == null) {
return;
}
Threads.start(() -> {
try {
// 获取当前路径的全部追踪器

View File

@@ -29,8 +29,9 @@ import cn.orionsec.kit.ext.tail.mode.FileOffsetMode;
import cn.orionsec.kit.spring.SpringHolder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.Const;
import org.dromara.visor.framework.websocket.core.utils.WebSockets;
import org.dromara.visor.module.asset.define.config.AppTrackerConfig;
import org.dromara.visor.module.asset.define.config.AppLogConfig;
import org.dromara.visor.module.asset.entity.dto.ExecHostLogTailDTO;
import org.dromara.visor.module.asset.handler.host.exec.log.constant.LogConst;
import org.springframework.web.socket.WebSocketSession;
@@ -45,7 +46,7 @@ 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 final WebSocketSession session;
@@ -76,9 +77,9 @@ public class ExecLogTracker implements IExecLogTracker {
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.delayMillis(appLogConfig.getTrackerDelay());
tracker.offset(FileOffsetMode.LINE, appLogConfig.getTrackerOffset());
tracker.notFoundMode(FileNotFoundMode.WAIT_COUNT, Const.N_10);
// 开始监听文件
tracker.run();
} catch (Exception e) {

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

@@ -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

@@ -99,7 +99,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

@@ -328,7 +328,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);
// 中断

View File

@@ -234,6 +234,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

@@ -0,0 +1,90 @@
/*
* 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.KEEP);
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 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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -1,35 +1,15 @@
import axios from 'axios';
import { dateFormat } from '@/utils';
/**
* 系统设置类型
*/
export type SystemSettingType = 'SFTP' | 'ENCRYPT';
/**
* 系统设置更新请求
*/
export interface SystemSettingUpdateRequest {
type?: SystemSettingType;
item?: string;
value?: any;
type?: string;
value?: string;
settings?: Record<string, any>;
}
/**
* 应用信息查询响应
*/
export interface SystemLicenseResponse {
userCount: number;
hostCount: number;
release: string;
releaseName: string;
issueDate: number;
expireDate: number;
expireDay: number;
uuid: string;
}
/**
* 应用信息查询响应
*/
@@ -47,28 +27,73 @@ export interface AppReleaseResponse {
}
/**
* 系统聚合设置响应
* RSA 密钥对响应
*/
export interface SystemSettingAggregateResponse {
sftp: SftpSetting;
encrypt: EncryptSetting;
}
/**
* SFTP 配置
*/
export interface SftpSetting {
previewSize: number;
}
/**
* 加密配置
*/
export interface EncryptSetting {
export interface RsaKeyPairResponse {
publicKey: string;
privateKey: string;
}
/**
* 系统设置
*/
export type SystemSetting = SftpSetting
& LoginSetting & EncryptSetting
& LogSetting & AutoClearSetting;
/**
* SFTP 设置
*/
export interface SftpSetting {
sftp_previewSize: number;
sftp_uploadPresentBackup: string;
sftp_uploadBackupFileName: string;
}
/**
* 登录设置
*/
export interface LoginSetting {
login_allowMultiDevice: string;
login_allowRefresh: string;
login_maxRefreshCount: number;
login_refreshInterval: number;
login_loginFailedLock: string;
login_loginFailedLockThreshold: number;
login_loginFailedLockTime: number;
login_loginFailedSend: string;
login_loginFailedSendThreshold: number;
login_loginSessionTime: number;
}
/**
* 加密设置
*/
export interface EncryptSetting {
encrypt_publicKey: string;
encrypt_privateKey: string;
}
/**
* 日志设置
*/
export interface LogSetting {
log_webScrollLines: number;
log_trackerLoadLines: number;
log_trackerLoadInterval: number;
log_execDetailLog: string;
}
/**
* 自动清理设置
*/
export interface AutoClearSetting {
autoClear_execLogEnabled: string;
autoClear_execLogKeepDays: number;
autoClear_terminalLogEnabled: string;
autoClear_terminalLogKeepDays: number;
}
/**
* 查询应用信息
*/
@@ -80,7 +105,7 @@ export function getSystemAppInfo() {
* 获取系统聚合设置
*/
export function getSystemAggregateSetting() {
return axios.get<SystemSettingAggregateResponse>('/infra/system-setting/setting');
return axios.get<Record<keyof SystemSetting, string>>('/infra/system-setting/setting');
}
/**
@@ -101,26 +126,26 @@ export function getAppLatestRelease() {
* 生成密钥对
*/
export function generatorKeypair() {
return axios.get<EncryptSetting>('/infra/system-setting/generator-keypair');
return axios.get<RsaKeyPairResponse>('/infra/system-setting/generator-keypair');
}
/**
* 更新系统设置-单个
*/
export function updateSystemSetting(request: SystemSettingUpdateRequest) {
return axios.put<number>('/infra/system-setting/update', request);
return axios.put<string>('/infra/system-setting/update', request);
}
/**
* 更新系统设置-多个
*/
export function updateSystemSettingBatch(request: SystemSettingUpdateRequest) {
return axios.put<number>('/infra/system-setting/update-batch', request);
return axios.put<string>('/infra/system-setting/update-batch', request);
}
/**
* 查询系统设置
*/
export function getSystemSetting<T>(type: SystemSettingType) {
return axios.get<T>('/infra/system-setting/get', { params: { type } });
export function getSystemSetting(type: string) {
return axios.get<Record<keyof SystemSetting, string>>('/infra/system-setting/get', { params: { type } });
}

View File

@@ -129,6 +129,21 @@ export function formatDuration(start: number, end?: number): string {
return result;
}
/**
* 转为匿名数字 number | undefined
*/
export function toAnonymousNumber(value: string | undefined): number {
if (value === undefined || value === null) {
return value as unknown as number;
}
const num = Number.parseInt(value);
if (Number.isNaN(num)) {
return undefined as unknown as number;
} else {
return num;
}
}
/**
* 格式化数字为 ,分割
*/

View File

@@ -12,7 +12,7 @@ export const encrypt = async (data: string | undefined): Promise<string | undefi
return data;
}
// 获取公钥
const publicKey = (await useCacheStore().loadSystemSetting()).encrypt?.publicKey;
const publicKey = (await useCacheStore().loadSystemSetting()).encrypt_publicKey;
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);

View File

@@ -0,0 +1,156 @@
<template>
<a-spin class="main-container" :loading="loading">
<!-- 标题 -->
<h3 class="setting-header">自动清理设置</h3>
<!-- 表单 -->
<a-form :model="setting"
ref="formRef"
class="setting-form"
label-align="right"
:auto-label-width="true">
<!-- 自动清理执行记录 -->
<a-form-item field="autoClear_execLogEnabled"
label="自动清理执行记录"
:rules="[{required: true, message: '请选择此项'}]"
hide-asterisk>
<a-switch v-model="setting.autoClear_execLogEnabled"
type="round"
checked-value="true"
unchecked-value="false"
checked-text="开启"
unchecked-text="关闭" />
<template #extra>
开启后将会在每天凌晨自动清理命令执行记录
</template>
</a-form-item>
<!-- 执行记录保留天数 -->
<a-form-item field="autoClear_execLogKeepDays"
label="执行记录保留天数"
:rules="[{required: true, message: '请输入执行记录保留天数'}]"
hide-asterisk>
<a-input-number v-model="setting.autoClear_execLogKeepDays"
class="input-wrapper"
:min="0"
:max="99999"
placeholder="请输入执行记录保留天数"
allow-clear
hide-button>
<template #suffix>
</template>
</a-input-number>
<template #extra>
自动清理命令执行记录时保留的天数
</template>
</a-form-item>
<!-- 自动清理终端记录 -->
<a-form-item field="autoClear_terminalLogEnabled"
label="自动清理终端记录"
:rules="[{required: true, message: '请选择此项'}]"
hide-asterisk>
<a-switch v-model="setting.autoClear_terminalLogEnabled"
type="round"
checked-value="true"
unchecked-value="false"
checked-text="开启"
unchecked-text="关闭" />
<template #extra>
开启后将会在每天凌晨自动清理终端连接记录
</template>
</a-form-item>
<!-- 终端记录保留天数 -->
<a-form-item field="autoClear_terminalLogKeepDays"
label="终端记录保留天数"
:rules="[{required: true, message: '请输入终端记录保留天数'}]"
hide-asterisk>
<a-input-number v-model="setting.autoClear_terminalLogKeepDays"
class="input-wrapper"
:min="0"
:max="99999"
placeholder="请输入终端记录保留天数"
allow-clear
hide-button>
<template #suffix>
</template>
</a-input-number>
<template #extra>
自动清理终端连接记录时保留的天数
</template>
</a-form-item>
<!-- 按钮 -->
<a-form-item v-permission="['infra:system-setting:update']">
<!-- 保存 -->
<a-button type="primary"
size="small"
@click="save">
保存
</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script lang="ts">
export default {
name: 'autoClearSetting',
};
</script>
<script lang="ts" setup>
import type { AutoClearSetting } from '@/api/system/setting';
import { onMounted, ref } from 'vue';
import { getSystemSetting, updateSystemSettingBatch } from '@/api/system/setting';
import useLoading from '@/hooks/loading';
import { Message } from '@arco-design/web-vue';
import { toAnonymousNumber } from '@/utils';
import { SystemSettingTypes } from '../types/const';
const { loading, setLoading } = useLoading();
const formRef = ref();
const setting = ref<AutoClearSetting>({} as AutoClearSetting);
// 保存
const save = async () => {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
setLoading(true);
try {
await updateSystemSettingBatch({
type: SystemSettingTypes.AUTO_CLEAR,
settings: setting.value
});
Message.success('修改成功');
} catch (e) {
} finally {
setLoading(false);
}
};
// 加载配置
onMounted(async () => {
setLoading(true);
try {
const { data } = await getSystemSetting(SystemSettingTypes.AUTO_CLEAR);
setting.value = {
...data,
autoClear_execLogKeepDays: toAnonymousNumber(data.autoClear_execLogKeepDays),
autoClear_terminalLogKeepDays: toAnonymousNumber(data.autoClear_terminalLogKeepDays),
};
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
.input-wrapper {
width: 368px;
}
</style>

View File

@@ -13,22 +13,22 @@
<a-alert>请输入 PKCS8 格式的 RSA Base64 密钥, 用于前后端传输时的数据加密</a-alert>
</a-form-item>
<!-- 加密公钥 -->
<a-form-item field="publicKey"
<a-form-item field="encrypt_publicKey"
label="加密公钥"
:rules="[{required: true, message: '请输入加密公钥'}]"
hide-asterisk>
<a-textarea v-model="setting.publicKey"
<a-textarea v-model="setting.encrypt_publicKey"
class="input-wrapper"
placeholder="RSA 公钥 Base64"
:auto-size="{ minRows: 5, maxRows: 5 }"
allow-clear />
</a-form-item>
<!-- 加密私钥 -->
<a-form-item field="privateKey"
<a-form-item field="encrypt_privateKey"
label="加密私钥"
:rules="[{required: true, message: '请输入加密私钥'}]"
hide-asterisk>
<a-textarea v-model="setting.privateKey"
<a-textarea v-model="setting.encrypt_privateKey"
class="input-wrapper"
placeholder="RSA 私钥 Base64"
:auto-size="{ minRows: 14, maxRows: 14 }"
@@ -97,8 +97,8 @@
setLoading(true);
try {
const { data } = await generatorKeypair();
setting.value.publicKey = data.publicKey;
setting.value.privateKey = data.privateKey;
setting.value.encrypt_publicKey = data.publicKey;
setting.value.encrypt_privateKey = data.privateKey;
} catch (e) {
} finally {
setLoading(false);
@@ -109,8 +109,10 @@
onMounted(async () => {
setLoading(true);
try {
const { data } = await getSystemSetting<EncryptSetting>(SystemSettingTypes.ENCRYPT);
setting.value = data;
const { data } = await getSystemSetting(SystemSettingTypes.ENCRYPT);
setting.value = {
...data
};
} catch (e) {
} finally {
setLoading(false);

View File

@@ -0,0 +1,162 @@
<template>
<a-spin class="main-container" :loading="loading">
<!-- 标题 -->
<h3 class="setting-header">日志设置</h3>
<!-- 表单 -->
<a-form :model="setting"
ref="formRef"
class="setting-form"
label-align="right"
:auto-label-width="true">
<!-- 执行详细日志 -->
<a-form-item field="log_execDetailLog"
label="执行详细日志"
:rules="[{required: true, message: '请选择此项'}]"
hide-asterisk>
<a-switch v-model="setting.log_execDetailLog"
type="round"
checked-value="true"
unchecked-value="false"
checked-text="详细输出"
unchecked-text="原始输出" />
<template #extra>
开启后在命令执行时会展示详细的日志信息(执行主机命令等), 关闭后则只显示命令的标准输出
</template>
</a-form-item>
<!-- 最大显示行数 -->
<a-form-item field="log_webScrollLines"
label="最大显示行数"
:rules="[{required: true, message: '请输入日志最大显示行数'}]"
hide-asterisk>
<a-input-number v-model="setting.log_webScrollLines"
class="input-wrapper"
:min="0"
:max="999999"
placeholder="请输入日志最大显示行数"
allow-clear
hide-button>
<template #suffix>
</template>
</a-input-number>
<template #extra>
前端日志组件最大显示的行数, 超出部分将会被覆盖 (数值越大内存占用越多)
</template>
</a-form-item>
<!-- 日志加载行数 -->
<a-form-item field="log_trackerLoadLines"
label="日志加载行数"
:rules="[{required: true, message: '请输入日志加载行数'}]"
hide-asterisk>
<a-input-number v-model="setting.log_trackerLoadLines"
class="input-wrapper"
:min="0"
:max="99999"
placeholder="请输入日志加载行数"
allow-clear
hide-button>
<template #suffix>
</template>
</a-input-number>
<template #extra>
当查看日志时, 默认读取的行数
</template>
</a-form-item>
<!-- 日志监听间隔 -->
<a-form-item field="log_trackerLoadInterval"
label="日志监听间隔"
:rules="[{required: true, message: '请输入日志监听间隔'}]"
hide-asterisk>
<a-input-number v-model="setting.log_trackerLoadInterval"
class="input-wrapper"
:min="0"
:max="99999"
placeholder="请输入日志监听间隔"
allow-clear
hide-button>
<template #suffix>
ms
</template>
</a-input-number>
<template #extra>
日志增量刷新的间隔 (毫秒)
</template>
</a-form-item>
<!-- 按钮 -->
<a-form-item v-permission="['infra:system-setting:update']">
<!-- 保存 -->
<a-button type="primary"
size="small"
@click="save">
保存
</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script lang="ts">
export default {
name: 'logSetting',
};
</script>
<script lang="ts" setup>
import type { LogSetting } from '@/api/system/setting';
import { onMounted, ref } from 'vue';
import { getSystemSetting, updateSystemSettingBatch } from '@/api/system/setting';
import useLoading from '@/hooks/loading';
import { Message } from '@arco-design/web-vue';
import { toAnonymousNumber } from '@/utils';
import { SystemSettingTypes } from '../types/const';
const { loading, setLoading } = useLoading();
const formRef = ref();
const setting = ref<LogSetting>({} as LogSetting);
// 保存
const save = async () => {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
setLoading(true);
try {
await updateSystemSettingBatch({
type: SystemSettingTypes.LOG,
settings: setting.value
});
Message.success('修改成功');
} catch (e) {
} finally {
setLoading(false);
}
};
// 加载配置
onMounted(async () => {
setLoading(true);
try {
const { data } = await getSystemSetting(SystemSettingTypes.LOG);
setting.value = {
...data,
log_webScrollLines: toAnonymousNumber(data.log_webScrollLines),
log_trackerLoadInterval: toAnonymousNumber(data.log_trackerLoadInterval),
log_trackerLoadLines: toAnonymousNumber(data.log_trackerLoadLines),
};
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
.input-wrapper {
width: 368px;
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<a-spin class="main-container" :loading="loading">
<!-- 标题 -->
<h3 class="setting-header">登录设置</h3>
<!-- 表单 -->
<a-form :model="setting"
ref="formRef"
class="setting-form"
label-align="right"
:auto-label-width="true">
<!-- 允许多端登录 -->
<a-form-item field="login_allowMultiDevice"
label="允许多端登录"
:rules="[{required: true, message: '请选择此项'}]"
hide-asterisk>
<a-switch v-model="setting.login_allowMultiDevice"
type="round"
checked-value="true"
unchecked-value="false"
checked-text="开启"
unchecked-text="关闭" />
<template #extra>
开启后一个账号可以多个设备同时登录
</template>
</a-form-item>
<!-- 允许凭证续签 -->
<a-form-item field="login_allowRefresh"
label="允许凭证续签"
:rules="[{required: true, message: '请选择此项'}]"
hide-asterisk>
<a-switch v-model="setting.login_allowRefresh"
type="round"
checked-value="true"
unchecked-value="false"
checked-text="开启"
unchecked-text="关闭" />
<template #extra>
开启后当凭证即将过期时系统会自动续签
</template>
</a-form-item>
<!-- 登录失败锁定 -->
<a-form-item field="login_loginFailedLock"
label="登录失败锁定"
:rules="[{required: true, message: '请选择此项'}]"
hide-asterisk>
<a-switch v-model="setting.login_loginFailedLock"
type="round"
checked-value="true"
unchecked-value="false"
checked-text="开启"
unchecked-text="关闭" />
<template #extra>
开启后当登录失败次数达到阈值时账号会自动锁定
</template>
</a-form-item>
<!-- 登录失败发信 -->
<a-form-item field="login_loginFailedSend"
label="登录失败发信"
:rules="[{required: true, message: '请选择此项'}]"
hide-asterisk>
<a-switch v-model="setting.login_loginFailedSend"
type="round"
checked-value="true"
unchecked-value="false"
checked-text="开启"
unchecked-text="关闭" />
<template #extra>
开启后当登录失败次数达到阈值时将发送站内信
</template>
</a-form-item>
<!-- 凭证有效期 -->
<a-form-item field="login_loginSessionTime"
label="凭证有效期"
:rules="[{required: true, message: '请输入凭证有效期'}]"
hide-asterisk>
<a-input-number v-model="setting.login_loginSessionTime"
class="input-wrapper"
:min="1"
:max="99999"
placeholder="请输入凭证有效期"
allow-clear
hide-button>
<template #suffix>
</template>
</a-input-number>
<template #extra>
设置登录凭证有效期时长()
</template>
</a-form-item>
<!-- 凭证续签间隔 -->
<a-form-item field="login_refreshInterval"
label="凭证续签间隔"
:rules="[{required: true, message: '请输入凭证续签间隔时间'}]"
hide-asterisk>
<a-input-number v-model="setting.login_refreshInterval"
class="input-wrapper"
:min="1"
:max="99999"
placeholder="请输入凭证续签间隔时间"
allow-clear
hide-button>
<template #suffix>
</template>
</a-input-number>
<template #extra>
当登录凭证过期但未超过续签间隔时系统会自动续签
</template>
</a-form-item>
<!-- 凭证续签最大次数 -->
<a-form-item field="login_maxRefreshCount"
label="凭证续签最大次数"
:rules="[{required: true, message: '请输入凭证续签最大次数'}]"
hide-asterisk>
<a-input-number v-model="setting.login_maxRefreshCount"
class="input-wrapper"
:min="0"
:max="99999"
placeholder="请输入凭证续签最大次数"
allow-clear
hide-button />
<template #extra>
凭证续签的最大次数
</template>
</a-form-item>
<!-- 登录失败锁定阈值 -->
<a-form-item field="login_loginFailedLockThreshold"
label="登录失败锁定阈值"
:rules="[{required: true, message: '请输入登录失败锁定阈值'}]"
hide-asterisk>
<a-input-number v-model="setting.login_loginFailedLockThreshold"
class="input-wrapper"
:min="0"
:max="99999"
placeholder="请输入登录失败锁定阈值"
allow-clear
hide-button />
<template #extra>
登录失败次数到达该值时账号会自动锁定
</template>
</a-form-item>
<!-- 登录失败锁定时间 -->
<a-form-item field="login_loginFailedLockTime"
label="登录失败锁定时间"
:rules="[{required: true, message: '请输入登录失败锁定时间'}]"
hide-asterisk>
<a-input-number v-model="setting.login_loginFailedLockTime"
class="input-wrapper"
:min="0"
:max="999999"
placeholder="请输入登录失败锁定时间"
allow-clear
hide-button>
<template #suffix>
</template>
</a-input-number>
<template #extra>
登录失败多次后账号锁定的时间()
</template>
</a-form-item>
<!-- 登录失败发信阈值 -->
<a-form-item field="login_loginFailedSendThreshold"
label="登录失败发信阈值"
:rules="[{required: true, message: '请输入登录失败发信阈值'}]"
hide-asterisk>
<a-input-number v-model="setting.login_loginFailedSendThreshold"
class="input-wrapper"
:min="0"
:max="99999"
placeholder="请输入登录失败发信阈值"
allow-clear
hide-button />
<template #extra>
登录失败次数到达该值时系统将发送站内信
</template>
</a-form-item>
<!-- 按钮 -->
<a-form-item v-permission="['infra:system-setting:update']">
<!-- 保存 -->
<a-button type="primary"
size="small"
@click="save">
保存
</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script lang="ts">
export default {
name: 'loginSetting',
};
</script>
<script lang="ts" setup>
import type { LoginSetting } from '@/api/system/setting';
import { onMounted, ref } from 'vue';
import { getSystemSetting, updateSystemSettingBatch } from '@/api/system/setting';
import useLoading from '@/hooks/loading';
import { Message } from '@arco-design/web-vue';
import { toAnonymousNumber } from '@/utils';
import { SystemSettingTypes } from '../types/const';
const { loading, setLoading } = useLoading();
const formRef = ref();
const setting = ref<LoginSetting>({} as LoginSetting);
// 保存
const save = async () => {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
setLoading(true);
try {
await updateSystemSettingBatch({
type: SystemSettingTypes.LOGIN,
settings: setting.value
});
Message.success('修改成功');
} catch (e) {
} finally {
setLoading(false);
}
};
// 加载配置
onMounted(async () => {
setLoading(true);
try {
const { data } = await getSystemSetting(SystemSettingTypes.LOGIN);
setting.value = {
...data,
login_loginSessionTime: toAnonymousNumber(data.login_loginSessionTime),
login_maxRefreshCount: toAnonymousNumber(data.login_maxRefreshCount),
login_refreshInterval: toAnonymousNumber(data.login_refreshInterval),
login_loginFailedLockThreshold: toAnonymousNumber(data.login_loginFailedLockThreshold),
login_loginFailedLockTime: toAnonymousNumber(data.login_loginFailedLockTime),
login_loginFailedSendThreshold: toAnonymousNumber(data.login_loginFailedSendThreshold),
};
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
.input-wrapper {
width: 368px;
}
</style>

View File

@@ -8,12 +8,40 @@
class="setting-form"
label-align="right"
:auto-label-width="true">
<!-- 重复文件备份 -->
<a-form-item field="sftp_uploadPresentBackup"
label="重复文件备份"
:rules="[{required: true, message: '请选择此项'}]"
hide-asterisk>
<a-switch v-model="setting.sftp_uploadPresentBackup"
type="round"
checked-value="true"
unchecked-value="false"
checked-text="备份"
unchecked-text="覆盖" />
<template #extra>
文件上传时, 若文件存在是否备份原始文件
</template>
</a-form-item>
<!-- 备份文件名称 -->
<a-form-item field="sftp_uploadBackupFileName"
label="备份文件名称"
:rules="[{required: true, message: '请输入备份文件名称'}]"
hide-asterisk>
<a-input v-model="setting.sftp_uploadBackupFileName"
class="input-wrapper"
placeholder="请输入备份文件名称模板"
allow-clear />
<template #extra>
${fileName} 文件名称, ${timestamp} 时间戳, ${time} 时间
</template>
</a-form-item>
<!-- 文件预览大小 -->
<a-form-item field="previewSize"
<a-form-item field="sftp_previewSize"
label="文件预览大小"
:rules="[{required: true, message: '请输入文件预览大小'}]"
hide-asterisk>
<a-input-number v-model="setting.previewSize"
<a-input-number v-model="setting.sftp_previewSize"
class="input-wrapper"
:min="0"
:max="200"
@@ -24,6 +52,9 @@
MB
</template>
</a-input-number>
<template #extra>
可以直接查看或编辑小于等于该大小的普通文件
</template>
</a-form-item>
<!-- 按钮 -->
<a-form-item v-permission="['infra:system-setting:update']">
@@ -50,6 +81,7 @@
import { getSystemSetting, updateSystemSettingBatch } from '@/api/system/setting';
import useLoading from '@/hooks/loading';
import { Message } from '@arco-design/web-vue';
import { toAnonymousNumber } from '@/utils';
import { SystemSettingTypes } from '../types/const';
const { loading, setLoading } = useLoading();
@@ -68,7 +100,7 @@
try {
await updateSystemSettingBatch({
type: SystemSettingTypes.SFTP,
settings: setting.value
settings: setting.value,
});
Message.success('修改成功');
} catch (e) {
@@ -81,8 +113,11 @@
onMounted(async () => {
setLoading(true);
try {
const { data } = await getSystemSetting<SftpSetting>(SystemSettingTypes.SFTP);
setting.value = data;
const { data } = await getSystemSetting(SystemSettingTypes.SFTP);
setting.value = {
...data,
sftp_previewSize: toAnonymousNumber(data.sftp_previewSize),
};
} catch (e) {
} finally {
setLoading(false);
@@ -93,6 +128,6 @@
<style lang="less" scoped>
.input-wrapper {
width: 328px;
width: 368px;
}
</style>

View File

@@ -11,12 +11,30 @@
title="SFTP">
<sftp-setting />
</a-tab-pane>
<!-- 登录设置 -->
<a-tab-pane v-permission="['infra:system-setting:update']"
key="login"
title="登录设置">
<login-setting />
</a-tab-pane>
<!-- 加密设置 -->
<a-tab-pane v-permission="['infra:system-setting:update']"
key="encrypt"
title="加密设置">
<encrypt-setting />
</a-tab-pane>
<!-- 日志设置 -->
<a-tab-pane v-permission="['infra:system-setting:update']"
key="log"
title="日志设置">
<log-setting />
</a-tab-pane>
<!-- 自动清理 -->
<a-tab-pane v-permission="['infra:system-setting:update']"
key="auto-clear"
title="自动清理">
<auto-clear-setting />
</a-tab-pane>
<!-- 关于 -->
<a-tab-pane key="about" title="关于">
<about-setting />
@@ -36,7 +54,10 @@
import { useRoute } from 'vue-router';
import usePermission from '@/hooks/permission';
import SftpSetting from './components/sftp-setting.vue';
import LoginSetting from './components/login-setting.vue';
import EncryptSetting from './components/encrypt-setting.vue';
import LogSetting from './components/log-setting.vue';
import AutoClearSetting from './components/auto-clear-setting.vue';
import AboutSetting from './components/about-setting.vue';
const route = useRoute();
@@ -96,6 +117,7 @@
margin-top: 0;
margin-bottom: 20px;
font-weight: 600;
user-select: none;
}
.setting-form {
@@ -104,6 +126,7 @@
.arco-descriptions-title {
font-weight: 600;
user-select: none;
}
.alert-href {

View File

@@ -1,7 +1,8 @@
import type { SystemSettingType } from '@/api/system/setting';
// 系统设置类型
export const SystemSettingTypes: Record<SystemSettingType, SystemSettingType> = {
SFTP: 'SFTP',
ENCRYPT: 'ENCRYPT',
export const SystemSettingTypes = {
SFTP: 'sftp',
ENCRYPT: 'encrypt',
LOGIN: 'login',
LOG: 'log',
AUTO_CLEAR: 'autoClear',
};