first commit

This commit is contained in:
2026-03-06 14:01:32 +08:00
commit def60e21c7
1074 changed files with 119423 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
<?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">
<parent>
<groupId>cn.orionsec.ops</groupId>
<artifactId>orion-ops-api</artifactId>
<version>1.3.1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>orion-ops-service</name>
<artifactId>orion-ops-service</artifactId>
<modelVersion>4.0.0</modelVersion>
<dependencies>
<!-- common -->
<dependency>
<groupId>cn.orionsec.ops</groupId>
<artifactId>orion-ops-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- model -->
<dependency>
<groupId>cn.orionsec.ops</groupId>
<artifactId>orion-ops-model</artifactId>
<version>${project.version}</version>
</dependency>
<!-- dao -->
<dependency>
<groupId>cn.orionsec.ops</groupId>
<artifactId>orion-ops-dao</artifactId>
<version>${project.version}</version>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- web socket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- tests -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- ding -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,20 @@
package cn.orionsec.ops.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
}

View File

@@ -0,0 +1,35 @@
package cn.orionsec.ops.config;
import cn.orionsec.kit.lang.utils.collect.Lists;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import java.util.List;
@Configuration
public class JsonSerializerConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
List<MediaType> mediaTypes = Lists.newList();
mediaTypes.add(MediaType.APPLICATION_JSON);
fastJsonHttpMessageConverter.setSupportedMediaTypes(mediaTypes);
fastJsonConfig.setSerializerFeatures(
SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.IgnoreNonFieldGetter
);
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
return new HttpMessageConverters(fastJsonHttpMessageConverter);
}
}

View File

@@ -0,0 +1,25 @@
package cn.orionsec.ops.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisSerializeConfig {
@Bean
public <T> RedisTemplate<String, T> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, T> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(RedisSerializer.json());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

View File

@@ -0,0 +1,23 @@
package cn.orionsec.ops.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@EnableScheduling
@Configuration
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(4);
scheduler.setRemoveOnCancelPolicy(true);
scheduler.setThreadNamePrefix("scheduling-task-");
return scheduler;
}
}

View File

@@ -0,0 +1,51 @@
package cn.orionsec.ops.handler.alarm;
import cn.orionsec.ops.constant.machine.MachineAlarmType;
import cn.orionsec.ops.entity.domain.UserInfoDO;
import lombok.Data;
import java.util.Date;
import java.util.Map;
@Data
public class MachineAlarmContext {
/**
* 报警机器id
*/
private Long machineId;
/**
* 报警机器名称
*/
private String machineName;
/**
* 报警主机
*/
private String machineHost;
/**
* 报警类型
*
* @see MachineAlarmType
*/
private Integer alarmType;
/**
* 报警值
*/
private Double alarmValue;
/**
* 报警时间
*/
private Date alarmTime;
/**
* 用户映射
*/
private Map<Long, UserInfoDO> userMapping;
}

View File

@@ -0,0 +1,103 @@
package cn.orionsec.ops.handler.alarm;
import cn.orionsec.kit.lang.able.Executable;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.alarm.AlarmGroupNotifyType;
import cn.orionsec.ops.dao.UserInfoDAO;
import cn.orionsec.ops.entity.domain.AlarmGroupNotifyDO;
import cn.orionsec.ops.entity.domain.AlarmGroupUserDO;
import cn.orionsec.ops.entity.domain.MachineAlarmGroupDO;
import cn.orionsec.ops.entity.domain.UserInfoDO;
import cn.orionsec.ops.handler.alarm.push.AlarmWebSideMessagePusher;
import cn.orionsec.ops.handler.alarm.push.AlarmWebhookPusher;
import cn.orionsec.ops.service.api.AlarmGroupNotifyService;
import cn.orionsec.ops.service.api.AlarmGroupUserService;
import cn.orionsec.ops.service.api.MachineAlarmGroupService;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
public class MachineAlarmExecutor implements Runnable, Executable {
private static final MachineAlarmGroupService machineAlarmGroupService = SpringHolder.getBean(MachineAlarmGroupService.class);
private static final AlarmGroupUserService alarmGroupUserService = SpringHolder.getBean(AlarmGroupUserService.class);
private static final AlarmGroupNotifyService alarmGroupNotifyService = SpringHolder.getBean(AlarmGroupNotifyService.class);
private static final UserInfoDAO userInfoDAO = SpringHolder.getBean(UserInfoDAO.class);
private final MachineAlarmContext context;
public MachineAlarmExecutor(MachineAlarmContext context) {
this.context = context;
}
@Override
public void exec() {
Threads.start(this, SchedulerPools.MACHINE_ALARM_SCHEDULER);
}
@Override
public void run() {
log.info("机器触发报警推送 context: {}", JSON.toJSONString(context));
// 查询报警组
List<Long> alarmGroupIdList = machineAlarmGroupService.selectByMachineId(context.getMachineId())
.stream()
.map(MachineAlarmGroupDO::getGroupId)
.collect(Collectors.toList());
log.info("机器触发报警推送 groupId: {}", alarmGroupIdList);
if (alarmGroupIdList.isEmpty()) {
return;
}
// 查询报警组员
List<Long> alarmUserIdList = alarmGroupUserService.selectByGroupIdList(alarmGroupIdList)
.stream()
.map(AlarmGroupUserDO::getUserId)
.distinct()
.collect(Collectors.toList());
log.info("机器触发报警推送 userId: {}", alarmUserIdList);
if (alarmGroupIdList.isEmpty()) {
return;
}
// 查询用户信息
Map<Long, UserInfoDO> userMapping = userInfoDAO.selectBatchIds(alarmUserIdList)
.stream()
.collect(Collectors.toMap(UserInfoDO::getId, Function.identity()));
context.setUserMapping(userMapping);
// 通知站内信
this.doAlarmPush(alarmGroupIdList);
}
/**
* 执行报警推送
*
* @param alarmGroupIdList 报警组id
*/
private void doAlarmPush(List<Long> alarmGroupIdList) {
// 通知站内信
new AlarmWebSideMessagePusher(context).push();
// 通知报警组
List<AlarmGroupNotifyDO> alarmNotifyList = alarmGroupNotifyService.selectByGroupIdList(alarmGroupIdList);
for (AlarmGroupNotifyDO alarmNotify : alarmNotifyList) {
Integer notifyType = alarmNotify.getNotifyType();
if (AlarmGroupNotifyType.WEBHOOK.getType().equals(notifyType)) {
// 通知 webhook
try {
new AlarmWebhookPusher(alarmNotify.getNotifyId(), context).push();
} catch (Exception e) {
log.error("机器报警 webhook 推送失败 id: {}", alarmNotify.getNotifyId(), e);
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
package cn.orionsec.ops.handler.alarm.push;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.math.Numbers;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.constant.machine.MachineAlarmType;
import cn.orionsec.ops.constant.message.MessageType;
import cn.orionsec.ops.handler.alarm.MachineAlarmContext;
import cn.orionsec.ops.service.api.WebSideMessageService;
import java.util.Map;
public class AlarmWebSideMessagePusher implements IAlarmPusher {
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
private final MachineAlarmContext context;
public AlarmWebSideMessagePusher(MachineAlarmContext context) {
this.context = context;
}
@Override
public void push() {
// 发送站内信
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.NAME, context.getMachineName());
params.put(EventKeys.HOST, context.getMachineHost());
params.put(EventKeys.TYPE, MachineAlarmType.of(context.getAlarmType()).getLabel());
params.put(EventKeys.VALUE, Numbers.setScale(context.getAlarmValue(), 2));
params.put(EventKeys.TIME, Dates.format(context.getAlarmTime()));
context.getUserMapping().forEach((k, v) -> {
webSideMessageService.addMessage(MessageType.MACHINE_ALARM, context.getMachineId(), k, v.getUsername(), params);
});
}
}

View File

@@ -0,0 +1,81 @@
package cn.orionsec.ops.handler.alarm.push;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.math.Numbers;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.constant.machine.MachineAlarmType;
import cn.orionsec.ops.constant.webhook.WebhookType;
import cn.orionsec.ops.dao.WebhookConfigDAO;
import cn.orionsec.ops.entity.domain.UserInfoDO;
import cn.orionsec.ops.entity.domain.WebhookConfigDO;
import cn.orionsec.ops.handler.alarm.MachineAlarmContext;
import cn.orionsec.ops.handler.webhook.DingRobotPusher;
import cn.orionsec.ops.utils.ResourceLoader;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class AlarmWebhookPusher implements IAlarmPusher {
private static final String DING_TEMPLATE = "/templates/push/machine-alarm-ding.template";
private static WebhookConfigDAO webhookConfigDAO = SpringHolder.getBean(WebhookConfigDAO.class);
private final Long webhookId;
private final MachineAlarmContext context;
public AlarmWebhookPusher(Long webhookId, MachineAlarmContext context) {
this.webhookId = webhookId;
this.context = context;
}
@Override
public void push() {
// 查询 webhook
WebhookConfigDO webhook = webhookConfigDAO.selectById(webhookId);
if (webhook == null) {
return;
}
// 触发 webhook
if (WebhookType.DING_ROBOT.getType().equals(webhook.getWebhookType())) {
// 钉钉机器人
this.doDingRobotPush(webhook);
}
}
/**
* 执行钉钉机器人推送
*
* @param webhook webhook
*/
private void doDingRobotPush(WebhookConfigDO webhook) {
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.NAME, context.getMachineName());
params.put(EventKeys.HOST, context.getMachineHost());
params.put(EventKeys.VALUE, Numbers.setScale(context.getAlarmValue(), 2));
params.put(EventKeys.TYPE, MachineAlarmType.of(context.getAlarmType()).getLabel());
params.put(EventKeys.TIME, Dates.format(context.getAlarmTime()));
String text = Strings.format(ResourceLoader.get(DING_TEMPLATE, AlarmWebhookPusher.class), params);
// @ 的用户
List<String> atMobiles = context.getUserMapping()
.values()
.stream()
.map(UserInfoDO::getContactPhone)
.collect(Collectors.toList());
// 推送
DingRobotPusher.builder()
.url(webhook.getWebhookUrl())
.title("机器发生报警")
.text(text)
.atMobiles(atMobiles)
.build()
.push();
}
}

View File

@@ -0,0 +1,10 @@
package cn.orionsec.ops.handler.alarm.push;
public interface IAlarmPusher {
/**
* 执行推送
*/
void push();
}

View File

@@ -0,0 +1,309 @@
package cn.orionsec.ops.handler.app.action;
import cn.orionsec.kit.lang.constant.Letters;
import cn.orionsec.kit.lang.define.io.OutputAppender;
import cn.orionsec.kit.lang.exception.ExecuteException;
import cn.orionsec.kit.lang.exception.LogException;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.net.host.ssh.ExitCode;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.app.ActionStatus;
import cn.orionsec.ops.constant.app.ActionType;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.dao.ApplicationActionLogDAO;
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
import cn.orionsec.ops.utils.Utils;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.OutputStream;
import java.util.Date;
@Slf4j
public abstract class AbstractActionHandler implements IActionHandler {
protected static ApplicationActionLogDAO applicationActionLogDAO = SpringHolder.getBean(ApplicationActionLogDAO.class);
protected Long id;
protected Long relId;
protected MachineActionStore store;
protected ApplicationActionLogDO action;
protected OutputStream outputSteam;
protected OutputAppender appender;
protected volatile boolean terminated;
protected Date startTime, endTime;
@Getter
protected volatile ActionStatus status;
public AbstractActionHandler(Long id, MachineActionStore store) {
this.id = id;
this.relId = store.getRelId();
this.store = store;
this.action = store.getActions().get(id);
this.status = ActionStatus.of(action.getRunStatus());
}
@Override
public void exec() {
log.info("应用操作执行-开始: relId: {}, id: {}", relId, id);
// 状态检查
if (!ActionStatus.WAIT.equals(status)) {
return;
}
Exception ex = null;
// 执行
try {
// 更新状态
this.updateStatus(ActionStatus.RUNNABLE);
// 打开日志
this.openLogger();
// 执行
this.handler();
} catch (Exception e) {
ex = e;
}
// 回调
try {
if (terminated) {
// 停止回调
this.terminatedCallback();
} else if (ex == null) {
// 成功回调
this.successCallback();
} else {
// 异常回调
this.exceptionCallback(ex);
throw Exceptions.runtime(ex.getMessage(), ex);
}
} finally {
// 释放资源
this.close();
}
}
/**
* 处理流程
*
* @throws Exception Exception
*/
protected abstract void handler() throws Exception;
@Override
public void skip() {
log.info("应用操作执行-跳过: relId: {}, id: {}", relId, id);
if (ActionStatus.WAIT.equals(status)) {
// 只能跳过等待中的任务
this.updateStatus(ActionStatus.SKIPPED);
}
}
@Override
public void terminate() {
log.info("应用操作执行-终止: relId: {}, id: {}", relId, id);
this.terminated = true;
}
/**
* 打开日志
*/
protected void openLogger() {
String logPath = Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), action.getLogPath());
log.info("应用操作执行-打开日志 relId: {}, id: {}, path: {}", relId, id, logPath);
File logFile = new File(logPath);
Files1.touch(logFile);
this.outputSteam = Files1.openOutputStreamFastSafe(logFile);
this.appender = OutputAppender.create(outputSteam).then(store.getSuperLogStream());
// 拼接开始日志
this.appendStartedLog();
}
/**
* 拼接开始日志
*/
@SneakyThrows
private void appendStartedLog() {
StringBuilder log = new StringBuilder()
.append(StainCode.prefix(StainCode.GLOSS_GREEN))
.append("# ").append(action.getActionName()).append(" 执行开始")
.append(StainCode.SUFFIX)
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
ActionType actionType = ActionType.of(action.getActionType());
if (ActionType.BUILD_COMMAND.equals(actionType) || ActionType.RELEASE_COMMAND.equals(actionType)) {
log.append(Letters.LF)
.append(Utils.getStainKeyWords("# 执行命令", StainCode.GLOSS_BLUE))
.append(Letters.LF)
.append(StainCode.prefix(StainCode.GLOSS_CYAN))
.append(Utils.getEndLfWithEof(action.getActionCommand()))
.append(StainCode.SUFFIX);
}
store.getSuperLogStream().write(Strings.bytes(Const.LF_3));
this.appendLog(log.toString());
}
/**
* 停止回调
*/
private void terminatedCallback() {
log.info("应用操作执行-终止回调: relId: {}, id: {}", relId, id);
// 修改状态
this.updateStatus(ActionStatus.TERMINATED);
// 拼接日志
StringBuilder log = new StringBuilder(Const.LF)
.append(StainCode.prefix(StainCode.GLOSS_YELLOW))
.append("# ").append(action.getActionName()).append(" 手动停止")
.append(StainCode.SUFFIX)
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(log.toString());
}
/**
* 成功回调
*/
private void successCallback() {
log.info("应用操作执行-成功回调: relId: {}, id: {}", relId, id);
// 修改状态
this.updateStatus(ActionStatus.FINISH);
// 拼接完成日志
this.appendFinishedLog(null);
}
/**
* 异常回调
*
* @param ex ex
*/
private void exceptionCallback(Exception ex) {
log.error("应用操作执行-异常回调: relId: {}, id: {}", relId, id, ex);
// 修改状态
this.updateStatus(ActionStatus.FAILURE);
// 拼接完成日志
this.appendFinishedLog(ex);
}
/**
* 拼接完成日志
*
* @param ex ex
*/
private void appendFinishedLog(Exception ex) {
StringBuilder log = new StringBuilder();
Integer actionType = action.getActionType();
if (ActionType.BUILD_COMMAND.getType().equals(actionType) || ActionType.RELEASE_COMMAND.getType().equals(actionType)) {
log.append(Const.LF);
}
if (ex != null) {
// 有异常
log.append(StainCode.prefix(StainCode.GLOSS_RED))
.append("# ").append(action.getActionName()).append(" 执行失败")
.append(StainCode.SUFFIX)
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE));
Integer exitCode = this.getExitCode();
if (exitCode != null) {
log.append(" exitcode: ")
.append(ExitCode.isSuccess(exitCode)
? Utils.getStainKeyWords(exitCode, StainCode.GLOSS_BLUE)
: Utils.getStainKeyWords(exitCode, StainCode.GLOSS_RED));
}
log.append(Letters.LF);
} else {
// 无异常
long used = endTime.getTime() - startTime.getTime();
log.append(StainCode.prefix(StainCode.GLOSS_GREEN))
.append("# ").append(action.getActionName()).append(" 执行完成")
.append(StainCode.SUFFIX)
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(" used ")
.append(Utils.getStainKeyWords(Utils.interval(used), StainCode.GLOSS_BLUE))
.append(" (")
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
.append(used)
.append("ms")
.append(StainCode.SUFFIX)
.append(")\n");
}
// 拼接异常
if (ex != null && !(ex instanceof ExecuteException)) {
log.append(Const.LF);
if (ex instanceof LogException) {
log.append(Utils.getStainKeyWords(ex.getMessage(), StainCode.GLOSS_RED));
} else {
log.append(Exceptions.getStackTraceAsString(ex));
}
log.append(Const.LF);
}
// 拼接日志
this.appendLog(log.toString());
}
/**
* 拼接日志
*
* @param log log
*/
@SneakyThrows
protected void appendLog(String log) {
appender.write(Strings.bytes(log));
appender.flush();
}
/**
* 更新状态
*
* @param status status
*/
protected void updateStatus(ActionStatus status) {
Date now = new Date();
this.status = status;
ApplicationActionLogDO update = new ApplicationActionLogDO();
update.setId(id);
update.setRunStatus(status.getStatus());
update.setUpdateTime(now);
switch (status) {
case RUNNABLE:
this.startTime = now;
update.setStartTime(now);
break;
case FINISH:
case FAILURE:
case TERMINATED:
this.endTime = now;
update.setEndTime(now);
update.setExitCode(this.getExitCode());
break;
default:
break;
}
// 更新状态
applicationActionLogDAO.updateById(update);
}
@Override
public void close() {
// 关闭日志
Streams.close(outputSteam);
}
}

View File

@@ -0,0 +1,106 @@
package cn.orionsec.ops.handler.app.action;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.MessageConst;
import cn.orionsec.ops.constant.app.ActionType;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.entity.domain.ApplicationRepositoryDO;
import cn.orionsec.ops.service.api.ApplicationRepositoryService;
import cn.orionsec.ops.utils.Utils;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ResetCommand;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import java.io.File;
public class CheckoutActionHandler extends AbstractActionHandler {
private static final ApplicationRepositoryService applicationRepositoryService = SpringHolder.getBean(ApplicationRepositoryService.class);
private Git git;
public CheckoutActionHandler(Long actionId, MachineActionStore store) {
super(actionId, store);
}
@Override
protected void handler() {
ApplicationRepositoryDO repo = applicationRepositoryService.selectById(store.getRepoId());
// 查询分支
String fullBranchName = store.getBranchName();
String remote = fullBranchName.substring(0, fullBranchName.indexOf("/"));
String branchName = fullBranchName.substring(fullBranchName.indexOf("/") + 1);
String commitId = store.getCommitId();
String repoClonePath = store.getRepoClonePath();
Files1.delete(repoClonePath);
// 拼接日志
StringBuilder log = new StringBuilder(Const.LF_2)
.append(Utils.getStainKeyWords(" *** 检出url ", StainCode.GLOSS_BLUE))
.append(Utils.getStainKeyWords(repo.getRepoUrl(), StainCode.GLOSS_CYAN))
.append(Const.LF);
log.append(Utils.getStainKeyWords(" *** 检出分支 ", StainCode.GLOSS_BLUE))
.append(Utils.getStainKeyWords(fullBranchName, StainCode.GLOSS_CYAN))
.append(Const.LF);
log.append(Utils.getStainKeyWords(" *** commitId ", StainCode.GLOSS_BLUE))
.append(Utils.getStainKeyWords(commitId, StainCode.GLOSS_CYAN))
.append(Const.LF);
log.append(Utils.getStainKeyWords(" *** 检出目录 ", StainCode.GLOSS_BLUE))
.append(Utils.getStainKeyWords(repoClonePath, StainCode.GLOSS_CYAN))
.append(Const.LF)
.append(Utils.getStainKeyWords(" *** 开始检出", StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(log.toString());
// clone
try {
CloneCommand clone = Git.cloneRepository()
.setURI(repo.getRepoUrl())
.setDirectory(new File(repoClonePath))
.setRemote(remote)
.setBranch(branchName);
// 设置密码
String[] pair = applicationRepositoryService.getRepositoryUsernamePassword(repo);
String username = pair[0];
String password = pair[1];
if (username != null) {
clone.setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password));
}
this.git = clone.call();
this.appendLog(Utils.getStainKeyWords(" *** 检出完成", StainCode.GLOSS_GREEN) + Const.LF_3);
} catch (Exception e) {
throw Exceptions.vcs(MessageConst.CHECKOUT_ERROR, e);
}
// 已停止则关闭
if (terminated) {
return;
}
// reset
try {
git.reset().setMode(ResetCommand.ResetType.HARD)
.setRef(commitId)
.call();
} catch (Exception e) {
throw Exceptions.vcs(MessageConst.RESET_ERROR, e);
}
}
@Override
public void terminate() {
super.terminate();
// 关闭git
Streams.close(git);
}
@Override
public void close() {
super.close();
// 关闭git
Streams.close(git);
}
}

View File

@@ -0,0 +1,58 @@
package cn.orionsec.ops.handler.app.action;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.net.host.ssh.ExitCode;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
import cn.orionsec.ops.constant.app.ActionType;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.utils.Utils;
import lombok.Getter;
public class CommandActionHandler extends AbstractActionHandler {
private CommandExecutor executor;
@Getter
private Integer exitCode;
public CommandActionHandler(Long actionId, MachineActionStore store) {
super(actionId, store);
}
@Override
protected void handler() throws Exception {
this.appendLog(Utils.getStainKeyWords("# 开始执行\n", StainCode.GLOSS_BLUE));
// 打开executor
this.executor = store.getSessionStore().getCommandExecutor(Strings.replaceCRLF(action.getActionCommand()));
// 执行命令
CommandExecutors.execCommand(executor, appender);
this.exitCode = executor.getExitCode();
if (!ExitCode.isSuccess(exitCode)) {
throw Exceptions.execute("*** 命令执行失败 exitCode: " + exitCode);
}
}
@Override
public void terminate() {
super.terminate();
// 关闭executor
Streams.close(executor);
}
@Override
public void write(String command) {
executor.write(command);
}
@Override
public void close() {
super.close();
// 关闭executor
Streams.close(executor);
}
}

View File

