Merge remote-tracking branch 'origin/master'

This commit is contained in:
HopeLi 2025-07-16 17:48:02 +08:00
commit 0887d3bc29
168 changed files with 10338 additions and 19 deletions

View File

@ -26,6 +26,7 @@ import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
import java.util.List;
@ -76,6 +77,18 @@ public class OpsWebSocketAutoConfiguration {
return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
}
/**
* @return websocket 缓冲区大小配置
*/
@Bean
public ServletServerContainerFactoryBean servletServerContainerFactoryBean(WebSocketProperties config) {
ServletServerContainerFactoryBean factory = new ServletServerContainerFactoryBean();
factory.setMaxBinaryMessageBufferSize(config.getBinaryBufferSize());
factory.setMaxTextMessageBufferSize(config.getBinaryBufferSize());
factory.setMaxSessionIdleTimeout(config.getSessionIdleTimeout());
return factory;
}
// ==================== Sender 相关 ====================
@Configuration

View File

@ -30,4 +30,15 @@ public class WebSocketProperties {
@NotNull(message = "WebSocket 的消息发送者不能为空")
private String senderType = "local";
/**
* 二进制消息缓冲区大小 byte
*/
private Integer binaryBufferSize;
/**
* session 最大超时时间 ms
*/
private Long sessionIdleTimeout;
}

View File

@ -8,9 +8,7 @@ import cd.casic.framework.websocket.core.util.WebSocketFrameworkUtils;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.TypeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.lang.reflect.Type;
@ -79,5 +77,16 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
}
}
//处理二进制消息
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
try {
WebSocketMessageListener<Object> messageListener = listeners.get("SFTP-access-message-send");
Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, message));
}
catch (Throwable ex) {
log.error("[handleBinaryMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
}
}
}

View File

@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@ -92,4 +93,10 @@ public class TestCaseInfoController {
return CommonResult.success(respList);
}
@PostMapping("/checkTestCaseExist")
public CommonResult<String> checkTestCaseExist(MultipartFile file){
String s = testCaseInfoService.checkTestCaseExist(file);
return CommonResult.success(s);
}
}

View File

@ -24,4 +24,6 @@ public class TestCaseInfoReq {
// 描述信息
private String remark;
private String sign;
}

View File

@ -1,12 +1,15 @@
package cd.casic.ci.process.process.dataObject.pipgroup;
import cd.casic.framework.commons.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class PipGroup extends BaseDO {
@TableId(type = IdType.ASSIGN_ID)
private String id;
private String groupName;
/**

View File

@ -35,4 +35,6 @@ public class TestCaseInfo extends PipBaseElement {
// 描述信息
private String remark;
private String sign;
}

View File

@ -8,7 +8,10 @@ import cd.casic.ci.process.process.converter.GroupConverter;
import cd.casic.ci.process.process.dao.pipeline.PipGroupDao;
import cd.casic.ci.process.process.dataObject.pipgroup.PipGroup;
import cd.casic.ci.process.process.service.group.GroupService;
import cd.casic.ci.process.process.service.pipeline.PipelineService;
import cd.casic.ci.process.util.WebFrameworkUtils;
import cd.casic.framework.commons.exception.ServiceException;
import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@ -20,6 +23,8 @@ public class GroupServiceImpl extends ServiceImpl<PipGroupDao, PipGroup> impleme
private GroupConverter converter;
@Resource
private PipGroupDao groupDao;
@Resource
private PipelineService pipelineService;
@Override
public void addGroup(GroupAddReq req) {
PipGroup pipGroup = converter.reqToDto(req);
@ -40,6 +45,9 @@ public class GroupServiceImpl extends ServiceImpl<PipGroupDao, PipGroup> impleme
@Override
public void deleteGroup(String id) {
if (pipelineService.getGroupCount(id) != 0) {
throw new ServiceException(GlobalErrorCodeConstants.PIPELINE_ERROR.getCode(),"分组下辖流水线不为空不允许删除");
}
removeById(id);
}
}

View File

@ -37,4 +37,5 @@ public interface PipelineService extends IService<PipPipeline> {
PipelineFindResp findPipelineById(@Valid PipelineQueryReq pipelineQueryReq);
TreeRunContextResp getPipelineRunState(String pipelineId);
Long getGroupCount(String groupId);
}

View File

@ -34,6 +34,7 @@ import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.security.dal.user.AdminUserDO;
import cd.casic.framework.tenant.core.service.AdminUserServiceImpl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -528,6 +529,17 @@ public class PipelineServiceImpl extends ServiceImpl<PipelineDao, PipPipeline> i
return new TreeRunContextResp();
}
@Override
public Long getGroupCount(String groupId) {
if (StringUtils.isEmpty(groupId)) {
return -1L;
}
LambdaQueryWrapper<PipPipeline> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PipPipeline::getGroupId,groupId);
return pipelineDao.selectCount(wrapper);
}
private String between(LocalDateTime startTime,LocalDateTime endTime){
if (startTime==null||endTime==null) {
return "";

View File

@ -8,6 +8,7 @@ import cd.casic.ci.process.process.dataObject.testCase.TestCaseInfo;
import cd.casic.framework.commons.pojo.PageResult;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@ -34,4 +35,5 @@ public interface TestCaseInfoService extends IService<TestCaseInfo> {
List<String> findFileTypeList();
List<TestCaseInfoResp> findByManagerIds(List<String> idList);
String checkTestCaseExist(MultipartFile file);
}

View File

@ -8,6 +8,7 @@ import cd.casic.ci.process.process.converter.TestCaseInfoConverter;
import cd.casic.ci.process.process.dao.testCase.TestCaseInfoDao;
import cd.casic.ci.process.process.dataObject.testCase.TestCaseInfo;
import cd.casic.ci.process.process.service.testCase.TestCaseInfoService;
import cd.casic.ci.process.util.FileUtil;
import cd.casic.framework.commons.exception.ServiceException;
import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
import cd.casic.framework.commons.pojo.PageResult;
@ -23,7 +24,9 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -151,6 +154,27 @@ public class TestCaseInfoServiceImpl extends ServiceImpl<TestCaseInfoDao, TestCa
return TestCaseInfoConverter.INSTANCE.toRespList(testCaseInfos);
}
@Override
public String checkTestCaseExist(MultipartFile file) {
try{
InputStream inputStream = file.getInputStream();
String md5Str = FileUtil.getMD5Str(inputStream);
if (checkMd5Exist(md5Str)) {
return md5Str;
}
} catch (Exception e ){
log.error("校验文件内容出现错误",e);
}
return "";
}
private Boolean checkMd5Exist(String sign){
if (StringUtils.isEmpty(sign)) {
return false;
}
LambdaQueryWrapper<TestCaseInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TestCaseInfo::getSign,sign);
return testCaseInfoDao.exists(wrapper);
}
private void setUserName(TestCaseInfoResp testCaseInfoResp) {
if (!StringUtils.isEmpty(testCaseInfoResp.getCreator())){

View File

@ -0,0 +1,28 @@
package cd.casic.ci.process.util;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class FileUtil {
public static String getMD5Str(InputStream inputStream) throws NoSuchAlgorithmException, IOException {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[1024];
int len = -1;
while((len=inputStream.read(buffer))!=-1){
md.update(buffer,0,len);
}
byte[] digest = md.digest();
StringBuilder stringBuilder = new StringBuilder();
for (byte b : digest) {
String format = String.format("%02x", b);
stringBuilder.append(format);
}
return stringBuilder.toString();
}
}

View File

@ -7,20 +7,40 @@
<!-- 这里暂时没有任何SQL方法 -->
<select id="getList" resultType="cd.casic.ci.process.dto.resp.group.GroupListResp">
SELECT * FROM
(SELECT * FROM pip_group
UNION ALL
(SELECT null AS id,
"未分组" AS group_name,
"PROJECT" AS type ,
"" AS creator,
NOW() AS create_time,
NOW() AS update_time,
"" AS updater)
) pg
LEFT JOIN
(SELECT group_id,COUNT(*) count FROM pip_pipeline GROUP BY group_id) pl
ON ((pg.id = pl.group_id ) OR (ISNULL(pl.group_id) AND ISNULL(pl.group_id)))
WHERE pg.`name` LIKE #{groupName} AND ((pg.creator = #{userId} AND pg.type = 'PERSON') OR (pg.type = 'PROJECT'))
SELECT id,
group_name,
type,
creator,
create_time,
updater,
update_time,
group_id,
IFNULL(count,0) `count` FROM
(SELECT
id,
group_name,
type,
creator,
create_time,
updater,
update_time
FROM pip_group
UNION ALL
(SELECT null AS id,
"未分组" AS group_name,
"PROJECT" AS type ,
"" AS creator,
NOW() AS create_time,
"" AS updater,
NOW() AS update_time)
) pg
LEFT JOIN (SELECT group_id,COUNT(*) count FROM pip_pipeline GROUP BY group_id) pl ON (pg.id = pl.group_id OR (ISNULL(pg.id) AND ISNULL(group_id)) )
<where>
<if test="groupName!=null and groupName!=''">
AND pg.`group_name` LIKE CONCAT('%',#{groupName},'%')
</if>
AND ((pg.creator = #{userId} AND pg.type = 'PERSON') OR (pg.type = 'PROJECT'))
</where>
</select>
</mapper>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cd.casic.boot</groupId>
<artifactId>modules</artifactId>
<version>${revision}</version>
</parent>
<!--======终端部分======-->
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<version>${revision}</version>
<artifactId>module-ci-terminal</artifactId>
<dependencies>
<dependency>
<groupId>cd.casic.boot</groupId>
<artifactId>module-ci-machine</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cd.casic.boot</groupId>
<artifactId>module-ci-commons</artifactId>
</dependency>
<dependency>
<groupId>cd.casic.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cd.casic.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>cd.casic.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<!--=======外部依赖======-->
<dependency>
<groupId>cn.orionsec.kit</groupId>
<artifactId>orion-net</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,74 @@
package cd.casic.module.terminal.common;
import com.alibaba.fastjson.JSON;
/**
* 标准数据处理策略 基类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/6/11 21:44
*/
public abstract class AbstractGenericsDataStrategy<M extends GenericsDataModel> implements GenericsDataStrategy<M> {
protected final Class<M> modelClass;
public AbstractGenericsDataStrategy(Class<M> modelClass) {
this.modelClass = modelClass;
}
/**
* 更新填充
*
* @param beforeModel 修改前的配置
* @param afterModel 修改后的配置
*/
protected void updateFill(M beforeModel, M afterModel) {
}
/**
* 预校验参数
*
* @param model model
*/
protected void preValid(M model) {
}
/**
* 校验参数
*
* @param model model
*/
protected void valid(M model) {
}
@Override
public void doValid(M beforeModel, M afterModel) {
// 预校验参数
this.preValid(afterModel);
// 更新填充
this.updateFill(beforeModel, afterModel);
// 校验参数
this.valid(afterModel);
}
@Override
public M parse(String serialModel) {
return JSON.parseObject(serialModel, modelClass);
}
@Override
public void toView(M model) {
}
@Override
public M toView(String serialModel) {
// 解析
M parse = this.parse(serialModel);
// 转为视图对象
this.toView(parse);
return parse;
}
}

View File

@ -0,0 +1,150 @@
package cd.casic.module.terminal.common;
import cn.orionsec.kit.lang.utils.Exceptions;
/**
* aes 数据加密工具类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/8 0:05
*/
public class AesEncryptUtils {
private static AesEncryptor delegate;
private AesEncryptUtils() {
}
/**
* 加密
*
* @param plain 明文
* @return 密文
*/
public static byte[] encrypt(byte[] plain) {
return delegate.encrypt(plain);
}
/**
* 加密
*
* @param plain 明文
* @return 密文
*/
public static byte[] encrypt(String plain) {
return delegate.encrypt(plain);
}
/**
* 加密
*
* @param plain 明文
* @return 密文
*/
public static String encryptAsString(String plain) {
return delegate.encryptAsString(plain);
}
/**
* 加密
*
* @param plain 明文
* @return 密文
*/
public static String encryptAsString(byte[] plain) {
return delegate.encryptAsString(plain);
}
/**
* 解密
*
* @param text 密文
* @return 明文
*/
public static byte[] decrypt(byte[] text) {
return delegate.decrypt(text);
}
/**
* 解密
*
* @param text 密文
* @return 明文
*/
public static byte[] decrypt(String text) {
return delegate.decrypt(text);
}
/**
* 解密
*
* @param text 密文
* @return 明文
*/
public static String decryptAsString(String text) {
return delegate.decryptAsString(text);
}
/**
* 解密
*
* @param text 密文
* @return 明文
*/
public static String decryptAsString(byte[] text) {
return delegate.decryptAsString(text);
}
/**
* 验证加密结果
*
* @param plain 明文
* @param text 密文
* @return 是否成功
*/
public static boolean verify(String plain, String text) {
return delegate.verify(plain, text);
}
/**
* 验证加密结果
*
* @param plain 明文
* @param text 密文
* @return 是否成功
*/
public static boolean verify(byte[] plain, byte[] text) {
return delegate.verify(plain, text);
}
/**
* 加密后 base62 编码
*
* @param plain 明文
* @return 密文
*/
public static String encryptBase62(String plain) {
return delegate.encryptBase62(plain);
}
/**
* base62 解码后解密
*
* @param text 密文
* @return 明文
*/
public static String decryptBase62(String text) {
return delegate.decryptBase62(text);
}
public static void setDelegate(AesEncryptor delegate) {
if (AesEncryptUtils.delegate != null) {
// unmodified
throw Exceptions.state();
}
AesEncryptUtils.delegate = delegate;
}
}

View File

@ -0,0 +1,36 @@
package cd.casic.module.terminal.common;
import cn.orionsec.kit.lang.utils.codec.Base62s;
import cn.orionsec.kit.lang.utils.crypto.symmetric.SymmetricCrypto;
/**
* aes 加密器
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/8 0:20
*/
public interface AesEncryptor extends SymmetricCrypto {
/**
* 加密后 base62 编码
*
* @param plain 明文
* @return 密文
*/
default String encryptBase62(String plain) {
return new String(Base62s.encode(this.encrypt(plain)));
}
/**
* base62 解码后解密
*
* @param text 密文
* @return 明文
*/
default String decryptBase62(String text) {
return new String(this.decrypt(Base62s.decode(text)));
}
}

View File

@ -0,0 +1,31 @@
package cd.casic.module.terminal.common;
import cn.orionsec.kit.lang.constant.OrionConst;
/**
* 项目常量
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/6/19 18:56
*/
public interface AppConst extends OrionConst {
/**
* ${orion.version} 迭代时候需要手动更改
*/
String VERSION = "2.3.9";
/**
* ${spring.application.name}
*/
String APP_NAME = "orion-visor";
String GITHUB = "https://github.com/dromara/orion-visor";
String GITEE = "https://gitee.com/dromara/orion-visor";
String ISSUES = "https://github.com/dromara/orion-visor/issues";
}

View File

@ -0,0 +1,67 @@
package cd.casic.module.terminal.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* boolean 枚举
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/9/19 10:32
*/
@Getter
@AllArgsConstructor
public enum BooleanBit {
/**
*
*/
FALSE(0),
/**
*
*/
TRUE(1),
;
private final Integer value;
/**
* 是否为布尔值
*
* @return boolean
*/
public boolean booleanValue() {
return this == TRUE;
}
public static BooleanBit of(boolean value) {
return value ? TRUE : FALSE;
}
public static BooleanBit of(Integer value) {
if (value == null) {
return null;
}
for (BooleanBit e : values()) {
if (e.value.equals(value)) {
return e;
}
}
return null;
}
/**
* 转为布尔值
*
* @param value value
* @return boolean
*/
public static boolean toBoolean(Integer value) {
return TRUE.value.equals(value);
}
}

View File

@ -0,0 +1,21 @@
package cd.casic.module.terminal.common;
/**
* 常量 - 中文
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/11/30 21:00
*/
public interface CnConst {
String CN_USER = "用户";
String CN_ROLE = "角色";
String CN_UNKNOWN = "未知";
String CN_INTRANET_IP = "内网IP";
}

View File

@ -0,0 +1,35 @@
package cd.casic.module.terminal.common;
/**
* 常量
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/6/23 18:49
*/
public interface Const extends cn.orionsec.kit.lang.constant.Const,FieldConst, CnConst{
Integer NOT_DELETE = 0;
Integer IS_DELETED = 1;
int BEARER_PREFIX_LEN = 7;
int MD5_LEN = 32;
Long ROOT_PARENT_ID = 0L;
Integer DEFAULT_SORT = 10;
Long NONE_ID = -1L;
Integer DEFAULT_VERSION = 1;
Long SYSTEM_USER_ID = 0L;
String SYSTEM_USERNAME = "system";
int BATCH_COUNT = 500;
}

View File

@ -0,0 +1,169 @@
package cd.casic.module.terminal.common;
import cn.orionsec.kit.lang.exception.ApplicationException;
import cn.orionsec.kit.lang.exception.argument.InvalidArgumentException;
/**
* 错误信息
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/7 18:51
*/
public interface ErrorMessage {
String MISSING = "{} 不能为空";
String PARAM_MISSING = "参数不能为空";
String PARAM_ERROR = "参数错误";
String ID_MISSING = "id 不能为空";
String INVALID_PARAM = "参数验证失败";
String DATA_MODIFIED = "数据发生变更, 请刷新后重试";
String DATA_ABSENT = "数据不存在";
String KEY_ABSENT = "主机密钥不存在";
String IDENTITY_ABSENT = "主机身份不存在";
String CONFIG_ABSENT = "配置不存在";
String CONFIG_PRESENT = "配置已存在";
String DATA_PRESENT = "数据已存在";
String NAME_PRESENT = "名称已存在";
String CODE_PRESENT = "编码已存在";
String NICKNAME_PRESENT = "花名已存在";
String USERNAME_PRESENT = "用户名已存在";
String ROLE_ABSENT = "角色不存在";
String ROLE_CODE_ABSENT = "角色 [{}] 不存在";
String INVALID_PARENT_MENU = "所选父菜单不合法";
String PARENT_MENU_ABSENT = "父菜单不存在";
String USERNAME_PASSWORD_ERROR = "用户名或密码错误";
String MAX_LOGIN_FAILED = "登录失败次数已上限";
String HISTORY_ABSENT = "历史值不存在";
String USER_ABSENT = "用户不存在";
String HOST_ABSENT = "主机不存在";
String GROUP_ABSENT = "分组不存在";
String HOST_TYPE_ERROR = "主机类型错误";
String HOST_NOT_ENABLED = "主机未启用";
String CONFIG_NOT_ENABLED = "配置未启用";
String UNABLE_OPERATE_ADMIN_ROLE = "无法操作管理员账号";
String UNSUPPORTED_CHARSET = "不支持的编码 [{}]";
String DECRYPT_ERROR = "数据解密失败";
String PASSWORD_MISSING = "请输入密码";
String BEFORE_PASSWORD_ERROR = "原密码错误";
String DATA_NO_PERMISSION = "数据无权限";
String ANY_NO_PERMISSION = "{}无权限";
String SESSION_PRESENT = "会话已存在";
String SESSION_ABSENT = "会话不存在";
String PATH_NOT_NORMALIZE = "路径不合法";
String OPERATE_ERROR = "操作失败";
String UNKNOWN_TYPE = "未知类型";
String ERROR_TYPE = "错误的类型";
String FILE_ABSENT = "文件不存在";
String FILE_ABSENT_CLEAR = "文件不存在 (可能已被清理)";
String LOG_ABSENT = "日志不存在";
String TASK_ABSENT = "任务不存在";
String CONNECT_ERROR = "连接失败";
String AUTH_ERROR = "认证失败";
String FILE_UPLOAD_ERROR = "文件上传失败";
String SCRIPT_UPLOAD_ERROR = "脚本上传失败";
String EXEC_ERROR = "执行失败";
String ILLEGAL_STATUS = "当前状态不支持此操作";
String CHECK_AUTHORIZED_HOST = "请选择已授权的主机";
String FILE_READ_ERROR = "文件读取失败";
String FILE_READ_ERROR_CLEAR = "文件读取失败 (可能已被清理)";
String PLEASE_CHECK_HOST_SSH = "请检查主机 {} 是否存在/权限/SSH配置";
String CLIENT_ABORT = "手动中断";
String UNABLE_DOWNLOAD_FOLDER = "无法下载文件夹";
/**
* 是否为业务异常
*
* @param ex ex
* @return biz exception
*/
static boolean isBizException(Exception ex) {
if (ex == null) {
return false;
}
return ex instanceof InvalidArgumentException
|| ex instanceof IllegalArgumentException
|| ex instanceof ApplicationException;
}
/**
* 获取错误信息
*
* @param ex ex
* @param defaultMsg defaultMsg
* @return message
*/
static String getErrorMessage(Exception ex, String defaultMsg) {
if (ex == null) {
return null;
}
String message = ex.getMessage();
if (message == null) {
return defaultMsg;
}
// 业务异常
if (isBizException(ex)) {
return message;
}
return defaultMsg;
}
}

View File

@ -0,0 +1,51 @@
package cd.casic.module.terminal.common;
/**
* 额外字段常量
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/28 18:34
*/
public interface ExtraFieldConst extends FieldConst {
String USER_ID = "userId";
String TRACE_ID = "traceId";
String IDENTITY = "identity";
String GROUP_NAME = "groupName";
String ID_LIST = "idList";
String USERNAME = "username";
String HOME = "home";
String STATUS_NAME = "statusName";
String KEY_NAME = "keyName";
String POSITION_NAME = "positionName";
String GRANT_TYPE = "grantType";
String GRANT_NAME = "grantName";
String CHANNEL_ID = "channelId";
String SESSION_ID = "sessionId";
String CONNECT_TYPE = "connectType";
String HOST_ID = "hostId";
String HOST_NAME = "hostName";
String LOG_ID = "logId";
String DARK = "dark";
}

View File

@ -0,0 +1,89 @@
package cd.casic.module.terminal.common;
/**
* 字段常量
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/10/17 12:44
*/
public interface FieldConst {
String ID = "id";
String KEY = "key";
String CODE = "code";
String NAME = "name";
String TITLE = "title";
String VALUE = "value";
String LABEL = "label";
String TYPE = "type";
String COLOR = "color";
String STATUS = "status";
String INFO = "info";
String EXTRA = "extra";
String REL_ID = "relId";
String BEFORE = "before";
String AFTER = "after";
String SOURCE = "source";
String TARGET = "target";
String CHARSET = "charset";
String TOKEN = "token";
String SEQ = "seq";
String PATH = "path";
String ADDRESS = "address";
String MOD = "mod";
String COUNT = "count";
String DATE = "date";
String TIME = "time";
String TEXT = "text";
String ISSUE = "issue";
String EXPIRE = "expire";
String LOCATION = "location";
String USER_AGENT = "userAgent";
String ERROR_MESSAGE = "errorMessage";
String UUID = "uuid";
String REDIRECT = "redirect";
String SCHEMA = "schema";
String FILTER = "filter";
String ALL = "all";
String CONFIG = "config";
}

View File

@ -0,0 +1,36 @@
package cd.casic.module.terminal.common;
import com.alibaba.fastjson.JSON;
import java.util.Map;
/**
* 标准数据模型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/20 22:07
*/
public interface GenericsDataModel {
/**
* 序列化
*
* @return json
*/
default String serial() {
return JSON.toJSONString(this);
}
/**
* 转为 map
*
* @return map
*/
default Map<String, Object> toMap() {
return JSON.parseObject(this.serial());
}
}

View File

@ -0,0 +1,53 @@
package cd.casic.module.terminal.common;
/**
* 标准数据处理策略
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/20 22:09
*/
public interface GenericsDataStrategy<M extends GenericsDataModel> {
/**
* 获取默认值
*
* @return 默认值
*/
M getDefault();
/**
* 执行完整验证链
* <p>
* preValid > updateFill > valid
*
* @param beforeModel beforeModel
* @param afterModel afterModel
*/
void doValid(M beforeModel, M afterModel);
/**
* 解析数据
*
* @param serialModel serialModel
* @return model
*/
M parse(String serialModel);
/**
* 转为视图配置
*
* @param model model
*/
void toView(M model);
/**
* 转为视图配置
*
* @param serialModel serialModel
* @return model
*/
M toView(String serialModel);
}

View File

@ -0,0 +1,75 @@
package cd.casic.module.terminal.common;
import cd.casic.module.terminal.common.holder.SpringHolder;
/**
* 标准数据定义
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/21 0:07
*/
@SuppressWarnings("unchecked")
public interface GenericsStrategyDefinition {
/**
* 获取数据处理策略
*
* @return class
*/
Class<? extends GenericsDataStrategy<? extends GenericsDataModel>> getStrategyClass();
/**
* 获取数据模型策略处理器
*
* @param <M> Model
* @param <S> Strategy
* @return Strategy Bean
*/
default <M extends GenericsDataModel, S extends GenericsDataStrategy<M>> S getStrategy() {
return (S) SpringHolder.getBean(this.getStrategyClass());
}
/**
* 获取默认值
*
* @param <M> model
* @return model
*/
default <M extends GenericsDataModel> M getDefault() {
return (M) this.getStrategy().getDefault();
}
/**
* 执行完整验证链
*
* @param beforeModel beforeModel
* @param afterModel afterModel
*/
default void doValid(GenericsDataModel beforeModel, GenericsDataModel afterModel) {
this.getStrategy().doValid(beforeModel, afterModel);
}
/**
* 反序列化对象
*
* @param serialModel serialModel
* @param <M> model
* @return model
*/
default <M extends GenericsDataModel> M parse(String serialModel) {
return (M) this.getStrategy().parse(serialModel);
}
/**
* 转为视图对象
*
* @param serialModel serialModel
* @param <M> model
* @return viewModel
*/
default <M extends GenericsDataModel> M toView(String serialModel) {
return (M) this.getStrategy().toView(serialModel);
}
}

View File

@ -0,0 +1,38 @@
package cd.casic.module.terminal.common;
import cn.orionsec.kit.lang.utils.Arrays1;
import jakarta.annotation.PostConstruct;
import cd.casic.module.terminal.common.annotation.Module;
/**
* 操作类型初始化器
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/10/13 17:45
*/
public abstract class InitializingOperatorTypes implements OperatorTypeDefinition {
@PostConstruct
public void init() {
// 获取模块注解
Module moduleDefinition = this.getClass().getAnnotation(Module.class);
if (moduleDefinition == null) {
return;
}
// 获取类型
OperatorType[] types = this.types();
if (Arrays1.isEmpty(types)) {
return;
}
// 定义类型
String module = moduleDefinition.value();
for (OperatorType type : types) {
type.setModule(module);
OperatorTypeHolder.set(type);
}
}
}

View File

@ -0,0 +1,103 @@
package cd.casic.module.terminal.common;
import lombok.Data;
import java.util.Date;
/**
* 操作日志模型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/10/9 18:44
*/
@Data
public class OperatorLogModel{
/**
* userId
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* traceId
*/
private String traceId;
/**
* 请求 ip
*/
private String address;
/**
* 请求地址
*/
private String location;
/**
* user-agent
*/
private String userAgent;
/**
* 日志
*/
private String logInfo;
/**
* 风险等级
*/
private String riskLevel;
/**
* 模块
*/
private String module;
/**
* 操作类型
*/
private String type;
/**
* 参数
*/
private String extra;
/**
* 操作结果 0失败 1成功
*/
private Integer result;
/**
* 错误信息
*/
private String errorMessage;
/**
* 返回值
*/
private String returnValue;
/**
* 操作时间
*/
private Integer duration;
/**
* 开始时间
*/
private Date startTime;
/**
* 结束时间
*/
private Date endTime;
}

View File

@ -0,0 +1,193 @@
package cd.casic.module.terminal.common;
import cd.casic.framework.security.core.LoginUser;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Maps;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializeFilter;
import java.util.Map;
/**
* 操作日志工具类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/10/10 11:32
*/
public class OperatorLogs implements ExtraFieldConst {
private static final String UN_SAVE_FLAG = "__un__save__";
private static SerializeFilter[] serializeFilters;
/**
* 拓展信息
*/
private static final ThreadLocal<Map<String, Object>> EXTRA_HOLDER = ThreadLocal.withInitial(Maps::newMap);
/**
* 当前用户 优先于登录用户
*/
private static final ThreadLocal<LoginUser> USER_HOLDER = new ThreadLocal<>();
private OperatorLogs() {
}
/**
* 添加日志参数
*
* @param key key
* @param value value
*/
public static void add(String key, Object value) {
EXTRA_HOLDER.get().put(key, value);
}
/**
* 添加日志参数 json
*
* @param key key
* @param value value
*/
public static void addJson(String key, Object value) {
EXTRA_HOLDER.get().put(key, JSON.parseObject(toJsonString(value)));
}
/**
* 添加日志参数
*
* @param map map
*/
public static void add(Map<String, ?> map) {
EXTRA_HOLDER.get().putAll(map);
}
/**
* 添加日志参数
*
* @param obj obj
*/
@SuppressWarnings("unchecked")
public static void add(Object obj) {
if (obj == null) {
return;
}
// if (obj instanceof JSONObject || obj instanceof com.alibaba.fastjson2.JSONObject) {
if (obj instanceof JSONObject || obj instanceof JSONObject) {
EXTRA_HOLDER.get().putAll(JSON.parseObject(toJsonString(obj)));
} else if (obj instanceof Map) {
EXTRA_HOLDER.get().putAll((Map<String, ?>) obj);
} else {
EXTRA_HOLDER.get().putAll(JSON.parseObject(toJsonString(obj)));
}
}
/**
* 获取 json
*
* @param value value
* @return json
*/
public static String toJsonString(Object value) {
return JSON.toJSONString(value, serializeFilters);
}
/**
* 设置不保存
*/
public static void unSave() {
setSave(false);
}
/**
* 设置是否保存
*
* @param save save
*/
public static void setSave(boolean save) {
if (save) {
EXTRA_HOLDER.get().remove(UN_SAVE_FLAG);
} else {
EXTRA_HOLDER.get().put(UN_SAVE_FLAG, UN_SAVE_FLAG);
}
}
/**
* 设置是否保存
*
* @return save
*/
public static boolean isSave() {
return !UN_SAVE_FLAG.equals(EXTRA_HOLDER.get().get(UN_SAVE_FLAG));
}
/**
* 获取参数
*
* @return map
*/
public static Map<String, Object> get() {
return EXTRA_HOLDER.get();
}
/**
* 设置用户信息
*
* @param user user
*/
public static void setUser(LoginUser user) {
USER_HOLDER.set(user);
}
/**
* 获取用户
*
* @return user
*/
public static LoginUser getUser() {
return USER_HOLDER.get();
}
/**
* 清空
*/
public static void clear() {
EXTRA_HOLDER.remove();
USER_HOLDER.remove();
}
/**
* 清空 html tag
*
* @param log log
* @return cleared
*/
public static String clearHtmlTag(String log) {
if (Strings.isBlank(log)) {
return log;
}
return log.replaceAll("<sb 0>", "")
.replaceAll("<sb 2>", "")
.replaceAll("<sb>", "")
.replaceAll("</sb>", "")
.replaceAll("<sr 0>", "")
.replaceAll("<sr 2>", "")
.replaceAll("<sr>", "")
.replaceAll("</sr>", "")
.replaceAll("<b>", "")
.replaceAll("</b>", "")
.replaceAll("<br/>", "\n");
}
public static void setSerializeFilters(SerializeFilter[] serializeFilters) {
if (OperatorLogs.serializeFilters != null) {
// unmodified
throw Exceptions.state();
}
OperatorLogs.serializeFilters = serializeFilters;
}
}

View File

@ -0,0 +1,33 @@
package cd.casic.module.terminal.common;
import lombok.Getter;
/**
* 操作风险等级
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/10/12 14:19
*/
@Getter
public enum OperatorRiskLevel {
/**
* 低风险
*/
L,
/**
* 中风险
*/
M,
/**
* 高风险
*/
H,
;
}

View File

@ -0,0 +1,53 @@
package cd.casic.module.terminal.common;
import lombok.Getter;
/**
* 操作类型定义
* <p>
* 因为枚举需要实现 注解中不可以使用 则需要使用对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/10/10 10:29
*/
@Getter
public class OperatorType {
/**
* 风险等级
*/
private final OperatorRiskLevel riskLevel;
/**
* 模块
*/
private String module;
/**
* 类型
*/
private final String type;
/**
* 模板
*/
private final String template;
public OperatorType(OperatorRiskLevel riskLevel, String type, String template) {
this(riskLevel, null, type, template);
}
public OperatorType(OperatorRiskLevel riskLevel, String module, String type, String template) {
this.riskLevel = riskLevel;
this.module = module;
this.type = type;
this.template = template;
}
public void setModule(String module) {
this.module = module;
}
}

View File

@ -0,0 +1,21 @@
package cd.casic.module.terminal.common;
/**
* 操作类型定义
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/10/13 18:23
*/
public interface OperatorTypeDefinition {
/**
* 获取操作类型
*
* @return 操作类型
*/
OperatorType[] types();
}

View File

@ -0,0 +1,40 @@
package cd.casic.module.terminal.common;
import java.util.HashMap;
import java.util.Map;
/**
* 操作日志类型实例
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/10/10 14:43
*/
public class OperatorTypeHolder {
private static final Map<String, OperatorType> TYPES = new HashMap<>();
private OperatorTypeHolder() {
}
/**
* 获取类型
*
* @param key key
* @return type
*/
public static OperatorType get(String key) {
return TYPES.get(key);
}
/**
* 设置类型
*
* @param type type
*/
public static void set(OperatorType type) {
TYPES.put(type.getType(), type);
}
}

View File

@ -0,0 +1,21 @@
package cd.casic.module.terminal.common;
/**
* rsa 解密器
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/5 20:58
*/
public interface RsaDecryptor {
/**
* 解密
*
* @param value value
* @return value
*/
String decrypt(String value);
}

View File

@ -0,0 +1,36 @@
package cd.casic.module.terminal.common;
import cd.casic.module.terminal.common.config.ConfigRef;
import cd.casic.module.terminal.common.config.ConfigStore;
import cd.casic.module.terminal.common.constant.ConfigKeys;
import cn.orionsec.kit.lang.utils.crypto.RSA;
import java.security.interfaces.RSAPrivateKey;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* rsa 加密器
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/7 11:32
*/
public class RsaDecryptorImpl implements RsaDecryptor {
private static final String SPLIT = "\\|";
private final ConfigRef<RSAPrivateKey> privateKey;
public RsaDecryptorImpl(ConfigStore configStore) {
this.privateKey = configStore.ref(ConfigKeys.ENCRYPT_PRIVATE_KEY, RSA::getPrivateKey);
}
@Override
public String decrypt(String value) {
return Arrays.stream(value.split(SPLIT))
.map(s -> RSA.decrypt(s, privateKey.value))
.collect(Collectors.joining());
}
}

View File

@ -0,0 +1,99 @@
package cd.casic.module.terminal.common;
import cn.orionsec.kit.lang.id.UUIds;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.MDC;
/**
* traceId 持有者
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/6/16 17:35
*/
public class TraceIdHolder {
public static final String TRACE_ID_HEADER = "trace-id";
public static final String TRACE_ID_MDC = "tid";
private TraceIdHolder() {
}
/**
* 请求序列
*/
private static final ThreadLocal<String> HOLDER = new TransmittableThreadLocal<>();
/**
* 获取 traceId
*
* @return traceId
*/
public static String get() {
return HOLDER.get();
}
/**
* 设置 traceId
*/
public static void set() {
set(createTraceId());
}
/**
* 设置 traceId
*
* @param traceId traceId
*/
public static void set(String traceId) {
// 设置应用上下文
HOLDER.set(traceId);
// 设置日志上下文
setMdc(traceId);
}
/**
* 删除 traceId
*/
public static void remove() {
// 移除应用上下文
HOLDER.remove();
// 移除日志上下文
removeMdc();
}
/**
* 从应用上下文 设置到日志上下文
*/
public static void setMdc() {
setMdc(HOLDER.get());
}
/**
* 设置到日志上下文
*
* @param traceId traceId
*/
public static void setMdc(String traceId) {
MDC.put(TRACE_ID_MDC, traceId);
}
/**
* 移除日志上下文
*/
public static void removeMdc() {
MDC.remove(TRACE_ID_MDC);
}
/**
* 创建 traceId
*
* @return traceId
*/
public static String createTraceId() {
return UUIds.random32();
}
}

View File

@ -0,0 +1,36 @@
package cd.casic.module.terminal.common;
import java.io.Serializable;
/**
* 更新密码操作
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/9/21 11:32
*/
public interface UpdatePasswordAction extends Serializable {
/**
* 是否使用新密码
*
* @return use
*/
Boolean getUseNewPassword();
/**
* 获取密码
*
* @return password
*/
String getPassword();
/**
* 设置密码
*
* @param password password
*/
void setPassword(String password);
}

View File

@ -0,0 +1,119 @@
package cd.casic.module.terminal.common;
import org.springframework.http.HttpHeaders;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketExtension;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.security.Principal;
import java.util.List;
import java.util.Map;
/**
* web socket 同步会话
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/5/20 10:12
*/
public class WebSocketSyncSession implements WebSocketSession {
private final WebSocketSession delegate;
public WebSocketSyncSession(WebSocketSession delegate) {
this.delegate = delegate;
}
@Override
public String getId() {
return this.delegate.getId();
}
@Override
public URI getUri() {
return this.delegate.getUri();
}
@Override
public HttpHeaders getHandshakeHeaders() {
return this.delegate.getHandshakeHeaders();
}
@Override
public Map<String, Object> getAttributes() {
return this.delegate.getAttributes();
}
@Override
public Principal getPrincipal() {
return this.delegate.getPrincipal();
}
@Override
public InetSocketAddress getLocalAddress() {
return this.delegate.getLocalAddress();
}
@Override
public InetSocketAddress getRemoteAddress() {
return this.delegate.getRemoteAddress();
}
@Override
public String getAcceptedProtocol() {
return this.delegate.getAcceptedProtocol();
}
@Override
public void setTextMessageSizeLimit(int messageSizeLimit) {
this.delegate.setTextMessageSizeLimit(messageSizeLimit);
}
@Override
public int getTextMessageSizeLimit() {
return this.delegate.getTextMessageSizeLimit();
}
@Override
public void setBinaryMessageSizeLimit(int messageSizeLimit) {
this.delegate.setBinaryMessageSizeLimit(messageSizeLimit);
}
@Override
public int getBinaryMessageSizeLimit() {
return this.delegate.getBinaryMessageSizeLimit();
}
@Override
public List<WebSocketExtension> getExtensions() {
return this.delegate.getExtensions();
}
@Override
public void sendMessage(WebSocketMessage<?> message) throws IOException {
synchronized (this.delegate) {
this.delegate.sendMessage(message);
}
}
@Override
public boolean isOpen() {
return this.delegate.isOpen();
}
@Override
public void close() throws IOException {
this.delegate.close();
}
@Override
public void close(CloseStatus status) throws IOException {
this.delegate.close(status);
}
}

View File

@ -0,0 +1,128 @@
package cd.casic.module.terminal.common;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Threads;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
/**
* websocket 工具类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2021/6/14 0:36
*/
@Slf4j
public class WebSockets {
private WebSockets() {
}
/**
* 创建同步会话
*
* @param session session
* @return session
*/
public static WebSocketSession createSyncSession(WebSocketSession session) {
return new WebSocketSyncSession(session);
}
/**
* 获取属性
*
* @param channel channel
* @param attr attr
* @param <E> T
* @return T
*/
@SuppressWarnings("unchecked")
public static <E> E getAttr(WebSocketSession channel, String attr) {
return (E) channel.getAttributes().get(attr);
}
/**
* 发送消息 忽略并发报错
*
* @param session session
* @param message message
*/
public static void sendJson(WebSocketSession session, Object message) {
sendText(session, JSON.toJSONString(message));
}
/**
* 发送消息 忽略并发报错
*
* @param session session
* @param message message
*/
public static void sendText(WebSocketSession session, String message) {
if (!session.isOpen()) {
return;
}
try {
if (session instanceof WebSocketSyncSession) {
// 发送消息
session.sendMessage(new TextMessage(message));
} else {
synchronized (session) {
// 发送消息
session.sendMessage(new TextMessage(message));
}
}
} catch (IllegalStateException e) {
// 并发异常
log.error("发送消息失败, 准备进行重试 {}", Exceptions.getDigest(e));
// 并发重试
retrySendText(session, message, Const.MS_100);
} catch (IOException e) {
throw Exceptions.ioRuntime(e);
}
}
/**
* 重试发送消息 忽略并发报错
*
* @param session session
* @param message message
* @param delay delay
*/
public static void retrySendText(WebSocketSession session, String message, long delay) {
if (!session.isOpen()) {
return;
}
try {
Threads.sleep(delay);
session.sendMessage(new TextMessage(message));
log.info("消息重发成功");
Threads.sleep(delay);
} catch (Exception ex) {
throw Exceptions.ioRuntime(ex);
}
}
/**
* 关闭会话
*
* @param session session
* @param code code
*/
public static void close(WebSocketSession session, WsCloseCode code) {
if (!session.isOpen()) {
return;
}
try {
session.close(new CloseStatus(code.getCode(), code.getReason()));
} catch (Exception e) {
log.error("websocket close failure", e);
}
}
}

View File

@ -0,0 +1,27 @@
package cd.casic.module.terminal.common;
/**
* ws 服务端关闭 code
*
* @author Jiahang Li
* @version 1.0.0
* @since 2021/6/16 15:18
*/
public interface WsCloseCode {
/**
* code
*
* @return code
*/
int getCode();
/**
* reason
*
* @return reason
*/
String getReason();
}

View File

@ -0,0 +1,25 @@
package cd.casic.module.terminal.common.annotation;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
/**
* 模块
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/10/9 18:44
*/
@Component
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Module {
/**
* 模块
*/
String value();
}

View File

@ -0,0 +1,58 @@
package cd.casic.module.terminal.common.config;
import lombok.extern.slf4j.Slf4j;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* 配置引用
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/6 18:01
*/
@Slf4j
public abstract class ConfigRef<T> {
public final String key;
public T value;
protected final Function<String, T> convert;
public ConfigRef(String key, Function<String, T> convert) {
this.key = key;
this.convert = convert;
}
/**
* 覆盖配置
*
* @param value value
*/
public abstract void override(String value);
/**
* 修改配置
*
* @param value value
*/
public abstract void set(T value);
/**
* 获取配置
*
* @return value
*/
public abstract T get();
/**
* 修改回调
*
* @param changeEvent changeEvent
* @return this
*/
public abstract ConfigRef<T> onChange(BiConsumer<T, T> changeEvent);
}

View File

@ -0,0 +1,60 @@
package cd.casic.module.terminal.common.config;
import cn.orionsec.kit.lang.utils.Objects1;
import lombok.extern.slf4j.Slf4j;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* 配置引用实现类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/14 16:10
*/
@Slf4j
public class ConfigRefImpl<T> extends ConfigRef<T> {
protected BiConsumer<T, T> changeEvent;
public ConfigRefImpl(String key, Function<String, T> convert) {
super(key, convert);
}
@Override
public void override(String value) {
try {
this.set(convert.apply(value));
} catch (Exception e) {
log.error("ConfigRef trigger override error key: {}, value: {}", key, value, e);
}
}
@Override
public void set(T value) {
T before = this.value;
this.value = value;
// 被修改
if (!Objects1.eq(before, value)) {
log.info("ConfigRef changed key: {}, value: {}", key, value);
// 触发事件
if (changeEvent != null) {
changeEvent.accept(value, before);
}
}
}
@Override
public T get() {
return value;
}
@Override
public ConfigRef<T> onChange(BiConsumer<T, T> changeEvent) {
this.changeEvent = changeEvent;
return this;
}
}

View File

@ -0,0 +1,244 @@
package cd.casic.module.terminal.common.config;
import java.util.function.Function;
/**
* 配置中心
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/5 21:30
*/
public interface ConfigStore {
/**
* 获取 string 配置
*
* @param key key
* @return config
*/
String getString(String key);
/**
* 获取 string 配置
*
* @param key key
* @param defaultValue defaultValue
* @return config
*/
String getString(String key, String defaultValue);
/**
* 获取 int 配置
*
* @param key key
* @return config
*/
Integer getInteger(String key);
/**
* 获取 int 配置
*
* @param key key
* @param defaultValue defaultValue
* @return config
*/
Integer getInteger(String key, Integer defaultValue);
/**
* 获取 long 配置
*
* @param key key
* @return config
*/
Long getLong(String key);
/**
* 获取 long 配置
*
* @param key key
* @param defaultValue defaultValue
* @return config
*/
Long getLong(String key, Long defaultValue);
/**
* 获取 double 配置
*
* @param key key
* @return config
*/
Double getDouble(String key);
/**
* 获取 double 配置
*
* @param key key
* @param defaultValue defaultValue
* @return config
*/
Double getDouble(String key, Double defaultValue);
/**
* 获取 boolean 配置
*
* @param key key
* @return config
*/
Boolean getBoolean(String key);
/**
* 获取 boolean 配置
*
* @param key key
* @param defaultValue defaultValue
* @return config
*/
Boolean getBoolean(String key, Boolean defaultValue);
/**
* 获取配置
*
* @param key key
* @return conf
*/
String getConfig(String key);
/**
* 获取配置
*
* @param key key
* @param defaultValue defaultValue
* @return conf
*/
String getConfig(String key, String defaultValue);
/**
* 获取配置
*
* @param key key
* @param convert convert
* @param <T> T
* @return conf
*/
<T> T getConfig(String key, Function<String, T> convert);
/**
* 获取配置
*
* @param key key
* @param convert convert
* @param defaultValue defaultValue
* @param <T> T
* @return conf
*/
<T> T getConfig(String key, Function<String, T> convert, T defaultValue);
/**
* 获取 string 配置
*
* @param key key
* @return ref
*/
ConfigRef<String> string(String key);
/**
* 获取 string 配置
*
* @param key key
* @param defaultValue defaultValue
* @return ref
*/
ConfigRef<String> string(String key, String defaultValue);
/**
* 获取 int 配置
*
* @param key key
* @return ref
*/
ConfigRef<Integer> int32(String key);
/**
* 获取 int 配置
*
* @param key key
* @param defaultValue defaultValue
* @return ref
*/
ConfigRef<Integer> int32(String key, Integer defaultValue);
/**
* 获取 long 配置
*
* @param key key
* @return ref
*/
ConfigRef<Long> int64(String key);
/**
* 获取 long 配置
*
* @param key key
* @param defaultValue defaultValue
* @return ref
*/
ConfigRef<Long> int64(String key, Long defaultValue);
/**
* 获取 double 配置
*
* @param key key
* @return ref
*/
ConfigRef<Double> float64(String key);
/**
* 获取 double 配置
*
* @param key key
* @param defaultValue defaultValue
* @return ref
*/
ConfigRef<Double> float64(String key, Double defaultValue);
/**
* 获取 boolean 配置
*
* @param key key
* @return ref
*/
ConfigRef<Boolean> bool(String key);
/**
* 获取 boolean 配置
*
* @param key key
* @param defaultValue defaultValue
* @return ref
*/
ConfigRef<Boolean> bool(String key, Boolean defaultValue);
/**
* 获取配置
*
* @param key key
* @param convert convert
* @param <T> T
* @return ref
*/
<T> ConfigRef<T> ref(String key, Function<String, T> convert);
/**
* 获取配置
*
* @param key key
* @param convert convert
* @param defaultValue defaultValue
* @param <T> T
* @return ref
*/
<T> ConfigRef<T> ref(String key, Function<String, T> convert, T defaultValue);
}

View File

@ -0,0 +1,33 @@
package cd.casic.module.terminal.common.config;
/**
* 可控配置中心
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/6 23:00
*/
public interface ManagementConfigStore extends ConfigStore {
/**
* 加载全部配置
*/
void loadAllConfig();
/**
* 覆盖配置
*
* @param key key
* @param value value
*/
void override(String key, String value);
/**
* 注册 ref
*
* @param ref ref
*/
void register(ConfigRef<?> ref);
}

View File

@ -0,0 +1,202 @@
package cd.casic.module.terminal.common.config;
import cn.orionsec.kit.lang.utils.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
/**
* 配置中心实现
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/6 17:20
*/
@Slf4j
@Component
public class ManagementConfigStoreImpl implements ManagementConfigStore {
private final ConcurrentHashMap<String, String> configMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, List<ConfigRef<?>>> configRefs = new ConcurrentHashMap<>();
@Override
public void loadAllConfig() {
/* configMap.putAll(service.getAllConfig());*/
}
@Override
public void override(String key, String value) {
log.info("ConfigStore.override key: {}, value: {}", key, value);
// 修改配置
configMap.put(key, value);
// 修改引用
List<ConfigRef<?>> refs = configRefs.get(key);
if (!Lists.isEmpty(refs)) {
refs.forEach(s -> s.override(value));
}
}
@Override
public void register(ConfigRef<?> ref) {
String key = ref.key;
log.info("ConfigStore.register ref key: {}", key);
// 注册引用
configRefs.computeIfAbsent(key, k -> new ArrayList<>()).add(ref);
}
@Override
public String getString(String key) {
return this.getConfig(key);
}
@Override
public String getString(String key, String defaultValue) {
return this.getConfig(key, defaultValue);
}
@Override
public Integer getInteger(String key) {
return this.getConfig(key, Integer::valueOf, null);
}
@Override
public Integer getInteger(String key, Integer defaultValue) {
return this.getConfig(key, Integer::valueOf, defaultValue);
}
@Override
public Long getLong(String key) {
return this.getConfig(key, Long::valueOf, null);
}
@Override
public Long getLong(String key, Long defaultValue) {
return this.getConfig(key, Long::valueOf, defaultValue);
}
@Override
public Double getDouble(String key) {
return this.getConfig(key, Double::valueOf, null);
}
@Override
public Double getDouble(String key, Double defaultValue) {
return this.getConfig(key, Double::valueOf, defaultValue);
}
@Override
public Boolean getBoolean(String key) {
return this.getConfig(key, Boolean::valueOf, null);
}
@Override
public Boolean getBoolean(String key, Boolean defaultValue) {
return this.getConfig(key, Boolean::valueOf, defaultValue);
}
@Override
public String getConfig(String key) {
return this.getConfig(key, Function.identity(), null);
}
@Override
public String getConfig(String key, String defaultValue) {
return this.getConfig(key, Function.identity(), defaultValue);
}
@Override
public <T> T getConfig(String key, Function<String, T> convert) {
return this.getConfig(key, convert, null);
}
@Override
public <T> T getConfig(String key, Function<String, T> convert, T defaultValue) {
// 获取配置
String conf = configMap.get(key);
// 默认值
if (conf == null) {
return defaultValue;
}
// 转换
return convert.apply(conf);
}
@Override
public ConfigRef<String> string(String key) {
return this.ref(key, Function.identity(), null);
}
@Override
public ConfigRef<String> string(String key, String defaultValue) {
return this.ref(key, Function.identity(), defaultValue);
}
@Override
public ConfigRef<Integer> int32(String key) {
return this.ref(key, Integer::valueOf, null);
}
@Override
public ConfigRef<Integer> int32(String key, Integer defaultValue) {
return this.ref(key, Integer::valueOf, defaultValue);
}
@Override
public ConfigRef<Long> int64(String key) {
return this.ref(key, Long::valueOf, null);
}
@Override
public ConfigRef<Long> int64(String key, Long defaultValue) {
return this.ref(key, Long::valueOf, defaultValue);
}
@Override
public ConfigRef<Double> float64(String key) {
return this.ref(key, Double::valueOf, null);
}
@Override
public ConfigRef<Double> float64(String key, Double defaultValue) {
return this.ref(key, Double::valueOf, defaultValue);
}
@Override
public ConfigRef<Boolean> bool(String key) {
return this.ref(key, Boolean::valueOf, null);
}
@Override
public ConfigRef<Boolean> bool(String key, Boolean defaultValue) {
return this.ref(key, Boolean::valueOf, defaultValue);
}
@Override
public <T> ConfigRef<T> ref(String key, Function<String, T> convert) {
return this.ref(key, convert, null);
}
@Override
public <T> ConfigRef<T> ref(String key, Function<String, T> convert, T defaultValue) {
// 创建引用
ConfigRef<T> ref = new ConfigRefImpl<>(key, convert);
// 设置值
String value = configMap.get(key);
if (value != null) {
ref.override(value);
} else {
ref.set(defaultValue);
}
// 注册引用
this.register(ref);
return ref;
}
}

View File

@ -0,0 +1,35 @@
package cd.casic.module.terminal.common.constant;
/**
* 配置项常量
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/1/14 16:15
*/
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_PRIVATE_KEY = "encrypt_privateKey";
}