@@ -0,0 +1,79 @@
package cn.orionsec.ops.handler.app.action;
import cn.orionsec.kit.lang.able.Executable;
import cn.orionsec.kit.lang.able.SafeCloseable;
import cn.orionsec.kit.lang.function.select.Branches;
import cn.orionsec.kit.lang.function.select.Selector;
import cn.orionsec.ops.constant.app.ActionStatus;
import cn.orionsec.ops.constant.app.ActionType;
import cn.orionsec.ops.constant.app.TransferMode;
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
import java.util.List;
import java.util.stream.Collectors;
public interface IActionHandler extends Executable, SafeCloseable {
/**
* 获取状态
*
* @return status
* @see ActionStatus
*/
ActionStatus getStatus();
/**
* 跳过
*/
void skip();
/**
* 终止
*/
void terminate();
/**
* 输入命令
*
* @param command command
*/
default void write(String command) {
}
/**
* 获取退出码
*
* @return exitCode
*/
default Integer getExitCode() {
return null;
}
/**
* 创建处理器
*
* @param actions actions
* @param store store
* @return handler
*/
static List<IActionHandler> createHandler(List<ApplicationActionLogDO> actions, MachineActionStore store) {
return actions.stream()
.map(action -> Selector.<ActionType, IActionHandler>of(ActionType.of(action.getActionType()))
.test(Branches.eq(ActionType.BUILD_CHECKOUT)
.then(() -> new CheckoutActionHandler(action.getId(), store)))
.test(Branches.in(ActionType.BUILD_COMMAND, ActionType.RELEASE_COMMAND)
.then(() -> new CommandActionHandler(action.getId(), store)))
.test(Branches.eq(ActionType.RELEASE_TRANSFER)
.then(() -> {
if (TransferMode.SCP.getValue().equals(store.getTransferMode())) {
return new ScpTransferActionHandler(action.getId(), store);
} else {
return new SftpTransferActionHandler(action.getId(), store);
}
}))
.get())
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,108 @@
package cn.orionsec.ops.handler.app.action;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.ops.constant.app.TransferMode;
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
import lombok.Data;
import java.io.OutputStream;
import java.util.Map;
@Data
public class MachineActionStore {
/**
* 引用id
*/
private Long relId;
/**
* action
*/
private Map<Long, ApplicationActionLogDO> actions;
/**
* 日志输出流
*/
private OutputStream superLogStream;
/**
* 机器id
*/
private Long machineId;
/**
* 机器用户
*/
private String machineUsername;
/**
* 机器主机
*/
private String machineHost;
/**
* 机器会话
*/
private SessionStore sessionStore;
/**
* 版本id
*
* @see CheckoutActionHandler
*/
private Long repoId;
/**
* 分支
*
* @see CheckoutActionHandler
*/
private String branchName;
/**
* 提交版本
*
* @see CheckoutActionHandler
*/
private String commitId;
/**
* 仓库 clone 路径
*
* @see CheckoutActionHandler
*/
private String repoClonePath;
/**
* 构建产物文件
*
* @see SftpTransferActionHandler
* @see ScpTransferActionHandler
*/
private String bundlePath;
/**
* 产物传输路径
*
* @see SftpTransferActionHandler
* @see ScpTransferActionHandler
*/
private String transferPath;
/**
* 产物传输方式
*
* @see SftpTransferActionHandler
* @see ScpTransferActionHandler
* @see TransferMode
*/
private String transferMode;
public MachineActionStore() {
this.actions = Maps.newLinkedMap();
}
}

View File

@@ -0,0 +1,104 @@
package cn.orionsec.ops.handler.app.action;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.ssh.ExitCode;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.app.ActionType;
import cn.orionsec.ops.constant.app.TransferMode;
import cn.orionsec.ops.constant.command.CommandConst;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.constant.env.EnvConst;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.service.api.MachineInfoService;
import cn.orionsec.ops.utils.Utils;
import lombok.Getter;
import java.io.File;
import java.util.Map;
public class ScpTransferActionHandler extends AbstractActionHandler {
protected static MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
private SessionStore session;
private CommandExecutor executor;
@Getter
private Integer exitCode;
public ScpTransferActionHandler(Long actionId, MachineActionStore store) {
super(actionId, store);
}
@Override
protected void handler() throws Exception {
// 检查文件
String bundlePath = Files1.getPath(SystemEnvAttr.DIST_PATH.getValue(), store.getBundlePath());
File bundleFile = new File(bundlePath);
if (!bundleFile.exists()) {
throw Exceptions.log("*** 产物文件不存在 " + bundlePath);
}
// 替换命令
String scpCommand = Strings.def(action.getActionCommand(), CommandConst.SCP_TRANSFER_DEFAULT);
Map<String, String> params = Maps.newMap();
params.put(EnvConst.BUNDLE_PATH, bundlePath);
// 目标文件有空格需要转义空格为 \\
params.put(EnvConst.TRANSFER_PATH, store.getTransferPath().replaceAll(Strings.SPACE, "\\\\\\\\ "));
params.put(EnvConst.TARGET_USERNAME, store.getMachineUsername());
params.put(EnvConst.TARGET_HOST, store.getMachineHost());
scpCommand = Strings.format(scpCommand, EnvConst.SYMBOL, params);
// 拼接日志
StringBuilder log = new StringBuilder(Const.LF)
.append(Utils.getStainKeyWords("# 执行 scp 传输命令", StainCode.GLOSS_BLUE))
.append(Const.LF)
.append(StainCode.prefix(StainCode.GLOSS_CYAN))
.append(Utils.getEndLfWithEof(scpCommand))
.append(StainCode.SUFFIX);
this.appendLog(log.toString());
// 打开session
this.session = machineInfoService.openSessionStore(Const.HOST_MACHINE_ID);
// 打开executor
this.executor = session.getCommandExecutor(Strings.replaceCRLF(scpCommand));
// 执行命令
CommandExecutors.execCommand(executor, appender);
this.exitCode = executor.getExitCode();
this.appendLog(Const.LF);
if (!ExitCode.isSuccess(exitCode)) {
throw Exceptions.execute("*** 命令执行失败 exitCode: " + exitCode);
}
}
@Override
public void write(String command) {
executor.write(command);
}
@Override
public void terminate() {
super.terminate();
// 关闭executor
Streams.close(executor);
// 关闭宿主机session
Streams.close(session);
}
@Override
public void close() {
super.close();
// 关闭executor
Streams.close(executor);
// 关闭宿主机session
Streams.close(session);
}
}

View File

@@ -0,0 +1,140 @@
package cn.orionsec.ops.handler.app.action;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.app.ActionType;
import cn.orionsec.ops.constant.app.TransferMode;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.service.api.MachineEnvService;
import cn.orionsec.ops.utils.Utils;
import java.io.File;
import java.util.List;
import java.util.Map;
public class SftpTransferActionHandler extends AbstractActionHandler {
private static final String SPACE = " ";
protected static MachineEnvService machineEnvService = SpringHolder.getBean(MachineEnvService.class);
private SftpExecutor executor;
public SftpTransferActionHandler(Long actionId, MachineActionStore store) {
super(actionId, store);
}
@Override
protected void handler() throws Exception {
// 检查文件
String bundlePath = Files1.getPath(SystemEnvAttr.DIST_PATH.getValue(), store.getBundlePath());
File bundleFile = new File(bundlePath);
if (!bundleFile.exists()) {
throw Exceptions.log("*** 产物文件不存在 " + bundlePath);
}
// 打开executor
String charset = machineEnvService.getSftpCharset(store.getMachineId());
this.executor = store.getSessionStore().getSftpExecutor(charset);
executor.connect();
// 拼接删除日志
String transferPath = store.getTransferPath();
String bundleAbsolutePath = bundleFile.getAbsolutePath();
// 拼接头文件
StringBuilder headerLog = new StringBuilder(Const.LF)
.append(SPACE)
.append(Utils.getStainKeyWords("开始传输文件", StainCode.GLOSS_BLUE))
.append(Const.LF)
.append(SPACE)
.append(Utils.getStainKeyWords("source: ", StainCode.GLOSS_GREEN))
.append(Utils.getStainKeyWords(bundleAbsolutePath, StainCode.GLOSS_BLUE))
.append(Const.LF)
.append(SPACE)
.append(Utils.getStainKeyWords("target: ", StainCode.GLOSS_GREEN))
.append(Utils.getStainKeyWords(transferPath, StainCode.GLOSS_BLUE))
.append(Const.LF_2);
headerLog.append(StainCode.prefix(StainCode.GLOSS_GREEN))
.append(SPACE)
.append("类型")
.append(SPACE)
.append(" target")
.append(StainCode.SUFFIX)
.append(Const.LF);
this.appendLog(headerLog.toString());
// 转化文件
Map<File, String> transferFiles = this.convertFile(bundleFile, transferPath);
for (Map.Entry<File, String> entity : transferFiles.entrySet()) {
File localFile = entity.getKey();
String remoteFile = entity.getValue();
// 文件夹则创建
if (localFile.isDirectory()) {
StringBuilder createDirLog = new StringBuilder(SPACE)
.append(Utils.getStainKeyWords("mkdir", StainCode.GLOSS_GREEN))
.append(SPACE)
.append(Utils.getStainKeyWords(remoteFile, StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(createDirLog.toString());
executor.makeDirectories(remoteFile);
continue;
}
// 文件则传输
StringBuilder transferLog = new StringBuilder(SPACE)
.append(Utils.getStainKeyWords("touch", StainCode.GLOSS_GREEN))
.append(SPACE)
.append(Utils.getStainKeyWords(remoteFile, StainCode.GLOSS_BLUE))
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
.append(" (")
.append(Files1.getSize(localFile.length()))
.append(")")
.append(StainCode.SUFFIX)
.append(Const.LF);
this.appendLog(transferLog.toString());
executor.uploadFile(remoteFile, Files1.openInputStreamFast(localFile), true);
}
this.appendLog(Const.LF);
}
/**
* 转化文件
*
* @param bundleFile 打包文件
* @param transferPath 传输目录
* @return transferFiles
*/
private Map<File, String> convertFile(File bundleFile, String transferPath) {
Map<File, String> map = Maps.newLinkedMap();
if (bundleFile.isFile()) {
map.put(bundleFile, transferPath);
return map;
}
// 如果是文件夹则需要截取
String bundleFileAbsolutePath = bundleFile.getAbsolutePath();
List<File> transferFiles = Files1.listFiles(bundleFile, true, true);
for (File transferFile : transferFiles) {
String remoteFile = Files1.getPath(transferPath, transferFile.getAbsolutePath().substring(bundleFileAbsolutePath.length() + 1));
map.put(transferFile, remoteFile);
}
return map;
}
@Override
public void terminate() {
super.terminate();
// 关闭executor
Streams.close(executor);
}
@Override
public void close() {
super.close();
// 关闭executor
Streams.close(executor);
}
}

View File

@@ -0,0 +1,46 @@
package cn.orionsec.ops.handler.app.build;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.ops.handler.app.machine.IMachineProcessor;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class BuildSessionHolder {
/**
* session
*/
private final ConcurrentHashMap<Long, IMachineProcessor> session = Maps.newCurrentHashMap();
/**
* 添加 session
*
* @param processor processor
*/
public void addSession(Long id, IMachineProcessor processor) {
session.put(id, processor);
}
/**
* 获取 session
*
* @param id id
* @return session
*/
public IMachineProcessor getSession(Long id) {
return session.get(id);
}
/**
* 移除 session
*
* @param id id
*/
public void removeSession(Long id) {
session.remove(id);
}
}

View File

@@ -0,0 +1,292 @@
package cn.orionsec.ops.handler.app.machine;
import cn.orionsec.kit.lang.constant.Letters;
import cn.orionsec.kit.lang.exception.DisabledException;
import cn.orionsec.kit.lang.exception.LogException;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.app.ActionStatus;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.handler.app.action.IActionHandler;
import cn.orionsec.ops.handler.tail.TailSessionHolder;
import cn.orionsec.ops.utils.Utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.OutputStream;
import java.util.Date;
import java.util.List;
@Slf4j
public abstract class AbstractMachineProcessor implements IMachineProcessor {
private static final TailSessionHolder tailSessionHolder = SpringHolder.getBean(TailSessionHolder.class);
protected Long id;
protected String logAbsolutePath;
protected OutputStream logStream;
protected Date startTime, endTime;
/**
* 处理器
*/
protected List<IActionHandler> handlerList;
/**
* 是否已终止
*/
protected volatile boolean terminated;
public AbstractMachineProcessor(Long id) {
this.id = id;
}
@Override
public void run() {
// 检查状态
if (!this.checkCanRunnable()) {
return;
}
Exception ex = null;
boolean isMainError = false;
// 执行
try {
// 更新状态
this.updateStatus(MachineProcessorStatus.RUNNABLE);
// 打开日志
this.openLogger();
// 打开机器连接
this.openMachineSession();
// 执行
for (IActionHandler handler : handlerList) {
if (ex == null && !terminated) {
try {
// 执行
handler.exec();
} catch (Exception e) {
// 强制停止的异常不算异常
if (!terminated) {
ex = e;
}
}
} else {
// 跳过
handler.skip();
}
}
// 完成回调
this.completeCallback(ex);
} catch (Exception e) {
// 异常
ex = e;
isMainError = true;
}
// 回调
try {
if (terminated) {
// 停止回调
this.terminatedCallback();
} else if (ex == null) {
// 成功回调
this.successCallback();
} else if (ex instanceof DisabledException) {
// 机器未启用回调
this.machineDisableCallback();
} else {
// 执行失败回调
this.exceptionCallback(isMainError, ex);
}
} finally {
// 释放资源
this.close();
}
}
/**
* 检查是否可执行
*
* @return 是否可执行
*/
protected abstract boolean checkCanRunnable();
/**
* 打开日志
*/
protected void openLogger() {
String logPath = Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), this.getLogPath());
File logFile = new File(logPath);
Files1.touch(logFile);
this.logStream = Files1.openOutputStreamFastSafe(logFile);
this.logAbsolutePath = logFile.getAbsolutePath();
// 拼接开始日志
this.appendStartedLog();
}
/**
* 获取日志文件路径
*
* @return logPath
*/
protected abstract String getLogPath();
/**
* 打开session
*/
protected abstract void openMachineSession();
/**
* 更新状态
*
* @param status status
*/
protected abstract void updateStatus(MachineProcessorStatus status);
/**
* 拼接开始日志
*/
protected abstract void appendStartedLog();
/**
* 完成回调
*
* @param e e
*/
protected void completeCallback(Exception e) {
log.info("机器任务执行-完成 relId: {}", id);
}
/**
* 停止回调
*/
protected void terminatedCallback() {
log.info("机器任务执行-停止 relId: {}", id);
// 修改状态
this.updateStatus(MachineProcessorStatus.TERMINATED);
// 拼接日志
StringBuilder log = new StringBuilder(Const.LF_2)
.append(Utils.getStainKeyWords("# 主机任务手动停止", StainCode.GLOSS_YELLOW))
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(log.toString());
}
/**
* 成功回调
*/
protected void successCallback() {
log.info("机器任务执行-成功 relId: {}", id);
// 修改状态
this.updateStatus(MachineProcessorStatus.FINISH);
long used = endTime.getTime() - startTime.getTime();
// 拼接日志
StringBuilder log = new StringBuilder(Const.LF_2)
.append(Utils.getStainKeyWords("# 主机任务执行完成", StainCode.GLOSS_GREEN))
.append(Const.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(" used ")
.append(Utils.getStainKeyWords(Utils.interval(used), StainCode.GLOSS_BLUE))
.append(" (")
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
.append(used)
.append("ms")
.append(StainCode.SUFFIX)
.append(")\n");
// 拼接日志
this.appendLog(log.toString());
}
/**
* 机器未启用回调
*/
private void machineDisableCallback() {
log.info("机器任务执行-机器未启用 relId: {}", id);
// 更新状态
this.updateStatus(MachineProcessorStatus.TERMINATED);
// 拼接日志
StringBuilder log = new StringBuilder(Const.LF_2)
.append(Utils.getStainKeyWords("# 主机任务执行机器未启用", StainCode.GLOSS_YELLOW))
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(log.toString());
}
/**
* 异常回调
*
* @param isMainError isMainError
* @param ex ex
*/
protected void exceptionCallback(boolean isMainError, Exception ex) {
log.error("机器任务执行-失败 relId: {}, isMainError: {}", id, isMainError, ex);
// 更新状态
this.updateStatus(MachineProcessorStatus.FAILURE);
// 拼接日志
StringBuilder log = new StringBuilder(Const.LF_2)
.append(Utils.getStainKeyWords("# 主机任务执行失败", StainCode.GLOSS_RED))
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Letters.LF);
// 拼接异常
if (isMainError) {
log.append(Const.LF);
if (ex instanceof LogException) {
log.append(Utils.getStainKeyWords(ex.getMessage(), StainCode.GLOSS_RED));
} else {
log.append(Exceptions.getStackTraceAsString(ex));
}
log.append(Const.LF);
}
this.appendLog(log.toString());
}
/**
* 拼接日志
*
* @param log log
*/
@SneakyThrows
protected void appendLog(String log) {
logStream.write(Strings.bytes(log));
logStream.flush();
}
@Override
public void terminate() {
// 设置状态为已停止
this.terminated = true;
// 结束正在执行的action
Lists.stream(handlerList)
.filter(s -> ActionStatus.RUNNABLE.equals(s.getStatus()))
.forEach(IActionHandler::terminate);
}
@Override
public void write(String command) {
Lists.stream(handlerList)
.filter(s -> ActionStatus.RUNNABLE.equals(s.getStatus()))
.forEach(s -> s.write(command));
}
@Override
public void close() {
// 关闭日志流
Streams.close(logStream);
// 异步关闭正在tail的日志
tailSessionHolder.asyncCloseTailFile(Const.HOST_MACHINE_ID, logAbsolutePath);
}
}

View File

@@ -0,0 +1,299 @@
package cn.orionsec.ops.handler.app.machine;
import cn.orionsec.kit.lang.able.Executable;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.io.compress.CompressTypeEnum;
import cn.orionsec.kit.lang.utils.io.compress.FileCompressor;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.app.ActionType;
import cn.orionsec.ops.constant.app.ApplicationEnvAttr;
import cn.orionsec.ops.constant.app.BuildStatus;
import cn.orionsec.ops.constant.app.StageType;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.constant.message.MessageType;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.dao.ApplicationBuildDAO;
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
import cn.orionsec.ops.entity.domain.ApplicationBuildDO;
import cn.orionsec.ops.handler.app.action.IActionHandler;
import cn.orionsec.ops.handler.app.action.MachineActionStore;
import cn.orionsec.ops.handler.app.build.BuildSessionHolder;
import cn.orionsec.ops.service.api.ApplicationActionLogService;
import cn.orionsec.ops.service.api.ApplicationEnvService;
import cn.orionsec.ops.service.api.MachineInfoService;
import cn.orionsec.ops.service.api.WebSideMessageService;
import cn.orionsec.ops.utils.Utils;
import com.alibaba.fastjson.JSON;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Slf4j
public class BuildMachineProcessor extends AbstractMachineProcessor implements Executable {
private static final ApplicationBuildDAO applicationBuildDAO = SpringHolder.getBean(ApplicationBuildDAO.class);
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
private static final ApplicationActionLogService applicationActionLogService = SpringHolder.getBean(ApplicationActionLogService.class);
private static final ApplicationEnvService applicationEnvService = SpringHolder.getBean(ApplicationEnvService.class);
private static final BuildSessionHolder buildSessionHolder = SpringHolder.getBean(BuildSessionHolder.class);
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
private final MachineActionStore store;
private ApplicationBuildDO record;
public BuildMachineProcessor(Long id) {
super(id);
this.store = new MachineActionStore();
}
@Override
public void exec() {
log.info("应用构建任务执行提交 buildId: {}", id);
Threads.start(this, SchedulerPools.APP_BUILD_SCHEDULER);
}
@Override
public void run() {
log.info("应用构建任务执行开始 buildId: {}", id);
// 初始化数据
this.initData();
// 执行
super.run();
}
/**
* 初始化数据
*/
private void initData() {
// 查询build
this.record = applicationBuildDAO.selectById(id);
log.info("应用构建任务-获取数据-build buildId: {}, record: {}", id, JSON.toJSONString(record));
// 检查状态
if (record == null || !BuildStatus.WAIT.getStatus().equals(record.getBuildStatus())) {
return;
}
// 查询action
List<ApplicationActionLogDO> actions = applicationActionLogService.selectActionByRelId(id, StageType.BUILD);
actions.forEach(s -> store.getActions().put(s.getId(), s));
log.info("应用构建任务-获取数据-action buildId: {}, actions: {}", id, JSON.toJSONString(actions));
// 插入store
Long repoId = record.getRepoId();
store.setRelId(id);
store.setRepoId(repoId);
store.setBranchName(record.getBranchName());
store.setCommitId(record.getCommitId());
if (repoId != null) {
String repoClonePath = Files1.getPath(SystemEnvAttr.REPO_PATH.getValue(), repoId + "/" + record.getId());
store.setRepoClonePath(repoClonePath);
}
// 创建handler
this.handlerList = IActionHandler.createHandler(actions, store);
}
@Override
protected boolean checkCanRunnable() {
return record != null && BuildStatus.WAIT.getStatus().equals(record.getBuildStatus());
}
@Override
protected void completeCallback(Exception e) {
super.completeCallback(e);
if (e == null && !terminated) {
// 复制产物文件
this.copyBundleFile();
}
}
@Override
protected void successCallback() {
// 完成回调
super.successCallback();
// 发送站内信
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.ID, record.getId());
params.put(EventKeys.SEQ, record.getBuildSeq());
params.put(EventKeys.PROFILE_NAME, record.getProfileName());
params.put(EventKeys.APP_NAME, record.getAppName());
params.put(EventKeys.BUILD_SEQ, record.getBuildSeq());
webSideMessageService.addMessage(MessageType.BUILD_SUCCESS, record.getId(), record.getCreateUserId(), record.getCreateUserName(), params);
}
@Override
protected void exceptionCallback(boolean isMainError, Exception ex) {
super.exceptionCallback(isMainError, ex);
// 发送站内信
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.ID, record.getId());
params.put(EventKeys.SEQ, record.getBuildSeq());
params.put(EventKeys.APP_NAME, record.getAppName());
params.put(EventKeys.PROFILE_NAME, record.getProfileName());
params.put(EventKeys.BUILD_SEQ, record.getBuildSeq());
webSideMessageService.addMessage(MessageType.BUILD_FAILURE, record.getId(), record.getCreateUserId(), record.getCreateUserName(), params);
}
@Override
protected void openLogger() {
super.openLogger();
store.setSuperLogStream(this.logStream);
}
/**
* 复制产物文件
*/
@SneakyThrows
private void copyBundleFile() {
// 查询应用产物目录
String bundlePath = applicationEnvService.getAppEnvValue(record.getAppId(), record.getProfileId(), ApplicationEnvAttr.BUNDLE_PATH.getKey());
if (!bundlePath.startsWith(Const.SLASH) && !Files1.isWindowsPath(bundlePath) && store.getRepoClonePath() != null) {
// 基于代码目录的相对路径
bundlePath = Files1.getPath(store.getRepoClonePath(), bundlePath);
}
// 检查产物文件是否存在
File bundleFile = new File(bundlePath);
if (!bundleFile.exists()) {
throw Exceptions.log("***** 构建产物不存在 " + bundlePath);
}
// 复制到dist目录下
String copyBundlePath = Files1.getPath(SystemEnvAttr.DIST_PATH.getValue(), record.getBundlePath());
StringBuilder copyLog = new StringBuilder(Const.LF_3)
.append(StainCode.prefix(StainCode.GLOSS_GREEN))
.append("***** 已生成产物文件 ")
.append(StainCode.SUFFIX)
.append(Utils.getStainKeyWords(copyBundlePath, StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(copyLog.toString());
if (bundleFile.isFile()) {
Files1.copy(bundleFile, new File(copyBundlePath));
} else {
// 复制文件夹
Files1.copyDir(bundleFile, new File(copyBundlePath), false);
// 文件夹打包
String compressFile = copyBundlePath + "." + Const.SUFFIX_ZIP;
FileCompressor compressor = CompressTypeEnum.ZIP.compressor().get();
compressor.addFile(bundleFile);
compressor.setAbsoluteCompressPath(compressFile);
compressor.compress();
StringBuilder compressLog = new StringBuilder()
.append(StainCode.prefix(StainCode.GLOSS_GREEN))
.append("***** 已生成产物文件zip ")
.append(StainCode.SUFFIX)
.append(Utils.getStainKeyWords(compressFile, StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(compressLog.toString());
}
}
@Override
protected String getLogPath() {
return record.getLogPath();
}
@Override
protected void openMachineSession() {
boolean hasCommand = store.getActions().values().stream()
.map(ApplicationActionLogDO::getActionType)
.anyMatch(ActionType.BUILD_COMMAND.getType()::equals);
if (!hasCommand) {
return;
}
// 打开session
SessionStore sessionStore = machineInfoService.openSessionStore(Const.HOST_MACHINE_ID);
store.setMachineId(Const.HOST_MACHINE_ID);
store.setSessionStore(sessionStore);
}
@Override
protected void updateStatus(MachineProcessorStatus status) {
Date now = new Date();
ApplicationBuildDO update = new ApplicationBuildDO();
update.setId(id);
update.setBuildStatus(BuildStatus.valueOf(status.name()).getStatus());
update.setUpdateTime(now);
switch (status) {
case RUNNABLE:
this.startTime = now;
update.setBuildStartTime(now);
// 添加session
buildSessionHolder.addSession(id, this);
break;
case FINISH:
case FAILURE:
case TERMINATED:
this.endTime = now;
update.setBuildEndTime(now);
break;
default:
break;
}
// 更新
applicationBuildDAO.updateById(update);
}
@Override
protected void appendStartedLog() {
StringBuilder log = new StringBuilder()
.append(Utils.getStainKeyWords("# 开始执行主机构建任务 ", StainCode.GLOSS_GREEN))
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
.append("#").append(record.getBuildSeq())
.append(StainCode.SUFFIX)
.append(Const.LF);
log.append("构建应用: ")
.append(Utils.getStainKeyWords(record.getAppName(), StainCode.GLOSS_BLUE))
.append(Const.LF);
log.append("构建环境: ")
.append(Utils.getStainKeyWords(record.getProfileName(), StainCode.GLOSS_BLUE))
.append(Const.LF);
log.append("执行用户: ")
.append(Utils.getStainKeyWords(record.getCreateUserName(), StainCode.GLOSS_BLUE))
.append(Const.LF);
log.append("开始时间: ")
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
if (!Strings.isBlank(record.getDescription())) {
log.append("构建描述: ")
.append(Utils.getStainKeyWords(record.getDescription(), StainCode.GLOSS_BLUE))
.append(Const.LF);
}
if (!Strings.isBlank(record.getBranchName())) {
log.append("branch: ")
.append(Utils.getStainKeyWords(record.getBranchName(), StainCode.GLOSS_BLUE))
.append(Const.LF);
log.append("commit: ")
.append(Utils.getStainKeyWords(record.getCommitId(), StainCode.GLOSS_BLUE))
.append(Const.LF);
}
this.appendLog(log.toString());
}
@Override
public void close() {
// 释放资源
super.close();
// 释放连接
Streams.close(store.getSessionStore());
// 移除session
buildSessionHolder.removeSession(id);
}
}

View File

@@ -0,0 +1,26 @@
package cn.orionsec.ops.handler.app.machine;
import cn.orionsec.kit.lang.able.SafeCloseable;
public interface IMachineProcessor extends Runnable, SafeCloseable {
/**
* 终止
*/
void terminate();
/**
* 输入命令
*
* @param command command
*/
void write(String command);
/**
* 跳过
*/
default void skip() {
}
}

View File

@@ -0,0 +1,38 @@
package cn.orionsec.ops.handler.app.machine;
public enum MachineProcessorStatus {
/**
* 未开始
*/
WAIT,
/**
* 进行中
*/
RUNNABLE,
/**
* 已完成
*/
FINISH,
/**
* 执行失败
*/
FAILURE,
/**
* 已跳过
*/
SKIPPED,
/**
* 已终止
*/
TERMINATED,
;
}

View File

@@ -0,0 +1,194 @@
package cn.orionsec.ops.handler.app.machine;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.app.ActionStatus;
import cn.orionsec.ops.constant.app.StageType;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.dao.ApplicationMachineDAO;
import cn.orionsec.ops.dao.ApplicationReleaseMachineDAO;
import cn.orionsec.ops.entity.domain.*;
import cn.orionsec.ops.handler.app.action.IActionHandler;
import cn.orionsec.ops.handler.app.action.MachineActionStore;
import cn.orionsec.ops.service.api.ApplicationActionLogService;
import cn.orionsec.ops.service.api.MachineInfoService;
import cn.orionsec.ops.utils.Utils;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.List;
@Slf4j
public class ReleaseMachineProcessor extends AbstractMachineProcessor {
protected static ApplicationReleaseMachineDAO applicationReleaseMachineDAO = SpringHolder.getBean(ApplicationReleaseMachineDAO.class);
protected static ApplicationMachineDAO applicationMachineDAO = SpringHolder.getBean(ApplicationMachineDAO.class);
protected static ApplicationActionLogService applicationActionLogService = SpringHolder.getBean(ApplicationActionLogService.class);
protected static MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
private final ApplicationReleaseDO release;
private final ApplicationReleaseMachineDO releaseMachine;
private final MachineActionStore store;
@Getter
private volatile ActionStatus status;
public ReleaseMachineProcessor(ApplicationReleaseDO release, ApplicationReleaseMachineDO releaseMachine) {
super(releaseMachine.getId());
this.release = release;
this.releaseMachine = releaseMachine;
this.status = ActionStatus.of(releaseMachine.getRunStatus());
this.store = new MachineActionStore();
store.setRelId(releaseMachine.getId());
store.setMachineId(releaseMachine.getMachineId());
store.setBundlePath(release.getBundlePath());
store.setTransferPath(release.getTransferPath());
store.setTransferMode(release.getTransferMode());
}
@Override
public void run() {
log.info("应用发布任务执行开始 releaseId: {}", id);
// 初始化数据
this.initData();
// 执行
super.run();
}
/**
* 初始化数据
*/
private void initData() {
// 查询机器发布操作
List<ApplicationActionLogDO> actions = applicationActionLogService.selectActionByRelId(id, StageType.RELEASE);
actions.forEach(s -> store.getActions().put(s.getId(), s));
log.info("应用发布器-获取数据-action releaseId: {}, actions: {}", id, JSON.toJSONString(actions));
// 创建handler
this.handlerList = IActionHandler.createHandler(actions, store);
}
@Override
public void skip() {
if (ActionStatus.WAIT.equals(status)) {
// 只能跳过等待中的任务
this.updateStatus(MachineProcessorStatus.SKIPPED);
}
}
@Override
protected boolean checkCanRunnable() {
return ActionStatus.WAIT.equals(status);
}
@Override
protected void openLogger() {
super.openLogger();
store.setSuperLogStream(this.logStream);
}
@Override
protected String getLogPath() {
return releaseMachine.getLogPath();
}
@Override
protected void successCallback() {
// 完成回调
super.successCallback();
// 更新应用机器发布版本
this.updateAppMachineVersion();
}
@Override
protected void exceptionCallback(boolean isMainError, Exception ex) {
super.exceptionCallback(isMainError, ex);
throw Exceptions.runtime(ex);
}
@Override
protected void openMachineSession() {
// 打开目标机器session
Long machineId = releaseMachine.getMachineId();
MachineInfoDO machine = machineInfoService.selectById(machineId);
SessionStore sessionStore = machineInfoService.openSessionStore(machine);
store.setSessionStore(sessionStore);
store.setMachineUsername(machine.getUsername());
store.setMachineHost(machine.getMachineHost());
}
@Override
protected void updateStatus(MachineProcessorStatus processorStatus) {
this.status = ActionStatus.valueOf(processorStatus.name());
Date now = new Date();
ApplicationReleaseMachineDO update = new ApplicationReleaseMachineDO();
update.setId(id);
update.setRunStatus(status.getStatus());
update.setUpdateTime(now);
switch (processorStatus) {
case RUNNABLE:
this.startTime = now;
update.setStartTime(now);
break;
case FINISH:
case TERMINATED:
case FAILURE:
this.endTime = now;
update.setEndTime(now);
break;
default:
break;
}
applicationReleaseMachineDAO.updateById(update);
}
@Override
protected void appendStartedLog() {
StringBuilder log = new StringBuilder()
.append(Utils.getStainKeyWords("# 开始执行主机发布任务", StainCode.GLOSS_GREEN))
.append(Const.LF);
log.append("机器名称: ")
.append(Utils.getStainKeyWords(releaseMachine.getMachineName(), StainCode.GLOSS_BLUE))
.append(Const.LF);
log.append("发布主机: ")
.append(Utils.getStainKeyWords(releaseMachine.getMachineHost(), StainCode.GLOSS_BLUE))
.append(Const.LF);
log.append("开始时间: ")
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(log.toString());
}
/**
* 更新应用机器版本
*/
private void updateAppMachineVersion() {
ApplicationMachineDO update = new ApplicationMachineDO();
update.setAppId(release.getAppId());
update.setProfileId(release.getProfileId());
update.setMachineId(releaseMachine.getMachineId());
update.setReleaseId(release.getId());
update.setBuildId(release.getBuildId());
update.setBuildSeq(release.getBuildSeq());
applicationMachineDAO.updateAppVersion(update);
}
@Override
public void close() {
// 释放资源
super.close();
// 释放连接
Streams.close(store.getSessionStore());
}
}

View File

@@ -0,0 +1,44 @@
package cn.orionsec.ops.handler.app.pipeline;
import cn.orionsec.kit.lang.able.Executable;
public interface IPipelineProcessor extends Executable, Runnable {
/**
* 获取明细id
*
* @return taskId
*/
Long getTaskId();
/**
* 停止执行
*/
void terminate();
/**
* 停止执行详情
*
* @param id id
*/
void terminateDetail(Long id);
/**
* 跳过执行详情
*
* @param id id
*/
void skipDetail(Long id);
/**
* 获取流水线执行器
*
* @param id id
* @return 流水线执行器
*/
static IPipelineProcessor with(Long id) {
return new PipelineProcessor(id);
}
}

View File

@@ -0,0 +1,247 @@
package cn.orionsec.ops.handler.app.pipeline;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.MessageConst;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.app.PipelineDetailStatus;
import cn.orionsec.ops.constant.app.PipelineStatus;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.constant.message.MessageType;
import cn.orionsec.ops.dao.ApplicationPipelineTaskDAO;
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDO;
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDetailDO;
import cn.orionsec.ops.handler.app.pipeline.stage.IStageHandler;
import cn.orionsec.ops.service.api.ApplicationPipelineTaskDetailService;
import cn.orionsec.ops.service.api.WebSideMessageService;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Slf4j
public class PipelineProcessor implements IPipelineProcessor {
private static final ApplicationPipelineTaskDAO applicationPipelineTaskDAO = SpringHolder.getBean(ApplicationPipelineTaskDAO.class);
private static final ApplicationPipelineTaskDetailService applicationPipelineTaskDetailService = SpringHolder.getBean(ApplicationPipelineTaskDetailService.class);
private static final PipelineSessionHolder pipelineSessionHolder = SpringHolder.getBean(PipelineSessionHolder.class);
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
@Getter
private final Long taskId;
private ApplicationPipelineTaskDO task;
private final Map<Long, IStageHandler> stageHandlers;
private volatile IStageHandler currentHandler;
private volatile boolean terminated;
public PipelineProcessor(Long taskId) {
this.taskId = taskId;
this.stageHandlers = Maps.newLinkedMap();
}
@Override
public void exec() {
log.info("已提交应用流水线任务 id: {}", taskId);
Threads.start(this, SchedulerPools.PIPELINE_SCHEDULER);
}
@Override
public void run() {
log.info("开始执行应用流水线 id: {}", taskId);
Exception ex = null;
try {
// 获取流水线数据
this.getPipelineData();
// 检查状态
if (task != null && !PipelineStatus.WAIT_RUNNABLE.getStatus().equals(task.getExecStatus())
&& !PipelineStatus.WAIT_SCHEDULE.getStatus().equals(task.getExecStatus())) {
return;
}
// 修改状态
this.updateStatus(PipelineStatus.RUNNABLE);
// 添加会话
pipelineSessionHolder.addSession(this);
// 执行
this.handlerPipeline();
} catch (Exception e) {
ex = e;
}
try {
// 回调
if (terminated) {
// 停止回调
this.terminatedCallback();
} else if (ex == null) {
// 成功回调
this.completeCallback();
} else {
// 异常回调
this.exceptionCallback(ex);
}
} finally {
pipelineSessionHolder.removeSession(taskId);
}
}
/**
* 获取流水线数据
*/
private void getPipelineData() {
// 获取主表
this.task = applicationPipelineTaskDAO.selectById(taskId);
if (task == null) {
return;
}
PipelineStatus status = PipelineStatus.of(task.getExecStatus());
if (!PipelineStatus.WAIT_RUNNABLE.equals(status)
&& !PipelineStatus.WAIT_SCHEDULE.equals(status)) {
throw Exceptions.argument(MessageConst.ILLEGAL_STATUS);
}
// 获取详情
List<ApplicationPipelineTaskDetailDO> details = applicationPipelineTaskDetailService.selectTaskDetails(taskId);
for (ApplicationPipelineTaskDetailDO detail : details) {
stageHandlers.put(detail.getId(), IStageHandler.with(task, detail));
}
}
/**
* 执行流水线操作
*
* @throws Exception exception
*/
private void handlerPipeline() throws Exception {
Exception ex = null;
Collection<IStageHandler> handlers = stageHandlers.values();
for (IStageHandler stageHandler : handlers) {
this.currentHandler = stageHandler;
// 停止或异常则跳过
if (terminated || ex != null) {
stageHandler.skip();
continue;
}
// 执行
try {
stageHandler.exec();
} catch (Exception e) {
ex = e;
}
}
this.currentHandler = null;
// 异常返回
if (ex != null) {
throw ex;
}
// 全部停止
final boolean allTerminated = handlers.stream()
.map(IStageHandler::getStatus)
.filter(s -> !PipelineDetailStatus.SKIPPED.equals(s))
.allMatch(PipelineDetailStatus.TERMINATED::equals);
if (allTerminated) {
this.terminated = true;
}
}
/**
* 停止回调
*/
private void terminatedCallback() {
log.info("应用流水线执行停止 id: {}", taskId);
this.updateStatus(PipelineStatus.TERMINATED);
}
/**
* 完成回调
*/
private void completeCallback() {
log.info("应用流水线执行完成 id: {}", taskId);
this.updateStatus(PipelineStatus.FINISH);
// 发送站内信
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.ID, task.getId());
params.put(EventKeys.NAME, task.getPipelineName());
params.put(EventKeys.TITLE, task.getExecTitle());
webSideMessageService.addMessage(MessageType.PIPELINE_EXEC_SUCCESS, task.getId(), task.getExecUserId(), task.getExecUserName(), params);
}
/**
* 异常回调
*
* @param ex ex
*/
private void exceptionCallback(Exception ex) {
log.error("应用流水线执行失败 id: {}", taskId, ex);
this.updateStatus(PipelineStatus.FAILURE);
// 发送站内信
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.ID, task.getId());
params.put(EventKeys.NAME, task.getPipelineName());
params.put(EventKeys.TITLE, task.getExecTitle());
webSideMessageService.addMessage(MessageType.PIPELINE_EXEC_FAILURE, task.getId(), task.getExecUserId(), task.getExecUserName(), params);
}
/**
* 更新状态
*
* @param status status
*/
private void updateStatus(PipelineStatus status) {
Date now = new Date();
ApplicationPipelineTaskDO update = new ApplicationPipelineTaskDO();
update.setId(taskId);
update.setExecStatus(status.getStatus());
update.setUpdateTime(now);
switch (status) {
case RUNNABLE:
update.setExecStartTime(now);
break;
case FINISH:
case TERMINATED:
case FAILURE:
update.setExecEndTime(now);
break;
default:
break;
}
applicationPipelineTaskDAO.updateById(update);
}
@Override
public void terminate() {
log.info("应用流水线执行停止 id: {}", taskId);
this.terminated = true;
if (currentHandler != null) {
currentHandler.terminate();
}
}
@Override
public void terminateDetail(Long id) {
IStageHandler stageHandler = stageHandlers.get(id);
if (stageHandler != null) {
stageHandler.terminate();
}
}
@Override
public void skipDetail(Long id) {
IStageHandler stageHandler = stageHandlers.get(id);
if (stageHandler != null) {
stageHandler.skip();
}
}
}

View File

@@ -0,0 +1,45 @@
package cn.orionsec.ops.handler.app.pipeline;
import cn.orionsec.kit.lang.utils.collect.Maps;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class PipelineSessionHolder {
/**
* session
*/
private final ConcurrentHashMap<Long, IPipelineProcessor> session = Maps.newCurrentHashMap();
/**
* 添加 session
*
* @param processor processor
*/
public void addSession(IPipelineProcessor processor) {
session.put(processor.getTaskId(), processor);
}
/**
* 获取 session
*
* @param id id
* @return session
*/
public IPipelineProcessor getSession(Long id) {
return session.get(id);
}
/**
* 移除 session
*
* @param id id
*/
public void removeSession(Long id) {
session.remove(id);
}
}

View File

@@ -0,0 +1,250 @@
package cn.orionsec.ops.handler.app.pipeline.stage;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.app.PipelineDetailStatus;
import cn.orionsec.ops.constant.app.PipelineLogStatus;
import cn.orionsec.ops.constant.app.StageType;
import cn.orionsec.ops.constant.user.RoleType;
import cn.orionsec.ops.dao.ApplicationPipelineTaskDetailDAO;
import cn.orionsec.ops.dao.ApplicationPipelineTaskLogDAO;
import cn.orionsec.ops.dao.UserInfoDAO;
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDO;
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDetailDO;
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskLogDO;
import cn.orionsec.ops.entity.domain.UserInfoDO;
import cn.orionsec.ops.entity.dto.user.UserDTO;
import cn.orionsec.ops.utils.UserHolder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.Optional;
@Slf4j
public abstract class AbstractStageHandler implements IStageHandler {
protected static final ApplicationPipelineTaskDetailDAO applicationPipelineTaskDetailDAO = SpringHolder.getBean(ApplicationPipelineTaskDetailDAO.class);
protected static final ApplicationPipelineTaskLogDAO applicationPipelineTaskLogDAO = SpringHolder.getBean(ApplicationPipelineTaskLogDAO.class);
protected static final UserInfoDAO userInfoDAO = SpringHolder.getBean(UserInfoDAO.class);
protected Long detailId;
protected ApplicationPipelineTaskDO task;
protected ApplicationPipelineTaskDetailDO detail;
protected StageType stageType;
@Getter
protected volatile PipelineDetailStatus status;
protected volatile boolean terminated;
public AbstractStageHandler(ApplicationPipelineTaskDO task, ApplicationPipelineTaskDetailDO detail) {
this.task = task;
this.detail = detail;
this.detailId = detail.getId();
this.status = PipelineDetailStatus.WAIT;
}
@Override
public void exec() {
log.info("流水线阶段操作-开始执行 detailId: {}", detailId);
// 状态检查
if (!this.checkCanRunnable()) {
return;
}
Exception ex = null;
// 执行
try {
// 更新状态
this.updateStatus(PipelineDetailStatus.RUNNABLE);
// 执行操作
this.execStageTask();
} catch (Exception e) {
ex = e;
}
// 回调
try {
if (terminated) {
// 停止回调
this.terminatedCallback();
} else if (ex == null) {
// 成功回调
this.successCallback();
} else {
// 异常回调
this.exceptionCallback(ex);
throw Exceptions.runtime(ex.getMessage(), ex);
}
} finally {
this.close();
}
}
/**
* 执行操作任务
*/
protected abstract void execStageTask();
/**
* 检查是否可执行
*
* @return 是否可执行
*/
protected boolean checkCanRunnable() {
ApplicationPipelineTaskDetailDO detail = applicationPipelineTaskDetailDAO.selectById(detailId);
if (detail == null) {
return false;
}
return PipelineDetailStatus.WAIT.getStatus().equals(detail.getExecStatus());
}
/**
* 停止回调
*/
protected void terminatedCallback() {
log.info("流水线阶段操作-终止回调 detailId: {}", detailId);
// 修改状态
this.updateStatus(PipelineDetailStatus.TERMINATED);
}
/**
* 成功回调
*/
protected void successCallback() {
log.info("流水线阶段操作-成功回调 detailId: {}", detailId);
// 修改状态
this.updateStatus(PipelineDetailStatus.FINISH);
}
/**
* 异常回调
*
* @param ex ex
*/
protected void exceptionCallback(Exception ex) {
log.error("流水线阶段操作-异常回调 detailId: {}", detailId, ex);
// 修改状态
this.updateStatus(PipelineDetailStatus.FAILURE);
}
/**
* 更新状态
*
* @param status 状态
*/
protected void updateStatus(PipelineDetailStatus status) {
this.status = status;
Date now = new Date();
ApplicationPipelineTaskDetailDO update = new ApplicationPipelineTaskDetailDO();
update.setId(detailId);
update.setExecStatus(status.getStatus());
update.setUpdateTime(now);
switch (status) {
case RUNNABLE:
update.setExecStartTime(now);
break;
case FINISH:
case FAILURE:
case TERMINATED:
update.setExecEndTime(now);
break;
default:
break;
}
// 更新
applicationPipelineTaskDetailDAO.updateById(update);
// 插入日志
String appName = detail.getAppName();
switch (status) {
case FINISH:
this.addLog(PipelineLogStatus.SUCCESS, appName);
break;
case FAILURE:
this.addLog(PipelineLogStatus.FAILURE, appName);
break;
case SKIPPED:
this.addLog(PipelineLogStatus.SKIP, appName);
break;
case TERMINATED:
this.addLog(PipelineLogStatus.TERMINATED, appName);
break;
default:
break;
}
}
@Override
public void terminate() {
log.info("流水线阶段操作-终止 detailId: {}", detailId);
this.terminated = true;
}
@Override
public void skip() {
log.info("流水线阶段操作-跳过 detailId: {}", detailId);
if (PipelineDetailStatus.WAIT.equals(status)) {
// 只能跳过等待中的任务
this.updateStatus(PipelineDetailStatus.SKIPPED);
}
}
/**
* 设置引用id
*
* @param relId relId
*/
protected void setRelId(Long relId) {
ApplicationPipelineTaskDetailDO update = new ApplicationPipelineTaskDetailDO();
update.setId(detailId);
update.setRelId(relId);
update.setUpdateTime(new Date());
applicationPipelineTaskDetailDAO.updateById(update);
}
/**
* 添加日志
*
* @param logStatus logStatus
* @param params 日志参数
*/
protected void addLog(PipelineLogStatus logStatus, Object... params) {
ApplicationPipelineTaskLogDO log = new ApplicationPipelineTaskLogDO();
log.setTaskId(task.getId());
log.setTaskDetailId(detail.getId());
log.setLogStatus(logStatus.getStatus());
log.setStageType(stageType.getType());
log.setLogInfo(logStatus.format(stageType, params));
applicationPipelineTaskLogDAO.insert(log);
}
/**
* 设置执行人用户上下文
*/
protected void setExecuteUserContext() {
UserInfoDO userInfo = userInfoDAO.selectById(task.getExecUserId());
UserDTO user = new UserDTO();
user.setId(task.getExecUserId());
user.setUsername(task.getExecUserName());
Integer roleType = Optional.ofNullable(userInfo)
.map(UserInfoDO::getRoleType)
.orElseGet(() -> task.getExecUserId().equals(task.getAuditUserId())
? RoleType.ADMINISTRATOR.getType()
: RoleType.DEVELOPER.getType());
user.setRoleType(roleType);
UserHolder.set(user);
}
/**
* 释放资源
*/
protected void close() {
UserHolder.remove();
}
}

View File

@@ -0,0 +1,93 @@
package cn.orionsec.ops.handler.app.pipeline.stage;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.MessageConst;
import cn.orionsec.ops.constant.app.BuildStatus;
import cn.orionsec.ops.constant.app.PipelineLogStatus;
import cn.orionsec.ops.constant.app.StageType;
import cn.orionsec.ops.dao.ApplicationBuildDAO;
import cn.orionsec.ops.entity.domain.ApplicationBuildDO;
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDO;
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDetailDO;
import cn.orionsec.ops.entity.dto.app.ApplicationPipelineStageConfigDTO;
import cn.orionsec.ops.entity.request.app.ApplicationBuildRequest;
import cn.orionsec.ops.handler.app.build.BuildSessionHolder;
import cn.orionsec.ops.handler.app.machine.BuildMachineProcessor;
import cn.orionsec.ops.handler.app.machine.IMachineProcessor;
import cn.orionsec.ops.service.api.ApplicationBuildService;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BuildStageHandler extends AbstractStageHandler {
private Long buildId;
private static final ApplicationBuildService applicationBuildService = SpringHolder.getBean(ApplicationBuildService.class);
private static final ApplicationBuildDAO applicationBuildDAO = SpringHolder.getBean(ApplicationBuildDAO.class);
private static final BuildSessionHolder buildSessionHolder = SpringHolder.getBean(BuildSessionHolder.class);
public BuildStageHandler(ApplicationPipelineTaskDO task, ApplicationPipelineTaskDetailDO detail) {
super(task, detail);
this.stageType = StageType.BUILD;
}
@Override
protected void execStageTask() {
// 设置用户上下文
this.setExecuteUserContext();
// 参数
ApplicationBuildRequest request = new ApplicationBuildRequest();
request.setAppId(detail.getAppId());
request.setProfileId(task.getProfileId());
// 配置
ApplicationPipelineStageConfigDTO config = JSON.parseObject(detail.getStageConfig(), ApplicationPipelineStageConfigDTO.class);
request.setBranchName(config.getBranchName());
request.setCommitId(config.getCommitId());
request.setDescription(config.getDescription());
log.info("执行流水线任务-构建阶段-开始创建 detailId: {}, 参数: {}", detailId, JSON.toJSONString(request));
// 创建构建任务
this.buildId = applicationBuildService.submitBuildTask(request, false);
// 设置构建id
this.setRelId(buildId);
// 插入创建日志
ApplicationBuildDO build = applicationBuildDAO.selectById(buildId);
this.addLog(PipelineLogStatus.CREATE, detail.getAppName(), build.getBuildSeq());
log.info("执行流水线任务-构建阶段-创建完成开始执行 detailId: {}, buildId: {}", detailId, buildId);
// 插入执行日志
this.addLog(PipelineLogStatus.EXEC, detail.getAppName());
// 执行构建任务
new BuildMachineProcessor(buildId).run();
// 检查执行结果
build = applicationBuildDAO.selectById(buildId);
if (BuildStatus.FAILURE.getStatus().equals(build.getBuildStatus())) {
// 异常抛出
throw Exceptions.runtime(MessageConst.OPERATOR_ERROR);
} else if (BuildStatus.TERMINATED.getStatus().equals(build.getBuildStatus())) {
this.terminated = true;
}
}
@Override
public void terminate() {
super.terminate();
// 获取数据
ApplicationBuildDO build = applicationBuildDAO.selectById(buildId);
// 检查状态
if (!BuildStatus.RUNNABLE.getStatus().equals(build.getBuildStatus())) {
return;
}
// 获取实例
IMachineProcessor session = buildSessionHolder.getSession(buildId);
if (session == null) {
return;
}
// 调用终止
session.terminate();
}
}

View File

@@ -0,0 +1,49 @@
package cn.orionsec.ops.handler.app.pipeline.stage;
import cn.orionsec.kit.lang.able.Executable;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.ops.constant.app.PipelineDetailStatus;
import cn.orionsec.ops.constant.app.StageType;
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDO;
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDetailDO;
public interface IStageHandler extends Executable {
/**
* 停止执行
*/
void terminate();
/**
* 跳过执行
*/
void skip();
/**
* 获取状态
*
* @return status
*/
PipelineDetailStatus getStatus();
/**
* 获取阶段处理器
*
* @param task task
* @param detail detail
* @return 阶段处理器
*/
static IStageHandler with(ApplicationPipelineTaskDO task, ApplicationPipelineTaskDetailDO detail) {
StageType stageType = StageType.of(detail.getStageType());
switch (stageType) {
case BUILD:
return new BuildStageHandler(task, detail);
case RELEASE:
return new ReleaseStageHandler(task, detail);
default:
throw Exceptions.argument();
}
}
}

View File

@@ -0,0 +1,183 @@
package cn.orionsec.ops.handler.app.pipeline.stage;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.CnConst;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.MessageConst;
import cn.orionsec.ops.constant.app.PipelineLogStatus;
import cn.orionsec.ops.constant.app.ReleaseStatus;
import cn.orionsec.ops.constant.app.StageType;
import cn.orionsec.ops.dao.ApplicationBuildDAO;
import cn.orionsec.ops.dao.ApplicationReleaseDAO;
import cn.orionsec.ops.entity.domain.*;
import cn.orionsec.ops.entity.dto.app.ApplicationPipelineStageConfigDTO;
import cn.orionsec.ops.entity.request.app.ApplicationReleaseRequest;
import cn.orionsec.ops.handler.app.release.IReleaseProcessor;
import cn.orionsec.ops.handler.app.release.ReleaseSessionHolder;
import cn.orionsec.ops.service.api.ApplicationMachineService;
import cn.orionsec.ops.service.api.ApplicationReleaseService;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class ReleaseStageHandler extends AbstractStageHandler {
private static final ApplicationBuildDAO applicationBuildDAO = SpringHolder.getBean(ApplicationBuildDAO.class);
private static final ApplicationReleaseDAO applicationReleaseDAO = SpringHolder.getBean(ApplicationReleaseDAO.class);
private static final ApplicationReleaseService applicationReleaseService = SpringHolder.getBean(ApplicationReleaseService.class);
private static final ApplicationMachineService applicationMachineService = SpringHolder.getBean(ApplicationMachineService.class);
private static final ReleaseSessionHolder releaseSessionHolder = SpringHolder.getBean(ReleaseSessionHolder.class);
private Long releaseId;
private ApplicationReleaseDO release;
public ReleaseStageHandler(ApplicationPipelineTaskDO task, ApplicationPipelineTaskDetailDO detail) {
super(task, detail);
this.stageType = StageType.RELEASE;
}
@Override
protected void execStageTask() {
// 创建
this.createReleaseTask();
// 审核
this.auditReleaseTask();
// 执行
this.execReleaseTask();
}
/**
* 创建发布任务
*/
protected void createReleaseTask() {
// 设置用户上下文
this.setExecuteUserContext();
Long profileId = task.getProfileId();
Long appId = detail.getAppId();
// 创建发布任务参数
ApplicationReleaseRequest request = new ApplicationReleaseRequest();
request.setProfileId(profileId);
request.setAppId(appId);
// 其他配置
ApplicationPipelineStageConfigDTO config = JSON.parseObject(detail.getStageConfig(), ApplicationPipelineStageConfigDTO.class);
request.setDescription(config.getDescription());
// 标题
request.setTitle(Strings.def(config.getTitle(), () -> CnConst.RELEASE + Const.SPACE + detail.getAppName()));
// 构建id
Long buildId = config.getBuildId();
ApplicationBuildDO build = null;
if (buildId == null) {
// 最新版本
List<ApplicationBuildDO> buildList = applicationBuildDAO.selectBuildReleaseList(appId, profileId, 1);
if (!buildList.isEmpty()) {
build = buildList.get(0);
buildId = build.getId();
}
} else {
build = applicationBuildDAO.selectById(buildId);
}
if (build == null) {
throw Exceptions.argument(Strings.format(MessageConst.APP_LAST_BUILD_VERSION_ABSENT, detail.getAppName()));
}
request.setBuildId(buildId);
config.setBuildId(buildId);
// 发布机器
List<Long> machineIdList = config.getMachineIdList();
if (Lists.isEmpty(machineIdList)) {
// 全部机器
machineIdList = applicationMachineService.getAppProfileMachineList(appId, profileId).stream()
.map(ApplicationMachineDO::getMachineId)
.collect(Collectors.toList());
}
request.setMachineIdList(machineIdList);
config.setMachineIdList(machineIdList);
// 更新详情配置
String configJson = JSON.toJSONString(config);
detail.setStageConfig(configJson);
ApplicationPipelineTaskDetailDO updateConfig = new ApplicationPipelineTaskDetailDO();
updateConfig.setId(detailId);
updateConfig.setStageConfig(configJson);
applicationPipelineTaskDetailDAO.updateById(updateConfig);
// 创建发布任务
log.info("执行流水线任务-发布阶段-开始创建 detailId: {}, 参数: {}", detailId, JSON.toJSONString(request));
this.releaseId = applicationReleaseService.submitAppRelease(request);
// 设置发布id
this.setRelId(releaseId);
// 插入日志
this.addLog(PipelineLogStatus.CREATE, detail.getAppName(), build.getBuildSeq());
}
/**
* 审核发布任务
*/
private void auditReleaseTask() {
// 查询发布任务
this.release = applicationReleaseDAO.selectById(releaseId);
if (!ReleaseStatus.WAIT_AUDIT.getStatus().equals(release.getReleaseStatus())) {
return;
}
ApplicationReleaseDO update = new ApplicationReleaseDO();
update.setId(releaseId);
update.setAuditUserId(task.getAuditUserId());
update.setAuditUserName(task.getAuditUserName());
update.setAuditReason(MessageConst.AUTO_AUDIT_RESOLVE);
update.setAuditTime(new Date());
log.info("执行流水线任务-发布阶段-审核 detailId: {}, releaseId: {}, 参数: {}", detailId, releaseId, JSON.toJSONString(update));
applicationReleaseDAO.updateById(update);
}
/**
* 执行发布任务
*/
private void execReleaseTask() {
log.info("执行流水线任务-发布阶段-开始执行 detailId: {}, releaseId: {}", detailId, releaseId);
// 提交发布任务
applicationReleaseService.runnableAppRelease(releaseId, true, false);
// 插入执行日志
this.addLog(PipelineLogStatus.EXEC, detail.getAppName());
// 执行发布任务
IReleaseProcessor.with(release).run();
// 检查执行结果
this.release = applicationReleaseDAO.selectById(releaseId);
if (ReleaseStatus.FAILURE.getStatus().equals(release.getReleaseStatus())) {
// 异常抛出
throw Exceptions.runtime(MessageConst.OPERATOR_ERROR);
} else if (ReleaseStatus.TERMINATED.getStatus().equals(release.getReleaseStatus())) {
// 停止
this.terminated = true;
}
}
@Override
public void terminate() {
super.terminate();
// 获取数据
this.release = applicationReleaseDAO.selectById(releaseId);
// 检查状态
if (!ReleaseStatus.RUNNABLE.getStatus().equals(release.getReleaseStatus())) {
return;
}
// 获取实例
IReleaseProcessor session = releaseSessionHolder.getSession(releaseId);
if (session == null) {
return;
}
// 调用终止
session.terminateAll();
}
}

View File

@@ -0,0 +1,219 @@
package cn.orionsec.ops.handler.app.release;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.app.ReleaseStatus;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.constant.message.MessageType;
import cn.orionsec.ops.dao.ApplicationReleaseDAO;
import cn.orionsec.ops.entity.domain.ApplicationReleaseDO;
import cn.orionsec.ops.entity.domain.ApplicationReleaseMachineDO;
import cn.orionsec.ops.handler.app.machine.ReleaseMachineProcessor;
import cn.orionsec.ops.service.api.ApplicationReleaseMachineService;
import cn.orionsec.ops.service.api.WebSideMessageService;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Slf4j
public abstract class AbstractReleaseProcessor implements IReleaseProcessor {
protected static final ApplicationReleaseDAO applicationReleaseDAO = SpringHolder.getBean(ApplicationReleaseDAO.class);
protected static final ApplicationReleaseMachineService applicationReleaseMachineService = SpringHolder.getBean(ApplicationReleaseMachineService.class);
protected static final ReleaseSessionHolder releaseSessionHolder = SpringHolder.getBean(ReleaseSessionHolder.class);
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
@Getter
private final Long releaseId;
protected ApplicationReleaseDO release;
protected Map<Long, ReleaseMachineProcessor> machineProcessors;
protected volatile boolean terminated;
public AbstractReleaseProcessor(Long releaseId) {
this.releaseId = releaseId;
this.machineProcessors = Maps.newLinkedMap();
}
@Override
public void exec() {
log.info("已提交应用发布执行任务 id: {}", releaseId);
Threads.start(this, SchedulerPools.RELEASE_MAIN_SCHEDULER);
}
@Override
public void run() {
log.info("应用发布任务执行开始 id: {}", releaseId);
// 执行
Exception ex = null;
try {
// 查询数据
this.getReleaseData();
// 检查状态
if (release != null && !ReleaseStatus.WAIT_RUNNABLE.getStatus().equals(release.getReleaseStatus())
&& !ReleaseStatus.WAIT_SCHEDULE.getStatus().equals(release.getReleaseStatus())) {
return;
}
// 修改状态
this.updateStatus(ReleaseStatus.RUNNABLE);
// 添加会话
releaseSessionHolder.addSession(this);
// 执行
this.handler();
} catch (Exception e) {
log.error("应用发布任务执行初始化失败 id: {}, {}", releaseId, e);
ex = e;
}
// 回调
try {
if (terminated) {
// 停止回调
this.terminatedCallback();
} else if (ex == null) {
// 成功回调
this.completeCallback();
} else {
// 异常回调
this.exceptionCallback(ex);
}
} finally {
// 释放资源
this.close();
}
}
/**
* 处理器
*
* @throws Exception Exception
*/
protected abstract void handler() throws Exception;
@Override
public void terminateAll() {
this.terminated = true;
}
@Override
public void terminateMachine(Long releaseMachineId) {
ReleaseMachineProcessor processor = machineProcessors.get(releaseMachineId);
if (processor != null) {
processor.terminate();
}
}
@Override
public void skipMachine(Long releaseMachineId) {
ReleaseMachineProcessor processor = machineProcessors.get(releaseMachineId);
if (processor != null) {
processor.skip();
}
}
@Override
public void writeMachine(Long releaseMachineId, String command) {
ReleaseMachineProcessor processor = machineProcessors.get(releaseMachineId);
if (processor != null) {
processor.write(command);
}
}
/**
* 获取发布数据
*/
protected void getReleaseData() {
// 查询发布信息主表
this.release = applicationReleaseDAO.selectById(releaseId);
if (release == null) {
return;
}
if (!ReleaseStatus.WAIT_RUNNABLE.getStatus().equals(release.getReleaseStatus())
&& !ReleaseStatus.WAIT_SCHEDULE.getStatus().equals(release.getReleaseStatus())) {
return;
}
// 查询发布机器
List<ApplicationReleaseMachineDO> machines = applicationReleaseMachineService.getReleaseMachines(releaseId);
for (ApplicationReleaseMachineDO machine : machines) {
machineProcessors.put(machine.getId(), new ReleaseMachineProcessor(release, machine));
}
}
/**
* 停止回调
*/
protected void terminatedCallback() {
log.info("应用发布任务执行执行停止 id: {}", releaseId);
this.updateStatus(ReleaseStatus.TERMINATED);
}
/**
* 完成回调
*/
protected void completeCallback() {
log.info("应用发布任务执行执行完成 id: {}", releaseId);
this.updateStatus(ReleaseStatus.FINISH);
// 发送站内信
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.ID, release.getId());
params.put(EventKeys.TITLE, release.getReleaseTitle());
webSideMessageService.addMessage(MessageType.RELEASE_SUCCESS, release.getId(), release.getReleaseUserId(), release.getReleaseUserName(), params);
}
/**
* 异常回调
*
* @param ex ex
*/
protected void exceptionCallback(Exception ex) {
log.error("应用发布任务执行执行失败 id: {}", releaseId, ex);
this.updateStatus(ReleaseStatus.FAILURE);
// 发送站内信
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.ID, release.getId());
params.put(EventKeys.TITLE, release.getReleaseTitle());
webSideMessageService.addMessage(MessageType.RELEASE_FAILURE, release.getId(), release.getReleaseUserId(), release.getReleaseUserName(), params);
}
/**
* 更新状态
*
* @param status status
*/
protected void updateStatus(ReleaseStatus status) {
Date now = new Date();
ApplicationReleaseDO update = new ApplicationReleaseDO();
update.setId(releaseId);
update.setReleaseStatus(status.getStatus());
update.setUpdateTime(now);
switch (status) {
case RUNNABLE:
update.setReleaseStartTime(now);
break;
case FINISH:
case TERMINATED:
case FAILURE:
update.setReleaseEndTime(now);
break;
default:
break;
}
applicationReleaseDAO.updateById(update);
}
@Override
public void close() {
releaseSessionHolder.removeSession(releaseId);
}
}

View File

@@ -0,0 +1,62 @@
package cn.orionsec.ops.handler.app.release;
import cn.orionsec.kit.lang.able.Executable;
import cn.orionsec.kit.lang.able.SafeCloseable;
import cn.orionsec.kit.lang.function.select.Branches;
import cn.orionsec.kit.lang.function.select.Selector;
import cn.orionsec.ops.constant.common.SerialType;
import cn.orionsec.ops.entity.domain.ApplicationReleaseDO;
public interface IReleaseProcessor extends Executable, Runnable, SafeCloseable {
/**
* 获取id
*
* @return id
*/
Long getReleaseId();
/**
* 终止
*/
void terminateAll();
/**
* 终止机器操作
*
* @param releaseMachineId releaseMachineId
*/
void terminateMachine(Long releaseMachineId);
/**
* 跳过机器操作
*
* @param releaseMachineId 机器id
*/
void skipMachine(Long releaseMachineId);
/**
* 输入机器命令
*
* @param releaseMachineId 机器id
* @param command command
*/
void writeMachine(Long releaseMachineId, String command);
/**
* 获取发布执行器
*
* @param release release
* @return 执行器
*/
static IReleaseProcessor with(ApplicationReleaseDO release) {
return Selector.<SerialType, IReleaseProcessor>of(SerialType.of(release.getReleaseSerialize()))
.test(Branches.eq(SerialType.SERIAL)
.then(() -> new SerialReleaseProcessor(release.getId())))
.test(Branches.eq(SerialType.PARALLEL)
.then(() -> new ParallelReleaseProcessor(release.getId())))
.get();
}
}

View File

@@ -0,0 +1,51 @@
package cn.orionsec.ops.handler.app.release;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.ops.constant.MessageConst;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.app.ActionStatus;
import cn.orionsec.ops.handler.app.machine.ReleaseMachineProcessor;
import java.util.Collection;
public class ParallelReleaseProcessor extends AbstractReleaseProcessor {
public ParallelReleaseProcessor(Long releaseId) {
super(releaseId);
}
@Override
protected void handler() throws Exception {
Collection<ReleaseMachineProcessor> processor = machineProcessors.values();
Threads.blockRun(processor, SchedulerPools.RELEASE_MACHINE_SCHEDULER);
// 检查是否停止
if (terminated) {
return;
}
// 全部停止
final boolean allTerminated = processor.stream()
.map(ReleaseMachineProcessor::getStatus)
.allMatch(ActionStatus.TERMINATED::equals);
if (allTerminated) {
this.terminated = true;
return;
}
// 全部完成
boolean allFinish = processor.stream()
.map(ReleaseMachineProcessor::getStatus)
.filter(s -> !ActionStatus.TERMINATED.equals(s))
.allMatch(ActionStatus.FINISH::equals);
if (!allFinish) {
throw Exceptions.log(MessageConst.OPERATOR_NOT_ALL_SUCCESS);
}
}
@Override
public void terminateAll() {
super.terminateAll();
machineProcessors.values().forEach(ReleaseMachineProcessor::terminate);
}
}

View File

@@ -0,0 +1,45 @@
package cn.orionsec.ops.handler.app.release;
import cn.orionsec.kit.lang.utils.collect.Maps;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ReleaseSessionHolder {
/**
* session
*/
private final ConcurrentHashMap<Long, IReleaseProcessor> session = Maps.newCurrentHashMap();
/**
* 添加 session
*
* @param processor processor
*/
public void addSession(IReleaseProcessor processor) {
session.put(processor.getReleaseId(), processor);
}
/**
* 获取 session
*
* @param id id
* @return session
*/
public IReleaseProcessor getSession(Long id) {
return session.get(id);
}
/**
* 移除 session
*
* @param id id
*/
public void removeSession(Long id) {
session.remove(id);
}
}

View File

@@ -0,0 +1,64 @@
package cn.orionsec.ops.handler.app.release;
import cn.orionsec.ops.constant.app.ActionStatus;
import cn.orionsec.ops.constant.common.ExceptionHandlerType;
import cn.orionsec.ops.handler.app.machine.IMachineProcessor;
import cn.orionsec.ops.handler.app.machine.ReleaseMachineProcessor;
import java.util.Collection;
public class SerialReleaseProcessor extends AbstractReleaseProcessor {
public SerialReleaseProcessor(Long releaseId) {
super(releaseId);
}
@Override
protected void handler() throws Exception {
// 异常处理策略
final boolean errorSkipAll = ExceptionHandlerType.SKIP_ALL.getType().equals(release.getExceptionHandler());
Exception ex = null;
Collection<ReleaseMachineProcessor> processors = machineProcessors.values();
for (ReleaseMachineProcessor processor : processors) {
// 停止则跳过
if (terminated) {
processor.skip();
continue;
}
// 发生异常并且异常处理策略是跳过所有则跳过
if (ex != null && errorSkipAll) {
processor.skip();
continue;
}
// 执行
try {
processor.run();
} catch (Exception e) {
ex = e;
}
}
// 异常返回
if (ex != null) {
throw ex;
}
// 全部停止
final boolean allTerminated = processors.stream()
.map(ReleaseMachineProcessor::getStatus)
.filter(s -> !ActionStatus.SKIPPED.equals(s))
.allMatch(ActionStatus.TERMINATED::equals);
if (allTerminated) {
this.terminated = true;
}
}
@Override
public void terminateAll() {
super.terminateAll();
// 获取当前执行中的机器执行器
machineProcessors.values().stream()
.filter(s -> s.getStatus().equals(ActionStatus.RUNNABLE))
.forEach(IMachineProcessor::terminate);
}
}