View File

@ -0,0 +1,132 @@
package cd.casic.module.terminal.common.holder;
import cd.casic.module.terminal.utils.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ConfigurableApplicationContext;
/**
*
* bean 工具类
* @author Yuru Pu
* @version 1.0
* @since 2025/7/8 17:44
*/
public class SpringHolder {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringHolder.class);
private static ApplicationContext applicationContext;
private static ConfigurableListableBeanFactory beanFactory;
private SpringHolder() {
}
public static class ApplicationContextAwareStore implements ApplicationContextAware, BeanFactoryPostProcessor {
public ApplicationContextAwareStore() {
LOGGER.info("init spring holder");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
LOGGER.info("inject spring holder ApplicationContext");
SpringHolder.applicationContext = applicationContext;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
LOGGER.info("inject spring holder BeanFactory");
SpringHolder.beanFactory = configurableListableBeanFactory;
}
}
/**
* 获取上下文容器
*
* @return ignore
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static ConfigurableListableBeanFactory getBeanFactory() {
return beanFactory;
}
/**
* 发布事件
*
* @param event event
*/
public static void publishEvent(ApplicationEvent event) {
applicationContext.publishEvent(event);
}
/**
* 发布事件
*
* @param event event
*/
public static void publishEvent(Object event) {
applicationContext.publishEvent(event);
}
@SuppressWarnings("unchecked")
public static <T> T getBean(String beanName) {
return ((T) applicationContext.getBean(beanName));
}
public static <T> T getBean(Class<T> type) {
return applicationContext.getBean(type);
}
public static <T> T getBean(String beanName, Class<T> type) {
return applicationContext.getBean(beanName, type);
}
public static boolean containsBean(String beanName) {
return applicationContext.containsBean(beanName);
}
public static String[] getBeanNames() {
return applicationContext.getBeanDefinitionNames();
}
public static boolean isSingletonBean(String beanName) {
return applicationContext.isSingleton(beanName);
}
public static Class<?> getType(String beanName) {
return applicationContext.getType(beanName);
}
public static boolean isType(String beanName, Class<?> beanType) {
return applicationContext.isTypeMatch(beanName, beanType);
}
public static String[] getBeanAliases(String beanName) {
return applicationContext.getAliases(beanName);
}
public static void close() {
if (applicationContext instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) applicationContext).close();
}
}
public static void refresh() {
Valid.isInstanceOf(applicationContext, ConfigurableApplicationContext.class);
ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) SpringHolder.applicationContext;
applicationContext.refresh();
}
}