View File

@@ -0,0 +1,357 @@
package cn.orionsec.ops.handler.exec;
import cn.orionsec.kit.lang.constant.Letters;
import cn.orionsec.kit.lang.exception.DisabledException;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.ssh.ExitCode;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.command.ExecStatus;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.constant.message.MessageType;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.dao.CommandExecDAO;
import cn.orionsec.ops.entity.domain.CommandExecDO;
import cn.orionsec.ops.entity.domain.MachineInfoDO;
import cn.orionsec.ops.handler.tail.TailSessionHolder;
import cn.orionsec.ops.service.api.MachineInfoService;
import cn.orionsec.ops.service.api.WebSideMessageService;
import cn.orionsec.ops.utils.Utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.OutputStream;
import java.util.Date;
import java.util.Map;
@Slf4j
public class CommandExecHandler implements IExecHandler {
private static final CommandExecDAO commandExecDAO = SpringHolder.getBean(CommandExecDAO.class);
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
private static final ExecSessionHolder execSessionHolder = SpringHolder.getBean(ExecSessionHolder.class);
private static final TailSessionHolder tailSessionHolder = SpringHolder.getBean(TailSessionHolder.class);
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
private final Long execId;
private CommandExecDO record;
private MachineInfoDO machine;
private SessionStore sessionStore;
private CommandExecutor executor;
private int exitCode;
private String logPath;
private OutputStream logOutputStream;
private Date startTime, endTime;
private volatile boolean terminated;
protected CommandExecHandler(Long execId) {
this.execId = execId;
}
@Override
public void exec() {
log.info("execHandler-提交 execId: {}", execId);
Threads.start(this, SchedulerPools.EXEC_SCHEDULER);
}
@Override
public void run() {
log.info("execHandler-执行开始 execId: {}", execId);
// 获取执行数据
this.getExecData();
// 检查状态
if (record == null || !ExecStatus.WAITING.getStatus().equals(record.getExecStatus())) {
return;
}
// 执行
Exception ex = null;
try {
// 更新状态
this.updateStatus(ExecStatus.RUNNABLE);
execSessionHolder.addSession(execId, this);
// 打开日志
this.openLogger();
// 打开executor
this.sessionStore = machineInfoService.openSessionStore(machine);
this.executor = sessionStore.getCommandExecutor(Strings.replaceCRLF(record.getExecCommand()));
// 执行命令
CommandExecutors.execCommand(executor, logOutputStream);
} catch (Exception e) {
ex = e;
}
// 回调
try {
if (terminated) {
// 停止回调
this.terminatedCallback();
} else if (ex == null) {
// 完成回调
this.completeCallback();
} else if (ex instanceof DisabledException) {
// 机器未启用回调
this.machineDisableCallback();
} else {
// 执行失败回调
this.exceptionCallback(ex);
}
} finally {
// 释放资源
this.close();
}
}
/**
* 获取执行数据
*/
private void getExecData() {
this.record = commandExecDAO.selectById(execId);
if (record == null) {
return;
}
if (!ExecStatus.WAITING.getStatus().equals(record.getExecStatus())) {
return;
}
// 查询机器信息
this.machine = machineInfoService.selectById(record.getMachineId());
// 设置日志信息
File logFile = new File(Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), record.getLogPath()));
Files1.touch(logFile);
this.logPath = logFile.getAbsolutePath();
}
@Override
public void write(String out) {
executor.write(out);
}
@Override
public void terminate() {
log.info("execHandler-停止 execId: {}", execId);
this.terminated = true;
Streams.close(executor);
}
/**
* 打开日志
*/
@SneakyThrows
private void openLogger() {
// 打开日志流
log.info("execHandler-打开日志流 {} {}", execId, logPath);
File logFile = new File(logPath);
this.logOutputStream = Files1.openOutputStreamFast(logFile);
StringBuilder sb = new StringBuilder()
.append(Utils.getStainKeyWords("# 准备执行命令", StainCode.GLOSS_GREEN))
.append(Letters.LF)
.append("@ssh: ")
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
.append(machine.getUsername()).append("@")
.append(machine.getMachineHost()).append(":")
.append(machine.getSshPort())
.append(StainCode.SUFFIX)
.append(Letters.LF);
sb.append("执行用户: ")
.append(Utils.getStainKeyWords(record.getUserName(), StainCode.GLOSS_BLUE))
.append(Letters.LF);
sb.append("执行任务: ")
.append(Utils.getStainKeyWords(execId, StainCode.GLOSS_BLUE))
.append(Letters.LF);
sb.append("执行机器: ")
.append(Utils.getStainKeyWords(machine.getMachineName(), StainCode.GLOSS_BLUE))
.append(Letters.LF);
sb.append("开始时间: ")
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
.append(Letters.LF);
String description = record.getDescription();
if (!Strings.isBlank(description)) {
sb.append("执行描述: ")
.append(Utils.getStainKeyWords(description, StainCode.GLOSS_BLUE))
.append(Letters.LF);
}
sb.append(Letters.LF)
.append(Utils.getStainKeyWords("# 执行命令", StainCode.GLOSS_GREEN))
.append(Letters.LF)
.append(StainCode.prefix(StainCode.GLOSS_CYAN))
.append(Utils.getEndLfWithEof(record.getExecCommand()))
.append(StainCode.SUFFIX)
.append(Utils.getStainKeyWords("# 开始执行", StainCode.GLOSS_GREEN))
.append(Letters.LF);
logOutputStream.write(Strings.bytes(sb.toString()));
logOutputStream.flush();
}
/**
* 停止回调
*/
private void terminatedCallback() {
log.info("execHandler-执行停止 execId: {}", execId);
// 更新状态
this.updateStatus(ExecStatus.TERMINATED);
// 拼接日志
StringBuilder log = new StringBuilder(Const.LF)
.append(Utils.getStainKeyWords("# 命令执行停止", StainCode.GLOSS_YELLOW))
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(log.toString());
}
/**
* 完成回调
*/
private void completeCallback() {
this.exitCode = executor.getExitCode();
log.info("execHandler-执行完成 execId: {} exitCode: {}", execId, exitCode);
// 更新状态
this.updateStatus(ExecStatus.COMPLETE);
// 拼接日志
long used = endTime.getTime() - startTime.getTime();
StringBuilder sb = new StringBuilder()
.append(Letters.LF)
.append(Utils.getStainKeyWords("# 命令执行完毕", StainCode.GLOSS_GREEN))
.append(Letters.LF);
sb.append("exitcode: ")
.append(ExitCode.isSuccess(exitCode)
? Utils.getStainKeyWords(exitCode, StainCode.GLOSS_BLUE)
: Utils.getStainKeyWords(exitCode, StainCode.GLOSS_RED))
.append(Letters.LF);
sb.append("结束时间: ")
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(" used ")
.append(Utils.getStainKeyWords(Utils.interval(used), StainCode.GLOSS_BLUE))
.append(" (")
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
.append(used)
.append("ms")
.append(StainCode.SUFFIX)
.append(")\n");
this.appendLog(sb.toString());
// 发送站内信
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.ID, record.getId());
params.put(EventKeys.NAME, record.getMachineName());
webSideMessageService.addMessage(MessageType.EXEC_SUCCESS, record.getId(), record.getUserId(), record.getUserName(), params);
}
/**
* 机器未启用回调
*/
private void machineDisableCallback() {
log.info("execHandler-机器停用停止 execId: {}", execId);
// 更新状态
this.updateStatus(ExecStatus.TERMINATED);
// 拼接日志
StringBuilder log = new StringBuilder()
.append(Const.LF)
.append(Utils.getStainKeyWords("# 命令执行机器未启用", StainCode.GLOSS_YELLOW))
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(log.toString());
}
/**
* 异常回调
*
* @param e e
*/
private void exceptionCallback(Exception e) {
log.error("execHandler-执行失败 execId: {}", execId, e);
// 更新状态
this.updateStatus(ExecStatus.EXCEPTION);
// 拼接日志
StringBuilder log = new StringBuilder()
.append(Const.LF)
.append(Utils.getStainKeyWords("# 命令执行异常", StainCode.GLOSS_RED))
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Letters.LF)
.append(Exceptions.getStackTraceAsString(e))
.append(Const.LF);
this.appendLog(log.toString());
// 发送站内信
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.ID, record.getId());
params.put(EventKeys.NAME, record.getMachineName());
webSideMessageService.addMessage(MessageType.EXEC_FAILURE, record.getId(), record.getUserId(), record.getUserName(), params);
}
@SneakyThrows
private void appendLog(String log) {
logOutputStream.write(Strings.bytes(log));
logOutputStream.flush();
}
/**
* 更新状态
*
* @param status status
*/
private void updateStatus(ExecStatus status) {
Date now = new Date();
// 更新
CommandExecDO update = new CommandExecDO();
update.setId(execId);
update.setExecStatus(status.getStatus());
update.setUpdateTime(now);
switch (status) {
case RUNNABLE:
this.startTime = now;
update.setStartDate(now);
break;
case COMPLETE:
this.endTime = now;
update.setEndDate(now);
update.setExitCode(exitCode);
break;
case EXCEPTION:
case TERMINATED:
this.endTime = now;
update.setEndDate(now);
break;
default:
}
int effect = commandExecDAO.updateById(update);
log.info("execHandler-更新状态 id: {}, status: {}, effect: {}", execId, status, effect);
}
@Override
public void close() {
log.info("execHandler-关闭 id: {}", execId);
// 移除会话
execSessionHolder.removeSession(execId);
// 释放资源
Streams.close(executor);
Streams.close(sessionStore);
Streams.close(logOutputStream);
// 异步关闭正在tail的日志
tailSessionHolder.asyncCloseTailFile(Const.HOST_MACHINE_ID, logPath);
}
}

View File

@@ -0,0 +1,48 @@
package cn.orionsec.ops.handler.exec;
import cn.orionsec.kit.lang.utils.collect.Maps;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class ExecSessionHolder {
/**
* key: execId
* value: IExecHandler
*/
private final Map<Long, IExecHandler> holder = Maps.newCurrentHashMap();
/**
* 添加session
*
* @param id id
* @param session session
*/
public void addSession(Long id, IExecHandler session) {
holder.put(id, session);
}
/**
* 获取session
*
* @param id id
* @return session
*/
public IExecHandler getSession(Long id) {
return holder.get(id);
}
/**
* 删除session
*
* @param id id
* @return session
*/
public IExecHandler removeSession(Long id) {
return holder.remove(id);
}
}

View File

@@ -0,0 +1,31 @@
package cn.orionsec.ops.handler.exec;
import cn.orionsec.kit.lang.able.Executable;
import cn.orionsec.kit.lang.able.SafeCloseable;
public interface IExecHandler extends Runnable, Executable, SafeCloseable {
/**
* 写入
*
* @param out out
*/
void write(String out);
/**
* 停止
*/
void terminate();
/**
* 获取实际执行 handler
*
* @param execId execId
* @return handler
*/
static IExecHandler with(Long execId) {
return new CommandExecHandler(execId);
}
}

View File

@@ -0,0 +1,22 @@
package cn.orionsec.ops.handler.http;
import cn.orionsec.kit.http.support.HttpMethod;
public interface HttpApiDefined {
/**
* 请求路径
*
* @return 路径
*/
String getPath();
/**
* 请求方法
*
* @return 方法
*/
HttpMethod getMethod();
}

View File

@@ -0,0 +1,52 @@
package cn.orionsec.ops.handler.http;
import cn.orionsec.kit.http.ok.OkRequest;
import cn.orionsec.kit.lang.constant.StandardContentType;
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
public class HttpApiRequest extends OkRequest {
public HttpApiRequest(String url, HttpApiDefined api) {
this.url = url + api.getPath();
this.method = api.getMethod().method();
}
/**
* 设置 json body
*
* @param body body
* @return this
*/
public HttpApiRequest jsonBody(Object body) {
this.contentType = StandardContentType.APPLICATION_JSON;
this.body(JSON.toJSONString(body));
return this;
}
/**
* 请求获取 httpWrapper
*
* @param dataClass T
* @param <T> T
* @return httpWrapper
*/
public <T> HttpWrapper<T> getHttpWrapper(Class<T> dataClass) {
return this.await().getJsonObjectBody(new TypeReference<HttpWrapper<T>>(dataClass) {
});
}
/**
* 请求获取 json
*
* @param type type
* @param <T> T
* @return T
*/
public <T> T getJson(TypeReference<T> type) {
return this.await().getJsonObjectBody(type);
}
}

View File

@@ -0,0 +1,72 @@
package cn.orionsec.ops.handler.http;
import cn.orionsec.kit.http.ok.OkResponse;
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
import com.alibaba.fastjson.TypeReference;
public interface HttpApiRequester<API extends HttpApiDefined> {
/**
* 请求 api
*
* @return OkResponse
*/
default OkResponse await() {
return this.getRequest().await();
}
/**
* 请求 api
*
* @param dataClass dataClass
* @param <T> T
* @return HttpWrapper
*/
default <T> HttpWrapper<T> request(Class<T> dataClass) {
return this.getRequest().getHttpWrapper(dataClass);
}
/**
* 请求 api
*
* @param type type
* @param <T> T
* @return T
*/
default <T> T request(TypeReference<T> type) {
return this.getRequest().getJson(type);
}
/**
* 请求 api
*
* @param requestBody requestBody
* @param dataClass dataClass
* @param <T> T
* @return HttpWrapper
*/
default <T> HttpWrapper<T> request(Object requestBody, Class<T> dataClass) {
return this.getRequest().jsonBody(requestBody).getHttpWrapper(dataClass);
}
/**
* 请求 api
*
* @param requestBody requestBody
* @param type type
* @param <T> T
* @return T
*/
default <T> T request(Object requestBody, TypeReference<T> type) {
return this.getRequest().jsonBody(requestBody).getJson(type);
}
/**
* 获取 api request
*
* @return request
*/
HttpApiRequest getRequest();
}

View File

@@ -0,0 +1,79 @@
package cn.orionsec.ops.handler.http;
import cn.orionsec.kit.http.support.HttpMethod;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum MachineMonitorHttpApi implements HttpApiDefined {
/**
* 端点 ping
*/
ENDPOINT_PING("/orion/machine-monitor-agent/api/endpoint/ping", HttpMethod.GET),
/**
* 端点 version
*/
ENDPOINT_VERSION("/orion/machine-monitor-agent/api/endpoint/version", HttpMethod.GET),
/**
* 端点 sync
*/
ENDPOINT_SYNC("/orion/machine-monitor-agent/api/endpoint/sync", HttpMethod.POST),
/**
* 指标 获取机器基本指标
*/
METRICS_BASE("/orion/machine-monitor-agent/api/metrics/base", HttpMethod.GET),
/**
* 指标 获取系统负载
*/
METRICS_SYSTEM_LOAD("/orion/machine-monitor-agent/api/metrics/system-load", HttpMethod.GET),
/**
* 指标 获取硬盘名称
*/
METRICS_DISK_NAME("/orion/machine-monitor-agent/api/metrics/disk-name", HttpMethod.GET),
/**
* 指标 获取 top 进程
*/
METRICS_TOP_PROCESSES("/orion/machine-monitor-agent/api/metrics/top-processes", HttpMethod.GET),
/**
* 监控 获取cpu数据
*/
MONITOR_CPU("/orion/machine-monitor-agent/api/monitor-statistic/cpu", HttpMethod.POST),
/**
* 监控 获取内存数据
*/
MONITOR_MEMORY("/orion/machine-monitor-agent/api/monitor-statistic/memory", HttpMethod.POST),
/**
* 监控 获取网络数据
*/
MONITOR_NET("/orion/machine-monitor-agent/api/monitor-statistic/net", HttpMethod.POST),
/**
* 监控 获取磁盘数据
*/
MONITOR_DISK("/orion/machine-monitor-agent/api/monitor-statistic/disk", HttpMethod.POST),
;
/**
* 请求路径
*/
private final String path;
/**
* 请求方法
*/
private final HttpMethod method;
}

View File

@@ -0,0 +1,30 @@
package cn.orionsec.ops.handler.http;
import cn.orionsec.ops.constant.monitor.MonitorConst;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MachineMonitorHttpApiRequester implements HttpApiRequester<MachineMonitorHttpApi> {
private static final String TAG = "machine-monitor";
private String url;
private MachineMonitorHttpApi api;
private String accessToken;
@Override
public HttpApiRequest getRequest() {
HttpApiRequest request = new HttpApiRequest(url, api);
request.tag(TAG);
request.header(MonitorConst.DEFAULT_ACCESS_HEADER, accessToken);
return request;
}
}

View File

@@ -0,0 +1,240 @@
package cn.orionsec.ops.handler.monitor;
import cn.orionsec.kit.lang.constant.Letters;
import cn.orionsec.kit.lang.utils.Arrays1;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
import cn.orionsec.kit.net.host.ssh.ExitCode;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.constant.message.MessageType;
import cn.orionsec.ops.constant.monitor.MonitorConst;
import cn.orionsec.ops.constant.monitor.MonitorStatus;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.entity.domain.MachineInfoDO;
import cn.orionsec.ops.entity.domain.MachineMonitorDO;
import cn.orionsec.ops.entity.dto.user.UserDTO;
import cn.orionsec.ops.service.api.MachineEnvService;
import cn.orionsec.ops.service.api.MachineInfoService;
import cn.orionsec.ops.service.api.MachineMonitorService;
import cn.orionsec.ops.service.api.WebSideMessageService;
import cn.orionsec.ops.utils.PathBuilders;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.OutputStream;
import java.util.Map;
@Slf4j
public class MonitorAgentInstallTask implements Runnable {
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
private static final MachineEnvService machineEnvService = SpringHolder.getBean(MachineEnvService.class);
private static final MachineMonitorService machineMonitorService = SpringHolder.getBean(MachineMonitorService.class);
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
private final Long machineId;
private final UserDTO user;
private SessionStore session;
private MachineInfoDO machine;
private OutputStream logStream;
public MonitorAgentInstallTask(Long machineId, UserDTO user) {
this.machineId = machineId;
this.user = user;
}
@Override
public void run() {
log.info("开始安装监控插件 machineId: {}", machineId);
try {
// 查询机器信息
this.machine = machineInfoService.selectById(machineId);
// 打开日志流
String logPath = PathBuilders.getInstallLogPath(machineId, MonitorConst.AGENT_FILE_NAME_PREFIX);
File logFile = new File(Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), logPath));
Files1.touch(logFile);
this.logStream = Files1.openOutputStreamFast(logFile);
// 打开会话
this.session = machineInfoService.openSessionStore(machineId);
String pluginDirectory = PathBuilders.getPluginPath(machine.getUsername());
String startScriptPath = pluginDirectory + "/" + MonitorConst.START_SCRIPT_FILE_NAME;
// 传输
this.transferAgentFile(pluginDirectory, startScriptPath);
// 启动
this.startAgentApp(startScriptPath);
// 同步等待
this.checkAgentRunStatus();
// 拼接日志
this.appendLog("安装成功 {}", Dates.current());
} catch (Exception e) {
// 拼接日志
this.appendLog("安装失败 {}", Exceptions.getStackTraceAsString(e));
// 更新状态
MachineMonitorDO update = new MachineMonitorDO();
update.setMonitorStatus(MonitorStatus.NOT_START.getStatus());
machineMonitorService.updateMonitorConfigByMachineId(machineId, update);
// 发送站内信
this.sendWebSideMessage(MessageType.MACHINE_AGENT_INSTALL_FAILURE);
} finally {
Streams.close(session);
Streams.close(logStream);
}
}
/**
* 传输文件
*
* @param pluginDirectory pluginDirectory
* @param startScriptPath startScriptPath
*/
private void transferAgentFile(String pluginDirectory, String startScriptPath) {
// 传输脚本目录
String agentPath = pluginDirectory + Const.LIB_DIR + "/" + MonitorConst.getAgentFileName();
SftpExecutor executor = null;
try {
// 打开 sftp 连接
String charset = machineEnvService.getSftpCharset(machineId);
executor = session.getSftpExecutor(charset);
executor.connect();
// 传输启动脚本文件
String startScript = this.getStartScript(agentPath);
this.appendLog("开始生成启动脚本 path: {}, command: \n{}", agentPath, startScript);
executor.write(startScriptPath, Strings.bytes(startScript));
executor.changeMode(startScriptPath, 777);
// 传输 agent 文件
File localAgentFile = new File(SystemEnvAttr.MACHINE_MONITOR_AGENT_PATH.getValue());
// 查询文件是否存在
long size = executor.getSize(agentPath);
long totalSize = localAgentFile.length();
if (totalSize != size) {
// 传输文件
this.appendLog("插件包不存在-开始传输 {} {}B", agentPath, totalSize);
executor.uploadFile(agentPath, localAgentFile);
this.appendLog("插件包传输完成 {}", agentPath);
} else {
this.appendLog("插件包已存在 {}", agentPath);
}
} catch (Exception e) {
throw Exceptions.sftp("文件上传失败", e);
} finally {
Streams.close(executor);
}
}
/**
* 启动 agent 应用
*
* @param startScriptPath startScriptPath
*/
private void startAgentApp(String startScriptPath) {
CommandExecutor executor = null;
try {
// 执行启动命令
this.appendLog("开始执行启动脚本 path: {}", startScriptPath);
// executor = session.getCommandExecutor("bash -l " + startScriptPath);
executor = session.getCommandExecutor(startScriptPath);
executor.getChannel().setPty(false);
CommandExecutors.execCommand(executor, logStream);
int exitCode = executor.getExitCode();
if (!ExitCode.isSuccess(exitCode)) {
throw Exceptions.runtime("执行启动失败");
}
this.appendLog("命令执行完成 exit: {}", exitCode);
} catch (Exception e) {
throw Exceptions.runtime("执行启动异常", e);
} finally {
Streams.close(executor);
}
}
/**
* 同步检查 agent 状态
*/
private void checkAgentRunStatus() {
// 查询配置
MachineMonitorDO monitor = machineMonitorService.selectByMachineId(machineId);
// 尝试进行同步 检查是否启动
String version = null;
for (int i = 0; i < 5; i++) {
Threads.sleep(Const.MS_S_10);
version = machineMonitorService.syncMonitorAgent(machineId, monitor.getMonitorUrl(), monitor.getAccessToken());
this.appendLog("检查agent状态 第{}次", i + 1);
if (version != null) {
break;
}
}
if (version == null) {
throw Exceptions.runtime("获取 agent 状态失败");
}
this.appendLog("agent启动成功 version: {}", version);
// 更新状态以及版本
MachineMonitorDO update = new MachineMonitorDO();
update.setMonitorStatus(MonitorStatus.RUNNING.getStatus());
update.setAgentVersion(version);
machineMonitorService.updateMonitorConfigByMachineId(machineId, update);
// 发送站内信
this.sendWebSideMessage(MessageType.MACHINE_AGENT_INSTALL_SUCCESS);
}
/**
* 发送站内信
*/
private void sendWebSideMessage(MessageType type) {
Map<String, Object> params = Maps.newMap();
params.put(EventKeys.NAME, machine.getMachineName());
webSideMessageService.addMessage(type, machine.getId(), user.getId(), user.getUsername(), params);
}
/**
* 获取启动脚本
*
* @param agentJarPath agentJar 路径
* @return 脚本内容
*/
private String getStartScript(String agentJarPath) {
Map<Object, Object> param = Maps.newMap();
param.put("processName", MonitorConst.AGENT_FILE_NAME_PREFIX);
param.put("machineId", machineId);
param.put("agentJarPath", agentJarPath);
return Strings.format(MonitorConst.START_SCRIPT_VALUE, param).replaceAll("\r\n", "\n");
}
/**
* 拼接日志
*
* @param logString log
* @param args args
*/
@SneakyThrows
private void appendLog(String logString, Object... args) {
if (!Arrays1.isEmpty(args)) {
log.info("安装监控插件-" + logString, args);
}
if (logStream != null) {
logStream.write(Strings.bytes(Strings.format(logString, args)));
logStream.write(Letters.LF);
logStream.flush();
}
}
}

View File

@@ -0,0 +1,189 @@
package cn.orionsec.ops.handler.scheduler;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.scheduler.SchedulerTaskStatus;
import cn.orionsec.ops.dao.SchedulerTaskDAO;
import cn.orionsec.ops.dao.SchedulerTaskRecordDAO;
import cn.orionsec.ops.entity.domain.SchedulerTaskDO;
import cn.orionsec.ops.entity.domain.SchedulerTaskMachineRecordDO;
import cn.orionsec.ops.entity.domain.SchedulerTaskRecordDO;
import cn.orionsec.ops.handler.scheduler.machine.ITaskMachineHandler;
import cn.orionsec.ops.handler.scheduler.machine.TaskMachineHandler;
import cn.orionsec.ops.service.api.SchedulerTaskMachineRecordService;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Slf4j
public abstract class AbstractTaskProcessor implements ITaskProcessor {
protected static final SchedulerTaskDAO schedulerTaskDAO = SpringHolder.getBean(SchedulerTaskDAO.class);
protected static final SchedulerTaskRecordDAO schedulerTaskRecordDAO = SpringHolder.getBean(SchedulerTaskRecordDAO.class);
protected static final SchedulerTaskMachineRecordService schedulerTaskMachineRecordService = SpringHolder.getBean(SchedulerTaskMachineRecordService.class);
protected static final TaskSessionHolder taskSessionHolder = SpringHolder.getBean(TaskSessionHolder.class);
protected final Long recordId;
protected SchedulerTaskDO task;
protected SchedulerTaskRecordDO record;
protected final Map<Long, ITaskMachineHandler> handlers;
protected volatile boolean terminated;
public AbstractTaskProcessor(Long recordId) {
this.recordId = recordId;
this.handlers = Maps.newLinkedMap();
}
@Override
public void run() {
log.info("开始执行调度任务-recordId: {}", recordId);
// 初始化
try {
// 填充数据
this.getTaskData();
// 判断状态
if (record == null || !SchedulerTaskStatus.WAIT.getStatus().equals(record.getTaskStatus())) {
return;
}
// 修改状态
this.updateStatus(SchedulerTaskStatus.RUNNABLE);
} catch (Exception e) {
log.error("执行调度任务初始化失败-recordId: {}", recordId, e);
this.updateStatus(SchedulerTaskStatus.FAILURE);
return;
}
// 执行
Exception ex = null;
try {
// 添加会话
taskSessionHolder.addSession(recordId, this);
// 处理
this.handler();
} catch (Exception e) {
// 执行失败
ex = e;
}
// 回调
try {
if (terminated) {
// 停止回调
this.updateStatus(SchedulerTaskStatus.TERMINATED);
log.info("执行调度任务执行停止-recordId: {}", recordId);
} else if (ex == null) {
// 完成回调
this.updateStatus(SchedulerTaskStatus.SUCCESS);
log.info("执行调度任务执行成功-recordId: {}", recordId);
} else {
// 异常回调
this.updateStatus(SchedulerTaskStatus.FAILURE);
log.error("执行调度任务执行失败-recordId: {}", recordId, ex);
}
} finally {
// 释放资源
this.close();
}
}
/**
* 处理
*
* @throws Exception 处理异常
*/
protected abstract void handler() throws Exception;
/**
* 填充数据
*/
private void getTaskData() {
// 查询明细
this.record = schedulerTaskRecordDAO.selectById(recordId);
if (record == null || !SchedulerTaskStatus.WAIT.getStatus().equals(record.getTaskStatus())) {
return;
}
// 查询任务
this.task = schedulerTaskDAO.selectById(record.getTaskId());
// 查询机器明细
List<SchedulerTaskMachineRecordDO> machineRecords = schedulerTaskMachineRecordService.selectByRecordId(recordId);
for (SchedulerTaskMachineRecordDO machineRecord : machineRecords) {
handlers.put(machineRecord.getId(), new TaskMachineHandler(machineRecord.getId()));
}
}
/**
* 更新状态
*
* @param status status
*/
protected void updateStatus(SchedulerTaskStatus status) {
Date now = new Date();
// 更新任务
SchedulerTaskDO updateTask = new SchedulerTaskDO();
updateTask.setId(task.getId());
updateTask.setUpdateTime(now);
updateTask.setLatelyStatus(status.getStatus());
// 更新明细
SchedulerTaskRecordDO updateRecord = new SchedulerTaskRecordDO();
updateRecord.setId(recordId);
updateRecord.setUpdateTime(now);
updateRecord.setTaskStatus(status.getStatus());
switch (status) {
case RUNNABLE:
updateRecord.setStartTime(now);
break;
case SUCCESS:
case FAILURE:
case TERMINATED:
default:
updateRecord.setEndTime(now);
break;
}
schedulerTaskDAO.updateById(updateTask);
schedulerTaskRecordDAO.updateById(updateRecord);
}
@Override
public void terminateAll() {
this.terminated = true;
}
@Override
public void terminateMachine(Long recordMachineId) {
ITaskMachineHandler machineHandler = handlers.get(recordMachineId);
if (machineHandler != null) {
machineHandler.terminate();
}
}
@Override
public void skipMachine(Long recordMachineId) {
ITaskMachineHandler machineHandler = handlers.get(recordMachineId);
if (machineHandler != null) {
machineHandler.skip();
}
}
@Override
public void writeMachine(Long recordMachineId, String command) {
ITaskMachineHandler machineHandler = handlers.get(recordMachineId);
if (machineHandler != null) {
machineHandler.write(command);
}
}
@Override
public void close() {
// 移除会话
taskSessionHolder.removeSession(recordId);
}
}

View File

@@ -0,0 +1,54 @@
package cn.orionsec.ops.handler.scheduler;
import cn.orionsec.kit.lang.able.SafeCloseable;
import cn.orionsec.kit.lang.function.select.Branches;
import cn.orionsec.kit.lang.function.select.Selector;
import cn.orionsec.ops.constant.common.SerialType;
public interface ITaskProcessor extends Runnable, SafeCloseable {
/**
* 停止全部
*/
void terminateAll();
/**
* 停止机器操作
*
* @param recordMachineId recordMachineId
*/
void terminateMachine(Long recordMachineId);
/**
* 跳过机器操作
*
* @param recordMachineId recordMachineId
*/
void skipMachine(Long recordMachineId);
/**
* 发送机器命令
*
* @param recordMachineId recordMachineId
* @param command command
*/
void writeMachine(Long recordMachineId, String command);
/**
* 获取实际执行处理器
*
* @param recordId recordId
* @param type type
* @return 处理器
*/
static ITaskProcessor with(Long recordId, SerialType type) {
return Selector.<SerialType, ITaskProcessor>of(type)
.test(Branches.eq(SerialType.SERIAL)
.then(() -> new SerialTaskProcessor(recordId)))
.test(Branches.eq(SerialType.PARALLEL)
.then(() -> new ParallelTaskProcessor(recordId)))
.get();
}
}

View File

@@ -0,0 +1,52 @@
package cn.orionsec.ops.handler.scheduler;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.ops.constant.MessageConst;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.scheduler.SchedulerTaskMachineStatus;
import cn.orionsec.ops.handler.scheduler.machine.ITaskMachineHandler;
import java.util.Collection;
public class ParallelTaskProcessor extends AbstractTaskProcessor {
public ParallelTaskProcessor(Long recordId) {
super(recordId);
}
@Override
protected void handler() throws Exception {
// 阻塞执行所有任务
Collection<ITaskMachineHandler> handlers = this.handlers.values();
Threads.blockRun(handlers, SchedulerPools.SCHEDULER_TASK_MACHINE_SCHEDULER);
// 检查是否停止
if (terminated) {
return;
}
// 全部停止
final boolean allTerminated = handlers.stream()
.map(ITaskMachineHandler::getStatus)
.allMatch(SchedulerTaskMachineStatus.TERMINATED::equals);
if (allTerminated) {
this.terminated = true;
return;
}
// 全部成功
final boolean allSuccess = handlers.stream()
.map(ITaskMachineHandler::getStatus)
.filter(s -> !SchedulerTaskMachineStatus.TERMINATED.equals(s))
.allMatch(SchedulerTaskMachineStatus.SUCCESS::equals);
if (!allSuccess) {
throw Exceptions.log(MessageConst.OPERATOR_NOT_ALL_SUCCESS);
}
}
@Override
public void terminateAll() {
super.terminateAll();
handlers.values().forEach(ITaskMachineHandler::terminate);
}
}

View File

@@ -0,0 +1,64 @@
package cn.orionsec.ops.handler.scheduler;
import cn.orionsec.ops.constant.common.ExceptionHandlerType;
import cn.orionsec.ops.constant.scheduler.SchedulerTaskMachineStatus;
import cn.orionsec.ops.handler.scheduler.machine.ITaskMachineHandler;
import java.util.Collection;
public class SerialTaskProcessor extends AbstractTaskProcessor {
public SerialTaskProcessor(Long recordId) {
super(recordId);
}
@Override
protected void handler() throws Exception {
// 异常处理策略
final boolean errorSkipAll = ExceptionHandlerType.SKIP_ALL.getType().equals(task.getExceptionHandler());
// 串行执行
Exception ex = null;
Collection<ITaskMachineHandler> handlers = this.handlers.values();
for (ITaskMachineHandler handler : handlers) {
// 停止跳过
if (terminated) {
handler.skip();
continue;
}
// 发生异常并且异常处理策略是跳过所有则跳过
if (ex != null && errorSkipAll) {
handler.skip();
continue;
}
// 执行
try {
handler.run();
} catch (Exception e) {
ex = e;
}
}
// 异常返回
if (ex != null) {
throw ex;
}
// 全部停止
final boolean allTerminated = handlers.stream()
.map(ITaskMachineHandler::getStatus)
.filter(s -> !SchedulerTaskMachineStatus.SKIPPED.equals(s))
.allMatch(SchedulerTaskMachineStatus.TERMINATED::equals);
if (allTerminated) {
this.terminated = true;
}
}
@Override
public void terminateAll() {
super.terminateAll();
// 获取当前执行中的机器执行器
handlers.values().stream()
.filter(s -> s.getStatus().equals(SchedulerTaskMachineStatus.RUNNABLE))
.forEach(ITaskMachineHandler::terminate);
}
}

View File

@@ -0,0 +1,48 @@
package cn.orionsec.ops.handler.scheduler;
import cn.orionsec.kit.lang.utils.collect.Maps;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class TaskSessionHolder {
/**
* key: recordId
* value: ITaskProcessor
*/
private final Map<Long, ITaskProcessor> holder = Maps.newCurrentHashMap();
/**
* 添加session
*
* @param id id
* @param session session
*/
public void addSession(Long id, ITaskProcessor session) {
holder.put(id, session);
}
/**
* 获取session
*
* @param id id
* @return session
*/
public ITaskProcessor getSession(Long id) {
return holder.get(id);
}
/**
* 删除session
*
* @param id id
* @return session
*/
public ITaskProcessor removeSession(Long id) {
return holder.remove(id);
}
}

View File

@@ -0,0 +1,33 @@
package cn.orionsec.ops.handler.scheduler.machine;
import cn.orionsec.kit.lang.able.SafeCloseable;
import cn.orionsec.ops.constant.scheduler.SchedulerTaskMachineStatus;
public interface ITaskMachineHandler extends Runnable, SafeCloseable {
/**
* 跳过 (未开始)
*/
void skip();
/**
* 停止 (进行中)
*/
void terminate();
/**
* 发送命令
*
* @param command command
*/
void write(String command);
/**
* 状态
*
* @return 获取状态
*/
SchedulerTaskMachineStatus getStatus();
}

View File

@@ -0,0 +1,309 @@
package cn.orionsec.ops.handler.scheduler.machine;
import cn.orionsec.kit.lang.constant.Letters;
import cn.orionsec.kit.lang.exception.DisabledException;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.ssh.ExitCode;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.common.StainCode;
import cn.orionsec.ops.constant.scheduler.SchedulerTaskMachineStatus;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.dao.SchedulerTaskMachineRecordDAO;
import cn.orionsec.ops.entity.domain.SchedulerTaskMachineRecordDO;
import cn.orionsec.ops.handler.tail.TailSessionHolder;
import cn.orionsec.ops.service.api.MachineInfoService;
import cn.orionsec.ops.utils.Utils;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.OutputStream;
import java.util.Date;
@Slf4j
public class TaskMachineHandler implements ITaskMachineHandler {
private static final SchedulerTaskMachineRecordDAO schedulerTaskMachineRecordDAO = SpringHolder.getBean(SchedulerTaskMachineRecordDAO.class);
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
private static final TailSessionHolder tailSessionHolder = SpringHolder.getBean(TailSessionHolder.class);
private final Long machineRecordId;
private String logPath;
private OutputStream logOutputStream;
private SessionStore sessionStore;
private CommandExecutor executor;
private SchedulerTaskMachineRecordDO machineRecord;
private Date startTime, endTime;
private Integer exitCode;
private volatile boolean terminated;
@Getter
private volatile SchedulerTaskMachineStatus status;
public TaskMachineHandler(Long machineRecordId) {
this.machineRecordId = machineRecordId;
this.status = SchedulerTaskMachineStatus.WAIT;
}
@Override
public void run() {
// 检查状态
log.info("调度任务-机器操作-开始 machineRecordId: {}", machineRecordId);
this.machineRecord = schedulerTaskMachineRecordDAO.selectById(machineRecordId);
this.status = SchedulerTaskMachineStatus.of(machineRecord.getExecStatus());
if (!SchedulerTaskMachineStatus.WAIT.equals(status)) {
return;
}
// 执行
Exception ex = null;
try {
this.updateStatus(SchedulerTaskMachineStatus.RUNNABLE);
// 打开日志
this.openLogger();
// 打开机器
this.sessionStore = machineInfoService.openSessionStore(machineRecord.getMachineId());
// 获取执行器
this.executor = sessionStore.getCommandExecutor(Strings.replaceCRLF(machineRecord.getExecCommand()));
// 开始执行
CommandExecutors.execCommand(executor, logOutputStream);
this.exitCode = executor.getExitCode();
} catch (Exception e) {
ex = e;
}
// 回调
try {
if (terminated) {
// 停止回调
this.terminatedCallback();
} else if (ex == null) {
// 完成回调
this.completeCallback();
} else if (ex instanceof DisabledException) {
// 机器未启用回调
this.machineDisableCallback();
} else {
// 执行异常回调
this.exceptionCallback(ex);
throw Exceptions.runtime(ex);
}
} finally {
// 释放资源
this.close();
}
}
/**
* 停止回调
*/
private void terminatedCallback() {
log.error("调度任务-机器操作-停止 machineRecordId: {}", machineRecordId);
// 更新状态
this.updateStatus(SchedulerTaskMachineStatus.TERMINATED);
// 拼接日志
StringBuilder log = new StringBuilder(Const.LF_2)
.append(Utils.getStainKeyWords("# 调度任务执行停止", StainCode.GLOSS_YELLOW))
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(log.toString());
}
/**
* 完成回调
*/
private void completeCallback() {
log.info("调度任务-机器操作-完成 machineRecordId: {}, exitCode: {}", machineRecordId, exitCode);
final boolean execSuccess = ExitCode.isSuccess(exitCode);
// 更新状态
if (execSuccess) {
this.updateStatus(SchedulerTaskMachineStatus.SUCCESS);
} else {
this.updateStatus(SchedulerTaskMachineStatus.FAILURE);
}
// 拼接日志
long used = endTime.getTime() - startTime.getTime();
StringBuilder log = new StringBuilder()
.append(Letters.LF)
.append(Utils.getStainKeyWords("# 调度任务执行完成", StainCode.GLOSS_GREEN))
.append(Letters.LF);
log.append("exitcode: ")
.append(execSuccess
? Utils.getStainKeyWords(exitCode, StainCode.GLOSS_BLUE)
: Utils.getStainKeyWords(exitCode, StainCode.GLOSS_RED))
.append(Letters.LF);
log.append("结束时间: ")
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(" used ")
.append(Utils.getStainKeyWords(Utils.interval(used), StainCode.GLOSS_BLUE))
.append(" (")
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
.append(used)
.append("ms")
.append(StainCode.SUFFIX)
.append(")\n");
this.appendLog(log.toString());
}
/**
* 机器未启用回调
*/
private void machineDisableCallback() {
log.error("调度任务-机器操作-机器停用停止 machineRecordId: {}", machineRecordId);
// 更新状态
this.updateStatus(SchedulerTaskMachineStatus.TERMINATED);
// 拼接日志
StringBuilder log = new StringBuilder()
.append(Const.LF)
.append(Utils.getStainKeyWords("# 调度任务执行机器未启用", StainCode.GLOSS_YELLOW))
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Const.LF);
this.appendLog(log.toString());
}
/**
* 异常回调
*/
private void exceptionCallback(Exception e) {
log.error("调度任务-机器操作-失败 machineRecordId: {}", machineRecordId, e);
// 更新状态
this.updateStatus(SchedulerTaskMachineStatus.FAILURE);
// 拼接日志
StringBuilder log = new StringBuilder()
.append(Const.LF)
.append(Utils.getStainKeyWords("# 调度任务执行失败", StainCode.GLOSS_RED))
.append(Letters.TAB)
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
.append(Letters.LF)
.append(Exceptions.getStackTraceAsString(e))
.append(Const.LF);
this.appendLog(log.toString());
}
@Override
public void skip() {
log.info("调度任务-机器操作-跳过 machineRecordId: {}, status: {}", machineRecordId, status);
if (SchedulerTaskMachineStatus.WAIT.equals(status)) {
// 只能跳过等待中的任务
this.updateStatus(SchedulerTaskMachineStatus.SKIPPED);
}
}
@Override
public void terminate() {
log.info("调度任务-机器操作-停止 machineRecordId: {}", machineRecordId);
// 只能停止进行中的任务
if (SchedulerTaskMachineStatus.RUNNABLE.equals(status)) {
this.terminated = true;
Streams.close(this.executor);
}
}
@Override
public void write(String command) {
executor.write(command);
}
/**
* 打开日志
*/
private void openLogger() {
File logFile = new File(Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), machineRecord.getLogPath()));
Files1.touch(logFile);
this.logPath = logFile.getAbsolutePath();
// 打开日志流
log.info("TaskMachineHandler-打开日志流 {} {}", machineRecordId, logPath);
this.logOutputStream = Files1.openOutputStreamFastSafe(logFile);
// 拼接开始日志
StringBuilder log = new StringBuilder()
.append(Utils.getStainKeyWords("# 开始执行调度任务 ", StainCode.GLOSS_GREEN))
.append(Const.LF);
log.append("执行机器: ")
.append(Utils.getStainKeyWords(machineRecord.getMachineName(), StainCode.GLOSS_BLUE))
.append(Const.LF);
log.append("开始时间: ")
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
.append(Const.LF_2);
log.append(Utils.getStainKeyWords("# 执行命令", StainCode.GLOSS_GREEN))
.append(Const.LF)
.append(StainCode.prefix(StainCode.GLOSS_CYAN))
.append(Utils.getEndLfWithEof(machineRecord.getExecCommand()))
.append(StainCode.SUFFIX)
.append(Utils.getStainKeyWords("# 开始执行", StainCode.GLOSS_GREEN))
.append(Const.LF);
this.appendLog(log.toString());
}
/**
* 拼接日志
*
* @param log log
*/
@SneakyThrows
private void appendLog(String log) {
logOutputStream.write(Strings.bytes(log));
logOutputStream.flush();
}
/**
* 更新状态
*
* @param status status
*/
private void updateStatus(SchedulerTaskMachineStatus status) {
Date now = new Date();
this.status = status;
SchedulerTaskMachineRecordDO update = new SchedulerTaskMachineRecordDO();
update.setId(machineRecordId);
update.setExecStatus(status.getStatus());
update.setUpdateTime(now);
switch (status) {
case RUNNABLE:
this.startTime = now;
update.setStartTime(now);
break;
case SUCCESS:
case FAILURE:
case TERMINATED:
if (startTime != null) {
this.endTime = now;
update.setEndTime(now);
update.setExitCode(exitCode);
}
break;
default:
}
schedulerTaskMachineRecordDAO.updateById(update);
}
@Override
public void close() {
// 释放资源
Streams.close(executor);
Streams.close(sessionStore);
Streams.close(logOutputStream);
// 异步关闭正在tail的日志
tailSessionHolder.asyncCloseTailFile(Const.HOST_MACHINE_ID, logPath);
}
}

View File

@@ -0,0 +1,222 @@
package cn.orionsec.ops.handler.sftp;
import cn.orionsec.kit.lang.support.progress.ByteTransferRateProgress;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.math.Numbers;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.sftp.SftpTransferStatus;
import cn.orionsec.ops.dao.FileTransferLogDAO;
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
import cn.orionsec.ops.entity.dto.sftp.FileTransferNotifyProgressDTO;
import cn.orionsec.ops.service.api.MachineEnvService;
import cn.orionsec.ops.service.api.MachineInfoService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class FileTransferProcessor implements IFileTransferProcessor {
protected static MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
protected static MachineEnvService machineEnvService = SpringHolder.getBean(MachineEnvService.class);
protected static FileTransferLogDAO fileTransferLogDAO = SpringHolder.getBean(FileTransferLogDAO.class);
protected static TransferProcessorManager transferProcessorManager = SpringHolder.getBean(TransferProcessorManager.class);
protected SessionStore sessionStore;
protected SftpExecutor executor;
protected FileTransferLogDO record;
protected Long userId;
protected Long machineId;
protected String fileToken;
protected ByteTransferRateProgress progress;
protected volatile boolean userCancel;
public FileTransferProcessor(FileTransferLogDO record) {
this.record = record;
this.fileToken = record.getFileToken();
this.userId = record.getUserId();
this.machineId = record.getMachineId();
}
@Override
public void run() {
// 判断是否可以传输
this.record = fileTransferLogDAO.selectById(record.getId());
if (record == null || !SftpTransferStatus.WAIT.getStatus().equals(record.getTransferStatus())) {
return;
}
transferProcessorManager.addProcessor(fileToken, this);
try {
// 开始传输
this.updateStatusAndNotify(SftpTransferStatus.RUNNABLE.getStatus());
// 打开连接
this.sessionStore = machineInfoService.openSessionStore(machineId);
String charset = machineEnvService.getSftpCharset(machineId);
this.executor = sessionStore.getSftpExecutor(charset);
executor.connect();
log.info("sftp传输文件-初始化完毕, 准备处理传输 fileToken: {}", fileToken);
// 检查是否可以用文件系统传输
if (SftpSupport.checkUseFileSystem(executor)) {
// 直接拷贝
SftpSupport.usingFsCopy(this);
} else {
// 处理
this.handler();
}
log.info("sftp传输文件-传输完毕 fileToken: {}", fileToken);
} catch (Exception e) {
log.error("sftp传输文件-出现异常 fileToken: {}, e: {}, message: {}", fileToken, e.getClass().getName(), e.getMessage());
// 程序错误并非传输错误修改状态
if (!userCancel) {
log.error("sftp传输文件-运行异常 fileToken: {}", fileToken, e);
this.updateStatusAndNotify(SftpTransferStatus.ERROR.getStatus());
}
e.printStackTrace();
} finally {
transferProcessorManager.removeProcessor(fileToken);
this.disconnected();
}
}
@Override
public void stop() {
log.info("sftp传输文件-用户暂停 fileToken: {}", fileToken);
this.userCancel = true;
this.updateStatusAndNotify(SftpTransferStatus.PAUSE.getStatus());
this.disconnected();
}
/**
* 处理操作
*/
protected abstract void handler();
/**
* 初始化进度条
*/
protected void initProgress(ByteTransferRateProgress progress) {
this.progress = progress;
progress.computeRate();
progress.rateExecutor(SchedulerPools.SFTP_TRANSFER_RATE_SCHEDULER);
progress.rateAcceptor(this::transferAccept);
progress.callback(this::transferDoneCallback);
}
/**
* 传输回调
*
* @param progress progress
*/
protected void transferAccept(ByteTransferRateProgress progress) {
try {
if (progress.isDone()) {
return;
}
String progressRate = Numbers.setScale(progress.getProgress() * 100, 2);
String transferRate = Files1.getSize(progress.getNowRate());
String transferCurrent = Files1.getSize(progress.getCurrent());
// debug
// log.info(transferCurrent + " " + progressRate + "% " + transferRate + "/s");
// notify progress
this.notifyProgress(transferRate, transferCurrent, progressRate);
} catch (Exception e) {
log.error("sftp-传输信息回调异常 fileToken: {}, digest: {}", fileToken, Exceptions.getDigest(e), e);
}
}
/**
* 传输完成回调
*
* @param pro progress
*/
protected void transferDoneCallback() {
try {
FileTransferLogDO update = new FileTransferLogDO();
update.setId(record.getId());
if (progress.isError()) {
// 非用户取消更新状态
if (!userCancel) {
this.updateStatusAndNotify(SftpTransferStatus.ERROR.getStatus());
}
} else {
String transferCurrent = Files1.getSize(progress.getCurrent());
String transferRate = Files1.getSize(progress.getNowRate());
// notify progress
this.notifyProgress(transferRate, transferCurrent, "100");
// notify status
this.updateStatusAndNotify(SftpTransferStatus.FINISH.getStatus(), 100D, progress.getEnd());
}
} catch (Exception e) {
log.error("sftp-传输完成回调异常 fileToken: {}, digest: {}", fileToken, Exceptions.getDigest(e));
e.printStackTrace();
}
}
protected void updateStatusAndNotify(Integer status) {
if (progress == null) {
this.updateStatusAndNotify(status, null, null);
} else {
this.updateStatusAndNotify(status, progress.getProgress() * 100, progress.getCurrent());
}
}
/**
* 更新状态并且通知
*
* @param status status
* @param progress progress
* @param currentSize currentSize
*/
protected void updateStatusAndNotify(Integer status, Double progress, Long currentSize) {
Long id = record.getId();
if (id == null) {
return;
}
record.setTransferStatus(status);
// 更新
FileTransferLogDO update = new FileTransferLogDO();
update.setId(id);
update.setTransferStatus(status);
update.setNowProgress(progress);
update.setCurrentSize(currentSize);
int effect = fileTransferLogDAO.updateById(update);
log.info("sftp传输文件-更新状态 fileToken: {}, status: {}, progress: {}, currentSize: {}, effect: {}", fileToken, status, progress, currentSize, effect);
// notify status
transferProcessorManager.notifySessionStatusEvent(userId, machineId, fileToken, status);
}
/**
* 通知进度
*
* @param rate 速度
* @param current 当前位置
* @param progress 进度
*/
protected void notifyProgress(String rate, String current, String progress) {
FileTransferNotifyProgressDTO notifyProgress = new FileTransferNotifyProgressDTO(rate, current, progress);
transferProcessorManager.notifySessionProgressEvent(userId, machineId, fileToken, notifyProgress);
}
/**
* 断开连接
*/
protected void disconnected() {
if (executor != null) {
executor.disconnect();
}
}
}

View File

@@ -0,0 +1,36 @@
package cn.orionsec.ops.handler.sftp;
import cn.orionsec.kit.lang.able.Executable;
import cn.orionsec.kit.lang.able.Stoppable;
import cn.orionsec.kit.lang.function.select.Branches;
import cn.orionsec.kit.lang.function.select.Selector;
import cn.orionsec.ops.constant.sftp.SftpTransferType;
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
import cn.orionsec.ops.handler.sftp.hint.FilePackageHint;
import cn.orionsec.ops.handler.sftp.hint.FileTransferHint;
import cn.orionsec.ops.handler.sftp.impl.DownloadFileProcessor;
import cn.orionsec.ops.handler.sftp.impl.PackageFileProcessor;
import cn.orionsec.ops.handler.sftp.impl.UploadFileProcessor;
public interface IFileTransferProcessor extends Runnable, Stoppable, Executable {
/**
* 获取执行processor
*
* @param hint hint
* @return IFileTransferProcessor
*/
static IFileTransferProcessor of(FileTransferHint hint) {
FileTransferLogDO record = hint.getRecord();
return Selector.<SftpTransferType, IFileTransferProcessor>of(hint.getType())
.test(Branches.eq(SftpTransferType.UPLOAD)
.then(() -> new UploadFileProcessor(record)))
.test(Branches.eq(SftpTransferType.DOWNLOAD)
.then(() -> new DownloadFileProcessor(record)))
.test(Branches.eq(SftpTransferType.PACKAGE)
.then(() -> new PackageFileProcessor(record, ((FilePackageHint) hint).getFileList())))
.get();
}
}

View File

@@ -0,0 +1,129 @@
package cn.orionsec.ops.handler.sftp;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.entity.domain.MachineInfoDO;
import cn.orionsec.ops.service.api.MachineEnvService;
import cn.orionsec.ops.service.api.MachineInfoService;
import cn.orionsec.ops.service.api.SftpService;
import cn.orionsec.ops.utils.EventParamsHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Component
public class SftpBasicExecutorHolder {
@Resource
private MachineInfoService machineInfoService;
@Resource
private MachineEnvService machineEnvService;
@Resource
private SftpService sftpService;
/**
* 基本操作的executor 不包含(upload, download)
* machineId: executor
*/
private final Map<Long, SftpExecutor> basicExecutorHolder = Maps.newCurrentHashMap();
/**
* 基本操作executor 最后使用时间
*/
private final Map<Long, Long> executorUsing = Maps.newCurrentHashMap();
/**
* 获取 sftp 基本操作 executor
*
* @param sessionToken sessionToken
* @return SftpExecutor
*/
public SftpExecutor getBasicExecutor(String sessionToken) {
// 获取executor
Long machineId = sftpService.getMachineId(sessionToken);
EventParamsHolder.addParam(EventKeys.MACHINE_ID, machineId);
return this.getBasicExecutor(machineId);
}
/**
* 获取 sftp 基本操作 executor
*
* @param machineId machineId
* @return SftpExecutor
*/
public SftpExecutor getBasicExecutor(Long machineId) {
return this.getBasicExecutor(machineId, null);
}
/**
* 获取 sftp 基本操作 executor
*
* @param machineId machineId
* @param machine machine
* @return SftpExecutor
*/
public SftpExecutor getBasicExecutor(Long machineId, MachineInfoDO machine) {
SftpExecutor executor = basicExecutorHolder.get(machineId);
if (executor != null) {
if (!executor.isConnected()) {
try {
executor.connect();
} catch (Exception e) {
// 无法连接则重新创建实例
executor = null;
}
}
}
// 如果没有重新建立连接
if (executor == null) {
if (machine == null) {
machine = machineInfoService.selectById(machineId);
}
// 获取charset
String charset = machineEnvService.getSftpCharset(machineId);
// 打开sftp连接
SessionStore sessionStore = machineInfoService.openSessionStore(machine);
executor = sessionStore.getSftpExecutor(charset);
executor.connect();
basicExecutorHolder.put(machineId, executor);
}
executorUsing.put(machineId, System.currentTimeMillis());
return executor;
}
/**
* 无效化一段时间(1分钟)未使用的执行器
*/
@Scheduled(cron = "0 */1 * * * ?")
private void invalidationUnusedExecutor() {
long curr = System.currentTimeMillis();
// 查询需要淘汰的executor的key
List<Long> expireKeys = basicExecutorHolder.keySet().stream()
.filter(key -> curr > executorUsing.get(key) + Const.MS_S_60 * 5)
.collect(Collectors.toList());
// 移除
expireKeys.forEach(key -> {
SftpExecutor sftpExecutor = basicExecutorHolder.get(key);
if (sftpExecutor == null) {
return;
}
log.info("淘汰sftp执行器: {}", key);
basicExecutorHolder.remove(key);
sftpExecutor.disconnect();
});
}
}

View File

@@ -0,0 +1,73 @@
package cn.orionsec.ops.handler.sftp;
import cn.orionsec.kit.lang.id.UUIds;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
import cn.orionsec.ops.constant.sftp.SftpTransferStatus;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.handler.sftp.impl.UploadFileProcessor;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
@Slf4j
public class SftpSupport {
private SftpSupport() {
}
/**
* 检查远程机器和本机是否是同一台机器
*
* @param executor executor
* @return 是否为本机
*/
public static boolean checkUseFileSystem(SftpExecutor executor) {
try {
// 创建一个临时文件
String checkPath = Files1.getPath(SystemEnvAttr.TEMP_PATH.getValue(), UUIds.random32() + ".ck");
File checkFile = new File(checkPath);
Files1.touch(checkFile);
checkFile.deleteOnExit();
// 查询远程机器是否有此文件 如果有则证明传输机器和宿主机是同一台
boolean exist = executor.getFile(checkFile.getAbsolutePath()) != null;
Files1.delete(checkFile);
return exist;
} catch (Exception e) {
log.error("无法使用FSC {}", Exceptions.getDigest(e));
return false;
}
}
/**
* 使用 file system copy
*
* @param processor processor
*/
public static void usingFsCopy(FileTransferProcessor processor) {
String remoteFile = processor.record.getRemoteFile();
String localFile = processor.record.getLocalFile();
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
log.info("sftp文件传输-使用FSC fileToken: {}, machineId: {}, local: {}, remote: {}",
processor.fileToken, processor.machineId, localAbsolutePath, remoteFile);
// 复制
File sourceFile;
File targetFile;
if (processor instanceof UploadFileProcessor) {
sourceFile = new File(localAbsolutePath);
targetFile = new File(remoteFile);
} else {
sourceFile = new File(remoteFile);
targetFile = new File(localAbsolutePath);
}
Files1.copy(sourceFile, targetFile);
// 通知进度
long fileSize = sourceFile.length();
processor.notifyProgress(Files1.getSize(fileSize), Files1.getSize(fileSize), "100");
// 通知状态
processor.updateStatusAndNotify(SftpTransferStatus.FINISH.getStatus(), 100D, fileSize);
}
}

View File

@@ -0,0 +1,232 @@
package cn.orionsec.ops.handler.sftp;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.convert.Converts;
import cn.orionsec.kit.lang.utils.json.Jsons;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.sftp.SftpNotifyType;
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
import cn.orionsec.ops.entity.dto.sftp.FileTransferNotifyDTO;
import cn.orionsec.ops.entity.dto.sftp.FileTransferNotifyProgressDTO;
import cn.orionsec.ops.entity.vo.sftp.FileTransferLogVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class TransferProcessorManager {
/**
* key: token
* value: processor
*/
private final Map<String, IFileTransferProcessor> transferProcessor = Maps.newCurrentHashMap();
/**
* sessionId 和 session 映射
* <p>
* key: sessionId
* value: webSocketSession
*/
private final Map<String, WebSocketSession> idMapping = Maps.newCurrentHashMap();
/**
* 机器 和 session 映射
* <p>
* key: userId_machineId
* value: sessionIdList
*/
private final Map<String, List<String>> userMachineSessionMapping = Maps.newCurrentHashMap();
/**
* 文件传输进度
* <p>
* key: token
* value: progress
*/
private final Map<String, String> TRANSFER_PROGRESS = Maps.newCurrentHashMap();
/**
* 添加processor
*
* @param token token
* @param processor processor
*/
public void addProcessor(String token, IFileTransferProcessor processor) {
transferProcessor.put(token, processor);
}
/**
* 删除processor
*
* @param token token
*/
public void removeProcessor(String token) {
transferProcessor.remove(token);
}
/**
* 获取processor
*
* @param token token
* @return processor
*/
public IFileTransferProcessor getProcessor(String token) {
return transferProcessor.get(token);
}
/**
* 注册session 通知
*
* @param id id
* @param session session
* @param userId userId
* @param machineId machineId
*/
public void registerSessionNotify(String id, WebSocketSession session, Long userId, Long machineId) {
idMapping.put(id, session);
userMachineSessionMapping.computeIfAbsent(this.getUserMachine(userId, machineId), s -> Lists.newList()).add(id);
}
/**
* 关闭session 通知
*
* @param id id
*/
public void closeSessionNotify(String id) {
idMapping.remove(id);
// 删除机器与会话的关联
userMachineSessionMapping.forEach((k, v) -> {
if (Lists.isEmpty(v)) {
return;
}
v.removeIf(s -> s.equals(id));
});
}
/**
* 通知session 添加事件
*
* @param userId userId
* @param machineId machineId
* @param record record
*/
public void notifySessionAddEvent(Long userId, Long machineId, FileTransferLogDO record) {
FileTransferNotifyDTO notify = new FileTransferNotifyDTO();
notify.setType(SftpNotifyType.ADD.getType());
notify.setFileToken(record.getFileToken());
notify.setBody(Jsons.toJsonWriteNull(Converts.to(record, FileTransferLogVO.class)));
this.notifySession(userId, machineId, notify);
}
/**
* 通知session 进度事件
*
* @param userId userId
* @param machineId machineId
* @param fileToken fileToken
* @param progress progress
*/
public void notifySessionProgressEvent(Long userId, Long machineId, String fileToken, FileTransferNotifyProgressDTO progress) {
// 设置进度
TRANSFER_PROGRESS.put(fileToken, progress.getProgress());
// 通知
FileTransferNotifyDTO notify = new FileTransferNotifyDTO();
notify.setType(SftpNotifyType.PROGRESS.getType());
notify.setFileToken(fileToken);
notify.setBody(Jsons.toJsonWriteNull(progress));
this.notifySession(userId, machineId, notify);
}
/**
* 通知session 状态事件
*
* @param userId userId
* @param machineId machineId
* @param fileToken fileToken
* @param status status
*/
public void notifySessionStatusEvent(Long userId, Long machineId, String fileToken, Integer status) {
// 清除进度
TRANSFER_PROGRESS.remove(fileToken);
// 通知
FileTransferNotifyDTO notify = new FileTransferNotifyDTO();
notify.setType(SftpNotifyType.CHANGE_STATUS.getType());
notify.setFileToken(fileToken);
notify.setBody(status);
this.notifySession(userId, machineId, notify);
}
/**
* 通知session
*
* @param userId userId
* @param machineId machineId
* @param notify notifyInfo
*/
public void notifySession(Long userId, Long machineId, FileTransferNotifyDTO notify) {
List<String> sessionIds = userMachineSessionMapping.get(this.getUserMachine(userId, machineId));
if (Lists.isEmpty(sessionIds)) {
return;
}
for (String sessionId : sessionIds) {
if (sessionId == null) {
continue;
}
WebSocketSession session = idMapping.get(sessionId);
if (session == null || !session.isOpen()) {
continue;
}
// 通知
Exception ex = null;
for (int i = 0; i < 3; i++) {
try {
session.sendMessage(new TextMessage(Jsons.toJsonWriteNull(notify)));
break;
} catch (Exception e) {
ex = e;
log.error("通知session失败 userId: {}, machineId: {} sessionId: {}, try: {}, e: {}", userId, machineId, session, (i + 1), Exceptions.getDigest(e));
Threads.sleep(Const.N_100);
}
}
if (ex != null) {
ex.printStackTrace();
}
}
}
/**
* 获取用户机器key
*
* @param userId userId
* @param machineId machineId
* @return key
*/
private String getUserMachine(Long userId, Long machineId) {
return userId + "_" + machineId;
}
/**
* 获取传输进度
*
* @param token token
* @return progress
*/
public Double getProgress(String token) {
String progress = TRANSFER_PROGRESS.get(token);
if (progress == null) {
return null;
}
return Double.valueOf(progress);
}
}