View File

@ -0,0 +1,26 @@
package cd.casic.module.terminal.configuration;
import cd.casic.module.terminal.common.holder.SpringHolder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
*
* spring 配置类
*
* @author Yuru Pu
* @version 1.0
* @since 2025/7/10 14:46
*/
@Configuration
public class SpringConfiguration {
/**
* @return spring 容器工具类
*/
@Bean
public SpringHolder.ApplicationContextAwareStore springHolderAware() {
return new SpringHolder.ApplicationContextAwareStore();
}
}

View File

@ -0,0 +1,46 @@
package cd.casic.module.terminal.controller;
import cd.casic.framework.commons.pojo.CommonResult;
import cd.casic.module.terminal.service.impl.TerminalService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static cd.casic.framework.commons.pojo.CommonResult.success;
/**
*
* 终端连接
* @author Yuru Pu
* @version 1.0
* @since 2025/7/8 11:36
*/
@Tag(name = "管理后台 - 终端")
@RestController
@RequestMapping("/asset/terminal")
@Validated
public class TerminalController {
@Resource
private TerminalService terminalService;
@GetMapping("/access")
@Operation(summary = "获取终端key")
@PreAuthorize("@ss.hasPermission('terminal:access')")
public CommonResult getTerminalAccessToken() {
return success(terminalService.getTerminalAccessToken());
}
@GetMapping("/transfer")
@Operation(summary = "获取终端 transferToken")
@PreAuthorize("@ss.hasPermission('asset:terminal:access')")
public CommonResult<String> getTerminalTransferToken() {
return success(terminalService.getTerminalTransferToken());
}
}

View File

@ -0,0 +1,63 @@
package cd.casic.module.terminal.controller;
import cd.casic.module.terminal.service.TerminalSftpService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import jakarta.servlet.http.HttpServletResponse;
/**
* SFTP 操作服务 api
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-26 22:09
*/
@Tag(name = "asset - SFTP 操作服务")
@Slf4j
@Validated
@RestController
@RequestMapping("/asset/terminal-sftp")
public class TerminalSftpController {
@Resource
private TerminalSftpService terminalSftpService;
@GetMapping("/get-content")
@Operation(summary = "获取文件内容")
@Parameter(name = "token", description = "token", required = true)
public void getFileContentByToken(@RequestParam("token") String token, HttpServletResponse response) throws Exception {
terminalSftpService.getFileContentByToken(token, response);
}
@PostMapping("/set-content")
@Operation(summary = "设置文件内容")
@Parameter(name = "token", description = "token", required = true)
@Parameter(name = "file", description = "file", required = true)
public Boolean setFileContentByToken(@RequestParam("token") String token,
@RequestParam("file") MultipartFile file) throws Exception {
terminalSftpService.setFileContentByToken(token, file);
return true;
}
@PermitAll
@GetMapping("/download")
@Operation(summary = "下载文件")
@Parameter(name = "channelId", description = "channelId", required = true)
@Parameter(name = "transferToken", description = "transferToken", required = true)
public StreamingResponseBody downloadWithTransferToken(@RequestParam("channelId") String channelId,
@RequestParam("transferToken") String transferToken,
HttpServletResponse response) {
return terminalSftpService.downloadWithTransferToken(channelId, transferToken, response);
}
}

View File

@ -0,0 +1,31 @@
package cd.casic.module.terminal.controller.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 终端访问参数
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/26 15:47
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
//@DesensitizeObject todo 自动脱敏的注解
@Schema(name = "TerminalAccessDTO", description = "终端访问参数")
public class TerminalAccessDTO {
@Schema(description = "userId")
private Long userId;
@Schema(description = "username")
private String username;
}

View File

@ -0,0 +1,86 @@
package cd.casic.module.terminal.controller.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 终端连接参数
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/26 15:47
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
//@DesensitizeObject
@Schema(name = "TerminalConnectDTO", description = "终端连接参数")
public class TerminalConnectDTO {
@Schema(description = "logId")
private Long logId;
@Schema(description = "连接类型")
private String connectType;
@Schema(description = "hostId")
private Long hostId;
@Schema(description = "hostName")
private String hostName;
@Schema(description = "主机编码")
private String hostCode;
@Schema(description = "主机地址")
private String hostAddress;
@Schema(description = "主机端口")
private Integer hostPort;
@Schema(description = "系统类型")
private String osType;
@Schema(description = "系统架构")
private String archType;
@Schema(description = "超时时间")
private Integer timeout;
@Schema(description = "SSH输出编码")
private String charset;
@Schema(description = "文件名称编码")
private String fileNameCharset;
@Schema(description = "文件内容编码")
private String fileContentCharset;
@Schema(description = "用户名")
private String username;
// @Desensitize(toEmpty = true)
@Schema(description = "密码")
private String password;
@Schema(description = "密钥id")
private Long keyId;
// @Desensitize(toEmpty = true)
@Schema(description = "公钥文本")
private String publicKey;
// @Desensitize(toEmpty = true)
@Schema(description = "私钥文本")
private String privateKey;
// @Desensitize(toEmpty = true)
@Schema(description = "私钥密码")
private String privateKeyPassword;
}

View File

@ -0,0 +1,30 @@
package cd.casic.module.terminal.controller.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 终端传输参数
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/26 15:47
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
//@DesensitizeObject
@Schema(name = "TerminalTransferDTO", description = "终端传输参数")
public class TerminalTransferDTO {
@Schema(description = "userId")
private Long userId;
@Schema(description = "username")
private String username;
}

View File

@ -0,0 +1,67 @@
package cd.casic.module.terminal.controller.request;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.*;
import org.springframework.data.annotation.Id;
import java.util.Date;
import java.util.List;
/**
*
* 终端连接日志 查询请求对象
*
* @author Yuru Pu
* @version 1.0
* @since 2025/7/9 14:07
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "TerminalConnectLogQueryRequest", description = "终端连接日志 查询请求对象")
public class TerminalConnectLogQueryRequest {
@NotNull(groups = Id.class)
@Schema(description = "id")
private Long id;
@Schema(description = "id")
private List<Long> idList;
@Schema(description = "用户id")
private Long userId;
@Schema(description = "主机id")
private Long hostId;
@Size(max = 128)
@Schema(description = "主机地址")
private String hostAddress;
@Size(max = 16)
@Schema(description = "类型")
private String type;
@Size(max = 64)
@Schema(description = "sessionId")
private String sessionId;
@Size(max = 16)
@Schema(description = "状态")
private String status;
@Schema(description = "开始时间-区间")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date[] startTimeRange;
@Schema(description = "创建时间 <=")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTimeLe;
@Schema(description = "状态")
private List<String> statusList;
}

View File

@ -0,0 +1,63 @@
package cd.casic.module.terminal.controller.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 终端连接日志 视图响应对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-26 22:09
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "TerminalConnectLogVO", description = "终端连接日志 视图响应对象")
public class TerminalConnectLogVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
private Long id;
@Schema(description = "用户id")
private Long userId;
@Schema(description = "用户名")
private String username;
@Schema(description = "主机id")
private Long hostId;
@Schema(description = "主机名称")
private String hostName;
@Schema(description = "主机地址")
private String hostAddress;
@Schema(description = "类型")
private String type;
@Schema(description = "状态")
private String status;
@Schema(description = "sessionId")
private String sessionId;
@Schema(description = "开始时间")
private Date startTime;
@Schema(description = "结束时间")
private Date endTime;
/*@Schema(description = "额外信息")
private TerminalConnectLogExtraDTO extra;*/
}

View File

@ -0,0 +1,37 @@
package cd.casic.module.terminal.convert;
import cd.casic.module.terminal.dal.dataobject.TerminalConnectLogDO;
import cd.casic.module.terminal.controller.dto.TerminalConnectDTO;
import cd.casic.module.terminal.controller.request.TerminalConnectLogQueryRequest;
import cd.casic.module.terminal.controller.vo.TerminalConnectLogVO;
import cd.casic.module.terminal.host.terminal.model.request.host.TerminalConnectLogCreateRequest;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
*
* 终端连接日志 内部对象转换器
*
* @author Yuru Pu
* @version 1.0
* @since 2025/7/8 13:54
*/
@Mapper
public interface TerminalConnectLogConvert {
TerminalConnectLogConvert MAPPER = Mappers.getMapper(TerminalConnectLogConvert.class);
TerminalConnectLogDO to(TerminalConnectLogCreateRequest request);
TerminalConnectLogDO to(TerminalConnectLogQueryRequest request);
TerminalConnectLogVO to(TerminalConnectLogDO domain);
TerminalConnectLogCreateRequest to(TerminalConnectDTO dto);
List<TerminalConnectLogVO> to(List<TerminalConnectLogDO> list);
}

View File

@ -0,0 +1,76 @@
package cd.casic.module.terminal.dal.dataobject;
import cd.casic.framework.commons.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 终端连接日志 实体对象
*
* @author Yuru Pu
* @version 1.0
* @since 2025/7/9 11:33
*/
@Data
@TableName(value = "terminal_connect_log", autoResultMap = true)
@KeySequence("terminal_connect_log_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class TerminalConnectLogDO extends BaseDO {
@TableId
private Long id;
@Schema(description = "用户id")
@TableField("user_id")
private Long userId;
@Schema(description = "用户名")
@TableField("username")
private String username;
@Schema(description = "主机id")
@TableField("host_id")
private Long hostId;
@Schema(description = "主机名称")
@TableField("host_name")
private String hostName;
@Schema(description = "主机地址")
@TableField("host_address")
private String hostAddress;
@Schema(description = "类型")
@TableField("type")
private String type;
@Schema(description = "sessionId")
@TableField("session_id")
private String sessionId;
@Schema(description = "状态")
@TableField("status")
private String status;
@Schema(description = "开始时间")
@TableField("start_time")
private Date startTime;
@Schema(description = "结束时间")
@TableField("end_time")
private Date endTime;
@Schema(description = "额外信息")
@TableField("extra_info")
private String extraInfo;
}

View File

@ -0,0 +1,18 @@
package cd.casic.module.terminal.dal.mysql;
import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
import cd.casic.module.terminal.dal.dataobject.TerminalConnectLogDO;
import org.apache.ibatis.annotations.Mapper;
/**
*
* 终端连接日志 Mapper 接口
*
* @author Yuru Pu
* @version 1.0
* @since 2025/7/9 11:38
*/
@Mapper
public interface TerminalConnectLogMapper extends BaseMapperX<TerminalConnectLogDO> {
}

View File

@ -0,0 +1,111 @@
package cd.casic.module.terminal.dal.redis;
import cn.orionsec.kit.lang.define.cache.key.CacheKeyDefine;
import cn.orionsec.kit.lang.utils.Strings;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
/**
* @author: Paul
* @create: 2025-07-10 15:31
*/
@Repository
public class TerminalRedisDAO {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 获取 json
*
* @param key key
* @param define define
* @param <T> T
* @return T
*/
public <T> T getJson(String key, CacheKeyDefine define) {
return (T) getJson(key, define.getType());
}
/**
* 获取 json
*
* @param key key
* @param type type
* @param <T> T
* @return T
*/
public <T> T getJson(String key, Class<T> type) {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
}
return (T) JSON.parseObject(value, type);
}
/**
* 设置值
*
* @param define define
* @param value value
*/
public void set(CacheKeyDefine define, Object value) {
set(define.getKey(), define, value);
}
/**
* 设置值
*
* @param key key
* @param define define
* @param value value
*/
public void set(String key, CacheKeyDefine define, Object value) {
if (value == null) {
value = Strings.EMPTY;
}
if (define == null || define.getTimeout() == 0) {
// 不过期
redisTemplate.opsForValue().set(key, value.toString());
} else {
// 过期
redisTemplate.opsForValue().set(key, value.toString(),
define.getTimeout(),
define.getUnit());
}
}
/**
* 设置 json
*
* @param key key
* @param define define
* @param value value
*/
public void setJson(String key, CacheKeyDefine define, Object value) {
if (define == null || define.getTimeout() == 0) {
// 不过期
redisTemplate.opsForValue().set(key, JSON.toJSONString(value));
} else {
// 过期
redisTemplate.opsForValue().set(key, JSON.toJSONString(value),
define.getTimeout(),
define.getUnit());
}
}
/**
* 删除 key
*
* @param key key
*/
public void delete(String key) {
redisTemplate.delete(key);
}
}

View File

@ -0,0 +1,110 @@
package cd.casic.module.terminal.define;
import cn.orionsec.kit.lang.constant.Const;
import cn.orionsec.kit.lang.define.thread.ExecutorBuilder;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 资产线程池
*
*/
public interface AssetThreadPools {
/**
* 超时检查线程池
*/
ThreadPoolExecutor TIMEOUT_CHECK = ExecutorBuilder.create()
.namedThreadFactory("timeout-check-")
.corePoolSize(1)
.maxPoolSize(Integer.MAX_VALUE)
.keepAliveTime(Const.MS_S_60)
.workQueue(new SynchronousQueue<>())
.allowCoreThreadTimeout(true)
.build();
/**
* terminal 标准输出线程池
*/
ThreadPoolExecutor TERMINAL_STDOUT = ExecutorBuilder.create()
.namedThreadFactory("terminal-stdout-")
.corePoolSize(1)
.maxPoolSize(Integer.MAX_VALUE)
.keepAliveTime(Const.MS_S_60)
.workQueue(new SynchronousQueue<>())
.allowCoreThreadTimeout(true)
.build();
/**
* terminal 操作线程池
*/
ThreadPoolExecutor TERMINAL_OPERATOR = ExecutorBuilder.create()
.namedThreadFactory("terminal-operator-")
.corePoolSize(1)
.maxPoolSize(Integer.MAX_VALUE)
.keepAliveTime(Const.MS_S_60)
.workQueue(new SynchronousQueue<>())
.allowCoreThreadTimeout(true)
.build();
/**
* 批量执行任务线程池
*/
ThreadPoolExecutor EXEC_TASK = ExecutorBuilder.create()
.namedThreadFactory("exec-task-")
.corePoolSize(1)
.maxPoolSize(Integer.MAX_VALUE)
.keepAliveTime(Const.MS_S_60)
.workQueue(new SynchronousQueue<>())
.allowCoreThreadTimeout(true)
.build();
/**
* 批量执行主机命令线程池
*/
ThreadPoolExecutor EXEC_HOST = ExecutorBuilder.create()
.namedThreadFactory("exec-host-")
.corePoolSize(1)
.maxPoolSize(Integer.MAX_VALUE)
.keepAliveTime(Const.MS_S_60)
.workQueue(new SynchronousQueue<>())
.allowCoreThreadTimeout(true)
.build();
/**
* 批量执行日志查看线程池
*/
ThreadPoolExecutor EXEC_LOG = ExecutorBuilder.create()
.namedThreadFactory("exec-log-")
.corePoolSize(1)
.maxPoolSize(Integer.MAX_VALUE)
.keepAliveTime(Const.MS_S_60)
.workQueue(new SynchronousQueue<>())
.allowCoreThreadTimeout(true)
.build();
/**
* 批量上传任务线程池
*/
ThreadPoolExecutor UPLOAD_TASK = ExecutorBuilder.create()
.namedThreadFactory("upload-task-")
.corePoolSize(1)
.maxPoolSize(Integer.MAX_VALUE)
.keepAliveTime(Const.MS_S_60)
.workQueue(new SynchronousQueue<>())
.allowCoreThreadTimeout(true)
.build();
/**
* 批量上传主机线程池
*/
ThreadPoolExecutor UPLOAD_HOST = ExecutorBuilder.create()
.namedThreadFactory("upload-host-")
.corePoolSize(1)
.maxPoolSize(Integer.MAX_VALUE)
.keepAliveTime(Const.MS_S_60)
.workQueue(new SynchronousQueue<>())
.allowCoreThreadTimeout(true)
.build();
}

View File

@ -0,0 +1,54 @@
package cd.casic.module.terminal.define.cache;
import cd.casic.module.terminal.controller.dto.TerminalAccessDTO;
import cd.casic.module.terminal.controller.dto.TerminalTransferDTO;
import cd.casic.module.terminal.host.terminal.dto.SftpGetContentCacheDTO;
import cd.casic.module.terminal.host.terminal.dto.SftpSetContentCacheDTO;
import cn.orionsec.kit.lang.define.cache.key.CacheKeyBuilder;
import cn.orionsec.kit.lang.define.cache.key.CacheKeyDefine;
import cn.orionsec.kit.lang.define.cache.key.struct.RedisCacheStruct;
import java.util.concurrent.TimeUnit;
/**
*
* 终端服务缓存 key
*
* @author Yuru Pu
* @version 1.0
* @since 2025/7/9 14:17
*/
public interface TerminalCacheKeyDefine {
CacheKeyDefine TERMINAL_ACCESS = new CacheKeyBuilder()
.key("terminal:access:{}")
.desc("终端访问token ${token}")
.type(TerminalAccessDTO.class)
.struct(RedisCacheStruct.STRING)
.timeout(3, TimeUnit.MINUTES)
.build();
CacheKeyDefine TERMINAL_TRANSFER = new CacheKeyBuilder()
.key("terminal:transfer:{}")
.desc("终端传输token ${token}")
.type(TerminalTransferDTO.class)
.struct(RedisCacheStruct.STRING)
.timeout(3, TimeUnit.MINUTES)
.build();
CacheKeyDefine TERMINAL_SFTP_GET_CONTENT = new CacheKeyBuilder()
.key("terminal:sftp:gc:{}")
.desc("sftp 获取文件内容 ${token}")
.type(SftpGetContentCacheDTO.class)
.struct(RedisCacheStruct.STRING)
.timeout(5, TimeUnit.MINUTES)
.build();
CacheKeyDefine TERMINAL_SFTP_SET_CONTENT = new CacheKeyBuilder()
.key("terminal:sftp:sc:{}")
.desc("sftp 设置文件内容 ${token}")
.type(SftpSetContentCacheDTO.class)
.struct(RedisCacheStruct.STRING)
.timeout(5, TimeUnit.MINUTES)
.build();
}

View File