View File

@@ -0,0 +1,105 @@
package cn.orionsec.ops.handler.sftp.direct;
import cn.orionsec.kit.lang.able.SafeCloseable;
import cn.orionsec.kit.lang.utils.Valid;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.MessageConst;
import cn.orionsec.ops.handler.sftp.SftpSupport;
import cn.orionsec.ops.service.api.MachineEnvService;
import cn.orionsec.ops.service.api.MachineInfoService;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.InputStream;
@Slf4j
public class DirectDownloader implements SafeCloseable {
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
private static final MachineEnvService machineEnvService = SpringHolder.getBean(MachineEnvService.class);
/**
* 机器id
*/
private final Long machineId;
/**
* session
*/
private SessionStore session;
/**
* 执行器
*/
private SftpExecutor executor;
public DirectDownloader(Long machineId) {
this.machineId = machineId;
}
/**
* 打开连接
*
* @return this
*/
public DirectDownloader open() {
log.info("直接下载远程文件-建立连接-开始 machineId: {}", machineId);
try {
this.session = machineInfoService.openSessionStore(machineId);
log.info("直接下载远程文件-建立连接-成功 machineId: {}", machineId);
return this;
} catch (Exception e) {
log.error("直接下载远程文件-建立连接-失败 machineId: {}, e: {}", machineId, e);
throw e;
}
}
/**
* 获取文件
*
* @param path path
* @return 文件流
* @throws IOException IOException
*/
public InputStream getFile(String path) throws IOException {
log.info("直接下载远程文件-开始执行 machineId: {}, path: {}", machineId, path);
Valid.notNull(session, MessageConst.UNCONNECTED);
try {
// 打开执行器
String charset = machineEnvService.getSftpCharset(machineId);
this.executor = session.getSftpExecutor(charset);
executor.connect();
// 检查是否为本机
if (SftpSupport.checkUseFileSystem(executor)) {
// 是本机则返回文件流
return Files1.openInputStreamFast(path);
} else {
// 不是本机获取sftp文件
return executor.openInputStream(path);
}
} catch (IOException e) {
log.error("直接下载远程文件-执行失败 machineId: {}, path: {}, e: {}", machineId, path, e);
throw e;
}
}
/**
* 关闭执行器
*/
public void closeExecutor() {
Streams.close(executor);
}
@Override
public void close() {
Streams.close(executor);
Streams.close(session);
}
}

View File

@@ -0,0 +1,26 @@
package cn.orionsec.ops.handler.sftp.hint;
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
public class FilePackageHint extends FileTransferHint {
/**
* 压缩文件列表
*/
private List<FileTransferLogDO> fileList;
public FilePackageHint(FileTransferLogDO record, List<FileTransferLogDO> fileList) {
super(record);
this.fileList = fileList;
}
}

View File

@@ -0,0 +1,49 @@
package cn.orionsec.ops.handler.sftp.hint;
import cn.orionsec.ops.constant.sftp.SftpTransferType;
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
import lombok.Data;
import java.util.List;
@Data
public class FileTransferHint {
/**
* record
*/
private FileTransferLogDO record;
/**
* 类型
*/
private SftpTransferType type;
public FileTransferHint(FileTransferLogDO record) {
this.record = record;
this.type = SftpTransferType.of(record.getTransferType());
}
/**
* 获取上传配置
*
* @param record record
* @return 上传配置
*/
public static FileTransferHint transfer(FileTransferLogDO record) {
return new FileTransferHint(record);
}
/**
* 获取打包配置
*
* @param record record
* @param fileList fileList
* @return 上传配置
*/
public static FilePackageHint packaged(FileTransferLogDO record, List<FileTransferLogDO> fileList) {
return new FilePackageHint(record, fileList);
}
}

View File

@@ -0,0 +1,42 @@
package cn.orionsec.ops.handler.sftp.impl;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.net.host.sftp.transfer.SftpDownloader;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
import cn.orionsec.ops.handler.sftp.FileTransferProcessor;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DownloadFileProcessor extends FileTransferProcessor {
public DownloadFileProcessor(FileTransferLogDO record) {
super(record);
}
@Override
public void exec() {
String localFile = record.getLocalFile();
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
log.info("sftp文件下载-提交任务 fileToken: {}, machineId: {}, local: {}, remote: {}, record: {}",
fileToken, machineId, localAbsolutePath, record.getRemoteFile(), JSON.toJSONString(record));
Threads.start(this, SchedulerPools.SFTP_DOWNLOAD_SCHEDULER);
}
@Override
protected void handler() {
String remoteFile = record.getRemoteFile();
String localFile = record.getLocalFile();
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
log.info("sftp文件下载-开始传输 fileToken: {}, machineId: {}, local: {}, remote: {}",
fileToken, machineId, localAbsolutePath, remoteFile);
SftpDownloader download = executor.download(remoteFile, localAbsolutePath);
this.initProgress(download.getProgress());
download.run();
}
}

View File

@@ -0,0 +1,276 @@
package cn.orionsec.ops.handler.sftp.impl;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.io.compress.CompressTypeEnum;
import cn.orionsec.kit.lang.utils.io.compress.FileCompressor;
import cn.orionsec.kit.lang.utils.math.Numbers;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.sftp.SftpTransferStatus;
import cn.orionsec.ops.constant.sftp.SftpTransferType;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.dao.FileTransferLogDAO;
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
import cn.orionsec.ops.entity.dto.sftp.FileTransferNotifyProgressDTO;
import cn.orionsec.ops.handler.sftp.IFileTransferProcessor;
import cn.orionsec.ops.handler.sftp.TransferProcessorManager;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
public class PackageFileProcessor implements IFileTransferProcessor {
protected static FileTransferLogDAO fileTransferLogDAO = SpringHolder.getBean(FileTransferLogDAO.class);
protected static TransferProcessorManager transferProcessorManager = SpringHolder.getBean(TransferProcessorManager.class);
private static final String FINISH_PROGRESS = "100";
/**
* 打包文件
*/
private final FileTransferLogDO packageFile;
/**
* 文件列表
*/
private final List<FileTransferLogDO> fileList;
/**
* 文件名映射
*/
private Map<String, FileTransferLogDO> nameMapping;
/**
* 当前大小
*/
private final AtomicLong currentSize;
/**
* 文件总大小
*/
private final long totalSize;
/**
* 文件压缩器
*/
private FileCompressor compressor;
/**
* 压缩文件路径
*/
private String compressPath;
private final Long userId;
private final Long machineId;
private final String fileToken;
private volatile boolean userCancel;
private volatile boolean done;
public PackageFileProcessor(FileTransferLogDO packageFile, List<FileTransferLogDO> fileList) {
this.packageFile = packageFile;
this.fileList = fileList;
this.fileToken = packageFile.getFileToken();
this.currentSize = new AtomicLong();
this.totalSize = packageFile.getFileSize();
this.userId = packageFile.getUserId();
this.machineId = packageFile.getMachineId();
}
@Override
public void exec() {
String localFile = packageFile.getLocalFile();
this.compressPath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
log.info("sftp文件打包-提交任务 fileToken: {} machineId: {}, local: {}, remote: {}, record: {}, fileList: {}",
fileToken, machineId, compressPath, packageFile.getRemoteFile(),
JSON.toJSONString(packageFile), JSON.toJSONString(fileList));
Threads.start(this, SchedulerPools.SFTP_PACKAGE_SCHEDULER);
}
@Override
public void run() {
// 判断是否可以传输
FileTransferLogDO fileTransferLog = fileTransferLogDAO.selectById(packageFile.getId());
if (fileTransferLog == null || !SftpTransferStatus.WAIT.getStatus().equals(fileTransferLog.getTransferStatus())) {
return;
}
transferProcessorManager.addProcessor(fileToken, this);
try {
// 通知状态runnable
this.updateStatus(SftpTransferStatus.RUNNABLE);
// 初始化压缩器
this.compressor = CompressTypeEnum.ZIP.compressor().get();
compressor.setAbsoluteCompressPath(compressPath);
compressor.compressNotify(this::notifyProgress);
// 添加压缩文件
this.initCompressFiles();
// 添加压缩清单
this.initCompressFileRaw();
// 二次检查状态 防止在添加文件过程中取消或者删除
fileTransferLog = fileTransferLogDAO.selectById(packageFile.getId());
if (fileTransferLog == null || !SftpTransferStatus.RUNNABLE.getStatus().equals(fileTransferLog.getTransferStatus())) {
return;
}
// 开始压缩
this.compressor.compress();
// 传输完成通知
this.updateStatus(SftpTransferStatus.FINISH);
} catch (Exception e) {
log.error("sftp压缩文件-出现异常 fileToken: {}, e: {}, message: {}", fileToken, e.getClass().getName(), e.getMessage());
// 程序错误并非传输错误修改状态
if (!userCancel) {
log.error("sftp传输文件-运行异常 fileToken: {}", fileToken, e);
this.updateStatus(SftpTransferStatus.ERROR);
}
e.printStackTrace();
} finally {
this.done = true;
transferProcessorManager.removeProcessor(fileToken);
}
}
@Override
public void stop() {
log.info("sftp传输打包-用户取消 fileToken: {}", fileToken);
this.userCancel = true;
// 修改状态为已取消
this.updateStatus(SftpTransferStatus.CANCEL);
// 取消
if (compressor != null) {
Streams.close(compressor.getCloseable());
}
}
/**
* 初始化压缩文件
*/
private void initCompressFiles() {
this.nameMapping = Maps.newLinkedMap();
for (int i = 0; i < fileList.size(); i++) {
FileTransferLogDO fileLog = fileList.get(i);
String remoteFile = fileLog.getRemoteFile();
String localFilePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), fileLog.getLocalFile());
if (!Files1.isFile(new File(localFilePath))) {
continue;
}
// 添加mapping
String remoteFileName;
if (nameMapping.containsKey(remoteFile)) {
remoteFileName = remoteFile + "_" + (i + 1);
} else {
remoteFileName = remoteFile;
}
nameMapping.put(remoteFileName, fileLog);
compressor.addFile(remoteFileName, localFilePath);
}
}
/**
* 添加压缩文件清单到压缩列表
*/
private void initCompressFileRaw() {
// 设置文件清单
List<String> compressFileRaw = Lists.newList();
for (FileTransferLogDO fileLog : fileList) {
String remoteFile = fileLog.getRemoteFile();
String label = SftpTransferType.of(fileLog.getTransferType()).getLabel();
String localFilePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), fileLog.getLocalFile());
String status = Files1.isFile(localFilePath) ? "成功" : "未找到文件";
// 添加raw
compressFileRaw.add(label + Const.SPACE + status + Const.SPACE + remoteFile);
}
// 设置文件清单文件
String compressRawListFile = String.join(Const.LF, compressFileRaw) + Const.LF;
InputStream compressRawListStream = Streams.toInputStream(compressRawListFile);
compressor.addFile(Const.COMPRESS_LIST_FILE, compressRawListStream);
}
/**
* 通知状态
*
* @param status status
*/
private void updateStatus(SftpTransferStatus status) {
FileTransferLogDO update = new FileTransferLogDO();
update.setId(packageFile.getId());
update.setTransferStatus(status.getStatus());
if (SftpTransferStatus.FINISH.equals(status)) {
// 设置压缩文件实际大小
File compressFile = new File(compressPath);
if (Files1.isFile(compressFile)) {
update.setFileSize(compressFile.length());
update.setCurrentSize(compressFile.length());
} else {
update.setCurrentSize(packageFile.getFileSize());
}
update.setNowProgress(100D);
}
int effect = fileTransferLogDAO.updateById(update);
log.info("sftp传输压缩-更新状态 fileToken: {}, status: {}, effect: {}", fileToken, status, effect);
if (SftpTransferStatus.FINISH.equals(status)) {
// 通知进度
FileTransferNotifyProgressDTO notifyProgress = new FileTransferNotifyProgressDTO(Strings.EMPTY, Files1.getSize(totalSize), FINISH_PROGRESS);
transferProcessorManager.notifySessionProgressEvent(userId, machineId, fileToken, notifyProgress);
}
// 通知状态
transferProcessorManager.notifySessionStatusEvent(userId, machineId, fileToken, status.getStatus());
}
/**
* 通知进度
*
* @param name name
*/
private void notifyProgress(String name) {
if (done) {
return;
}
FileTransferLogDO compressedFile = nameMapping.get(name);
if (compressedFile == null) {
return;
}
// 计算进度
long curr = currentSize.addAndGet(compressedFile.getFileSize());
double progress = this.getProgress();
String progressRate = Numbers.setScale(progress, 2);
// 更新进度
FileTransferLogDO update = new FileTransferLogDO();
update.setId(packageFile.getId());
update.setCurrentSize(curr);
update.setNowProgress(progress);
fileTransferLogDAO.updateById(update);
// 通知进度
FileTransferNotifyProgressDTO notifyProgress = new FileTransferNotifyProgressDTO(Strings.EMPTY, Files1.getSize(curr), progressRate);
transferProcessorManager.notifySessionProgressEvent(userId, machineId, fileToken, notifyProgress);
}
/**
* 获取当前进度
*
* @return 当前进度
*/
protected double getProgress() {
if (totalSize == 0) {
return 0;
}
return ((double) currentSize.get() / (double) totalSize) * 100;
}
}

View File

@@ -0,0 +1,42 @@
package cn.orionsec.ops.handler.sftp.impl;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.net.host.sftp.transfer.SftpUploader;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
import cn.orionsec.ops.handler.sftp.FileTransferProcessor;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UploadFileProcessor extends FileTransferProcessor {
public UploadFileProcessor(FileTransferLogDO record) {
super(record);
}
@Override
public void exec() {
String localFile = record.getLocalFile();
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
log.info("sftp文件上传-提交任务 fileToken: {}, machineId: {}, local: {}, remote: {}, record: {}",
fileToken, machineId, localAbsolutePath, record.getRemoteFile(), JSON.toJSONString(record));
Threads.start(this, SchedulerPools.SFTP_UPLOAD_SCHEDULER);
}
@Override
protected void handler() {
String remoteFile = record.getRemoteFile();
String localFile = record.getLocalFile();
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
log.info("sftp文件上传-开始传输 fileToken: {}, machineId: {}, local: {}, remote: {}",
fileToken, machineId, localAbsolutePath, remoteFile);
SftpUploader upload = executor.upload(remoteFile, localAbsolutePath);
this.initProgress(upload.getProgress());
upload.run();
}
}

View File

@@ -0,0 +1,119 @@
package cn.orionsec.ops.handler.sftp.notify;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.ws.WsCloseCode;
import cn.orionsec.ops.entity.dto.sftp.SftpSessionTokenDTO;
import cn.orionsec.ops.entity.dto.user.UserDTO;
import cn.orionsec.ops.handler.sftp.TransferProcessorManager;
import cn.orionsec.ops.service.api.PassportService;
import cn.orionsec.ops.service.api.SftpService;
import cn.orionsec.ops.utils.WebSockets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@Component
@Slf4j
public class FileTransferNotifyHandler implements WebSocketHandler {
@Resource
private TransferProcessorManager transferProcessorManager;
@Resource
private PassportService passportService;
@Resource
private SftpService sftpService;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String id = session.getId();
String token = WebSockets.getToken(session);
try {
SftpSessionTokenDTO tokenInfo = sftpService.getTokenInfo(token);
Long userId = tokenInfo.getUserId();
Long machineId = tokenInfo.getMachineId();
List<Long> machineIdList = tokenInfo.getMachineIdList();
if (machineIdList == null) {
machineIdList = Lists.newList();
}
if (machineId != null) {
machineIdList.add(machineId);
}
session.getAttributes().put(WebSockets.UID, userId);
session.getAttributes().put(WebSockets.MID, machineIdList);
log.info("sftp-Notify 建立连接成功 id: {}, token: {}, userId: {}, machineId: {}, machineIdList: {}", id, token, userId, machineId, machineIdList);
} catch (Exception e) {
log.error("sftp-Notify 建立连接失败-未查询到token信息 id: {}, token: {}", id, token, e);
WebSockets.close(session, WsCloseCode.FORGE_TOKEN);
}
}
@Override
@SuppressWarnings("unchecked")
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
String id = session.getId();
Map<String, Object> attributes = session.getAttributes();
if (attributes.get(WebSockets.AUTHED) != null) {
return;
}
if (!(message instanceof TextMessage)) {
return;
}
// 获取body
String authToken = ((TextMessage) message).getPayload();
if (Strings.isEmpty(authToken)) {
log.info("sftp-Notify 认证失败-body为空 id: {}", id);
WebSockets.close(session, WsCloseCode.INCORRECT_TOKEN);
return;
}
// 获取认证用户
UserDTO user = passportService.getUserByToken(authToken, null);
if (user == null) {
log.info("sftp-Notify 认证失败-未查询到用户 id: {}, authToken: {}", id, authToken);
WebSockets.close(session, WsCloseCode.INCORRECT_TOKEN);
return;
}
// 检查认证用户是否匹配
Long userId = user.getId();
Long tokenUserId = (Long) attributes.get(WebSockets.UID);
final boolean valid = userId.equals(tokenUserId);
if (!valid) {
log.info("sftp-Notify 认证失败-用户不匹配 id: {}, userId: {}, tokenUserId: {}", id, userId, tokenUserId);
WebSockets.close(session, WsCloseCode.VALID);
return;
}
attributes.put(WebSockets.AUTHED, Const.ENABLE);
// 注册会话
List<Long> machineIdList = (List<Long>) attributes.get(WebSockets.MID);
Lists.forEach(machineIdList, i -> {
log.info("sftp-Notify 认证成功 id: {}, userId: {}, machineId: {}", id, userId, i);
transferProcessorManager.registerSessionNotify(id, session, userId, i);
});
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("sftp-Notify 操作异常拦截 id: {}", session.getId());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String id = session.getId();
transferProcessorManager.closeSessionNotify(id);
log.info("sftp-Notify 关闭连接 id: {}, code: {}, reason: {}", id, status.getCode(), status.getReason());
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}

View File

@@ -0,0 +1,66 @@
package cn.orionsec.ops.handler.tail;
import cn.orionsec.kit.lang.able.SafeCloseable;
import cn.orionsec.kit.lang.function.select.Branches;
import cn.orionsec.kit.lang.function.select.Selector;
import cn.orionsec.ops.constant.tail.FileTailMode;
import cn.orionsec.ops.handler.tail.impl.ExecTailFileHandler;
import cn.orionsec.ops.handler.tail.impl.TrackerTailFileHandler;
import org.springframework.web.socket.WebSocketSession;
public interface ITailHandler extends SafeCloseable {
/**
* 开始
*
* @throws Exception Exception
*/
void start() throws Exception;
/**
* 获取机器id
*
* @return 机器id
*/
Long getMachineId();
/**
* 获取文件路径
*
* @return 文件路径
*/
String getFilePath();
/**
* 设置最后修改时间
*/
default void setLastModify() {
}
/**
* 写入命令
*
* @param command command
*/
default void write(String command) {
}
/**
* 获取实际执行 handler
*
* @param mode mode
* @param hint hint
* @param session session
* @return handler
*/
static ITailHandler with(FileTailMode mode, TailFileHint hint, WebSocketSession session) {
return Selector.<FileTailMode, ITailHandler>of(mode)
.test(Branches.eq(FileTailMode.TRACKER)
.then(() -> new TrackerTailFileHandler(hint, session)))
.test(Branches.eq(FileTailMode.TAIL)
.then(() -> new ExecTailFileHandler(hint, session)))
.get();
}
}

View File

@@ -0,0 +1,94 @@
package cn.orionsec.ops.handler.tail;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.constant.tail.FileTailMode;
import cn.orionsec.ops.entity.dto.file.FileTailDTO;
import cn.orionsec.ops.utils.WebSockets;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import javax.annotation.Resource;
@Component
@Slf4j
public class TailFileHandler implements WebSocketHandler {
@Resource
private TailSessionHolder tailSessionHolder;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String id = session.getId();
FileTailDTO config = (FileTailDTO) session.getAttributes().get(WebSockets.CONFIG);
String token = (String) session.getAttributes().get(WebSockets.TOKEN);
log.info("tail 建立ws连接 token: {}, id: {}, config: {}", token, id, JSON.toJSONString(config));
try {
this.openTailHandler(session, config, token);
} catch (Exception e) {
log.error("tail 打开处理器-失败 id: {}", id, e);
}
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("tail 操作异常拦截 token: {}, id: {}", WebSockets.getToken(session), session.getId(), exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String token = (String) session.getAttributes().get(WebSockets.TOKEN);
// 释放资源
ITailHandler handler = tailSessionHolder.removeSession(token);
if (handler == null) {
return;
}
handler.close();
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 打开文件处理器
*
* @param session session
* @param fileTail tailDTO
* @param token token
* @throws Exception Exception
*/
private void openTailHandler(WebSocketSession session, FileTailDTO fileTail, String token) throws Exception {
TailFileHint hint = new TailFileHint();
hint.setToken(token);
hint.setMachineId(fileTail.getMachineId());
hint.setPath(fileTail.getFilePath());
hint.setOffset(fileTail.getOffset());
hint.setCharset(fileTail.getCharset());
hint.setCommand(fileTail.getCommand());
FileTailMode mode = FileTailMode.of(fileTail.getMode());
if (FileTailMode.TRACKER.equals(mode)) {
// 获取 delay
String delayValue = SystemEnvAttr.TRACKER_DELAY_TIME.getValue();
int delay = Strings.isInteger(delayValue) ? Integer.parseInt(delayValue) : Const.TRACKER_DELAY_MS;
hint.setDelay(Math.max(delay, Const.MIN_TRACKER_DELAY_MS));
}
log.info("tail 打开处理器-开始 token: {}, mode: {}, hint: {}", token, mode, JSON.toJSONString(hint));
ITailHandler handler = ITailHandler.with(mode, hint, session);
tailSessionHolder.addSession(token, handler);
handler.start();
}
}

View File

@@ -0,0 +1,44 @@
package cn.orionsec.ops.handler.tail;
import lombok.Data;
@Data
public class TailFileHint {
/**
* token
*/
private String token;
/**
* 文件
*/
private String path;
/**
* 机器id
*/
private Long machineId;
/**
* 尾行偏移量
*/
private Integer offset;
/**
* 编码格式
*/
private String charset;
/**
* tail 命令
*/
private String command;
/**
* 延迟时间
*/
private int delay;
}

View File

@@ -0,0 +1,105 @@
package cn.orionsec.ops.handler.tail;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.ops.constant.Const;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@Component
public class TailSessionHolder {
/**
* key: token
* value: ITailHandler
*/
private final Map<String, ITailHandler> holder = Maps.newCurrentHashMap();
/**
* key: machineId:filePath
* value: token
*/
private final Map<String, List<String>> fileTokenMapping = Maps.newCurrentHashMap();
/**
* 添加session
*
* @param token token
* @param session session
*/
public void addSession(String token, ITailHandler session) {
holder.put(token, session);
fileTokenMapping.computeIfAbsent(session.getMachineId() + ":" + session.getFilePath(), s -> Lists.newList()).add(token);
}
/**
* 获取session
*
* @param token token
* @return session
*/
public ITailHandler getSession(String token) {
return holder.get(token);
}
/**
* 获取session
*
* @param machineId machineId
* @param path path
* @return session
*/
public List<ITailHandler> getSession(Long machineId, String path) {
List<String> tokenList = fileTokenMapping.get(machineId + ":" + path);
if (Lists.isEmpty(tokenList)) {
return Lists.empty();
}
return tokenList.stream()
.map(holder::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 删除session
*
* @param token token
* @return session
*/
public ITailHandler removeSession(String token) {
ITailHandler handler = holder.remove(token);
if (handler != null) {
fileTokenMapping.remove(handler.getMachineId() + ":" + handler.getFilePath());
}
return handler;
}
/**
* 异步关闭进行中的 tail
*
* @param machineId machineId
* @param path path
*/
public void asyncCloseTailFile(Long machineId, String path) {
Threads.start(() -> {
try {
Threads.sleep(Const.MS_S_1);
this.getSession(machineId, path).forEach(ITailHandler::setLastModify);
Threads.sleep(Const.MS_S_5);
this.getSession(machineId, path).forEach(ITailHandler::close);
} catch (Exception e) {
log.error("关闭tailingFile失败 machineId: {}, path: {}", machineId, path, e);
}
});
}
}

View File

@@ -0,0 +1,166 @@
package cn.orionsec.ops.handler.tail.impl;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.ws.WsCloseCode;
import cn.orionsec.ops.handler.tail.ITailHandler;
import cn.orionsec.ops.handler.tail.TailFileHint;
import cn.orionsec.ops.service.api.MachineInfoService;
import cn.orionsec.ops.utils.WebSockets;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.InputStream;
@Slf4j
public class ExecTailFileHandler implements ITailHandler {
protected static MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
@Getter
private final String token;
/**
* session
*/
private final WebSocketSession session;
/**
* hint
*/
private final TailFileHint hint;
private SessionStore sessionStore;
private CommandExecutor executor;
private volatile boolean close;
public ExecTailFileHandler(TailFileHint hint, WebSocketSession session) {
this.token = hint.getToken();
this.hint = hint;
this.session = session;
log.info("tail EXEC_TAIL 监听文件初始化 token: {}, hint: {}", token, JSON.toJSONString(hint));
}
@Override
public void start() throws Exception {
try {
// 打开session
this.sessionStore = machineInfoService.openSessionStore(hint.getMachineId());
log.info("tail 建立连接成功 machineId: {}", hint.getMachineId());
} catch (Exception e) {
WebSockets.openSessionStoreThrowClose(session, e);
log.error("tail 建立连接失败-连接远程服务器失败 e: {}, machineId: {}", e, hint.getMachineId());
return;
}
// 打开 command
this.executor = sessionStore.getCommandExecutor(Strings.replaceCRLF(hint.getCommand()));
executor.merge();
executor.callback(this::callback);
executor.streamHandler(this::handler);
executor.connect();
SchedulerPools.TAIL_SCHEDULER.execute(executor);
log.info("tail EXEC_TAIL 监听文件开始 token: {}", token);
}
@Override
public void write(String command) {
executor.write(command);
}
@Override
public Long getMachineId() {
return hint.getMachineId();
}
@Override
public String getFilePath() {
return hint.getPath();
}
/**
* 回调
*/
private void callback() {
log.info("tail EXEC_TAIL 监听文件结束 token: {}", token);
WebSockets.close(session, WsCloseCode.EOF);
}
/**
* 处理标准输入流
*
* @param input 流
*/
@SneakyThrows
private void handler(InputStream input) {
byte[] buffer = new byte[Const.BUFFER_KB_8];
int read;
while ((read = input.read(buffer)) != -1) {
if (session.isOpen()) {
session.sendMessage(new BinaryMessage(buffer, 0, read, true));
}
}
// 2.0 tail 换行有问题
// BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, hint.getCharset()), Const.BUFFER_KB_8);
// String line;
// while ((line = reader.readLine()) != null) {
// session.sendMessage(new TextMessage(line));
// }
// 1.0 tracker 换行有问题
// char[] buffer = new char[Const.BUFFER_KB_4];
// int read;
// StringBuilder sb = new StringBuilder();
// // tail命令结合BufferedReader对CR处理有问题 所以不能使用readLine
// while ((read = reader.read(buffer)) != -1) {
// int mark = -1;
// for (int i = 0; i < read; i++) {
// // 读取到行结尾
// if (buffer[i] == Letters.LF) {
// sb.append(buffer, mark + 1, i - mark - 1);
// if (session.isOpen()) {
// String payload = sb.toString().replaceAll(Const.CR, Const.EMPTY);
// session.sendMessage(new TextMessage(payload));
// }
// sb = new StringBuilder();
// mark = i;
// }
// }
// // 不是完整的一行
// if (mark == -1) {
// sb.append(buffer, 0, read);
// } else if (mark != read - 1) {
// sb.append(buffer, mark + 1, read - mark - 1);
// }
// }
}
@Override
@SneakyThrows
public void close() {
if (close) {
return;
}
this.close = true;
Streams.close(executor);
Streams.close(sessionStore);
}
@Override
public String toString() {
return hint.getPath();
}
}

View File

@@ -0,0 +1,112 @@
package cn.orionsec.ops.handler.tail.impl;
import cn.orionsec.kit.ext.tail.Tracker;
import cn.orionsec.kit.ext.tail.delay.DelayTrackerListener;
import cn.orionsec.kit.ext.tail.handler.DataHandler;
import cn.orionsec.kit.ext.tail.mode.FileNotFoundMode;
import cn.orionsec.kit.ext.tail.mode.FileOffsetMode;
import cn.orionsec.kit.lang.define.thread.HookRunnable;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.ws.WsCloseCode;
import cn.orionsec.ops.handler.tail.ITailHandler;
import cn.orionsec.ops.handler.tail.TailFileHint;
import cn.orionsec.ops.utils.WebSockets;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.WebSocketSession;
@Slf4j
public class TrackerTailFileHandler implements ITailHandler, DataHandler {
@Getter
private final String token;
/**
* session
*/
private final WebSocketSession session;
/**
* hint
*/
private final TailFileHint hint;
private DelayTrackerListener tracker;
private volatile boolean close;
@Getter
private final String filePath;
public TrackerTailFileHandler(TailFileHint hint, WebSocketSession session) {
this.token = hint.getToken();
this.hint = hint;
this.filePath = hint.getPath();
this.session = session;
log.info("tail TRACKER 监听文件初始化 token: {}, hint: {}", token, JSON.toJSONString(hint));
}
@Override
public void start() {
this.tracker = new DelayTrackerListener(filePath, this);
tracker.delayMillis(hint.getDelay());
tracker.charset(hint.getCharset());
tracker.offset(FileOffsetMode.LINE, hint.getOffset());
tracker.notFoundMode(FileNotFoundMode.WAIT_COUNT, 10);
Threads.start(new HookRunnable(() -> {
log.info("tail TRACKER 开始监听文件 token: {}", token);
tracker.tail();
}, this::callback), SchedulerPools.TAIL_SCHEDULER);
}
@Override
public void setLastModify() {
tracker.setFileLastModifyTime();
}
@Override
public Long getMachineId() {
return Const.HOST_MACHINE_ID;
}
/**
* 回调
*/
@SneakyThrows
private void callback() {
log.info("tail TRACKER 监听文件结束 token: {}", token);
WebSockets.close(session, WsCloseCode.EOF);
}
@SneakyThrows
@Override
public void read(byte[] bytes, int len, Tracker tracker) {
if (session.isOpen()) {
session.sendMessage(new BinaryMessage(bytes, 0, len, true));
}
}
@Override
@SneakyThrows
public void close() {
if (close) {
return;
}
this.close = true;
if (tracker != null) {
tracker.stop();
}
}
@Override
public String toString() {
return filePath;
}
}

View File

@@ -0,0 +1,59 @@
package cn.orionsec.ops.handler.terminal;
import cn.orionsec.kit.lang.able.SafeCloseable;
import cn.orionsec.ops.constant.terminal.TerminalClientOperate;
import cn.orionsec.ops.entity.config.TerminalConnectConfig;
import cn.orionsec.ops.handler.terminal.manager.TerminalManagementHandler;
import cn.orionsec.ops.handler.terminal.watcher.ITerminalWatcherProcessor;
public interface IOperateHandler extends TerminalManagementHandler, SafeCloseable {
/**
* 建立连接
*/
void connect();
/**
* 断开连接
*/
void disconnect();
/**
* 处理消息
*
* @param operate 操作
* @param body body
* @throws Exception ex
*/
void handleMessage(TerminalClientOperate operate, String body) throws Exception;
/**
* 心跳是否结束
*
* @return true结束
*/
boolean isDown();
/**
* 获取token
*
* @return token
*/
String getToken();
/**
* 获取终端配置
*
* @return 终端配置
*/
TerminalConnectConfig getHint();
/**
* 获取监视器
*
* @return processor
*/
ITerminalWatcherProcessor getWatcher();
}

View File

@@ -0,0 +1,192 @@
package cn.orionsec.ops.handler.terminal;
import cn.orionsec.kit.lang.define.wrapper.Tuple;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.ops.constant.terminal.TerminalClientOperate;
import cn.orionsec.ops.constant.ws.WsCloseCode;
import cn.orionsec.ops.constant.ws.WsProtocol;
import cn.orionsec.ops.entity.config.TerminalConnectConfig;
import cn.orionsec.ops.entity.domain.MachineInfoDO;
import cn.orionsec.ops.entity.domain.MachineTerminalLogDO;
import cn.orionsec.ops.entity.dto.terminal.TerminalConnectDTO;
import cn.orionsec.ops.entity.dto.user.UserDTO;
import cn.orionsec.ops.handler.terminal.manager.TerminalSessionManager;
import cn.orionsec.ops.service.api.MachineInfoService;
import cn.orionsec.ops.service.api.MachineTerminalService;
import cn.orionsec.ops.service.api.PassportService;
import cn.orionsec.ops.utils.WebSockets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import javax.annotation.Resource;
import java.util.Date;
@Slf4j
@Component("terminalMessageHandler")
public class TerminalMessageHandler implements WebSocketHandler {
@Resource
private TerminalSessionManager terminalSessionManager;
@Resource
private PassportService passportService;
@Resource
private MachineInfoService machineInfoService;
@Resource
private MachineTerminalService machineTerminalService;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("terminal 已建立连接 token: {}, id: {}, params: {}", WebSockets.getToken(session), session.getId(), session.getAttributes());
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
if (!(message instanceof TextMessage)) {
return;
}
String token = session.getId();
try {
// 解析请求
Tuple tuple = WebSockets.parsePayload(((TextMessage) message).getPayload());
if (tuple == null) {
WebSockets.sendText(session, WsProtocol.ERROR.get());
return;
}
TerminalClientOperate operate = tuple.get(0);
String body = tuple.get(1);
// 建立连接
if (operate == TerminalClientOperate.CONNECT) {
// 建立连接
if (session.getAttributes().get(WebSockets.CONNECTED) != null) {
return;
}
this.connect(session, token, body);
return;
}
// 检查连接
if (session.getAttributes().get(WebSockets.CONNECTED) == null) {
WebSockets.close(session, WsCloseCode.VALID);
return;
}
// 获取连接
IOperateHandler handler = terminalSessionManager.getSession(token);
if (handler == null) {
WebSockets.close(session, WsCloseCode.UNKNOWN_CONNECT);
return;
}
// 操作
handler.handleMessage(operate, body);
} catch (Exception e) {
log.error("terminal 处理操作异常 token: {}", token, e);
WebSockets.close(session, WsCloseCode.RUNTIME_EXCEPTION);
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("terminal 操作异常拦截 token: {}", session.getId(), exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String token = session.getId();
int code = status.getCode();
log.info("terminal 关闭连接 token: {}, code: {}, reason: {}", token, code, status.getReason());
// 释放资源
IOperateHandler handler = terminalSessionManager.removeSession(token);
if (handler == null) {
return;
}
handler.close();
// 修改日志
MachineTerminalLogDO updateLog = new MachineTerminalLogDO();
updateLog.setCloseCode(code);
updateLog.setDisconnectedTime(new Date());
Integer effect = machineTerminalService.updateAccessLog(token, updateLog);
log.info("terminal 连接关闭更新日志 token: {}, effect: {}", token, effect);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 建立连接
*
* @param session session
* @param token token
* @param body body
*/
private void connect(WebSocketSession session, String token, String body) {
log.info("terminal 尝试建立连接 token: {}, body: {}", token, body);
// 检查参数
TerminalConnectDTO connectInfo = TerminalUtils.parseConnectBody(body);
if (connectInfo == null) {
WebSockets.sendText(session, WsProtocol.ERROR.get());
return;
}
Long userId = (Long) session.getAttributes().get(WebSockets.UID);
Long machineId = (Long) session.getAttributes().get(WebSockets.MID);
// 获取登录用户
UserDTO userDTO = passportService.getUserByToken(connectInfo.getLoginToken(), null);
if (userDTO == null || !userId.equals(userDTO.getId())) {
log.info("terminal 建立连接拒绝-用户认证失败 token: {}", token);
WebSockets.close(session, WsCloseCode.IDENTITY_MISMATCH);
return;
}
// 获取机器信息
MachineInfoDO machine = machineInfoService.selectById(machineId);
if (machine == null) {
log.info("terminal 建立连接拒绝-未查询到机器信息 token: {}, machineId: {}", token, machineId);
WebSockets.close(session, WsCloseCode.INVALID_MACHINE);
return;
}
session.getAttributes().put(WebSockets.CONNECTED, 1);
// 建立连接
SessionStore sessionStore;
try {
// 打开session
sessionStore = machineInfoService.openSessionStore(machine);
WebSockets.sendText(session, WsProtocol.CONNECTED.get());
} catch (Exception e) {
WebSockets.openSessionStoreThrowClose(session, e);
log.error("terminal 建立连接失败-连接远程服务器失败 uid: {}, machineId: {}", userId, machineId, e);
return;
}
// 配置
TerminalConnectConfig hint = new TerminalConnectConfig();
String terminalType = machineTerminalService.getMachineConfig(machineId).getTerminalType();
hint.setUserId(userId);
hint.setUsername(userDTO.getUsername());
hint.setMachineId(machineId);
hint.setMachineName(machine.getMachineName());
hint.setMachineHost(machine.getMachineHost());
hint.setMachineTag(machine.getMachineTag());
hint.setCols(connectInfo.getCols());
hint.setRows(connectInfo.getRows());
hint.setTerminalType(terminalType);
TerminalOperateHandler terminalHandler = new TerminalOperateHandler(token, hint, session, sessionStore);
try {
// 打开shell
log.info("terminal 尝试建立连接-尝试打开shell token: {}", token);
terminalHandler.connect();
log.info("terminal 建立连接成功-打开shell成功 token: {}", token);
} catch (Exception e) {
WebSockets.close(session, WsCloseCode.OPEN_SHELL_EXCEPTION);
log.error("terminal 建立连接失败-打开shell失败 machineId: {}, uid: {}", machineId, userId, e);
return;
}
terminalSessionManager.addSession(token, terminalHandler);
log.info("terminal 建立连接成功 uid: {}, machineId: {}", userId, machineId);
}
}

View File

@@ -0,0 +1,290 @@
package cn.orionsec.ops.handler.terminal;
import cn.orionsec.kit.lang.constant.Letters;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.io.Files1;
import cn.orionsec.kit.lang.utils.io.Streams;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.net.host.SessionStore;
import cn.orionsec.kit.net.host.ssh.shell.ShellExecutor;
import cn.orionsec.kit.spring.SpringHolder;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.constant.terminal.TerminalClientOperate;
import cn.orionsec.ops.constant.terminal.TerminalConst;
import cn.orionsec.ops.constant.ws.WsCloseCode;
import cn.orionsec.ops.constant.ws.WsProtocol;
import cn.orionsec.ops.entity.config.TerminalConnectConfig;
import cn.orionsec.ops.entity.domain.MachineTerminalLogDO;
import cn.orionsec.ops.entity.dto.terminal.TerminalSizeDTO;
import cn.orionsec.ops.handler.terminal.screen.TerminalScreenEnv;
import cn.orionsec.ops.handler.terminal.screen.TerminalScreenHeader;
import cn.orionsec.ops.handler.terminal.watcher.ITerminalWatcherProcessor;
import cn.orionsec.ops.handler.terminal.watcher.TerminalWatcherProcessor;
import cn.orionsec.ops.service.api.MachineTerminalService;
import cn.orionsec.ops.utils.PathBuilders;
import cn.orionsec.ops.utils.Utils;
import cn.orionsec.ops.utils.WebSockets;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.WebSocketSession;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
@Slf4j
public class TerminalOperateHandler implements IOperateHandler {
private static final MachineTerminalService machineTerminalService = SpringHolder.getBean(MachineTerminalService.class);
private static final String SCREEN_BODY_TEMPLATE = "[{}, \"o\", \"{}\"]";
@Getter
private final String token;
@Getter
private final TerminalConnectConfig hint;
@Getter
private final ITerminalWatcherProcessor watcher;
private final WebSocketSession session;
private final SessionStore sessionStore;
private ShellExecutor executor;
private long connectedTime;
/**
* 录屏流
*/
private OutputStream screenStream;
/**
* 最后一次心跳通讯时间
*/
private volatile long lastHeartbeat;
protected volatile boolean close;
public TerminalOperateHandler(String token, TerminalConnectConfig hint, WebSocketSession session, SessionStore sessionStore) {
this.token = token;
this.hint = hint;
this.watcher = new TerminalWatcherProcessor();
this.session = session;
this.sessionStore = sessionStore;
this.lastHeartbeat = System.currentTimeMillis();
this.initShell();
}
@Override
public void connect() {
executor.connect();
executor.streamHandler(this::streamHandler);
// 连接成功后初始化日志信息
this.initLog();
// 开始监听输出
SchedulerPools.TERMINAL_SCHEDULER.execute(executor);
watcher.watch();
}
/**
* 初始化 shell
*/
private void initShell() {
// 初始化 shell 执行器
this.executor = sessionStore.getShellExecutor();
executor.terminalType(hint.getTerminalType());
executor.size(hint.getCols(), hint.getRows());
}
/**
* 初始化日志
*/
private void initLog() {
this.connectedTime = System.currentTimeMillis();
hint.setConnectedTime(new Date(connectedTime));
// 初始化录屏
String screenPath = this.initScreenStream();
log.info("terminal 开始记录用户操作录屏: {} {}", token, screenPath);
// 记录日志
MachineTerminalLogDO logEntity = new MachineTerminalLogDO();
logEntity.setAccessToken(token);
logEntity.setUserId(hint.getUserId());
logEntity.setUsername(hint.getUsername());
logEntity.setMachineId(hint.getMachineId());
logEntity.setMachineName(hint.getMachineName());
logEntity.setMachineTag(hint.getMachineTag());
logEntity.setMachineHost(hint.getMachineHost());
logEntity.setConnectedTime(hint.getConnectedTime());
logEntity.setScreenPath(screenPath);
Long logId = machineTerminalService.addTerminalLog(logEntity);
hint.setLogId(logId);
log.info("terminal 保存用户操作日志: {} logId: {}", token, logId);
}
/**
* 初始化录屏流
*
* @return path
*/
@SneakyThrows
private String initScreenStream() {
// 初始化流
String screenPath = PathBuilders.getTerminalScreenPath(hint.getUserId(), hint.getMachineId());
String realScreenPath = Files1.getPath(SystemEnvAttr.SCREEN_PATH.getValue(), screenPath);
this.screenStream = Files1.openOutputStreamFastSafe(realScreenPath);
// 设置头
TerminalScreenHeader header = new TerminalScreenHeader();
String title = Strings.format("{}({}) {} {}", hint.getMachineName(), hint.getMachineHost(),
hint.getUsername(), Dates.format(hint.getConnectedTime()));
header.setTitle(title);
header.setCols(hint.getCols());
header.setRows(hint.getRows());
header.setTimestamp(connectedTime / Dates.SECOND_STAMP);
header.setEnv(new TerminalScreenEnv(hint.getTerminalType()));
// 拼接头
screenStream.write(JSON.toJSONBytes(header));
screenStream.write(Letters.LF);
screenStream.flush();
return screenPath;
}
/**
* 标准输出处理
*
* @param inputStream stream
*/
private void streamHandler(InputStream inputStream) {
byte[] bs = new byte[Const.BUFFER_KB_4];
BufferedInputStream in = new BufferedInputStream(inputStream, Const.BUFFER_KB_4);
int read;
try {
while (session.isOpen() && (read = in.read(bs)) != -1) {
// 响应
byte[] msg = WsProtocol.OK.msg(bs, 0, read);
WebSockets.sendText(session, msg);
// 响应监视
watcher.sendMessage(msg);
// 记录录屏
String row = Strings.format(SCREEN_BODY_TEMPLATE,
((double) (System.currentTimeMillis() - connectedTime)) / Dates.SECOND_STAMP,
Utils.convertControlUnicode(new String(bs, 0, read)));
screenStream.write(Strings.bytes(row));
screenStream.write(Letters.LF);
screenStream.flush();
}
} catch (IOException ex) {
log.error("terminal 读取流失败", ex);
WebSockets.close(session, WsCloseCode.READ_EXCEPTION);
}
// eof
if (close) {
return;
}
WebSockets.close(session, WsCloseCode.EOF);
log.info("terminal eof回调 {}", token);
}
@Override
public void disconnect() {
if (close) {
return;
}
this.close = true;
try {
Streams.close(screenStream);
Streams.close(executor);
Streams.close(sessionStore);
} catch (Exception e) {
log.error("terminal 断开连接 失败 token: {}", token, e);
}
}
@Override
public void forcedOffline() {
WebSockets.close(session, WsCloseCode.FORCED_OFFLINE);
log.info("terminal 管理员强制断连 {}", token);
}
@Override
public void heartbeatDownClose() {
WebSockets.close(session, WsCloseCode.HEART_DOWN);
log.info("terminal 心跳结束断连 {}", token);
}
@Override
public void sendHeartbeat() {
WebSockets.sendText(session, WsProtocol.PING.get());
}
@Override
public boolean isDown() {
return (System.currentTimeMillis() - lastHeartbeat) > TerminalConst.TERMINAL_CONNECT_DOWN;
}
@Override
public void handleMessage(TerminalClientOperate operate, String body) {
if (close) {
return;
}
switch (operate) {
case KEY:
executor.write(Strings.bytes(body));
return;
case PING:
// client 主动发送 ping
this.lastHeartbeat = System.currentTimeMillis();
WebSockets.sendText(session, WsProtocol.PONG.get());
return;
case PONG:
// server 主动发送 ping, client 响应 pong
this.lastHeartbeat = System.currentTimeMillis();
return;
case RESIZE:
this.resize(body);
return;
case COMMAND:
executor.write(Strings.bytes(body));
executor.write(new byte[]{Letters.LF});
case CLEAR:
executor.write(new byte[]{12});
return;
default:
}
}
@Override
public void close() {
this.disconnect();
Streams.close(watcher);
}
/**
* 重置大小
*/
private void resize(String body) {
// 检查参数
TerminalSizeDTO window = TerminalUtils.parseResizeBody(body);
if (window == null) {
WebSockets.sendText(session, WsProtocol.ERROR.get());
return;
}
hint.setCols(window.getCols());
hint.setRows(window.getRows());
if (!executor.isConnected()) {
executor.connect();
}
executor.size(window.getCols(), window.getRows());
executor.resize();
}
}

View File

@@ -0,0 +1,60 @@
package cn.orionsec.ops.handler.terminal;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.ops.entity.dto.terminal.TerminalConnectDTO;
import cn.orionsec.ops.entity.dto.terminal.TerminalSizeDTO;
public class TerminalUtils {
private TerminalUtils() {
}
/**
* 解析连接参数
* <p>
* .e.g cols|rows|loginToken
*
* @param body body
* @return connect
*/
public static TerminalConnectDTO parseConnectBody(String body) {
String[] arr = body.split("\\|");
if (arr.length != 3) {
return null;
}
// 解析 size
if (!Strings.isInteger(arr[0]) || !Strings.isInteger(arr[1])) {
return null;
}
TerminalConnectDTO connect = new TerminalConnectDTO();
connect.setCols(Integer.parseInt(arr[0]));
connect.setRows(Integer.parseInt(arr[1]));
connect.setLoginToken(arr[2]);
return connect;
}
/**
* 解析修改大小参数
* <p>
* .e.g cols|rows
*
* @param body body
* @return size
*/
public static TerminalSizeDTO parseResizeBody(String body) {
String[] arr = body.split("\\|");
if (arr.length != 2) {
return null;
}
// 解析 size
if (!Strings.isInteger(arr[0]) || !Strings.isInteger(arr[1])) {
return null;
}
TerminalSizeDTO size = new TerminalSizeDTO();
size.setCols(Integer.parseInt(arr[0]));
size.setRows(Integer.parseInt(arr[1]));
return size;
}
}

View File

@@ -0,0 +1,25 @@
package cn.orionsec.ops.handler.terminal.manager;
public interface TerminalManagementHandler {
/**
* 管理员强制下线
*
* @throws Exception Exception
*/
void forcedOffline() throws Exception;
/**
* 心跳结束下线
*
* @throws Exception Exception
*/
void heartbeatDownClose() throws Exception;
/**
* 主动发送心跳
*/
void sendHeartbeat();
}

View File

@@ -0,0 +1,167 @@
package cn.orionsec.ops.handler.terminal.manager;
import cn.orionsec.kit.lang.define.wrapper.DataGrid;
import cn.orionsec.kit.lang.id.UUIds;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.Valid;
import cn.orionsec.kit.lang.utils.collect.Lists;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.kit.lang.utils.convert.Converts;
import cn.orionsec.kit.lang.utils.time.DateRanges;
import cn.orionsec.ops.constant.KeyConst;
import cn.orionsec.ops.constant.MessageConst;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.entity.config.TerminalConnectConfig;
import cn.orionsec.ops.entity.dto.terminal.TerminalWatcherDTO;
import cn.orionsec.ops.entity.request.machine.MachineTerminalManagerRequest;
import cn.orionsec.ops.entity.vo.machine.MachineTerminalManagerVO;
import cn.orionsec.ops.entity.vo.machine.TerminalWatcherVO;
import cn.orionsec.ops.handler.terminal.IOperateHandler;
import cn.orionsec.ops.utils.Currents;
import cn.orionsec.ops.utils.EventParamsHolder;
import com.alibaba.fastjson.JSON;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
public class TerminalSessionManager {
@Resource
private RedisTemplate<String, String> redisTemplate;
/**
* 会话 sessionId:handler
*/
private final Map<String, IOperateHandler> sessionHolder = Maps.newCurrentHashMap();
/**
* session 列表
*
* @param request request
* @return dataGrid
*/
public DataGrid<MachineTerminalManagerVO> getOnlineTerminal(MachineTerminalManagerRequest request) {
List<MachineTerminalManagerVO> sessionList = sessionHolder.values()
.stream()
.filter(s -> Optional.ofNullable(request.getToken())
.filter(Strings::isNotBlank)
.map(t -> s.getToken().contains(t))
.orElse(true))
.filter(s -> Optional.ofNullable(request.getMachineName())
.filter(Strings::isNotBlank)
.map(t -> s.getHint().getMachineName().toLowerCase().contains(t.toLowerCase()))
.orElse(true))
.filter(s -> Optional.ofNullable(request.getMachineTag())
.filter(Strings::isNotBlank)
.map(t -> s.getHint().getMachineTag().contains(t))
.orElse(true))
.filter(s -> Optional.ofNullable(request.getMachineHost())
.filter(Strings::isNotBlank)
.map(t -> s.getHint().getMachineHost().contains(t))
.orElse(true))
.filter(s -> Optional.ofNullable(request.getUserId())
.map(t -> s.getHint().getUserId().equals(t))
.orElse(true))
.filter(s -> Optional.ofNullable(request.getUsername())
.map(t -> s.getHint().getUsername().toLowerCase().contains(t.toLowerCase()))
.orElse(true))
.filter(s -> Optional.ofNullable(request.getMachineId())
.map(t -> s.getHint().getMachineId().equals(t))
.orElse(true))
.filter(s -> {
if (request.getConnectedTimeStart() == null || request.getConnectedTimeEnd() == null) {
return true;
}
return DateRanges.inRange(request.getConnectedTimeStart(), request.getConnectedTimeEnd(), s.getHint().getConnectedTime());
})
.map(s -> {
MachineTerminalManagerVO vo = Converts.to(s.getHint(), MachineTerminalManagerVO.class);
vo.setToken(s.getToken());
return vo;
})
.sorted(Comparator.comparing(MachineTerminalManagerVO::getConnectedTime).reversed())
.collect(Collectors.toList());
List<MachineTerminalManagerVO> page = Lists.newLimitList(sessionList)
.limit(request.getLimit())
.page(request.getPage());
return DataGrid.of(page, sessionList.size());
}
/**
* 强制下线
*
* @param token token
*/
public void forceOffline(String token) {
IOperateHandler handler = sessionHolder.get(token);
Valid.notNull(handler, MessageConst.SESSION_PRESENT);
try {
// 下线
handler.forcedOffline();
// 设置日志参数
TerminalConnectConfig hint = handler.getHint();
EventParamsHolder.addParam(EventKeys.TOKEN, token);
EventParamsHolder.addParam(EventKeys.USERNAME, hint.getUsername());
EventParamsHolder.addParam(EventKeys.NAME, hint.getMachineName());
} catch (Exception e) {
throw Exceptions.app(MessageConst.OPERATOR_ERROR, e);
}
}
/**
* 获取终端监视 token
*
* @param token token
* @param readonly readonly
* @return watcher
*/
public TerminalWatcherVO getWatcherToken(String token, Integer readonly) {
IOperateHandler handler = sessionHolder.get(token);
Valid.notNull(handler, MessageConst.SESSION_PRESENT);
// 设置缓存
String watcherToken = UUIds.random32();
TerminalWatcherDTO cache = TerminalWatcherDTO.builder()
.userId(Currents.getUserId())
.token(token)
.readonly(readonly)
.build();
String key = Strings.format(KeyConst.TERMINAL_WATCHER_TOKEN, watcherToken);
redisTemplate.opsForValue().set(key, JSON.toJSONString(cache),
KeyConst.TERMINAL_WATCHER_TOKEN_EXPIRE, TimeUnit.SECONDS);
// 设置返回
TerminalConnectConfig hint = handler.getHint();
return TerminalWatcherVO.builder()
.token(watcherToken)
.readonly(readonly)
.cols(hint.getCols())
.rows(hint.getRows())
.build();
}
public Map<String, IOperateHandler> getSessionHolder() {
return sessionHolder;
}
public IOperateHandler getSession(String key) {
return sessionHolder.get(key);
}
public IOperateHandler removeSession(String key) {
return sessionHolder.remove(key);
}
public void addSession(String key, IOperateHandler handler) {
sessionHolder.put(key, handler);
}
}

View File

@@ -0,0 +1,37 @@
package cn.orionsec.ops.handler.terminal.screen;
import cn.orionsec.kit.net.host.ssh.TerminalType;
import cn.orionsec.ops.constant.Const;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
@Data
public class TerminalScreenEnv {
/**
* 终端类型
*/
@JSONField(name = "TERM")
private String term;
/**
* shell 类型
*/
@JSONField(name = "SHELL")
private String shell;
public TerminalScreenEnv() {
this(TerminalType.XTERM.getType(), Const.DEFAULT_SHELL);
}
public TerminalScreenEnv(String term) {
this(term, Const.DEFAULT_SHELL);
}
public TerminalScreenEnv(String term, String shell) {
this.term = term;
this.shell = shell;
}
}

View File

@@ -0,0 +1,48 @@
package cn.orionsec.ops.handler.terminal.screen;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
@Data
public class TerminalScreenHeader {
private static final Integer CAST_VERSION = 2;
/**
* 版本
*/
private Integer version;
/**
* 标题
*/
private String title;
/**
* cols
*/
@JSONField(name = "width")
private Integer cols;
/**
* rows
*/
@JSONField(name = "height")
private Integer rows;
/**
* 开始时间戳
*/
private Long timestamp;
/**
* 环境变量
*/
private TerminalScreenEnv env;
public TerminalScreenHeader() {
this.version = CAST_VERSION;
}
}

View File

@@ -0,0 +1,31 @@
package cn.orionsec.ops.handler.terminal.watcher;
import cn.orionsec.kit.lang.able.SafeCloseable;
import cn.orionsec.kit.lang.able.Watchable;
import org.springframework.web.socket.WebSocketSession;
public interface ITerminalWatcherProcessor extends Watchable, SafeCloseable {
/**
* 发送消息
*
* @param message message
*/
void sendMessage(byte[] message);
/**
* 添加 watcher
*
* @param session session
*/
void addWatcher(WebSocketSession session);
/**
* 移除 watcher
*
* @param id id
*/
void removeWatcher(String id);
}

View File

@@ -0,0 +1,133 @@
package cn.orionsec.ops.handler.terminal.watcher;
import cn.orionsec.kit.lang.define.wrapper.Tuple;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.terminal.TerminalClientOperate;
import cn.orionsec.ops.constant.ws.WsCloseCode;
import cn.orionsec.ops.constant.ws.WsProtocol;
import cn.orionsec.ops.entity.dto.user.UserDTO;
import cn.orionsec.ops.handler.terminal.IOperateHandler;
import cn.orionsec.ops.handler.terminal.manager.TerminalSessionManager;
import cn.orionsec.ops.service.api.PassportService;
import cn.orionsec.ops.utils.WebSockets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import javax.annotation.Resource;
@Slf4j
@Component("terminalWatcherHandler")
public class TerminalWatcherHandler implements WebSocketHandler {
@Resource
private TerminalSessionManager terminalSessionManager;
@Resource
private PassportService passportService;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("terminal-watcher 已建立连接 token: {}, id: {}, params: {}", WebSockets.getToken(session), session.getId(), session.getAttributes());
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
if (!(message instanceof TextMessage)) {
return;
}
String token = session.getId();
try {
// 解析请求
Tuple tuple = WebSockets.parsePayload(((TextMessage) message).getPayload());
if (tuple == null) {
WebSockets.sendText(session, WsProtocol.ERROR.get());
return;
}
TerminalClientOperate operate = tuple.get(0);
String body = tuple.get(1);
// 建立连接
if (operate == TerminalClientOperate.CONNECT) {
// 建立连接
if (session.getAttributes().get(WebSockets.AUTHED) != null) {
return;
}
this.auth(session, body);
return;
}
if (operate != TerminalClientOperate.KEY && operate != TerminalClientOperate.CLEAR) {
return;
}
// 检查连接
if (session.getAttributes().get(WebSockets.AUTHED) == null) {
WebSockets.close(session, WsCloseCode.VALID);
return;
}
// 检查是否只读
final boolean readonly = Const.ENABLE.equals(session.getAttributes().get(WebSockets.READONLY));
if (operate == TerminalClientOperate.KEY && readonly) {
return;
}
// 获取连接
String terminalToken = session.getAttributes().get(WebSockets.TOKEN).toString();
IOperateHandler handler = terminalSessionManager.getSession(terminalToken);
if (handler == null) {
WebSockets.close(session, WsCloseCode.UNKNOWN_CONNECT);
return;
}
// 操作
handler.handleMessage(operate, body);
} catch (Exception e) {
log.error("terminal 处理操作异常 token: {}", token, e);
WebSockets.close(session, WsCloseCode.RUNTIME_EXCEPTION);
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("terminal-watcher 操作异常拦截 token: {}", session.getId(), exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
log.info("terminal-watcher 关闭连接 token: {}, code: {}, reason: {}", session.getId(), status.getCode(), status.getReason());
// 这时候主连接可能已经关了
IOperateHandler handler = terminalSessionManager.getSession((String) session.getAttributes().get(WebSockets.TOKEN));
if (handler == null) {
return;
}
handler.getWatcher().removeWatcher(session.getId());
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 认证
*
* @param session session
* @param loginToken loginToken
*/
private void auth(WebSocketSession session, String loginToken) {
// 检查参数
Long userId = (Long) session.getAttributes().get(WebSockets.UID);
// 获取登录用户
UserDTO userDTO = passportService.getUserByToken(loginToken, null);
if (userDTO == null || !userId.equals(userDTO.getId())) {
WebSockets.close(session, WsCloseCode.IDENTITY_MISMATCH);
return;
}
session.getAttributes().put(WebSockets.AUTHED, 1);
// 获取连接
String terminalToken = session.getAttributes().get(WebSockets.TOKEN).toString();
IOperateHandler handler = terminalSessionManager.getSession(terminalToken);
// 设置 watcher
handler.getWatcher().addWatcher(session);
WebSockets.sendText(session, WsProtocol.CONNECTED.get());
}
}

View File

@@ -0,0 +1,73 @@
package cn.orionsec.ops.handler.terminal.watcher;
import cn.orionsec.kit.lang.utils.Threads;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.SchedulerPools;
import cn.orionsec.ops.constant.ws.WsCloseCode;
import cn.orionsec.ops.utils.WebSockets;
import lombok.SneakyThrows;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class TerminalWatcherProcessor implements ITerminalWatcherProcessor, Runnable {
private final Map<String, WebSocketSession> sessions;
private final LinkedBlockingQueue<byte[]> queue;
private volatile boolean run;
public TerminalWatcherProcessor() {
this.sessions = Maps.newCurrentHashMap();
this.queue = new LinkedBlockingQueue<>();
}
@Override
public void watch() {
this.run = true;
Threads.start(this, SchedulerPools.TERMINAL_WATCHER_SCHEDULER);
}
@SneakyThrows
@Override
public void run() {
while (run) {
byte[] message = queue.poll(Const.MS_S_10, TimeUnit.MILLISECONDS);
if (message == null || !run) {
continue;
}
for (WebSocketSession session : sessions.values()) {
WebSockets.sendText(session, message);
}
}
}
@Override
public void sendMessage(byte[] message) {
queue.add(message);
}
@Override
public void addWatcher(WebSocketSession session) {
sessions.put(session.getId(), session);
}
@Override
public void removeWatcher(String id) {
sessions.remove(id);
}
@Override
public void close() {
sessions.forEach((k, s) -> WebSockets.close(s, WsCloseCode.EOF));
sessions.clear();
queue.clear();
this.run = false;
}
}

View File

@@ -0,0 +1,69 @@
package cn.orionsec.ops.handler.webhook;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.collect.Lists;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DingRobotPusher implements IWebhookPusher {
/**
* 推送 url
*/
private String url;
/**
* 推送标题
*/
private String title;
/**
* 推送内容
*/
private String text;
/**
* @ 用户的手机号
*/
private List<String> atMobiles;
@Override
public void push() {
OapiRobotSendRequest.Markdown content = new OapiRobotSendRequest.Markdown();
content.setTitle(title);
content.setText(text);
// 执行推送请求
DingTalkClient client = new DefaultDingTalkClient(url);
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("markdown");
request.setMarkdown(content);
if (!Lists.isEmpty(atMobiles)) {
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setAtMobiles(atMobiles);
request.setAt(at);
}
try {
OapiRobotSendResponse response = client.execute(request);
if (!response.isSuccess()) {
log.error("钉钉机器人推送失败 url: {}", url);
}
} catch (Exception e) {
log.error("钉钉机器人推送异常 url: {}", url, e);
throw Exceptions.httpRequest(url, "ding push error", e);
}
}
}

View File

@@ -0,0 +1,11 @@
package cn.orionsec.ops.handler.webhook;
public interface IWebhookPusher {
/**
* 执行推送
*/
void push();
}

View File

@@ -0,0 +1,79 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.constant.StandardContentType;
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.web.servlet.web.Servlets;
import cn.orionsec.ops.annotation.IgnoreAuth;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.ResultCode;
import cn.orionsec.ops.constant.common.EnableType;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.entity.dto.user.UserDTO;
import cn.orionsec.ops.service.api.PassportService;
import cn.orionsec.ops.utils.Currents;
import cn.orionsec.ops.utils.UserHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AuthenticateInterceptor implements HandlerInterceptor {
@Resource
private PassportService passportService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 是否跳过
final boolean ignore = ((HandlerMethod) handler).hasMethodAnnotation(IgnoreAuth.class);
HttpWrapper<?> rejectWrapper = null;
String loginToken = Currents.getLoginToken(request);
if (!Strings.isEmpty(loginToken)) {
String ip = null;
// 如果开启用户 ip 绑定 则获取 ip
if (EnableType.of(SystemEnvAttr.LOGIN_IP_BIND.getValue()).getValue()) {
ip = Servlets.getRemoteAddr(request);
}
// 获取用户登录信息
UserDTO user = passportService.getUserByToken(loginToken, ip);
if (user != null) {
if (Const.DISABLE.equals(user.getUserStatus())) {
rejectWrapper = HttpWrapper.of(ResultCode.USER_DISABLED);
} else {
UserHolder.set(user);
}
} else {
rejectWrapper = HttpWrapper.of(ResultCode.UNAUTHORIZED);
}
} else if (!ignore) {
rejectWrapper = HttpWrapper.of(ResultCode.UNAUTHORIZED);
}
// 匿名接口直接返回
if (ignore) {
return true;
}
// 驳回接口设置返回
if (rejectWrapper != null) {
response.setContentType(StandardContentType.APPLICATION_JSON);
Servlets.transfer(response, rejectWrapper.toJsonString().getBytes());
return false;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserHolder.remove();
}
}

View File

@@ -0,0 +1,28 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.ops.annotation.DemoDisableApi;
import cn.orionsec.ops.constant.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
@Aspect
@Slf4j
@Order(20)
public class DemoDisableApiAspect {
@Pointcut("@annotation(e)")
public void disableApi(DemoDisableApi e) {
}
@Before(value = "disableApi(e)", argNames = "e")
public void beforeDisableApi(DemoDisableApi e) {
throw Exceptions.httpWrapper(HttpWrapper.of(ResultCode.DEMO_DISABLE_API));
}
}

View File

@@ -0,0 +1,45 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.constant.StandardContentType;
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
import cn.orionsec.kit.web.servlet.web.Servlets;
import cn.orionsec.ops.annotation.IgnoreCheck;
import cn.orionsec.ops.constant.ResultCode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class ExposeApiHeaderInterceptor implements HandlerInterceptor {
@Value("${expose.api.access.header}")
private String accessHeader;
@Value("${expose.api.access.secret}")
private String accessSecret;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 是否跳过
final boolean ignore = ((HandlerMethod) handler).hasMethodAnnotation(IgnoreCheck.class);
if (ignore) {
return true;
}
final boolean access = accessSecret.equals(request.getHeader(accessHeader));
if (!access) {
response.setContentType(StandardContentType.APPLICATION_JSON);
Servlets.transfer(response, HttpWrapper.of(ResultCode.ILLEGAL_ACCESS).toJsonString().getBytes());
}
return access;
}
}

View File

@@ -0,0 +1,39 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.utils.Booleans;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.ops.constant.KeyConst;
import cn.orionsec.ops.utils.WebSockets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.annotation.Resource;
import java.util.Map;
@Slf4j
@Component
public class FileTransferNotifyInterceptor implements HandshakeInterceptor {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
String token = WebSockets.getToken(request);
String tokenKey = Strings.format(KeyConst.SFTP_SESSION_TOKEN, token);
boolean access = Booleans.isTrue(redisTemplate.hasKey(tokenKey));
log.info("sftp通知 尝试建立ws连接开始 token: {}, 结果: {}", token, access);
return access;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
}

View File

@@ -0,0 +1,107 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.constant.StandardContentType;
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.web.servlet.web.Servlets;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.ResultCode;
import cn.orionsec.ops.utils.Utils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class IpFilterInterceptor implements HandlerInterceptor {
/**
* 是否启用
*/
private boolean enable;
/**
* 是否为白名单
*/
private boolean isWhiteList;
/**
* 过滤器
*/
private List<String> filters;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 停用
if (!enable) {
return true;
}
if (!(handler instanceof HandlerMethod)) {
return true;
}
String ip = Servlets.getRemoteAddr(request);
// 本机不过滤
if (Const.LOCALHOST_IP_V4.equals(ip)) {
return true;
}
// 过滤
boolean contains = false;
for (String filter : filters) {
if (Strings.isBlank(filter)) {
continue;
}
// 检测
contains = Utils.checkIpIn(ip, filter);
if (contains) {
break;
}
}
// 结果
boolean pass;
if (isWhiteList) {
pass = contains;
} else {
pass = !contains;
}
// 返回
if (!pass) {
response.setContentType(StandardContentType.APPLICATION_JSON);
Servlets.transfer(response, HttpWrapper.of(ResultCode.IP_BAN).toJsonString().getBytes());
}
return pass;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
/**
* 配置启用类型
*
* @param enable 是否启用
* @param isWhiteList 是否为白名单
* @param filter 规则
*/
public void set(boolean enable, boolean isWhiteList, String filter) {
this.enable = enable;
this.isWhiteList = isWhiteList;
if (Strings.isBlank(filter)) {
this.enable = false;
} else {
this.filters = Arrays.stream(filter.split(Const.LF))
.filter(Strings::isNotBlank)
.collect(Collectors.toList());
if (filters.isEmpty()) {
this.enable = false;
}
}
}
}

View File

@@ -0,0 +1,149 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.id.UUIds;
import cn.orionsec.kit.lang.utils.Arrays1;
import cn.orionsec.kit.lang.utils.Exceptions;
import cn.orionsec.kit.lang.utils.time.Dates;
import cn.orionsec.kit.web.servlet.web.Servlets;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.utils.UserHolder;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.PropertyFilter;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Optional;
@Slf4j
@Order(10)
@Component
public class LogPrintInterceptor implements MethodInterceptor {
@Value("#{'${log.interceptor.ignore.fields:}'.split(',')}")
private String[] ignoreFields;
/**
* 请求序列
*/
public static final ThreadLocal<String> SEQ_HOLDER = ThreadLocal.withInitial(UUIds::random32);
/**
* 开始时间
*/
private static final ThreadLocal<Date> START_HOLDER = ThreadLocal.withInitial(Date::new);
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 打印开始日志
this.beforeLogPrint(invocation);
try {
// 执行方法
Object ret = invocation.proceed();
// 返回打印
this.afterReturnLogPrint(ret);
return ret;
} catch (Throwable t) {
// 异常打印
this.afterThrowingLogPrint(t);
throw t;
} finally {
// 删除threadLocal
SEQ_HOLDER.remove();
START_HOLDER.remove();
}
}
/**
* 方法进入打印
*
* @param invocation invocation
*/
public void beforeLogPrint(MethodInvocation invocation) {
StringBuilder requestLog = new StringBuilder("\napi请求-开始-seq: ").append(SEQ_HOLDER.get()).append('\n');
// 登录用户
requestLog.append("\t当前用户: ").append(JSON.toJSONString(UserHolder.get())).append('\n');
// http请求信息
Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.map(s -> (ServletRequestAttributes) s)
.map(ServletRequestAttributes::getRequest)
.ifPresent(request -> {
// url
requestLog.append("\t").append(Servlets.getMethod(request)).append(" ")
.append(Servlets.getRequestUrl(request)).append('\n');
// query
requestLog.append("\tip: ").append(Servlets.getRemoteAddr(request)).append('\n')
.append("\tquery: ").append(Servlets.getQueryString(request)).append('\n');
// header
Servlets.getHeaderMap(request).forEach((hk, hv) -> requestLog.append('\t')
.append(hk).append(": ")
.append(hv).append('\n'));
});
// 方法信息
Method method = invocation.getMethod();
requestLog.append("\t开始时间: ").append(Dates.format(START_HOLDER.get(), Dates.YMD_HMSS)).append('\n')
.append("\t方法签名: ").append(method.getDeclaringClass().getName()).append('#')
.append(method.getName()).append("\n")
.append("\t请求参数: ").append(this.argsToString(invocation.getArguments()));
log.info(requestLog.toString());
}
/**
* 返回打印
*
* @param ret return
*/
private void afterReturnLogPrint(Object ret) {
Date endTime = new Date();
// 响应日志
StringBuilder responseLog = new StringBuilder("\napi请求-结束-seq: ").append(SEQ_HOLDER.get()).append('\n');
responseLog.append("\t结束时间: ").append(Dates.format(endTime, Dates.YMD_HMSS))
.append(" used: ").append(endTime.getTime() - START_HOLDER.get().getTime()).append("ms \n")
.append("\t响应结果: ").append(this.argsToString(ret));
log.info(responseLog.toString());
}
/**
* 异常打印
*
* @param throwable ex
*/
private void afterThrowingLogPrint(Throwable throwable) {
Date endTime = new Date();
// 响应日志
StringBuilder responseLog = new StringBuilder("\napi请求-异常-seq: ").append(SEQ_HOLDER.get()).append('\n');
responseLog.append("\t结束时间: ").append(Dates.format(endTime, Dates.YMD_HMSS))
.append(" used: ").append(endTime.getTime() - START_HOLDER.get().getTime()).append("ms \n")
.append("\t异常摘要: ").append(Exceptions.getDigest(throwable));
log.error(responseLog.toString());
}
/**
* 参数转json
*
* @param o object
* @return json
*/
private String argsToString(Object o) {
try {
if (ignoreFields.length == 1 && Const.EMPTY.equals(ignoreFields[0])) {
// 不过滤
return JSON.toJSONString(o);
} else {
return JSON.toJSONString(o, (PropertyFilter) (object, name, value) -> !Arrays1.contains(ignoreFields, name));
}
} catch (Exception e) {
return String.valueOf(o);
}
}
}

View File

@@ -0,0 +1,52 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.constant.StandardContentType;
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
import cn.orionsec.kit.lang.utils.Arrays1;
import cn.orionsec.kit.web.servlet.web.Servlets;
import cn.orionsec.ops.annotation.RequireRole;
import cn.orionsec.ops.constant.ResultCode;
import cn.orionsec.ops.constant.user.RoleType;
import cn.orionsec.ops.entity.dto.user.UserDTO;
import cn.orionsec.ops.utils.UserHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class RoleInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
RequireRole role = ((HandlerMethod) handler).getMethodAnnotation(RequireRole.class);
if (role == null) {
return true;
}
UserDTO user = UserHolder.get();
if (user == null) {
response.setContentType(StandardContentType.APPLICATION_JSON);
Servlets.transfer(response, HttpWrapper.of(ResultCode.UNAUTHORIZED).toJsonString().getBytes());
return false;
}
RoleType[] hasRoles = role.value();
if (Arrays1.isEmpty(hasRoles)) {
return true;
}
for (RoleType roleType : hasRoles) {
if (roleType.getType().equals(user.getRoleType())) {
return true;
}
}
response.setContentType(StandardContentType.APPLICATION_JSON);
Servlets.transfer(response, HttpWrapper.of(ResultCode.NO_PERMISSION).toJsonString().getBytes());
return false;
}
}

View File

@@ -0,0 +1,49 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.ops.constant.KeyConst;
import cn.orionsec.ops.entity.dto.file.FileTailDTO;
import cn.orionsec.ops.utils.WebSockets;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.annotation.Resource;
import java.util.Map;
@Component
@Slf4j
public class TailFileInterceptor implements HandshakeInterceptor {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
String token = WebSockets.getToken(request);
String tokenKey = Strings.format(KeyConst.FILE_TAIL_ACCESS_TOKEN, token);
String tokenValue = redisTemplate.opsForValue().get(tokenKey);
boolean access = false;
if (!Strings.isBlank(tokenValue)) {
// 设置信息
access = true;
attributes.put(WebSockets.CONFIG, JSON.parseObject(tokenValue, FileTailDTO.class));
attributes.put(WebSockets.TOKEN, token);
// 删除 token
redisTemplate.delete(tokenKey);
}
log.info("tail 尝试建立ws连接开始 token: {}, 结果: {}", token, access);
return access;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
}

View File

@@ -0,0 +1,49 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.ops.constant.KeyConst;
import cn.orionsec.ops.utils.WebSockets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.annotation.Resource;
import java.util.Map;
@Component
@Slf4j
public class TerminalAccessInterceptor implements HandshakeInterceptor {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
// 获取 token
String token = WebSockets.getToken(request);
String tokenKey = Strings.format(KeyConst.TERMINAL_ACCESS_TOKEN, token);
String tokenValue = redisTemplate.opsForValue().get(tokenKey);
boolean access = false;
if (!Strings.isBlank(tokenValue)) {
// 设置用户机器信息
access = true;
String[] pair = tokenValue.split("_");
attributes.put(WebSockets.UID, Long.valueOf(pair[0]));
attributes.put(WebSockets.MID, Long.valueOf(pair[1]));
// 删除 token
redisTemplate.delete(tokenKey);
}
log.info("terminal尝试打开ws连接开始 token: {}, 结果: {}", token, access);
return access;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
}

View File

@@ -0,0 +1,51 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.ops.constant.KeyConst;
import cn.orionsec.ops.entity.dto.terminal.TerminalWatcherDTO;
import cn.orionsec.ops.utils.WebSockets;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.annotation.Resource;
import java.util.Map;
@Component
@Slf4j
public class TerminalWatcherInterceptor implements HandshakeInterceptor {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
// 获取 token
String token = WebSockets.getToken(request);
String tokenKey = Strings.format(KeyConst.TERMINAL_WATCHER_TOKEN, token);
String tokenValue = redisTemplate.opsForValue().get(tokenKey);
boolean access = false;
if (!Strings.isBlank(tokenValue)) {
access = true;
TerminalWatcherDTO watcher = JSON.parseObject(tokenValue, TerminalWatcherDTO.class);
attributes.put(WebSockets.UID, watcher.getUserId());
attributes.put(WebSockets.TOKEN, watcher.getToken());
attributes.put(WebSockets.READONLY, watcher.getReadonly());
// 删除 token
redisTemplate.delete(tokenKey);
}
log.info("terminal尝试监视ws连接开始 token: {}, 结果: {}", token, access);
return access;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
}

View File

@@ -0,0 +1,118 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.collect.Maps;
import cn.orionsec.ops.constant.Const;
import cn.orionsec.ops.constant.KeyConst;
import cn.orionsec.ops.constant.common.EnableType;
import cn.orionsec.ops.constant.event.EventKeys;
import cn.orionsec.ops.constant.event.EventType;
import cn.orionsec.ops.constant.system.SystemEnvAttr;
import cn.orionsec.ops.dao.UserInfoDAO;
import cn.orionsec.ops.entity.dto.user.UserDTO;
import cn.orionsec.ops.service.api.UserEventLogService;
import cn.orionsec.ops.utils.Currents;
import cn.orionsec.ops.utils.EventParamsHolder;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Component
public class UserActiveInterceptor implements HandlerInterceptor {
/**
* key: 用户id
* value: 活跃时间戳
*/
private final Map<Long, Long> activeUsers = Maps.newCurrentHashMap();
@Resource
private UserInfoDAO userInfoDAO;
@Resource
private UserEventLogService userEventLogService;
@Resource
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 处理
this.handleActive();
return true;
}
/**
* 处理用户活跃信息
*/
private void handleActive() {
UserDTO user = Currents.getUser();
if (user == null) {
return;
}
long now = System.currentTimeMillis();
Long before = activeUsers.put(user.getId(), now);
if (before == null) {
// 应用启动/多端登录(单端登出) token有效
this.refreshActive(user);
return;
}
// 获取活跃域值
long activeThresholdHour = Long.parseLong(SystemEnvAttr.LOGIN_TOKEN_AUTO_RENEW_THRESHOLD.getValue());
long activeThreshold = TimeUnit.HOURS.toMillis(activeThresholdHour);
if (before + activeThreshold < now) {
// 超过活跃域值
this.refreshActive(user);
}
}
/**
* 刷新活跃
*
* @param user user
*/
private void refreshActive(UserDTO user) {
Long userId = user.getId();
// 刷新登录时间
userInfoDAO.updateLastLoginTime(userId);
// 记录日志
EventParamsHolder.addParam(EventKeys.REFRESH_LOGIN, Const.ENABLE);
EventParamsHolder.setDefaultEventParams();
userEventLogService.recordLog(EventType.LOGIN, true);
// 如果开启自动续签 刷新登录token 绑定token
if (EnableType.of(SystemEnvAttr.LOGIN_TOKEN_AUTO_RENEW.getValue()).getValue()) {
long expire = Long.parseLong(SystemEnvAttr.LOGIN_TOKEN_EXPIRE.getValue());
String loginKey = Strings.format(KeyConst.LOGIN_TOKEN_KEY, userId);
String bindKey = Strings.format(KeyConst.LOGIN_TOKEN_BIND_KEY, userId, user.getCurrentBindTimestamp());
redisTemplate.expire(loginKey, expire, TimeUnit.HOURS);
redisTemplate.expire(bindKey, expire, TimeUnit.HOURS);
}
}
/**
* 设置活跃时间
*
* @param id id
* @param time 登录时间
*/
public void setActiveTime(Long id, Long time) {
activeUsers.put(id, time);
}
/**
* 删除活跃时间
*
* @param id id
*/
public void deleteActiveTime(Long id) {
activeUsers.remove(id);
}
}

View File

@@ -0,0 +1,44 @@
package cn.orionsec.ops.interceptor;
import cn.orionsec.ops.annotation.EventLog;
import cn.orionsec.ops.service.api.UserEventLogService;
import cn.orionsec.ops.utils.EventParamsHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
@Aspect
@Slf4j
@Order(30)
public class UserEventLogAspect {
@Resource
private UserEventLogService userEventLogService;
@Pointcut("@annotation(e)")
public void eventLogPoint(EventLog e) {
}
@Before(value = "eventLogPoint(e)", argNames = "e")
public void beforeLogRecord(EventLog e) {
EventParamsHolder.remove();
// 设置默认参数
EventParamsHolder.setDefaultEventParams();
}
@AfterReturning(pointcut = "eventLogPoint(e)", argNames = "e")
public void afterLogRecord(EventLog e) {
userEventLogService.recordLog(e.value(), true);
}
@AfterThrowing(pointcut = "eventLogPoint(e)", argNames = "e")
public void afterLogRecordThrowing(EventLog e) {
userEventLogService.recordLog(e.value(), false);
}
}

View File

@@ -0,0 +1,42 @@
package cn.orionsec.ops.service.api;
import cn.orionsec.ops.entity.domain.AlarmGroupNotifyDO;
import java.util.List;
public interface AlarmGroupNotifyService {
/**
* 通过 groupId 查询
*
* @param groupId groupId
* @return rows
*/
List<AlarmGroupNotifyDO> selectByGroupId(Long groupId);
/**
* 通过 groupId 查询
*
* @param groupIdList groupIdList
* @return rows
*/
List<AlarmGroupNotifyDO> selectByGroupIdList(List<Long> groupIdList);
/**
* 通过 groupId 删除
*
* @param groupId groupId
* @return effect
*/
Integer deleteByGroupId(Long groupId);
/**
* 通过 webhookId 删除
*
* @param webhookId webhookId
* @return effect
*/
Integer deleteByWebhookId(Long webhookId);
}

View File

@@ -0,0 +1,50 @@
package cn.orionsec.ops.service.api;
import cn.orionsec.kit.lang.define.wrapper.DataGrid;
import cn.orionsec.ops.entity.request.alarm.AlarmGroupRequest;
import cn.orionsec.ops.entity.vo.alarm.AlarmGroupVO;
public interface AlarmGroupService {
/**
* 添加报警组
*
* @param request request
* @return id
*/
Long addAlarmGroup(AlarmGroupRequest request);
/**
* 更新报警组
*
* @param request request
* @return effect
*/
Integer updateAlarmGroup(AlarmGroupRequest request);
/**
* 删除报警组
*
* @param id id
* @return effect
*/
Integer deleteAlarmGroup(Long id);
/**
* 查询列表
*
* @param request request
* @return rows
*/
DataGrid<AlarmGroupVO> getAlarmGroupList(AlarmGroupRequest request);
/**
* 查询详情
*
* @param id id
* @return row
*/
AlarmGroupVO getAlarmGroupDetail(Long id);
}

View File

@@ -0,0 +1,42 @@
package cn.orionsec.ops.service.api;
import cn.orionsec.ops.entity.domain.AlarmGroupUserDO;
import java.util.List;
public interface AlarmGroupUserService {
/**
* 通过 groupId 查询
*
* @param groupId groupId
* @return rows
*/
List<AlarmGroupUserDO> selectByGroupId(Long groupId);
/**
* 通过 groupId 查询
*
* @param groupIdList groupIdList
* @return rows
*/
List<AlarmGroupUserDO> selectByGroupIdList(List<Long> groupIdList);
/**
* 通过 groupId 删除
*
* @param groupId groupId
* @return effect
*/
Integer deleteByGroupId(Long groupId);
/**
* 通过 userId 删除
*
* @param userId userId
* @return effect
*/
Integer deleteByUserId(Long userId);
}

View File

@@ -0,0 +1,96 @@
package cn.orionsec.ops.service.api;
import cn.orionsec.ops.constant.app.StageType;
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
import cn.orionsec.ops.entity.vo.app.ApplicationActionLogVO;
import java.util.List;
public interface ApplicationActionLogService {
/**
* 通过 id 查询详情
*
* @param id id
* @return detail
*/
ApplicationActionLogVO getDetailById(Long id);
/**
* 通过 id 查询状态
*
* @param id id
* @return detail
*/
ApplicationActionLogVO getStatusById(Long id);
/**
* 获取操作执行日志
*
* @param relId relId
* @param stageType stageType
* @return rows
*/
List<ApplicationActionLogVO> getActionLogsByRelId(Long relId, StageType stageType);
/**
* 删除
*
* @param relId relId
* @param stageType stageType
* @return effect
*/
Integer deleteByRelId(Long relId, StageType stageType);
/**
* 删除
*
* @param relIdList relIdList
* @param stageType stageType
* @return effect
*/
Integer deleteByRelIdList(List<Long> relIdList, StageType stageType);
/**
* 通过 relId 查询 action
*
* @param relId relId
* @param stageType stageType
* @return action
*/
List<ApplicationActionLogDO> selectActionByRelId(Long relId, StageType stageType);
/**
* 通过 relIdList 查询 action
*
* @param relIdList relIdList
* @param stageType stageType
* @return action
*/
List<ApplicationActionLogDO> selectActionByRelIdList(List<Long> relIdList, StageType stageType);
/**
* 更新 action
*
* @param record record
*/
void updateActionById(ApplicationActionLogDO record);
/**
* 获取操作执行日志路径
*
* @param id id
* @return path
*/
String getActionLogPath(Long id);
/**
* 设置操作状态
*
* @param relId relId
* @param stageType stageType
*/
void resetActionStatus(Long relId, StageType stageType);
}

View File

@@ -0,0 +1,75 @@
package cn.orionsec.ops.service.api;
import cn.orionsec.ops.entity.domain.ApplicationActionDO;
import cn.orionsec.ops.entity.request.app.ApplicationConfigRequest;
import java.util.List;
import java.util.Map;
public interface ApplicationActionService {
/**
* 删除发布流程
*
* @param appId appId
* @param profileId profileId
* @return effect
*/
Integer deleteAppActionByAppProfileId(Long appId, Long profileId);
/**
* 获取发布流程
*
* @param appId appId
* @param profileId profileId
* @param stageType stageType
* @return actions
*/
List<ApplicationActionDO> getAppProfileActions(Long appId, Long profileId, Integer stageType);
/**
* 通过appId profileId查询操作流程数量数量
*
* @param appId appId
* @param profileId profileId
* @param stageType stageType
* @return count
*/
Integer getAppProfileActionCount(Long appId, Long profileId, Integer stageType);
/**
* 配置app发布流程
*
* @param request request
*/
void configAppAction(ApplicationConfigRequest request);
/**
* 同步app操作流程
*
* @param appId appId
* @param profileId profileId
* @param syncProfileId 需要同步的profileId
*/
void syncAppProfileAction(Long appId, Long profileId, Long syncProfileId);
/**
* 复制发布流程
*
* @param appId appId
* @param targetAppId targetAppId
*/
void copyAppAction(Long appId, Long targetAppId);
/**
* 获取app是否已配置
*
* @param profileId profileId
* @param appIdList appIdList
* @return appId, isConfig
*/
Map<Long, Boolean> getAppIsConfig(Long profileId, List<Long> appIdList);
}

View File

@@ -0,0 +1,128 @@
package cn.orionsec.ops.service.api;
import cn.orionsec.kit.lang.define.wrapper.DataGrid;
import cn.orionsec.ops.entity.domain.ApplicationBuildDO;
import cn.orionsec.ops.entity.request.app.ApplicationBuildRequest;
import cn.orionsec.ops.entity.vo.app.ApplicationBuildReleaseListVO;
import cn.orionsec.ops.entity.vo.app.ApplicationBuildStatusVO;
import cn.orionsec.ops.entity.vo.app.ApplicationBuildVO;
import java.util.List;
public interface ApplicationBuildService {
/**
* 提交执行
*
* @param request request
* @param execute 是否提交任务
* @return id
*/
Long submitBuildTask(ApplicationBuildRequest request, boolean execute);
/**
* 获取构建列表
*
* @param request request
* @return rows
*/
DataGrid<ApplicationBuildVO> getBuildList(ApplicationBuildRequest request);
/**
* 获取构建详情
*
* @param id id
* @return row
*/
ApplicationBuildVO getBuildDetail(Long id);
/**
* 查询构建状态
*
* @param id id
* @return status
*/
ApplicationBuildStatusVO getBuildStatus(Long id);
/**
* 查询构建状态列表
*
* @param buildIdList id
* @return key: id value: status
*/
List<ApplicationBuildStatusVO> getBuildStatusList(List<Long> buildIdList);
/**
* 停止构建
*
* @param id id
*/
void terminateBuildTask(Long id);
/**
* 输入命令
*
* @param id id
* @param command command
*/
void writeBuildTask(Long id, String command);
/**
* 删除构建
*
* @param idList idList
* @return effect
*/
Integer deleteBuildTask(List<Long> idList);
/**
* 重新构建
*
* @param id id
* @return id
*/
Long rebuild(Long id);
/**
* 通过id查询
*
* @param id id
* @return row
*/
ApplicationBuildDO selectById(Long id);
/**
* 获取构建日志路径
*
* @param id id
* @return path
*/
String getBuildLogPath(Long id);
/**
* 获取构建产物路径
*
* @param id id
* @return path
*/
String getBuildBundlePath(Long id);
/**
* 检查并且获取构建目录
*
* @param build build
* @return 构建产物路径
*/
String checkBuildBundlePath(ApplicationBuildDO build);
/**
* 获取构建发布序列
*
* @param appId appId
* @param profileId profileId
* @return rows
*/
List<ApplicationBuildReleaseListVO> getBuildReleaseList(Long appId, Long profileId);
}

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