@ -0,0 +1,51 @@
package cd.casic.module.terminal.define.config;
import cd.casic.module.terminal.common.config.ConfigRef;
import cd.casic.module.terminal.common.config.ConfigStore;
import cd.casic.module.terminal.common.constant.ConfigKeys;
import org.springframework.stereotype.Component;
/**
* 应用 sftp 配置
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/4/15 22:00
*/
@Component
public class AppSftpConfig {
/**
* 文件预览大小
*/
private final ConfigRef<Integer> previewSize;
/**
* 重复文件备份
*/
private final ConfigRef<Boolean> uploadPresentBackup;
/**
* 备份文件名称
*/
private final ConfigRef<String> uploadBackupFileName;
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

@ -0,0 +1,69 @@
package cd.casic.module.terminal.define.operator;
import cd.casic.module.terminal.common.annotation.Module;
import cd.casic.module.terminal.common.InitializingOperatorTypes;
import cd.casic.module.terminal.common.OperatorType;
import cn.orionsec.kit.lang.utils.collect.Lists;
import java.util.List;
import static cd.casic.module.terminal.common.OperatorRiskLevel.*;
/**
* 终端 操作日志类型
*
*/
@Module("asset:terminal")
public class TerminalOperatorType extends InitializingOperatorTypes {
public static final String CONNECT = "terminal:connect";
public static final String DELETE_SFTP_LOG = "terminal:delete-sftp-log";
public static final String SFTP_MKDIR = "terminal:sftp-mkdir";
public static final String SFTP_TOUCH = "terminal:sftp-touch";
public static final String SFTP_MOVE = "terminal:sftp-move";
public static final String SFTP_REMOVE = "terminal:sftp-remove";
public static final String SFTP_TRUNCATE = "terminal:sftp-truncate";
public static final String SFTP_CHMOD = "terminal:sftp-chmod";
public static final String SFTP_SET_CONTENT = "terminal:sftp-set-content";
public static final String SFTP_UPLOAD = "terminal:sftp-upload";
public static final String SFTP_DOWNLOAD = "terminal:sftp-download";
public static final List<String> SFTP_TYPES = Lists.of(
SFTP_MKDIR,
SFTP_TOUCH,
SFTP_MOVE,
SFTP_REMOVE,
SFTP_TRUNCATE,
SFTP_CHMOD,
SFTP_SET_CONTENT,
SFTP_UPLOAD,
SFTP_DOWNLOAD
);
@Override
public OperatorType[] types() {
return new OperatorType[]{
new OperatorType(L, CONNECT, "连接主机 ${connectType} <sb>${hostName}</sb>"),
new OperatorType(H, DELETE_SFTP_LOG, "删除 SFTP 操作日志 <sb>${count}</sb> 条"),
new OperatorType(L, SFTP_MKDIR, "创建文件夹 ${hostName} <sb>${path}</sb>"),
new OperatorType(L, SFTP_TOUCH, "创建文件 ${hostName} <sb>${path}</sb>"),
new OperatorType(M, SFTP_MOVE, "移动文件 ${hostName} <sb>${path}</sb> 至 <sb>${target}</sb>"),
new OperatorType(H, SFTP_REMOVE, "删除文件 ${hostName} <sb>${path}</sb>"),
new OperatorType(H, SFTP_TRUNCATE, "截断文件 ${hostName} <sb>${path}</sb>"),
new OperatorType(M, SFTP_CHMOD, "文件提权 ${hostName} <sb>${path}</sb> <sb>${mod}</sb>"),
new OperatorType(M, SFTP_SET_CONTENT, "修改文件内容 ${hostName} <sb>${path}</sb>"),
new OperatorType(M, SFTP_UPLOAD, "上传文件 ${hostName} <sb>${path}</sb>"),
new OperatorType(M, SFTP_DOWNLOAD, "下载文件 ${hostName} <sb>${path}</sb>"),
};
}
}

View File

@ -0,0 +1,54 @@
package cd.casic.module.terminal.enums;
import cd.casic.module.terminal.common.GenericsDataModel;
import cd.casic.module.terminal.common.GenericsDataStrategy;
import cd.casic.module.terminal.common.GenericsStrategyDefinition;
import cd.casic.module.terminal.host.extra.strategy.HostLabelExtraStrategy;
import cd.casic.module.terminal.host.extra.strategy.HostSpecExtraStrategy;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 主机额外配置项枚举
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/20 22:48
*/
@Getter
@AllArgsConstructor
public enum HostExtraItemEnum implements GenericsStrategyDefinition {
/**
* SSH 额外配置
*/
// SSH(HostSshExtraStrategy.class, true),
/**
* 标签额外配置
*/
LABEL(HostLabelExtraStrategy.class, true),
/**
* 规格信息配置
*/
SPEC(HostSpecExtraStrategy.class, false),
;
private final Class<? extends GenericsDataStrategy<? extends GenericsDataModel>> strategyClass;
private final boolean userExtra;
public static HostExtraItemEnum of(String item) {
if (item == null) {
return null;
}
for (HostExtraItemEnum value : values()) {
if (value.name().equals(item)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,41 @@
package cd.casic.module.terminal.enums;
/**
* 主机认证类型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/20 21:41
*/
public enum HostExtraSshAuthTypeEnum {
/**
* 默认认证方式
*/
DEFAULT,
/**
* 自定义密钥认证
*/
CUSTOM_KEY,
/**
* 自定义身份认证
*/
CUSTOM_IDENTITY,
;
public static HostExtraSshAuthTypeEnum of(String type) {
if (type == null) {
return DEFAULT;
}
for (HostExtraSshAuthTypeEnum value : values()) {
if (value.name().equals(type)) {
return value;
}
}
return DEFAULT;
}
}

View File

@ -0,0 +1,36 @@
package cd.casic.module.terminal.enums;
/**
* 主机身份类型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/9/21 19:01
*/
public enum HostIdentityTypeEnum {
/**
* 密码
*/
PASSWORD,
/**
* 密钥
*/
KEY,
;
public static HostIdentityTypeEnum of(String type) {
if (type == null) {
return null;
}
for (HostIdentityTypeEnum value : values()) {
if (value.name().equals(type)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,41 @@
package cd.casic.module.terminal.enums;
/**
* 主机认证类型 - ssh
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/9/21 19:01
*/
public enum HostSshAuthTypeEnum {
/**
* 密码认证
*/
PASSWORD,
/**
* 密钥认证
*/
KEY,
/**
* 身份认证
*/
IDENTITY,
;
public static HostSshAuthTypeEnum of(String type) {
if (type == null) {
return PASSWORD;
}
for (HostSshAuthTypeEnum value : values()) {
if (value.name().equals(type)) {
return value;
}
}
return PASSWORD;
}
}

View File

@ -0,0 +1,36 @@
package cd.casic.module.terminal.enums;
/**
* 主机状态
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/7/17 16:07
*/
public enum HostStatusEnum {
/**
* 停用
*/
DISABLED,
/**
* 启用
*/
ENABLED,
;
public static HostStatusEnum of(String name) {
if (name == null) {
return null;
}
for (HostStatusEnum value : values()) {
if (value.name().equals(name)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,48 @@
package cd.casic.module.terminal.enums;
import cd.casic.module.terminal.common.GenericsDataModel;
import cd.casic.module.terminal.common.GenericsDataStrategy;
import cd.casic.module.terminal.common.GenericsStrategyDefinition;
import cd.casic.module.terminal.host.config.strategy.HostSshConfigStrategy;
import cn.orionsec.kit.lang.constant.Const;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 主机配置类型枚举
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/9/11 14:37
*/
@Getter
@AllArgsConstructor
public enum HostTypeEnum implements GenericsStrategyDefinition {
/**
* SSH
*/
SSH(HostSshConfigStrategy.class),
;
private final Class<? extends GenericsDataStrategy<? extends GenericsDataModel>> strategyClass;
public static HostTypeEnum of(String type) {
if (type == null) {
return null;
}
for (HostTypeEnum value : values()) {
if (value.name().equals(type)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,36 @@
package cd.casic.module.terminal.enums;
/**
* 终端连接类型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/26 22:27
*/
public enum TerminalConnectTypeEnum {
/**
* ssh
*/
SSH,
/**
* sftp
*/
SFTP,
;
public static TerminalConnectTypeEnum of(String type) {
if (type == null) {
return null;
}
for (TerminalConnectTypeEnum value : values()) {
if (value.name().equalsIgnoreCase(type)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,100 @@
package cd.casic.module.terminal.host.config.model;
import cd.casic.module.terminal.common.GenericsDataModel;
import cd.casic.module.terminal.common.UpdatePasswordAction;
import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
* 主机 SSH 配置
*
* @author Yuru Pu
* @version 1.0
* @since 2025/7/15 15:22
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HostSshConfigModel implements GenericsDataModel, UpdatePasswordAction {
/**
* 主机端口
*/
@NotNull
@Min(value = 1)
@Max(value = 65535)
private Integer port;
/**
* 用户名
*/
@Size(max = 128)
private String username;
/**
* 认证方式
*/
@NotBlank
@Size(max = 12)
private String authType;
/**
* 密码
*/
private String password;
/**
* 身份id
*/
private Long identityId;
/**
* 密钥id
*/
private Long keyId;
/**
* 连接超时时间
*/
@NotNull
@Min(value = 1)
@Max(value = 100000)
private Integer connectTimeout;
/**
* SSH输出编码
*/
@NotBlank
@Size(max = 12)
private String charset;
/**
* 文件名称编码
*/
@NotBlank
@Size(max = 12)
private String fileNameCharset;
/**
* 文件内容编码
*/
@NotBlank
@Size(max = 12)
private String fileContentCharset;
/**
* 是否使用新密码 仅参数
*/
private Boolean useNewPassword;
/**
* 是否已设置密码 仅返回
*/
private Boolean hasPassword;
}

View File

@ -0,0 +1,128 @@
package cd.casic.module.terminal.host.config.strategy;
import cd.casic.module.machine.dal.mysql.MachineInfoMapper;
import cd.casic.module.machine.dal.mysql.SecretKeyMapper;
import cd.casic.module.terminal.common.AbstractGenericsDataStrategy;
import cd.casic.module.terminal.common.AesEncryptUtils;
import cd.casic.module.terminal.common.ErrorMessage;
import cd.casic.module.terminal.enums.HostSshAuthTypeEnum;
import cd.casic.module.terminal.host.config.model.HostSshConfigModel;
import cd.casic.module.terminal.utils.RsaParamDecryptUtils;
import cd.casic.module.terminal.utils.Valid;
import cn.orionsec.kit.lang.constant.Const;
import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.Charsets;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 主机 SSH 配置策略
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/9/19 14:26
*/
@Component
public class HostSshConfigStrategy extends AbstractGenericsDataStrategy<HostSshConfigModel> {
@Autowired
private SecretKeyMapper secretKeyMapper;
@Autowired
private MachineInfoMapper machineInfoMapper;
private static final String USERNAME = "root";
public HostSshConfigStrategy() {
super(HostSshConfigModel.class);
}
@Override
public HostSshConfigModel getDefault() {
return HostSshConfigModel.builder()
.port(22)
.username(USERNAME)
.authType(HostSshAuthTypeEnum.PASSWORD.name())
.connectTimeout(Const.MS_S_10)
.charset(Const.UTF_8)
.fileNameCharset(Const.UTF_8)
.fileContentCharset(Const.UTF_8)
.build();
}
@Override
protected void preValid(HostSshConfigModel model) {
// 验证编码格式
this.validCharset(model.getCharset());
this.validCharset(model.getFileNameCharset());
this.validCharset(model.getFileContentCharset());
// 检查主机密钥是否存在
Long keyId = model.getKeyId();
if (keyId != null) {
Valid.notNull(secretKeyMapper.selectById(keyId), ErrorMessage.KEY_ABSENT);
}
// 检查主机身份是否存在
Long identityId = model.getIdentityId();
if (identityId != null) {
Valid.notNull(machineInfoMapper.selectById(identityId), ErrorMessage.IDENTITY_ABSENT);
}
}
@Override
protected void valid(HostSshConfigModel model) {
// 验证填充后的参数
Valid.valid(model);
}
@Override
protected void updateFill(HostSshConfigModel beforeModel, HostSshConfigModel afterModel) {
// 加密密码
this.checkEncryptPassword(beforeModel, afterModel);
afterModel.setHasPassword(null);
afterModel.setUseNewPassword(null);
}
@Override
public void toView(HostSshConfigModel model) {
if (model == null) {
return;
}
model.setHasPassword(Strings.isNotBlank(model.getPassword()));
model.setPassword(null);
}
/**
* 检查加密密码
*
* @param before before
* @param after after
*/
private void checkEncryptPassword(HostSshConfigModel before, HostSshConfigModel after) {
// 非密码认证/使用原始密码则直接赋值
if (!HostSshAuthTypeEnum.PASSWORD.name().equals(after.getAuthType())
|| !Booleans.isTrue(after.getUseNewPassword())) {
if (before != null) {
after.setPassword(before.getPassword());
}
return;
}
// 检查新密码
String newPassword = Valid.notBlank(after.getPassword(), ErrorMessage.PASSWORD_MISSING);
// 解密密码
newPassword = RsaParamDecryptUtils.decrypt(newPassword);
Valid.notBlank(newPassword, ErrorMessage.DECRYPT_ERROR);
// 设置密码
after.setPassword(AesEncryptUtils.encryptAsString(newPassword));
}
/**
* 检查编码格式
*
* @param charset charset
*/
private void validCharset(String charset) {
Valid.isTrue(Charsets.isSupported(charset), ErrorMessage.UNSUPPORTED_CHARSET, charset);
}
}

View File

@ -0,0 +1,32 @@
package cd.casic.module.terminal.host.extra.model;
import cd.casic.module.terminal.common.GenericsDataModel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 主机拓展信息 - 标签模型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/29 23:16
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HostLabelExtraModel implements GenericsDataModel {
/**
* 别名
*/
private String alias;
/**
* 颜色
*/
private String color;
}

View File

@ -0,0 +1,119 @@
package cd.casic.module.terminal.host.extra.model;
import cd.casic.module.terminal.common.GenericsDataModel;
import lombok.*;
import java.util.Date;
import java.util.List;
/**
* 主机拓展信息 - 规格模型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/3/24 0:34
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HostSpecExtraModel implements GenericsDataModel {
/**
* sn
*/
private String sn;
/**
* 系统名称
*/
private String osName;
/**
* cpu 核心数
*/
private Integer cpuCore;
/**
* cpu 频率
*/
private Double cpuFrequency;
/**
* cpu 型号
*/
private String cpuModel;
/**
* 内存大小
*/
private Double memorySize;
/**
* 硬盘大小
*/
private Double diskSize;
/**
* 上行带宽
*/
private Integer inBandwidth;
/**
* 下行带宽
*/
private Integer outBandwidth;
/**
* 公网 ip 列表
*/
private List<String> publicIpAddress;
/**
* 内网 ip 列表
*/
private List<String> privateIpAddress;
/**
* 负责人
*/
private String chargePerson;
/**
* 创建时间
*/
private Date createdTime;
/**
* 到期时间
*/
private Date expiredTime;
/**
* 扩展信息
*/
@Singular
private List<HostSpecExtraItem> items;
/**
* 扩展信息项
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class HostSpecExtraItem {
/**
* 标签
*/
private String label;
/**
*
*/
private String value;
}
}

View File

@ -0,0 +1,42 @@
package cd.casic.module.terminal.host.extra.model;
import cd.casic.module.terminal.common.GenericsDataModel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 主机拓展信息 - ssh 模型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/20 21:36
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HostSshExtraModel implements GenericsDataModel {
/**
* 认证方式
*/
private String authType;
/**
* 认证方式
*/
private String username;
/**
* 主机密钥
*/
private Long keyId;
/**
* 主机身份
*/
private Long identityId;
}

View File

@ -0,0 +1,43 @@
package cd.casic.module.terminal.host.extra.strategy;
import cd.casic.module.terminal.common.AbstractGenericsDataStrategy;
import cd.casic.module.terminal.host.extra.model.HostLabelExtraModel;
import cn.orionsec.kit.lang.constant.Const;
import org.springframework.stereotype.Component;
/**
* 主机拓展信息 - 标签模型处理策略
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/29 23:16
*/
@Component
public class HostLabelExtraStrategy extends AbstractGenericsDataStrategy<HostLabelExtraModel> {
public HostLabelExtraStrategy() {
super(HostLabelExtraModel.class);
}
@Override
public HostLabelExtraModel getDefault() {
return HostLabelExtraModel.builder()
// 透明
.color(Const.EMPTY)
// 无别名
.alias(Const.EMPTY)
.build();
}
@Override
public void updateFill(HostLabelExtraModel beforeModel, HostLabelExtraModel afterModel) {
// 为空则覆盖
if (afterModel.getAlias() == null) {
afterModel.setAlias(beforeModel.getAlias());
}
if (afterModel.getColor() == null) {
afterModel.setColor(beforeModel.getColor());
}
}
}

View File

@ -0,0 +1,27 @@
package cd.casic.module.terminal.host.extra.strategy;
import cd.casic.module.terminal.common.AbstractGenericsDataStrategy;
import cd.casic.module.terminal.host.extra.model.HostSpecExtraModel;
import org.springframework.stereotype.Component;
/**
* 主机规格额外信息策略
*
* @author Jiahang Li
* @version 1.0.0
* @since 2025/3/24 0:21
*/
@Component
public class HostSpecExtraStrategy extends AbstractGenericsDataStrategy<HostSpecExtraModel> {
public HostSpecExtraStrategy() {
super(HostSpecExtraModel.class);
}
@Override
public HostSpecExtraModel getDefault() {
return new HostSpecExtraModel();
}
}

View File

@ -0,0 +1,18 @@
package cd.casic.module.terminal.host.jsch;
/**
* 连接消息
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/7/11 16:30
*/
public interface SessionMessage {
String AUTHENTICATION_FAILURE = "身份认证失败. {}";
String SERVER_UNREACHABLE = "无法连接至服务器. {}";
String CONNECTION_TIMEOUT = "连接服务器超时. {}";
}

View File

@ -0,0 +1,134 @@
package cd.casic.module.terminal.host.jsch;
import cd.casic.module.terminal.common.AesEncryptUtils;
import cd.casic.module.terminal.common.AppConst;
import cd.casic.module.terminal.controller.dto.TerminalConnectDTO;
import cn.orionsec.kit.lang.constant.Const;
import cn.orionsec.kit.lang.exception.AuthenticationException;
import cn.orionsec.kit.lang.exception.argument.InvalidArgumentException;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.net.host.SessionHolder;
import cn.orionsec.kit.net.host.SessionStore;
import lombok.extern.slf4j.Slf4j;
import java.util.Optional;
/**
* sessionStore 工具类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/7/11 16:58
*/
@Slf4j
public class SessionStores {
protected static final ThreadLocal<String> CURRENT_ADDRESS = new ThreadLocal<>();
/**
* 打开 sessionStore
*
* @param conn conn
* @return sessionStore
*/
public static SessionStore openSessionStore(TerminalConnectDTO conn) {
Long hostId = conn.getHostId();
String address = conn.getHostAddress();
String username = conn.getUsername();
log.info("SessionStores-open-start hostId: {}, address: {}, username: {}", hostId, address, username);
try {
CURRENT_ADDRESS.set(address);
// 创建会话
SessionHolder sessionHolder = SessionHolder.create();
// sessionHolder.setLogger(SessionLogger.INFO);
SessionStore session = createSessionStore(conn, sessionHolder);
// 设置版本
session.getSession().setClientVersion("SSH-2.0-ORION_VISOR_V" + AppConst.VERSION);
// 连接
session.connect();
log.info("SessionStores-open-success hostId: {}, address: {}, username: {}", hostId, address, username);
return session;
} catch (Exception e) {
String message = e.getMessage();
log.error("SessionStores-open-error hostId: {}, address: {}, username: {}, message: {}", hostId, address, username, message, e);
throw Exceptions.app(getErrorMessage(e), e);
} finally {
CURRENT_ADDRESS.remove();
}
}
/**
* 创建 sessionStore
*
* @param conn conn
* @param sessionHolder sessionHolder
* @return sessionStore
*/
private static SessionStore createSessionStore(TerminalConnectDTO conn, SessionHolder sessionHolder) {
final boolean useKey = conn.getKeyId() != null;
// 使用密钥认证
if (useKey) {
// 加载密钥
String publicKey = Optional.ofNullable(conn.getPublicKey())
.map(AesEncryptUtils::decryptAsString)
.orElse(null);
String privateKey = Optional.ofNullable(conn.getPrivateKey())
.map(AesEncryptUtils::decryptAsString)
.orElse(null);
String password = Optional.ofNullable(conn.getPrivateKeyPassword())
.map(AesEncryptUtils::decryptAsString)
.orElse(null);
sessionHolder.addIdentityValue(String.valueOf(conn.getKeyId()),
privateKey,
publicKey,
password);
}
// 获取会话
SessionStore session = sessionHolder.getSession(conn.getHostAddress(), conn.getHostPort(), conn.getUsername());
// 使用密码认证
if (!useKey) {
String password = conn.getPassword();
if (!Strings.isEmpty(password)) {
// session.password(AesEncryptUtils.decryptAsString(password));
session.password(password);
}
}
// 超时时间
session.timeout(conn.getTimeout());
return session;
}
/**
* 获取错误信息
*
* @param e e
* @return errorMessage
*/
private static String getErrorMessage(Exception e) {
if (e == null) {
return null;
}
String host = CURRENT_ADDRESS.get();
String message = e.getMessage();
if (Strings.contains(message, Const.TIMEOUT)) {
// 连接超时
return Strings.format(SessionMessage.CONNECTION_TIMEOUT, host);
} else if (Exceptions.isCausedBy(e, AuthenticationException.class)) {
// 认证失败
return Strings.format(SessionMessage.AUTHENTICATION_FAILURE, host);
} else if (Exceptions.isCausedBy(e, InvalidArgumentException.class)) {
// 参数错误
if (Strings.isBlank(message)) {
return Strings.format(SessionMessage.SERVER_UNREACHABLE, host);
} else {
return message;
}
} else {
// 其他错误
return Strings.format(SessionMessage.SERVER_UNREACHABLE, host);
}
}
}

View File

@ -0,0 +1,26 @@
package cd.casic.module.terminal.host.terminal.constant;
/**
* 终端信息常量
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/1/3 16:57
*/
public interface TerminalMessage {
String CONFIG_DISABLED = "SSH configuration has been disabled.";
String AUTHENTICATION_FAILURE = "authentication failed. please check the configuration.";
String SERVER_UNREACHABLE = "remote server unreachable. please check the configuration.";
String CONNECTION_FAILED = "connection failed.";
String CONNECTION_TIMEOUT = "connection timeout.";
String CONNECTION_CLOSED = "connection closed.";
String FORCED_OFFLINE = "forced offline.";
}

View File

@ -0,0 +1,33 @@
package cd.casic.module.terminal.host.terminal.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* sftp 获取文件内容缓存对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/10/10 20:49
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "SftpGetContentCacheDTO", description = "sftp 获取文件内容缓存对象")
public class SftpGetContentCacheDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主机id")
private Long hostId;
@Schema(description = "文件路径")
private String path;
}

View File

@ -0,0 +1,33 @@
package cd.casic.module.terminal.host.terminal.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* sftp 获取文件内容缓存对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/10/10 20:49
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "SftpSetContentCacheDTO", description = "sftp 设置文件内容缓存对象")
public class SftpSetContentCacheDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主机id")
private Long hostId;
@Schema(description = "文件路径")
private String path;
}

View File

@ -0,0 +1,260 @@
package cd.casic.module.terminal.host.terminal.enums;
import cd.casic.module.terminal.common.holder.SpringHolder;
import cd.casic.module.terminal.host.terminal.handler.*;
import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload;
import cd.casic.module.terminal.host.terminal.model.request.*;
import com.alibaba.fastjson.JSONObject;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import org.springframework.stereotype.Component;
/**
* 输入操作类型枚举
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/29 15:33
*/
public enum InputTypeEnum {
/**
* 终端连接检查
*/
CHECK("ck",
TerminalCheckHandler.class,
new String[]{"type", "sessionId", "hostId", "connectType"},
TerminalCheckRequest.class,
true),
/**
* 连接主机
*/
CONNECT("co",
TerminalConnectHandler.class,
new String[]{"type", "sessionId", "terminalType", "cols", "rows"},
TerminalConnectRequest.class,
true),
/**
* 关闭连接
*/
CLOSE("cl",
TerminalCloseHandler.class,
new String[]{"type", "sessionId"},
TerminalBasePayload.class,
true),
/**
* ping
*/
PING("p",
TerminalPingHandler.class,
new String[]{"type"},
TerminalBasePayload.class,
true),
/**
* SSH 修改大小
*/
SSH_RESIZE("rs",
SshResizeHandler.class,
new String[]{"type", "sessionId", "cols", "rows"},
SshResizeRequest.class,
true),
/**
* SSH 输入
*/
SSH_INPUT("i",
SshInputHandler.class,
new String[]{"type", "sessionId", "command"},
SshInputRequest.class,
false),
/**
* SFTP 文件列表
*/
SFTP_LIST("ls",
SftpListHandler.class,
new String[]{"type", "sessionId", "showHiddenFile", "path"},
SftpListRequest.class,
true),
/**
* SFTP 创建文件夹
*/
SFTP_MKDIR("mk",
SftpMakeDirectoryHandler.class,
new String[]{"type", "sessionId", "path"},
SftpBaseRequest.class,
true),
/**
* SFTP 创建文件
*/
SFTP_TOUCH("to",
SftpTouchHandler.class,
new String[]{"type", "sessionId", "path"},
SftpBaseRequest.class,
true),
/**
* SFTP 移动文件
*/
SFTP_MOVE("mv",
SftpMoveHandler.class,
new String[]{"type", "sessionId", "path", "target"},
SftpMoveRequest.class,
true),
/**
* SFTP 删除文件
*/
SFTP_REMOVE("rm",
SftpRemoveHandler.class,
new String[]{"type", "sessionId", "path"},
SftpBaseRequest.class,
true),
/**
* SFTP 截断文件
*/
SFTP_TRUNCATE("tc",
SftpTruncateHandler.class,
new String[]{"type", "sessionId", "path"},
SftpBaseRequest.class,
true),
/**
* SFTP 修改文件权限
*/
SFTP_CHMOD("cm",
SftpChangeModeHandler.class,
new String[]{"type", "sessionId", "path", "mod"},
SftpChangeModeRequest.class,
true),
/**
* SFTP 下载文件夹展开文件
*/
SFTP_DOWNLOAD_FLAT_DIRECTORY("df",
SftpDownloadFlatDirectoryHandler.class,
new String[]{"type", "sessionId", "currentPath", "path"},
SftpDownloadFlatDirectoryRequest.class,
true),
/**
* SFTP 获取内容
*/
SFTP_GET_CONTENT("gc",
SftpGetContentHandler.class,
new String[]{"type", "sessionId", "path"},
SftpBaseRequest.class,
true),
/**
* SFTP 修改内容
*/
SFTP_SET_CONTENT("sc",
SftpSetContentHandler.class,
new String[]{"type", "sessionId", "path"},
SftpBaseRequest.class,
true),
;
private static final char SEPARATOR = '|';
@Getter
private final String type;
private final Class<? extends ITerminalHandler<? extends TerminalBasePayload>> handlerBean;
private final String[] payloadDefine;
private final Class<? extends TerminalBasePayload> payloadClass;
@Getter
private final boolean asyncExec;
@Getter
private ITerminalHandler<? extends TerminalBasePayload> handler;
<T extends TerminalBasePayload> InputTypeEnum(String type,
Class<? extends ITerminalHandler<T>> handlerBean,
String[] payloadDefine,
Class<T> payloadClass,
boolean asyncExec) {
this.type = type;
this.handlerBean = handlerBean;
this.payloadDefine = payloadDefine;
this.payloadClass = payloadClass;
this.asyncExec = asyncExec;
}
public static InputTypeEnum of(String payload) {
if (payload == null) {
return null;
}
for (InputTypeEnum value : values()) {
if (payload.startsWith(value.type + SEPARATOR) || payload.equals(value.type)) {
return value;
}
}
return null;
}
/**
* 解析请求
*
* @param payload payload
* @param <T> T
* @return payload
*/
@SuppressWarnings("unchecked")
public <T extends TerminalBasePayload> T parse(String payload) {
JSONObject object = new JSONObject();
int curr = 0;
int len = payload.length();
for (int i = 0, pl = payloadDefine.length; i < pl; i++) {
if (i == pl - 1) {
// 最后一次
object.put(payloadDefine[i], payload.substring(curr, len));
} else {
// 非最后一次
StringBuilder tmp = new StringBuilder();
for (; curr < len; curr++) {
char c = payload.charAt(curr);
if (c == SEPARATOR) {
object.put(payloadDefine[i], tmp.toString());
curr++;
break;
} else {
tmp.append(c);
}
}
}
}
return (T) object.toJavaObject(payloadClass);
}
/**
* 类型字段定义
*/
@Component
static class TypeFieldDefinition {
@PostConstruct
public void init() {
for (InputTypeEnum value : InputTypeEnum.values()) {
value.handler = SpringHolder.getBean(value.handlerBean);
}
}
}
}

View File

@ -0,0 +1,121 @@
package cd.casic.module.terminal.host.terminal.enums;
import cn.orionsec.kit.lang.utils.json.matcher.ReplacementFormatters;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 输出操作类型枚举
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/29 15:33
*/
@Getter
@AllArgsConstructor
public enum OutputTypeEnum {
/**
* 终端连接检查
*/
CHECK("ck", "${type}|${sessionId}|${result}|${msg}"),
/**
* 终端连接
*/
CONNECT("co", "${type}|${sessionId}|${result}|${msg}"),
/**
* 关闭连接
*/
CLOSE("cl", "${type}|${sessionId}|${forceClose}|${msg}"),
/**
* pong
*/
PONG("p", "${type}"),
/**
* SSH 输出
*/
SSH_OUTPUT("o", "${type}|${sessionId}|${body}"),
/**
* SFTP 文件列表
*/
SFTP_LIST("ls", "${type}|${sessionId}|${path}|${result}|${msg}|${body}"),
/**
* SFTP 创建文件夹
*/
SFTP_MKDIR("mk", "${type}|${sessionId}|${result}|${msg}"),
/**
* SFTP 创建文件
*/
SFTP_TOUCH("to", "${type}|${sessionId}|${result}|${msg}"),
/**
* SFTP 移动文件
*/
SFTP_MOVE("mv", "${type}|${sessionId}|${result}|${msg}"),
/**
* SFTP 删除文件
*/
SFTP_REMOVE("rm", "${type}|${sessionId}|${result}|${msg}"),
/**
* SFTP 截断文件
*/
SFTP_TRUNCATE("tc", "${type}|${sessionId}|${result}|${msg}"),
/**
* SFTP 修改文件权限
*/
SFTP_CHMOD("cm", "${type}|${sessionId}|${result}|${msg}"),
/**
* SFTP 下载文件夹展开文件
*/
SFTP_DOWNLOAD_FLAT_DIRECTORY("df", "${type}|${sessionId}|${currentPath}|${result}|${msg}|${body}"),
/**
* SFTP 获取文件内容
*/
SFTP_GET_CONTENT("gc", "${type}|${sessionId}|${result}|${msg}|${token}"),
/**
* SFTP 修改文件内容
*/
SFTP_SET_CONTENT("sc", "${type}|${sessionId}|${result}|${msg}|${token}"),
;
private final String type;
private final String template;
/**
* 格式化
*
* @param o o
* @return 格式化
*/
public String format(Object o) {
return ReplacementFormatters.format(this.template, o);
}
public static OutputTypeEnum of(String type) {
if (type == null) {
return null;
}
for (OutputTypeEnum value : values()) {
if (value.type.equals(type)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,51 @@
package cd.casic.module.terminal.host.terminal.enums;
import cn.orionsec.kit.lang.utils.collect.Lists;
import java.util.List;
/**
* 终端连接状态
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/26 22:27
*/
public enum TerminalConnectStatusEnum {
/**
* 连接中
*/
CONNECTING,
/**
* 完成
*/
COMPLETE,
/**
* 失败
*/
FAILED,
/**
* 强制下线
*/
FORCE_OFFLINE,
;
public static final List<String> FINISH_STATUS_LIST = Lists.of(COMPLETE.name(), FAILED.name(), FORCE_OFFLINE.name());
public static TerminalConnectStatusEnum of(String type) {
if (type == null) {
return null;
}
for (TerminalConnectStatusEnum value : values()) {
if (value.name().equals(type)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,98 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.ErrorMessage;
import cd.casic.module.terminal.common.OperatorLogs;
import cd.casic.module.terminal.common.WebSockets;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.manager.TerminalManager;
import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload;
import cd.casic.module.terminal.host.terminal.model.TerminalConfig;
import cd.casic.module.terminal.host.terminal.session.ITerminalSession;
import jakarta.annotation.Resource;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
/**
* 终端消息处理器 基类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/29 18:59
*/
public abstract class AbstractTerminalHandler<T extends TerminalBasePayload> implements ITerminalHandler<T> {
@Resource
protected TerminalManager terminalManager;
/**
* 发送消息
*
* @param channel channel
* @param type type
* @param body body
* @param <E> E
*/
public <E extends TerminalBasePayload> void send(WebSocketSession channel, OutputTypeEnum type, E body) {
body.setType(type.getType());
// 发送消息
this.send(channel, type.format(body));
}
/**
* 发送消息
*
* @param channel channel
* @param message message
*/
protected void send(WebSocketSession channel, String message) {
WebSockets.sendText(channel, message);
}
/**
* 保存操作日志
*
* @param payload payload
* @param channel channel
* @param extra extra
* @param type type
* @param startTime startTime
* @param ex ex
*/
protected void saveOperatorLog(T payload,
WebSocketSession channel,
Map<String, Object> extra,
String type,
long startTime,
Exception ex) {
String channelId = channel.getId();
String sessionId = payload.getSessionId();
// 获取会话并且设置参数
ITerminalSession session = terminalManager.getSession(channelId, sessionId);
if (session != null) {
TerminalConfig config = session.getConfig();
extra.put(OperatorLogs.HOST_ID, config.getHostId());
extra.put(OperatorLogs.HOST_NAME, config.getHostName());
extra.put(OperatorLogs.ADDRESS, config.getAddress());
}
extra.put(OperatorLogs.CHANNEL_ID, channelId);
extra.put(OperatorLogs.SESSION_ID, sessionId);
// 获取日志
// OperatorLogModel model = TerminalUtils.getOperatorLogModel(channel, extra, type, startTime, ex);
// 保存
// operatorLogFrameworkService.insert(model);
}
/**
* 获取错误信息
*
* @param ex ex
* @return msg
*/
protected String getErrorMessage(Exception ex) {
// 获取错误信息
return ErrorMessage.getErrorMessage(ex, ErrorMessage.OPERATE_ERROR);
}
}

View File

@ -0,0 +1,23 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload;
import org.springframework.web.socket.WebSocketSession;
/**
* 终端消息处理器
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/29 18:53
*/
public interface ITerminalHandler<T extends TerminalBasePayload> {
/**
* 处理消息
*
* @param channel channel
* @param payload payload
*/
void handle(WebSocketSession channel, T payload);
}

View File

@ -0,0 +1,65 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.common.OperatorLogs;
import cd.casic.module.terminal.define.operator.TerminalOperatorType;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpChangeModeRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.utils.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
/**
* sftp 修改文件权限
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/19 11:13
*/
@Slf4j
@Component
public class SftpChangeModeHandler extends AbstractTerminalHandler<SftpChangeModeRequest> {
@Override
public void handle(WebSocketSession channel, SftpChangeModeRequest payload) {
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
Integer mod = payload.getMod();
log.info("SftpChangeModeHandler-handle start sessionId: {}, path: {}, mod: {}", sessionId, path, mod);
Exception ex = null;
// 修改权限
try {
session.chmod(path, mod);
log.info("SftpChangeModeHandler-handle success sessionId: {}, path: {}, mod: {}", sessionId, path, mod);
} catch (Exception e) {
log.error("SftpChangeModeHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_CHMOD,
SftpBaseResponse.builder()
.sessionId(sessionId)
.result(BooleanBit.of(ex == null).getValue())
.msg(this.getErrorMessage(ex))
.build());
// 保存操作日志
Map<String, Object> extra = Maps.newMap();
extra.put(OperatorLogs.PATH, path);
extra.put(OperatorLogs.MOD, mod);
this.saveOperatorLog(payload, channel,
extra, TerminalOperatorType.SFTP_CHMOD,
startTime, ex);
}
}

View File

@ -0,0 +1,58 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpDownloadFlatDirectoryRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpDownloadFlatDirectoryResponse;
import cd.casic.module.terminal.host.terminal.model.response.SftpFileVO;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.utils.collect.Lists;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Arrays;
import java.util.List;
/**
* sftp 下载文件夹展开文件
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/19 11:13
*/
@Slf4j
@Component
public class SftpDownloadFlatDirectoryHandler extends AbstractTerminalHandler<SftpDownloadFlatDirectoryRequest> {
@Override
public void handle(WebSocketSession channel, SftpDownloadFlatDirectoryRequest payload) {
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String[] paths = payload.getPath().split("\\|");
log.info("SftpDownloadFlatDirectoryHandler-handle start sessionId: {}, paths: {}", sessionId, Arrays.toString(paths));
Exception ex = null;
List<SftpFileVO> files = Lists.empty();
// 展开文件夹内的全部文件
try {
files = session.flatDirectory(paths);
log.info("SftpDownloadFlatDirectoryHandler-handle success sessionId: {}, paths: {}", sessionId, Arrays.toString(paths));
} catch (Exception e) {
log.error("SftpDownloadFlatDirectoryHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_DOWNLOAD_FLAT_DIRECTORY,
SftpDownloadFlatDirectoryResponse.builder()
.sessionId(sessionId)
.currentPath(payload.getCurrentPath())
.body(JSON.toJSONString(files))
.result(BooleanBit.of(ex == null).getValue())
.msg(this.getErrorMessage(ex))
.build());
}
}

View File

@ -0,0 +1,66 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.dal.redis.TerminalRedisDAO;
import cd.casic.module.terminal.define.cache.TerminalCacheKeyDefine;
import cd.casic.module.terminal.host.terminal.dto.SftpGetContentCacheDTO;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpGetContentResponse;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.id.UUIds;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
/**
* sftp 获取文件内容
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/19 11:13
*/
@Slf4j
@Component
public class SftpGetContentHandler extends AbstractTerminalHandler<SftpBaseRequest> {
@Resource
private TerminalRedisDAO terminalRedisDAO;
@Override
public void handle(WebSocketSession channel, SftpBaseRequest payload) {
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpGetContentHandler-handle start sessionId: {}, path: {}", sessionId, path);
String token = UUIds.random32();
Exception ex = null;
try {
// 检查文件是否可编辑
session.checkCanEdit(path);
// 设置缓存
String key = TerminalCacheKeyDefine.TERMINAL_SFTP_GET_CONTENT.format(token);
SftpGetContentCacheDTO cache = SftpGetContentCacheDTO.builder()
.hostId(session.getConfig().getHostId())
.path(path)
.build();
terminalRedisDAO.setJson(key, TerminalCacheKeyDefine.TERMINAL_SFTP_GET_CONTENT, cache);
log.info("SftpGetContentHandler-handle success sessionId: {}, path: {}", sessionId, path);
} catch (Exception e) {
log.error("SftpGetContentHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_GET_CONTENT,
SftpGetContentResponse.builder()
.sessionId(sessionId)
.result(BooleanBit.of(ex == null).getValue())
.token(token)
.msg(this.getErrorMessage(ex))
.build());
}
}

View File

@ -0,0 +1,62 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpListRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpFileVO;
import cd.casic.module.terminal.host.terminal.model.response.SftpListResponse;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.utils.collect.Lists;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.List;
/**
* sftp 文件列表
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/29 15:32
*/
@Slf4j
@Component
public class SftpListHandler extends AbstractTerminalHandler<SftpListRequest> {
private static final String HOME_PATH = "~";
@Override
public void handle(WebSocketSession channel, SftpListRequest payload) {
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpListHandler-handle start sessionId: {}, path: {}", sessionId, path);
Exception ex = null;
List<SftpFileVO> list = Lists.empty();
try {
// 空目录则直接获取 home 目录
if (HOME_PATH.equals(path)) {
path = session.getHome();
}
// 文件列表
list = session.list(path, BooleanBit.toBoolean(payload.getShowHiddenFile()));
log.info("SftpListHandler-handle success sessionId: {}, path: {}", sessionId, path);
} catch (Exception e) {
log.error("SftpListHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_LIST,
SftpListResponse.builder()
.sessionId(sessionId)
.result(BooleanBit.of(ex == null).getValue())
.path(path)
.body(JSON.toJSONString(list))
.build());
}
}

View File

@ -0,0 +1,60 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.common.OperatorLogs;
import cd.casic.module.terminal.define.operator.TerminalOperatorType;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.utils.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
/**
* sftp 创建文件夹
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/19 11:13
*/
@Slf4j
@Component
public class SftpMakeDirectoryHandler extends AbstractTerminalHandler<SftpBaseRequest> {
@Override
public void handle(WebSocketSession channel, SftpBaseRequest payload) {
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpMakeDirectoryHandler-handle start sessionId: {}, path: {}", sessionId, path);
Exception ex = null;
// 创建文件夹
try {
session.mkdir(path);
log.info("SftpMakeDirectoryHandler-handle success sessionId: {}, path: {}", sessionId, path);
} catch (Exception e) {
log.error("SftpMakeDirectoryHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_MKDIR,
SftpBaseResponse.builder()
.sessionId(sessionId)
.result(BooleanBit.of(ex == null).getValue())
.msg(this.getErrorMessage(ex))
.build());
// 保存操作日志
Map<String, Object> extra = Maps.newMap();
extra.put(OperatorLogs.PATH, path);
this.saveOperatorLog(payload, channel,
extra, TerminalOperatorType.SFTP_MKDIR,
startTime, ex);
}
}

View File

@ -0,0 +1,62 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.common.OperatorLogs;
import cd.casic.module.terminal.define.operator.TerminalOperatorType;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpMoveRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.utils.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
/**
* sftp 移动文件
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/19 11:13
*/
@Slf4j
@Component
public class SftpMoveHandler extends AbstractTerminalHandler<SftpMoveRequest> {
@Override
public void handle(WebSocketSession channel, SftpMoveRequest payload) {
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
String target = payload.getTarget();
log.info("SftpMoveHandler-handle start sessionId: {}, path: {}, target: {}", sessionId, path, target);
Exception ex = null;
// 移动
try {
session.move(path, target);
log.info("SftpMoveHandler-handle success sessionId: {}, path: {}, target: {}", sessionId, path, target);
} catch (Exception e) {
log.error("SftpMoveHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_MOVE,
SftpBaseResponse.builder()
.sessionId(sessionId)
.result(BooleanBit.of(ex == null).getValue())
.msg(this.getErrorMessage(ex))
.build());
// 保存操作日志
Map<String, Object> extra = Maps.newMap();
extra.put(OperatorLogs.PATH, path);
extra.put(OperatorLogs.TARGET, target);
this.saveOperatorLog(payload, channel,
extra, TerminalOperatorType.SFTP_MOVE,
startTime, ex);
}
}

View File

@ -0,0 +1,62 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.common.OperatorLogs;
import cd.casic.module.terminal.define.operator.TerminalOperatorType;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.utils.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Arrays;
import java.util.Map;
/**
* sftp 删除文件
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/19 11:13
*/
@Slf4j
@Component
public class SftpRemoveHandler extends AbstractTerminalHandler<SftpBaseRequest> {
@Override
public void handle(WebSocketSession channel, SftpBaseRequest payload) {
long startTime = System.currentTimeMillis();
String path = payload.getPath();
String sessionId = payload.getSessionId();
// 获取会话
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String[] paths = path.split("\\|");
log.info("SftpRemoveHandler-handle start sessionId: {}, path: {}", sessionId, Arrays.toString(paths));
Exception ex = null;
// 删除
try {
session.remove(paths);
log.info("SftpRemoveHandler-handle success sessionId: {}, path: {}", sessionId, Arrays.toString(paths));
} catch (Exception e) {
log.error("SftpRemoveHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_REMOVE,
SftpBaseResponse.builder()
.sessionId(sessionId)
.result(BooleanBit.of(ex == null).getValue())
.msg(this.getErrorMessage(ex))
.build());
// 保存操作日志
Map<String, Object> extra = Maps.newMap();
extra.put(OperatorLogs.PATH, path);
this.saveOperatorLog(payload, channel,
extra, TerminalOperatorType.SFTP_REMOVE,
startTime, ex);
}
}

View File

@ -0,0 +1,78 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.common.OperatorLogs;
import cd.casic.module.terminal.dal.redis.TerminalRedisDAO;
import cd.casic.module.terminal.define.cache.TerminalCacheKeyDefine;
import cd.casic.module.terminal.define.operator.TerminalOperatorType;
import cd.casic.module.terminal.host.terminal.dto.SftpSetContentCacheDTO;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpSetContentResponse;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.id.UUIds;
import cn.orionsec.kit.lang.utils.collect.Maps;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
/**
* sftp 设置文件内容
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/19 11:13
*/
@Slf4j
@Component
public class SftpSetContentHandler extends AbstractTerminalHandler<SftpBaseRequest> {
@Resource
private TerminalRedisDAO terminalRedisDAO;
@Override
public void handle(WebSocketSession channel, SftpBaseRequest payload) {
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpSetContentHandler-handle start sessionId: {}, path: {}", sessionId, path);
String token = UUIds.random32();
Exception ex = null;
try {
// 检查文件是否可编辑
session.checkCanEdit(path);
// 设置缓存
String key = TerminalCacheKeyDefine.TERMINAL_SFTP_SET_CONTENT.format(token);
SftpSetContentCacheDTO cache = SftpSetContentCacheDTO.builder()
.hostId(session.getConfig().getHostId())
.path(path)
.build();
terminalRedisDAO.setJson(key, TerminalCacheKeyDefine.TERMINAL_SFTP_SET_CONTENT, cache);
log.info("SftpSetContentHandler-handle success sessionId: {}, path: {}", sessionId, path);
} catch (Exception e) {
log.error("SftpSetContentHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_SET_CONTENT,
SftpSetContentResponse.builder()
.sessionId(sessionId)
.result(BooleanBit.of(ex == null).getValue())
.token(token)
.msg(this.getErrorMessage(ex))
.build());
// 保存操作日志
Map<String, Object> extra = Maps.newMap();
extra.put(OperatorLogs.PATH, path);
this.saveOperatorLog(payload, channel,
extra, TerminalOperatorType.SFTP_SET_CONTENT,
startTime, ex);
}
}

View File

@ -0,0 +1,60 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.common.OperatorLogs;
import cd.casic.module.terminal.define.operator.TerminalOperatorType;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.utils.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
/**
* sftp 创建文件
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/19 11:13
*/
@Slf4j
@Component
public class SftpTouchHandler extends AbstractTerminalHandler<SftpBaseRequest> {
@Override
public void handle(WebSocketSession channel, SftpBaseRequest payload) {
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpTouchHandler-handle start sessionId: {}, path: {}", sessionId, path);
Exception ex = null;
// 创建文件
try {
session.touch(path);
log.info("SftpTouchHandler-handle success sessionId: {}, path: {}", sessionId, path);
} catch (Exception e) {
log.error("SftpTouchHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_TOUCH,
SftpBaseResponse.builder()
.sessionId(sessionId)
.result(BooleanBit.of(ex == null).getValue())
.msg(this.getErrorMessage(ex))
.build());
// 保存操作日志
Map<String, Object> extra = Maps.newMap();
extra.put(OperatorLogs.PATH, path);
this.saveOperatorLog(payload, channel,
extra, TerminalOperatorType.SFTP_TOUCH,
startTime, ex);
}
}

View File

@ -0,0 +1,61 @@
package cd.casic.module.terminal.host.terminal.handler;
import cd.casic.module.terminal.common.BooleanBit;
import cd.casic.module.terminal.common.OperatorLogs;
import cd.casic.module.terminal.define.operator.TerminalOperatorType;
import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum;
import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest;
import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse;
import cd.casic.module.terminal.host.terminal.session.ISftpSession;
import cn.orionsec.kit.lang.utils.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
/**
* sftp 截断文件
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/19 11:13
*/
@Slf4j
@Component
public class SftpTruncateHandler extends AbstractTerminalHandler<SftpBaseRequest> {
@Override
public void handle(WebSocketSession channel, SftpBaseRequest payload) {
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpTruncateHandler-handle start sessionId: {}, path: {}", sessionId, path);
Exception ex = null;
// 截断文件
try {
session.truncate(path);
log.info("SftpTruncateHandler-handle success sessionId: {}, path: {}", sessionId, path);
} catch (Exception e) {
log.error("SftpTruncateHandler-handle error sessionId: {}", sessionId, e);
ex = e;
}
// 返回
this.send(channel,
OutputTypeEnum.SFTP_TRUNCATE,
SftpBaseResponse.builder()
.sessionId(sessionId)
.result(BooleanBit.of(ex == null).getValue())
.msg(this.getErrorMessage(ex))
.build());
// 保存操作日志
Map<String, Object> extra = Maps.newMap();
extra.put(OperatorLogs.PATH, path);
this.saveOperatorLog(payload, channel,
extra, TerminalOperatorType.SFTP_TRUNCATE,
startTime, ex);
}
}

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