From 2f3a2ae28fd2fe42998c3bbccfc77e3b019afda3 Mon Sep 17 00:00:00 2001 From: zyj <2660555181@qq.com> Date: Tue, 24 Jun 2025 16:20:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E8=BF=9E=E6=8E=A5=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=80=BB=E8=BE=91=E4=BF=AE=E6=94=B9=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E7=94=A8websocket(=E8=81=94=E8=B0=83=E4=BB=BB=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E9=97=AE=E9=A2=98)=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E6=9C=BA=E5=99=A8=E5=AF=B9=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=A0=91=E7=9A=84=E6=93=8D=E4=BD=9C=EF=BC=8C=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=B7=A5=E6=95=B4=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/module-ci-machine/pom.xml | 17 +- .../WebSocketHandshakeInterceptor.java | 85 +++ .../machine/component/FileTreeComponent.java | 214 ++++++ .../component/WebSocketConnection.java | 230 +++++++ .../component/WebSocketSessionManager.java | 114 ++++ .../configuration/AliYunOssConfig.java | 2 +- .../configuration/WebSocketConfig.java | 40 ++ .../contants/MachineErrorCodeConstants.java | 32 +- .../controller/MachineInfoController.java | 78 +-- .../controller/SecretKeyController.java | 27 +- .../machine/controller/vo/SecretKeyVO.java | 2 + .../machine/convert/MachineEnvConvert.java | 25 - .../module/machine/dal/model/FileNode.java | 56 ++ .../machine/dal/mysql/MachineInfoMapper.java | 5 + .../machine/dal/mysql/SecretKeyMapper.java | 1 - .../machine/enums/ConnectionStatus.java | 29 +- .../module/machine/enums/SSHChanelType.java | 35 + .../machine/handler/ConnectionSession.java | 642 ------------------ .../handler/MachineWebSocketHandler.java | 67 ++ .../machine/service/MachineInfoService.java | 88 +-- .../machine/service/SecretKeyService.java | 11 + .../service/impl/MachineInfoServiceImpl.java | 243 ++----- .../service/impl/MachineProxyServiceImpl.java | 2 - .../service/impl/SecretKeyServiceImpl.java | 83 ++- .../module/machine/utils/AliYunOssClient.java | 3 +- .../module/machine/utils/PropertyUtils.java | 35 - 26 files changed, 1175 insertions(+), 991 deletions(-) create mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/Interceptor/WebSocketHandshakeInterceptor.java create mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/FileTreeComponent.java create mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/WebSocketConnection.java create mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/WebSocketSessionManager.java create mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/configuration/WebSocketConfig.java delete mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/convert/MachineEnvConvert.java create mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/model/FileNode.java create mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/enums/SSHChanelType.java delete mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/handler/ConnectionSession.java create mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/handler/MachineWebSocketHandler.java delete mode 100644 modules/module-ci-machine/src/main/java/cd/casic/module/machine/utils/PropertyUtils.java diff --git a/modules/module-ci-machine/pom.xml b/modules/module-ci-machine/pom.xml index ce137127..6f221741 100644 --- a/modules/module-ci-machine/pom.xml +++ b/modules/module-ci-machine/pom.xml @@ -28,6 +28,12 @@ 3.15.1 + + + org.springframework.boot + spring-boot-starter-websocket + + com.jcraft @@ -63,7 +69,14 @@ module-infra-biz ${revision} - - + + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/Interceptor/WebSocketHandshakeInterceptor.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/Interceptor/WebSocketHandshakeInterceptor.java new file mode 100644 index 00000000..a0a6f890 --- /dev/null +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/Interceptor/WebSocketHandshakeInterceptor.java @@ -0,0 +1,85 @@ +package cd.casic.module.machine.Interceptor; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.http.HttpStatus; +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 java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception; +import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*; + +@Component +@Slf4j +public class WebSocketHandshakeInterceptor implements HandshakeInterceptor { + @Override + public boolean beforeHandshake( + @NotNull ServerHttpRequest request, + @NotNull ServerHttpResponse response, + @NotNull WebSocketHandler wsHandler, + @NotNull Map attributes + ) { + log.info("WebSocket握手请求: {}", request.getURI()); + + // 从URL参数中获取id + String id = extractIdFromUrl(request.getURI()); + + System.out.println("-----------------------------------------"); + if (id != null) { + attributes.put("machineId", id); // 将id存入attributes + log.info("握手验证成功 - ID: {}", id); + return true; + } else { + response.setStatusCode(HttpStatus.BAD_REQUEST); + log.warn("握手验证失败 - 缺少 id URL 参数"); + return false; + } + } + + @Override + public void afterHandshake( + @NotNull ServerHttpRequest request, + @NotNull ServerHttpResponse response, + @NotNull WebSocketHandler wsHandler, + Exception exception + ) { + if (exception == null) { + log.info("WebSocket握手完成 - URI: {}", + request.getURI()); + } else { + log.error("WebSocket握手异常 - URI: {}, 异常信息: {}", + request.getURI(), + exception.getMessage(), + exception); + } + } + + // 从URI中提取id参数的辅助方法 + private String extractIdFromUrl(URI uri) { + try { + String query = uri.getQuery(); + if (query != null) { + String[] params = query.split("&"); + for (String param : params) { + String[] keyValue = param.split("="); + if (keyValue.length == 2 && "id".equalsIgnoreCase(keyValue[0])) { // 修改为匹配id + return URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); + } + } + } + } catch (Exception e) { + log.error("解析URL参数失败", e); + throw exception(FAILED_TO_PARSE_URL_PARAMETERS); + } + return null; + } + +} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/FileTreeComponent.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/FileTreeComponent.java new file mode 100644 index 00000000..d42fa416 --- /dev/null +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/FileTreeComponent.java @@ -0,0 +1,214 @@ +package cd.casic.module.machine.component; + +import cd.casic.module.machine.dal.model.FileNode; +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.SftpATTRS; +import com.jcraft.jsch.SftpException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception; +import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*; + +@Slf4j +@Component +public class FileTreeComponent { + + private ChannelSftp sftp; + //todo缓存过期还未设置 + private final Map directoryCache = new ConcurrentHashMap<>(); + private static final long CACHE_EXPIRE_TIME = 60 * 1000; // 缓存过期时间:1分钟 + private final Map cacheTimeStamp = new ConcurrentHashMap<>(); + + // 将文件树转换为JSON友好的Map结构(仅一级目录) + public static Map convertToMap(FileNode node) { + Map map = new HashMap<>(); + map.put("name", node.getName()); + map.put("isDirectory", node.isDirectory()); + map.put("size", node.getSize()); + map.put("permissions", node.getPermissions()); + map.put("modifiedTime", node.getModifiedTime()); + // 仅添加直接子项,不递归 + if (node.isDirectory() && !node.getChildren().isEmpty()) { + List> children = node.getChildren().stream() + .map(FileTreeComponent::convertToMap) + .collect(Collectors.toList()); + map.put("children", children); + } + return map; + } + + // 获取指定路径的直接子项(不递归) + public List listDirectChildren(String path) { + if (sftp == null || !isSftpConnected()) { + log.error("SFTP连接无效,无法列出文件"); + throw exception(CONNECTION_LOST); + } + + List entries = Collections.synchronizedList(new ArrayList<>()); + //定义ChannelSftp下LsEntrySelector接口中select方法 + ChannelSftp.LsEntrySelector selector = entry -> { + entries.add(entry); + return ChannelSftp.LsEntrySelector.CONTINUE; + }; + + try { + sftp.ls(path, selector); + } catch (SftpException e) { + log.error("读取远程文件目录结构失败 [错误码: {}, 信息: {}]", + e.id, e.getMessage(), e); + throw exception(READ_REMOTE_DIRECTORY_FAIL); + } + return new ArrayList<>(entries); + } + + // 创建文件节点(支持使用完整路径或文件名) + private static FileNode createFileNode(String name, SftpATTRS attrs, boolean useFullPath) { + String displayName = useFullPath ? name : name.substring(name.lastIndexOf('/') + 1); + return new FileNode( + displayName, + attrs.isDir(), + attrs.getSize(), + attrs.getPermissionsString(), + attrs.getMTime() * 1000L // 转换为毫秒 + ); + } + + // 获取远程文件树的方法(仅展示下一级) + public Map getRemoteFileTree(Session session, String path) { + path = normalizePath(path); + Channel channel = null; + try { + // 缓存有效性检查 + String cacheKey = "ls:" + path; + long currentTime = System.currentTimeMillis(); + + // 检查缓存是否存在且未过期 + if (directoryCache.containsKey(cacheKey) && + currentTime - cacheTimeStamp.getOrDefault(cacheKey, 0L) < CACHE_EXPIRE_TIME) { + log.debug("从缓存获取目录内容: {}", path); + // 构建缓存中的根节点 + FileNode root = directoryCache.get(cacheKey); + return sortFileInfo(convertToMap(root)); + } + + // 打开SFTP通道 + channel = session.openChannel("sftp"); + channel.connect(); + sftp = (ChannelSftp) channel; + + // 先通过stat获取当前路径信息 + SftpATTRS rootAttrs = sftp.stat(path); + FileNode root = createFileNode(path, rootAttrs, true); + + // 仅获取直接子项,不递归 + List entries = listDirectChildren(path); + //循环添加子节点 + for (ChannelSftp.LsEntry entry : entries) { + String fileName = entry.getFilename(); + if (fileName.equals(".") || fileName.equals("..")) continue; + SftpATTRS attrs = entry.getAttrs(); + FileNode childNode = createFileNode(fileName, attrs, false); + root.addChild(childNode); + } + // 更新缓 + directoryCache.put(cacheKey, root); + cacheTimeStamp.put(cacheKey, currentTime); + return sortFileInfo(convertToMap(root)); + } catch (JSchException e) { + log.error("SFTP通道创建或连接失败: {}", e.getMessage(), e); + if (e.getMessage().contains("open")) { + throw exception(CREATE_CHANEL_ERROR); + } else { + throw exception(CHANEL_CONNECT_FAIL); + } + } catch (SftpException e) { + if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + // 仅捕获路径不存在的错误 + log.error("路径不存在: {}", path); + throw exception(PATH_NOT_EXISTS); // 自定义异常 + } else if (e.id == ChannelSftp.SSH_FX_PERMISSION_DENIED) { + // 处理权限问题(非路径不存在) + log.error("无路径访问权限: {}", path); + throw exception(NO_PATH_PERMISSION); + } else { + log.error("获取目录内容失败: {}", e.getMessage(), e); + throw exception(READ_REMOTE_DIRECTORY_FAIL); + } + } finally { + // 确保资源释放 + if (sftp != null) { + try { + sftp.disconnect(); + } catch (Exception ex) { + log.warn("关闭SFTP连接失败", ex); + } + } + if (channel != null && channel.isConnected()) { + channel.disconnect(); + } + } + } + + // 检查SFTP连接状态 + private boolean isSftpConnected() { + if (sftp == null) return false; + try { + sftp.pwd(); + return true; + } catch (Exception e) { + log.warn("SFTP连接状态检查失败", e); + return false; + } + } + + // 规范化路径:确保末尾有斜杠(根路径除外) + private String normalizePath(String path) { + // 移除多余的斜杠(多个连续的斜杠会被替换为一个) + path = path.replaceAll("/+", "/"); + + // 如果路径不为根路径且末尾没有斜杠,则添加斜杠 + if (!"/".equals(path) && !path.endsWith("/")) { + path += "/"; + } + + return path; + } + + /** + * 对文件和目录信息进行排序(仅处理直接子级) + * + * @param fileInfoMap 文件/目录信息映射 + * @return 排序后的映射 + */ + public Map sortFileInfo(Map fileInfoMap) { + // 检查是否包含子节点 + if (fileInfoMap.containsKey("children")) { + List> children = (List>) fileInfoMap.get("children"); + // 对子节点列表进行排序 + if (children != null && !children.isEmpty()) { + children.sort((a, b) -> { + // 获取isDirectory属性并比较 + boolean isADirectory = (boolean) a.get("isDirectory"); + boolean isBDirectory = (boolean) b.get("isDirectory"); + // 目录排在文件前面 + return Boolean.compare(isBDirectory, isADirectory); + }); + // 更新排序后的子节点列表 + fileInfoMap.put("children", children); + } + } + return fileInfoMap; + } +} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/WebSocketConnection.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/WebSocketConnection.java new file mode 100644 index 00000000..1674e8f7 --- /dev/null +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/WebSocketConnection.java @@ -0,0 +1,230 @@ +package cd.casic.module.machine.component; + +import cd.casic.framework.commons.exception.ServiceException; +import cd.casic.module.machine.dal.dataobject.MachineInfoDO; +import cd.casic.module.machine.enums.AuthenticationType; +import cd.casic.module.machine.enums.ConnectionStatus; +import cd.casic.module.machine.enums.SSHChanelType; +import cd.casic.module.machine.service.SecretKeyService; +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception; +import static cd.casic.module.machine.contants.MachineErrorCodeConstants.SECRET_KEY_NULL; +import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*; + +@Slf4j +@Data +@Component +public class WebSocketConnection { + private SecretKeyService secretKeyService; + + private MachineInfoDO machineInfo; + private ConnectionStatus connectionStatus = ConnectionStatus.DISCONNECTED; + private Session sshSession; + + public WebSocketConnection(SecretKeyService secretKeyService) { + this.secretKeyService = secretKeyService; + } + + private static final int CONNECTION_TIMEOUT = 5000; // 连接超时时间(毫秒) + + public void initConnection(MachineInfoDO machineInfo) { + try { + this.machineInfo = machineInfo; + this.sshSession = doConnect(machineInfo); + log.info("已成功建立 SSH 连接至 {} ", machineInfo.getHostIp()); + this.connectionStatus = ConnectionStatus.CONNECTING; + } catch (ServiceException e) { + log.warn("SSH 连接失败: {}", e.getMessage()); + throw e; + } + } + + public void disconnect() { + if (sshSession != null && sshSession.isConnected()) { + try { + sshSession.disconnect(); + log.info("SSH连接关闭: {}", machineInfo.getHostIp()); + } catch (Exception e) { + log.error("关闭SSH连接失败: {}", e.getMessage()); + throw exception(CLOSE_CLOSE_SESSION_ERROR); + } + } + connectionStatus = ConnectionStatus.DISCONNECTED; + } + + /** + * 执行远程命令,支持超时和中断处理 + */ + public void executeCommand(WebSocketSession webSocketSession, String command) { + // 1. 检查连接状态 + if (sshSession == null || !sshSession.isConnected()) { + sendErrorMessage(webSocketSession, "SSH连接未建立或已断开"); + return; + } + try { + // 2. 创建SSH命令执行通道 + Channel channel; + try { + channel = sshSession.openChannel(SSHChanelType.EXEC.getMessage()); + } catch (JSchException e) { + throw exception(CREATE_CHANEL_ERROR); + } + ((ChannelExec) channel).setCommand(command); + // 3. 设置输入/输出流 + channel.setInputStream(null); + ((ChannelExec) channel).setErrStream(System.err); + // 4. 获取命令输出流 + InputStream inputStream = channel.getInputStream(); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + BufferedReader reader = new BufferedReader(inputStreamReader); + // 5. 连接并执行命令 + channel.connect(); + // 6. 读取命令输出并实时发送给客户端 + String line; + while ((line = reader.readLine()) != null) { + // 实时发送输出到客户端 + webSocketSession.sendMessage(new TextMessage(line)); + } + // 7. 等待命令执行完成 + int exitStatus = channel.getExitStatus(); + // 8. 发送命令执行完毕的消息 + webSocketSession.sendMessage(new TextMessage( + "[系统] 命令执行完毕,退出状态: " + exitStatus + )); + // 9. 关闭通道 + channel.disconnect(); + } catch (JSchException | IOException e) { + throw exception(EXECUTE_COMMAND_FAIL); + } + } + + // 发送错误消息的辅助方法 + public void sendErrorMessage(WebSocketSession webSocketSession, String message) { + try { + if (webSocketSession.isOpen()) { + webSocketSession.sendMessage(new TextMessage("[错误] " + message)); + } + } catch (IOException e) { + log.error("发送错误消息失败", e); + throw exception(WEBSOCKET_SEND_MESSAGE_ERROR); + } + } + + /** + * 实际执行连接逻辑 + */ + private Session doConnect(MachineInfoDO machineInfo) { + JSch jsch = new JSch(); + // 配置认证方式 + configureAuthentication(jsch, machineInfo); + Session session; + // 创建SSH会话 + try { + session = jsch.getSession( + machineInfo.getUsername(), + machineInfo.getHostIp(), + machineInfo.getSshPort() != null ? machineInfo.getSshPort() : 22 + ); + } catch (JSchException e) { + throw exception(CREATE_SESSION_ERROR); + } + // 配置连接参数 + configureSession(session, machineInfo); + // 建立连接 + try { + session.connect(CONNECTION_TIMEOUT); + } catch (JSchException e) { + throw exception(SESSION_CONNECT_ERROR); + } + return session; + } + + /** + * 配置认证方式(密码或密钥) + */ + private void configureAuthentication(JSch jsch, MachineInfoDO machineInfo) { + if (machineInfo.getAuthenticationType() == AuthenticationType.SECRET_KEY.getCode()) { + // 密钥认证 + if (machineInfo.getSecretKeyId() == null) { + throw exception(SECRET_KEY_NULL); + } + String pubKeyContent = secretKeyService.getKeyContent(machineInfo.getSecretKeyId()); + // 验证秘钥格式 + if (!pubKeyContent.startsWith("-----BEGIN")) { + log.error("无效的密钥格式{}", pubKeyContent); + throw exception(INVALID_kEY_FORMAT); + } + try { + // 尝试加载秘钥私钥 + jsch.addIdentity( + machineInfo.getName(), + pubKeyContent.getBytes(StandardCharsets.UTF_8), + null, + null + ); + log.info("密钥加载成功 {}", machineInfo.getHostIp()); + } catch (JSchException e) { + log.error("密钥加载失败: {}", e.getMessage()); + throw exception(READ_SECRET_CONTENT_ERROR); + } + } else if (machineInfo.getAuthenticationType() == AuthenticationType.PASSWORD.getCode()) { + // 密码认证 + if (!StringUtils.hasText(machineInfo.getPassword())) { + throw exception(PASSWORD_NOT_EXISTS); + } + } else { + log.error("不支持该验证类型:{}", machineInfo.getAuthenticationType()); + throw exception(NOT_SUPPORT_AUTHENTICATION_TYPE); + } + } + + /** + * 配置SSH会话参数(安全增强) + */ + private void configureSession(Session session, MachineInfoDO machineInfo) { + Properties config = new Properties(); + // 根据认证类型配置不同的认证策略 + if (machineInfo.getAuthenticationType() == 1) { // 密码认证 + // 设置密码 + session.setPassword(machineInfo.getPassword()); + config.put("StrictHostKeyChecking", "no"); + // 仅使用密码认证(禁用其他认证方式) + config.put("PreferredAuthentications", "password"); + // 禁用公钥相关配置(避免干扰) + config.put("PubkeyAuthentication", "no"); + } else { // 密钥认证 + try { + String preKeyPath = secretKeyService.getSecretKey(machineInfo.getSecretKeyId()).getPath(); + JSch jsch = new JSch(); + jsch.addIdentity(preKeyPath); // 添加私钥 + // 保持默认认证顺序(公钥优先) + config.put("PreferredAuthentications", "publicKey,password,keyboard-interactive"); + } catch (JSchException e) { + log.error("SSH密钥配置失败", e); + throw exception(SSH_KEY_CONFIGURATION_FAIL); + } + } + config.put("ServerAliveInterval", "30"); // 每30秒发送一次心跳 + config.put("ServerAliveCountMax", "3"); // 允许3次心跳失败 + session.setConfig(config); + } + +} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/WebSocketSessionManager.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/WebSocketSessionManager.java new file mode 100644 index 00000000..e07eab42 --- /dev/null +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/component/WebSocketSessionManager.java @@ -0,0 +1,114 @@ +package cd.casic.module.machine.component; + +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; + +@Component("machineWebSocketSessionManger") +//管理webSocketSession +public class WebSocketSessionManager { + + //webSocketSessionId - WebSocketSession 保存 WebSocketSession 对象与会话 ID 的映射 + private static final ConcurrentHashMap WebSocketSessionMap = new ConcurrentHashMap<>(); + + //webSocketSessionId - WebSocketConnection 与远程机器的会话管理 + private static final ConcurrentHashMap sessionConnectionMap = new ConcurrentHashMap<>(); + + //机器id - WebSocketConnection + private static final ConcurrentHashMap webSocketSessionConnectionMap = new ConcurrentHashMap<>(); + + public static void addWebSocketSession(String sessionId, WebSocketSession session) { + WebSocketSessionMap.put(sessionId, session); + } + + /** + * 获取 WebSocketSession + */ + public static WebSocketSession getWebSocketSession(String sessionId) { + return WebSocketSessionMap.get(sessionId); + } + + /** + * 移除 WebSocketSession + */ + public static void removeWebSocketSession(String sessionId) { + WebSocketSessionMap.remove(sessionId); + } + + /** + * 检查 sessionId 是否存在 + */ + public static boolean containsWebSocketSession(String sessionId) { + return WebSocketSessionMap.containsKey(sessionId); + } + + /** + * 获取所有 WebSocketSession + */ + public static Collection getAllWebSocketSessions() { + return WebSocketSessionMap.values(); + } + + public static void addWebSocketConnection(String sessionId, WebSocketConnection connection) { + sessionConnectionMap.put(sessionId, connection); + } + + /** + * 获取 WebSocketConnection + */ + public static WebSocketConnection getWebSocketConnection(String sessionId) { + return sessionConnectionMap.get(sessionId); + } + + /** + * 移除 WebSocketConnection + */ + public static void removeWebSocketConnection(String sessionId) { + sessionConnectionMap.remove(sessionId); + } + + /** + * 检查 sessionId 是否存在 + */ + public static boolean containsWebSocketConnection(String sessionId) { + return sessionConnectionMap.containsKey(sessionId); + } + + /** + * 获取所有 WebSocketConnection + */ + public static ConcurrentHashMap getAllWebSocketConnections() { + return webSocketSessionConnectionMap; + } + + /** + * 添加 WebSocketConnection + */ + public static void addWebSocketConnectionByMachineId(Long machineId, WebSocketConnection connection) { + webSocketSessionConnectionMap.put(machineId, connection); + } + + /** + * 获取 WebSocketConnection + */ + public static WebSocketConnection getWebSocketConnectionByMachineId(Long machineId) { + return webSocketSessionConnectionMap.get(machineId); + } + + /** + * 移除 WebSocketConnection + */ + public static void removeWebSocketConnectionByMachineId(Long machineId) { + webSocketSessionConnectionMap.remove(machineId); + } + + /** + * 检查 machineId 是否存在 + */ + public static boolean containsMachineId(Long machineId) { + return webSocketSessionConnectionMap.containsKey(machineId); + } + +} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/configuration/AliYunOssConfig.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/configuration/AliYunOssConfig.java index a7ab3183..fc66eda6 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/configuration/AliYunOssConfig.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/configuration/AliYunOssConfig.java @@ -7,7 +7,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration - public class AliYunOssConfig extends S3FileClientConfig{ +public class AliYunOssConfig extends S3FileClientConfig{ @Value("${aliyun.oss.endpoint}") private String endpoint; @Value("${aliyun.oss.accessKeyId}") diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/configuration/WebSocketConfig.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/configuration/WebSocketConfig.java new file mode 100644 index 00000000..c44d5d9e --- /dev/null +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/configuration/WebSocketConfig.java @@ -0,0 +1,40 @@ +package cd.casic.module.machine.configuration; + +import cd.casic.module.machine.Interceptor.WebSocketHandshakeInterceptor; +import cd.casic.module.machine.handler.MachineWebSocketHandler; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; + +@Configuration +@EnableWebSocket +//WebSocket端点配置 +public class WebSocketConfig implements WebSocketConfigurer { + @Resource + private MachineWebSocketHandler machineWebSocketHandler; + + @Resource + private WebSocketHandshakeInterceptor webSocketHandshakeInterceptor; + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(machineWebSocketHandler, "/ssh/terminal") + .addInterceptors(webSocketHandshakeInterceptor) + .setAllowedOrigins("*"); // 允许跨域(生产环境需限制) + } + + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + return new ServletServerContainerFactoryBean(); + } + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/contants/MachineErrorCodeConstants.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/contants/MachineErrorCodeConstants.java index dc6ad9eb..2d14d8e1 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/contants/MachineErrorCodeConstants.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/contants/MachineErrorCodeConstants.java @@ -41,13 +41,33 @@ public interface MachineErrorCodeConstants { // ========== 密钥模块 1-003-004-000 ========== ErrorCode SECRET_KEY_NULL = new ErrorCode(1_003_004_000, "密钥为空"); ErrorCode SECRET_KEY_NOT_EXISTS = new ErrorCode(1_003_004_001, "密钥不存在"); - ErrorCode SECRET_KEY_NAME_ILLEGAL = new ErrorCode(1_003_004_002, "密钥名称不合法"); - ErrorCode SECRET_KEY_PATH_ILLEGAL = new ErrorCode(1_003_004_003, "密钥路径不合法"); - ErrorCode SECRET_KEY_PATH_NULL = new ErrorCode(1_003_004_004, "密钥路径为空"); - ErrorCode SECRET_KEY_UPLOAD_FAIL = new ErrorCode(1_003_004_005, "密钥上传失败"); + ErrorCode SECRET_KEY_PATH_NULL = new ErrorCode(1_003_004_002, "密钥路径为空"); + ErrorCode INVALID_kEY_FORMAT = new ErrorCode(1_003_004_003, "无效的密钥格式"); + ErrorCode READ_SECRET_CONTENT_ERROR = new ErrorCode(1_003_004_004, "读取密钥加载失败"); // ========== 其他模块 1-003-005-000 ========== ErrorCode OSS_PARAM_NULL = new ErrorCode(1_003_005_000, "oss参数无法读取"); - ErrorCode SECRETKEY_NULL = new ErrorCode(1_003_005_001, "密钥为空"); - ErrorCode PARAMETER_ERROR = new ErrorCode(1_003_005_002, "参数错误"); + ErrorCode FILE_UPLOAD_ERROR = new ErrorCode(1_003_005_001, "文件上传失败"); + ErrorCode FILE_DOWNLOAD_ERROR = new ErrorCode(1_003_005_002, "文件下载失败"); + + //========== 会话连接模块 1-003-006-000 ========== + ErrorCode SESSION_CONNECT_ERROR = new ErrorCode(1_003_006_001, "会话连接失败"); + ErrorCode CLOSE_CLOSE_SESSION_ERROR = new ErrorCode(1_003_006_002, "关闭连接失败"); + ErrorCode SESSION_NOT_CONNECT = new ErrorCode(1_003_006_003, "会话未连接"); + ErrorCode EXECUTE_COMMAND_FAIL = new ErrorCode(1_003_006_004, "命令执行失败"); + ErrorCode PASSWORD_NOT_EXISTS = new ErrorCode(1_003_006_005, "密码不存在"); + ErrorCode SSH_KEY_CONFIGURATION_FAIL = new ErrorCode(1_003_006_006, "SSH密钥配置失败"); + ErrorCode NOT_SUPPORT_AUTHENTICATION_TYPE = new ErrorCode(1_003_006_007, "认证类型不支持"); + ErrorCode CREATE_SESSION_ERROR = new ErrorCode(1_003_006_008, "创建会话失败"); + ErrorCode WEBSOCKET_SEND_MESSAGE_ERROR = new ErrorCode(1_003_006_009, "websocket发送消息失败"); + ErrorCode CREATE_CHANEL_ERROR = new ErrorCode(1_003_006_010, "执行通道创建失败"); + ErrorCode CHANEL_CONNECT_FAIL = new ErrorCode(1_003_006_011, "通道连接失败"); + ErrorCode FAILED_TO_PARSE_URL_PARAMETERS = new ErrorCode(1_003_006_012, "解析URL参数失败"); + + //========== 远程文件树模块 1-003-007-000 ========== + ErrorCode NOT_DIRECTORY_NODE = new ErrorCode(1_003_007_001, "非目录节点不能添加子节点"); + ErrorCode READ_REMOTE_DIRECTORY_FAIL = new ErrorCode(1_003_007_002, "读取远程文件目录结构失败"); + ErrorCode CONNECTION_LOST = new ErrorCode(1_003_007_003, "SFTP连接无效,无法列出文件"); + ErrorCode PATH_NOT_EXISTS = new ErrorCode(1_003_007_004, "路径不存在"); + ErrorCode NO_PATH_PERMISSION = new ErrorCode(1_003_007_005, "无路径访问权限"); } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/MachineInfoController.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/MachineInfoController.java index 95d9cb18..8e4fbe4f 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/MachineInfoController.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/MachineInfoController.java @@ -65,8 +65,8 @@ public class MachineInfoController { @DeleteMapping("/delete") @Operation(summary = "机器信息删除") // @PreAuthorize("@ss.hasPermission('ci:machineInfo:delete')") - public CommonResult deleteMachineInfo(@RequestParam("id") Long id) { - machineInfoService.deleteMachineInfo(id); + public CommonResult deleteMachineInfo(@RequestParam("machineInfoId") Long machineInfoId) { + machineInfoService.deleteMachineInfo(machineInfoId); return success(true); } @@ -78,60 +78,54 @@ public class MachineInfoController { return success(true); } - @PostMapping("/test") + @GetMapping("/test") @Operation(summary = "测试机器连接") public CommonResult testConnection(@RequestParam("id") Long id) { return success(machineInfoService.testConnection(id)); } - @GetMapping("/status/{machineName}") + @GetMapping("/status") @Operation(summary = "获取机器连接状态") - public CommonResult getConnectionStatus(@PathVariable String machineName) { - return success(machineInfoService.getConnectionStatus(machineName)); + public CommonResult getConnectionStatus(@RequestParam Long id) { + return success(machineInfoService.getConnectionStatus(id)); } @GetMapping("/status/all") @Operation(summary = "获取所有机器连接状态") - public CommonResult> getAllConnectionStatus() { + public CommonResult> getAllConnectionStatus() { return success(machineInfoService.getAllConnectionStatus()); } - @PostMapping("/connect") - @Operation(summary = "建立机器连接") - public CommonResult connect(@Valid @RequestBody MachineInfoDO machineInfoDO) { - return success(machineInfoService.connect(machineInfoDO)); + @GetMapping("/connect") + @Operation(summary = "建立连接") + public CommonResult> connect(@RequestParam Long id) { + return success(machineInfoService.connect(id)); } - @GetMapping("/disconnect/{sessionId}") - @Operation(summary = "断开机器连接") - public CommonResult disconnect(@PathVariable String sessionId) { - return success(machineInfoService.disconnect(sessionId)); - } - - @GetMapping("/execute/{sessionId}") - @Operation(summary = "执行远程命令") - public CommonResult executeCommand( - @PathVariable String sessionId, - @RequestBody String command) { - - return success(machineInfoService.executeCommand(sessionId, command)); - } - - @GetMapping("/upload/{sessionId}") - @Operation(summary = "上传文件到远程机器") - public CommonResult uploadFile( - @PathVariable String sessionId, - @RequestParam String localFilePath, - @RequestParam String remoteFilePath) { - return success(machineInfoService.uploadFile(sessionId, localFilePath, remoteFilePath)); - } - - @GetMapping("/download/{sessionId}") - @Operation(summary = "从远程机器下载文件") - public CommonResult downloadFile( - @PathVariable String sessionId, - @RequestParam String remoteFilePath, - @RequestParam String localFilePath) { - return success(machineInfoService.downloadFile(sessionId, remoteFilePath, localFilePath)); + @GetMapping("/fileTreeNode") + @Operation(summary = "获得文件树") + public CommonResult> fileTreeNode( + @RequestParam Long machineId, + @RequestParam(required = false, defaultValue = "/") String path + ) { + return CommonResult.success(machineInfoService.fileTreeNode(machineId, path)); } +// @GetMapping("/upload") +// @Operation(summary = "上传文件到远程机器") +// public CommonResult uploadFile( +// @RequestParam String sessionId, +// @RequestParam String localFilePath, +// @RequestParam String remoteFilePath +// ) { +// return success(machineInfoService.uploadFile(sessionId, localFilePath, remoteFilePath)); +// } +// +// @GetMapping("/download") +// @Operation(summary = "从远程机器下载文件") +// public CommonResult downloadFile( +// @RequestParam String sessionId, +// @RequestParam String remoteFilePath, +// @RequestParam String localFilePath) { +// return success(machineInfoService.downloadFile(sessionId, remoteFilePath, localFilePath)); +// } } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/SecretKeyController.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/SecretKeyController.java index 668e05ed..00ae348e 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/SecretKeyController.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/SecretKeyController.java @@ -3,6 +3,7 @@ package cd.casic.module.machine.controller; import cd.casic.framework.commons.pojo.CommonResult; import cd.casic.framework.commons.pojo.PageResult; import cd.casic.framework.commons.util.object.BeanUtils; +import cd.casic.module.machine.dal.dataobject.MachineInfoDO; import cd.casic.module.machine.dal.dataobject.SecretKeyDO; import cd.casic.module.machine.service.SecretKeyService; import cd.casic.module.machine.controller.vo.SecretKeyVO; @@ -11,6 +12,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -51,20 +54,32 @@ public class SecretKeyController { return success(true); } - @PutMapping("unbindMachine") + @PutMapping("/unbindMachine") @Operation(summary = "解绑机器") public CommonResult unbindMachine(@Valid @RequestBody SecretKeyVO secretKeyVO) { secretKeyService.unbindMachine(secretKeyVO); return success(true); } + @GetMapping("/getBindMachine") + @Operation(summary = "获取密钥绑定的机器列表") + public CommonResult> getBindMachine(@RequestParam Long secretKeyId) { + return success(secretKeyService.getBindMachine(secretKeyId)); + } + @GetMapping("/getSecretKey") - @Operation(summary = "获取机器的环境变量") + @Operation(summary = "获取机器的密钥") public CommonResult getSecretKey(@RequestParam("id") Long id) { SecretKeyVO secretKeyVO = secretKeyService.getSecretKey(id); return success(secretKeyVO); } + @DeleteMapping("/delete") + @Operation(summary = "密钥信息删除") + public CommonResult delete(@RequestParam("id") Long id) { + secretKeyService.deleteSecretKey(id); + return success(true); + } @DeleteMapping("/deleteList") @Operation(summary = "批量删除密钥") @@ -76,11 +91,17 @@ public class SecretKeyController { @PostMapping("/list") @Operation(summary = "获取密钥信息列表") - public CommonResult> getSecretKeypage(@Valid @RequestBody SecretKeyVO secretKeyVO) { + public CommonResult> getSecretKeyPage(@Valid @RequestBody SecretKeyVO secretKeyVO) { PageResult pageResult = secretKeyService.getSecretKeypage(secretKeyVO); if (CollUtil.isEmpty(pageResult.getList())) { return success(new PageResult<>(pageResult.getTotal())); } return success(BeanUtils.toBean(pageResult, SecretKeyVO.class)); } + + @GetMapping("/download") + @Operation(summary = "下载密钥文件") + public ResponseEntity downloadSecretFile(@RequestParam("id") Long id) { + return secretKeyService.downloadSecretFile(id); + } } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/vo/SecretKeyVO.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/vo/SecretKeyVO.java index b2f8145f..f3716509 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/vo/SecretKeyVO.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/vo/SecretKeyVO.java @@ -4,7 +4,9 @@ import cd.casic.framework.commons.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import lombok.experimental.Accessors; +import org.springframework.web.multipart.MultipartFile; +import java.io.File; import java.time.LocalDateTime; import java.util.List; diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/convert/MachineEnvConvert.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/convert/MachineEnvConvert.java deleted file mode 100644 index 92a947b4..00000000 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/convert/MachineEnvConvert.java +++ /dev/null @@ -1,25 +0,0 @@ -package cd.casic.module.machine.convert; - -import cd.casic.module.machine.controller.vo.MachineEnvVO; -import cd.casic.module.machine.dal.dataobject.MachineEnvDO; -import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; - -@Mapper -public interface MachineEnvConvert { - - MachineEnvConvert INSTANCE = Mappers.getMapper(MachineEnvConvert.class); - - // 转换实体为VO - default MachineEnvVO convertToVO(MachineEnvDO machineEnvDO) { - MachineEnvVO VO = new MachineEnvVO(); - VO.setId(machineEnvDO.getId()); - VO.setEnvKey(machineEnvDO.getEnvKey()); - VO.setEnvValue(machineEnvDO.getEnvValue()); - VO.setDescription(machineEnvDO.getDescription()); - VO.setCreateTime(machineEnvDO.getCreateTime()); - VO.setUpdateTime(machineEnvDO.getUpdateTime()); - return VO; - } - -} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/model/FileNode.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/model/FileNode.java new file mode 100644 index 00000000..9741fa0d --- /dev/null +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/model/FileNode.java @@ -0,0 +1,56 @@ +package cd.casic.module.machine.dal.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception; +import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*; + +import java.util.ArrayList; +import java.util.List; + +//文件节点类 +@Data +/* + * 远程文件系统的节点信息,用于构建文件树结构 + */ +@Schema(description = "远程文件系统节点") +public class FileNode { + @Schema(description = "文件名或目录名") + private String name; + + @Schema(description = "是否为目录") + private boolean isDirectory; + + @Schema(description = "文件大小(字节)") + private long size; + + @Schema(description = "文件权限字符串(如 rwxr-xr-x)") + private String permissions; + + @Schema(description = "最后修改时间(时间戳,毫秒)") + private long modifiedTime; + + @Schema(description = "子节点列表(仅目录有此属性)") + private List children; + + public FileNode(String name, boolean isDirectory, long size, String permissions, long modifiedTime) { + this.name = name; + this.isDirectory = isDirectory; + this.size = size; + this.permissions = permissions; + this.modifiedTime = modifiedTime; + + if (isDirectory) { + this.children = new ArrayList<>(); + } + + } + + public void addChild(FileNode child) { + if (this.children == null) { + throw exception(NOT_DIRECTORY_NODE); + } + this.children.add(child); + } +} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/mysql/MachineInfoMapper.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/mysql/MachineInfoMapper.java index 5b23cbe8..985a5a81 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/mysql/MachineInfoMapper.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/mysql/MachineInfoMapper.java @@ -48,5 +48,10 @@ public interface MachineInfoMapper extends BaseMapperX { return selectPage(machineInfoVO, machineInfoDOLambdaQueryWrapperX); } + default List selectBindMachineBySecretKey(Long secretKeyId) { + LambdaQueryWrapperX lambdaQueryWrapperX = new LambdaQueryWrapperX() + .eq(MachineInfoDO::getSecretKeyId, secretKeyId); + return selectList(lambdaQueryWrapperX); + } } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/mysql/SecretKeyMapper.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/mysql/SecretKeyMapper.java index a5907cdd..f159c478 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/mysql/SecretKeyMapper.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/dal/mysql/SecretKeyMapper.java @@ -18,7 +18,6 @@ public interface SecretKeyMapper extends BaseMapperX { return selectPage(secretKeyVO, new LambdaQueryWrapperX() .likeIfPresent(SecretKeyDO::getName, secretKeyVO.getName()) .likeIfPresent(SecretKeyDO::getDescription, secretKeyVO.getDescription())); - } default void bindingMachine(Long machineInfoId, List secretKeyId) { diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/enums/ConnectionStatus.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/enums/ConnectionStatus.java index 5e3101fd..a4c81e5f 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/enums/ConnectionStatus.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/enums/ConnectionStatus.java @@ -1,22 +1,33 @@ package cd.casic.module.machine.enums; +import cd.casic.framework.commons.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.Arrays; + /** * 连接状态枚举 */ @Getter @AllArgsConstructor -public enum ConnectionStatus { - DISCONNECTED("断开连接"), - CONNECTING("正在连接"), - CONNECTED("已连接"), - AUTH_FAILED("认证失败"), - CONNECTION_TIMEOUT("连接超时"), - CONNECTION_ERROR("连接错误"), - CLOSED("已关闭"); +public enum ConnectionStatus implements IntArrayValuable { + DISCONNECTED(1, "断开连接"), + CONNECTING(2, "正在连接"), + CONNECTED(3, "已连接"), + AUTH_FAILED(4, "认证失败"), + CONNECTION_TIMEOUT(5, "连接超时"), + CONNECTION_ERROR(6, "连接错误"), + CLOSED(7, "已关闭"); - private final String description; + private final int code; + private final String message; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ConnectionStatus::getCode).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/enums/SSHChanelType.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/enums/SSHChanelType.java new file mode 100644 index 00000000..c915797e --- /dev/null +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/enums/SSHChanelType.java @@ -0,0 +1,35 @@ +package cd.casic.module.machine.enums; + +import cd.casic.framework.commons.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +//ssh远程连接管道类型 +public enum SSHChanelType implements IntArrayValuable { + EXEC(1, "单条命令返回结果"), + SHELL(2, "多行命令持续交互"), + SUBSYSTEM(3, "文件传输或其它特点服务"), + DIRECT_TCP_IP(4, "TCP 端口转发"), + X11(5, "X Window 系统转发"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SSHChanelType::getCode).toArray(); + + /** + * 状态值 + */ + private final Integer code; + + /** + * 状态名 + */ + private final String message; + + @Override + public int[] array() { + return ARRAYS; + } +} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/handler/ConnectionSession.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/handler/ConnectionSession.java deleted file mode 100644 index 4c5e87e6..00000000 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/handler/ConnectionSession.java +++ /dev/null @@ -1,642 +0,0 @@ -package cd.casic.module.machine.handler; -import cd.casic.module.machine.controller.vo.SecretKeyVO; -import cd.casic.module.machine.dal.dataobject.MachineInfoDO; -import cd.casic.module.machine.enums.AuthenticationType; -import cd.casic.module.machine.utils.AliYunOssClient; -import cd.casic.module.machine.enums.ConnectionStatus; -import cd.casic.module.machine.service.SecretKeyService; -import com.jcraft.jsch.*; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.util.StreamUtils; -import org.springframework.util.StringUtils; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicBoolean; - -import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception; -import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*; -/** - * 优化后的SSH连接会话类 - */ -@Slf4j -@Component -public class ConnectionSession implements AutoCloseable { - @Resource - SecretKeyService secretKeyService; - - @Resource - AliYunOssClient aliYunOssClient; - - private MachineInfoDO machineInfo; - private Session sshSession; - private ConnectionStatus status = ConnectionStatus.DISCONNECTED; - private final AtomicBoolean isExecuting = new AtomicBoolean(false); - - // todo连接配置常量 - private static final int CONNECTION_TIMEOUT = 5000; // 连接超时时间(毫秒) - private static final int COMMAND_TIMEOUT = 30000; // 命令执行超时时间(毫秒) - private static final int RETRY_COUNT = 3; // 重试次数 - private static final int RETRY_DELAY = 1000; // 重试间隔(毫秒) - - public ConnectionSession() { - - } - - // 使用setter注入MachineInfo - public void setMachineInfo(MachineInfoDO machineInfo) { - this.machineInfo = Objects.requireNonNull(machineInfo, "MachineInfo cannot be null"); - log.debug("MachineInfo 已注入: {}", machineInfo.getHostIp()); - } - - - - /** - * 建立SSH连接,支持重试机制 - */ - public synchronized void connect() throws JSchException { - if (status == ConnectionStatus.CONNECTED) { - log.debug("已经连接到 {}", machineInfo.getHostIp()); - return; - } - - status = ConnectionStatus.CONNECTING; - JSchException lastException = null; - - for (int attempt = 1; attempt <= RETRY_COUNT; attempt++) { - try { - doConnect(); - status = ConnectionStatus.CONNECTED; - log.info("SSH connection established successfully to {} (attempt {}/{})", - machineInfo.getHostIp(), attempt, RETRY_COUNT); - return; - } catch (JSchException e) { - lastException = e; - status = ConnectionStatus.CONNECTION_ERROR; - log.error("SSH connection attempt {}/{} failed: {}", - attempt, RETRY_COUNT, e.getMessage()); - - // 认证失败直接退出,无需重试 - if (e.getMessage().contains("Auth fail")) { - status = ConnectionStatus.AUTH_FAILED; - throw e; - } - - // 重试前等待 - if (attempt < RETRY_COUNT) { - try { - Thread.sleep(RETRY_DELAY); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new JSchException("Connection attempt interrupted", ie); - } - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - // 所有重试都失败 - throw new JSchException("Failed to connect after " + RETRY_COUNT + " attempts", lastException); - } - - /** - * 实际执行连接逻辑 - */ - private void doConnect() throws JSchException, IOException { - JSch jsch = new JSch(); - - // 配置认证方式 - configureAuthentication(jsch); - - // 创建SSH会话 - sshSession = jsch.getSession( - machineInfo.getUsername(), - machineInfo.getHostIp(), - machineInfo.getSshPort() != null ? machineInfo.getSshPort() : 22 - ); - - // 配置连接参数 - configureSession(sshSession); - - // 建立连接 - sshSession.connect(CONNECTION_TIMEOUT); - } - - /** - * 配置认证方式(密码或密钥) - */ - private void configureAuthentication(JSch jsch) throws JSchException { - if (machineInfo.getAuthenticationType() == AuthenticationType.SECRET_KEY.getCode()) { - // 密钥认证 - if (machineInfo.getSecretKeyId() == null) { - throw exception(SECRET_KEY_NULL); - } - - String privateKeyContent = getPrivateKeyContent(machineInfo.getSecretKeyId()); - // 验证私钥格式 - if (!privateKeyContent.startsWith("-----BEGIN")) { - throw new JSchException("Invalid private key format. Expected OpenSSH format."); - } - - try { - // 尝试加载私钥 - jsch.addIdentity( - machineInfo.getName(), - privateKeyContent.getBytes(StandardCharsets.UTF_8), - null, - null - ); - log.info("Private key loaded successfully for {}", machineInfo.getHostIp()); - } catch (JSchException e) { - log.error("Failed to load private key: {}", e.getMessage()); - throw e; - } - } else if (machineInfo.getAuthenticationType() == AuthenticationType.PASSWORD.getCode()) { - // 密码认证 - if (StringUtils.isEmpty(machineInfo.getPassword())) { - throw new JSchException("Password is required for password-based authentication"); - } - } else { - throw new JSchException("Unsupported authentication type: " + machineInfo.getAuthenticationType()); - } - } - - /** - * 配置SSH会话参数(安全增强) - */ - private void configureSession(Session session) { - Properties config = new Properties(); - - // 安全增强:默认验证主机密钥 - if (isTrustedEnvironment()) { - log.warn("Running in trusted environment - disabling strict host key checking for {}", - machineInfo.getHostIp()); - config.put("StrictHostKeyChecking", "no"); - } else { - config.put("StrictHostKeyChecking", "yes"); - // 可选:配置已知主机文件路径 - //直接配置阿里云密钥地址 - config.put("UserKnownHostsFile", secretKeyService.getSecretKey(machineInfo.getSecretKeyId()).getPath()); - } - - // 其他安全配置 - config.put("PreferredAuthentications", "publicKey,password,keyboard-interactive"); - config.put("ServerAliveInterval", "30"); // 每30秒发送一次心跳 - config.put("ServerAliveCountMax", "3"); // 允许3次心跳失败 - - session.setConfig(config); - } - - /** - * 判断是否为可信环境(生产环境应返回false) - */ - private boolean isTrustedEnvironment() { - // todo实际项目中应基于配置或环境变量判断 - return System.getProperty("environment", "production").equalsIgnoreCase("development"); - } - - @Override - public synchronized void close() { - disconnect(); - } - - public synchronized void disconnect() { - if (sshSession != null && sshSession.isConnected()) { - try { - sshSession.disconnect(); - log.info("SSH connection closed: {}", machineInfo.getHostIp()); - } catch (Exception e) { - log.error("Error closing SSH session: {}", e.getMessage()); - } - } - status = ConnectionStatus.DISCONNECTED; - } - - /** - * 执行远程命令,支持超时和中断处理 - */ - public String executeCommand(String command) throws JSchException, IOException { - if (!isConnected()) { - throw new IllegalStateException("Session is not connected"); - } - - if (!isExecuting.compareAndSet(false, true)) { - throw new IllegalStateException("Another command is already executing"); - } - - Channel channel = null; - InputStream inputStream = null; - ByteArrayOutputStream outputStream = null; - - try { - channel = createExecChannel(command); - inputStream = channel.getInputStream(); - outputStream = new ByteArrayOutputStream(); - - // 连接通道并设置超时 - channel.connect(COMMAND_TIMEOUT); - - // 读取命令输出 - return readCommandOutput(inputStream, outputStream, channel); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Command execution interrupted", e); - } finally { - // 释放资源 - closeResources(channel, inputStream, outputStream); - isExecuting.set(false); - } - } - - /** - * 创建并配置命令执行通道 - */ - private Channel createExecChannel(String command) throws JSchException { - Channel channel = sshSession.openChannel("exec"); - ((ChannelExec) channel).setCommand(command); - - // 配置通道 - channel.setInputStream(null); - ((ChannelExec) channel).setErrStream(new ByteArrayOutputStream()); // 捕获错误输出 - - return channel; - } - - /** - * 读取命令输出 - */ - private String readCommandOutput(InputStream inputStream, - ByteArrayOutputStream outputStream, - Channel channel) throws IOException, InterruptedException { - byte[] buffer = new byte[1024]; - - - // 使用线程中断机制实现超时控制 - Thread readerThread = new Thread(() -> { - int bytesRead; - try { - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - } catch (IOException e) { - // 通道关闭或读取异常 - if (channel.isConnected()) { - log.warn("Error reading command output: {}", e.getMessage()); - } - } - }); - - readerThread.start(); - - // 等待命令执行完成或超时 - readerThread.join(COMMAND_TIMEOUT); - - // 如果线程仍在运行,中断并关闭通道 - if (readerThread.isAlive()) { - readerThread.interrupt(); - channel.disconnect(); - throw new IOException("Command execution timed out after " + COMMAND_TIMEOUT + "ms"); - } - - // 等待通道完全关闭 - while (channel.isConnected()) { - Thread.sleep(100); - } - - return outputStream.toString(StandardCharsets.UTF_8); - } - - /** - * 关闭资源 - */ - private void closeResources(Channel channel, InputStream inputStream, OutputStream outputStream) { - if (outputStream != null) { - try { - outputStream.close(); - } catch (IOException e) { - log.warn("Error closing output stream: {}", e.getMessage()); - } - } - - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - log.warn("Error closing input stream: {}", e.getMessage()); - } - } - - if (channel != null && channel.isConnected()) { - channel.disconnect(); - } - } - - /** - * 上传文件到远程服务器 - */ - public boolean uploadFile(String localFilePath, String remoteFilePath) throws IOException { - if (!isConnected()) { - throw new IllegalStateException("Cannot upload file: SSH session is not connected"); - } - - // 检查本地文件是否存在且可读 - File localFile = new File(localFilePath); - if (!localFile.exists()) { - throw new FileNotFoundException("Local file not found: " + localFilePath); - } - if (!localFile.canRead()) { - throw new IOException("Cannot read local file: " + localFilePath); - } - - ChannelSftp channel = null; - boolean uploadSuccess = false; - - try { - // 创建并连接SFTP通道,设置超时 - channel = (ChannelSftp) sshSession.openChannel("sftp"); - channel.connect(CONNECTION_TIMEOUT); - - // 确保目标目录存在 - createRemoteDirectoryIfNotExists(channel, getParentDirectory(remoteFilePath)); - - // 使用更健壮的上传方式 - channel.put( - new FileInputStream(localFile), - remoteFilePath, - new ProgressMonitorAdapter(localFile.length()), - ChannelSftp.OVERWRITE - ); - - uploadSuccess = true; - log.info("File uploaded successfully: {} -> {}", localFilePath, remoteFilePath); - return true; - - } catch (SftpException e) { - log.error("SFTP error during file upload ({} -> {}): {}", - localFilePath, remoteFilePath, e.getMessage(), e); - throw new IOException("SFTP error: " + e.getMessage(), e); - } catch (FileNotFoundException e) { - log.error("Local file not found during upload: {}", localFilePath, e); - throw e; - } catch (IOException e) { - log.error("IO error during file upload: {}", e.getMessage(), e); - throw e; - } catch (Exception e) { - log.error("Unexpected error during file upload: {}", e.getMessage(), e); - throw new IOException("Unexpected error: " + e.getMessage(), e); - } finally { - // 确保通道始终被关闭 - disconnectChannel(channel); - -// // 如果上传失败,尝试删除不完整的文件 -// if (!uploadSuccess && remoteFilePath != null && !remoteFilePath.isEmpty()) { -// tryDeleteIncompleteFile(remoteFilePath); -// } - } - } - - public boolean downloadFile(String remoteFilePath, String localFilePath) throws IOException { - if (!isConnected()) { - throw new IllegalStateException("Cannot download file: SSH session is not connected"); - } - - // 检查本地目录是否存在且可写 - File localFile = new File(localFilePath); - File parentDir = localFile.getParentFile(); - if (parentDir != null && !parentDir.exists()) { - if (!parentDir.mkdirs()) { - throw new IOException("Failed to create local directory: " + parentDir.getAbsolutePath()); - } - } - if (parentDir != null && !parentDir.canWrite()) { - throw new IOException("Cannot write to local directory: " + parentDir.getAbsolutePath()); - } - - ChannelSftp channel = null; - boolean downloadSuccess = false; - File tempFile = null; - - try { - // 创建并连接SFTP通道,设置超时 - channel = (ChannelSftp) sshSession.openChannel("sftp"); - channel.connect(CONNECTION_TIMEOUT); - - // 检查远程文件是否存在 - SftpATTRS attrs = channel.stat(remoteFilePath); - long fileSize = attrs.getSize(); - - // 使用临时文件避免部分下载覆盖完整文件 - tempFile = new File(localFilePath + ".part"); - - // 执行下载并监控进度 - channel.get( - remoteFilePath, - new FileOutputStream(tempFile).toString(), - new ProgressMonitorAdapter(fileSize), - ChannelSftp.OVERWRITE - ); - - // 验证下载完整性 - if (tempFile.length() != fileSize) { - throw new IOException("Download incomplete: expected " + fileSize + - " bytes, but got " + tempFile.length() + " bytes"); - } - - // 重命名临时文件为目标文件(原子操作) - if (!tempFile.renameTo(localFile)) { - throw new IOException("Failed to rename temporary file to: " + localFilePath); - } - - downloadSuccess = true; - log.info("File downloaded successfully: {} -> {}", remoteFilePath, localFilePath); - return true; - - } catch (SftpException e) { - log.error("SFTP error during file download ({} -> {}): {}", - remoteFilePath, localFilePath, e.getMessage(), e); - if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { - throw new FileNotFoundException("Remote file not found: " + remoteFilePath); - } - throw new IOException("SFTP error: " + e.getMessage(), e); - } catch (IOException e) { - log.error("IO error during file download: {}", e.getMessage(), e); - throw e; - } catch (Exception e) { - log.error("Unexpected error during file download: {}", e.getMessage(), e); - throw new IOException("Unexpected error: " + e.getMessage(), e); - } finally { - // 确保通道始终被关闭 - disconnectChannel(channel); - - // 如果下载失败,清理临时文件 - if (!downloadSuccess && tempFile != null && tempFile.exists()) { - if (tempFile.delete()) { - log.debug("Deleted incomplete temporary file: {}", tempFile.getAbsolutePath()); - } else { - log.warn("Failed to delete incomplete temporary file: {}", tempFile.getAbsolutePath()); - } - } - } - } - - // 创建远程目录(如果不存在) - private void createRemoteDirectoryIfNotExists(ChannelSftp channel, String directory) throws SftpException { - if (directory == null || directory.isEmpty() || directory.equals("/")) { - return; - } - - try { - channel.stat(directory); - } catch (SftpException e) { - // 目录不存在,尝试创建 - createRemoteDirectoryIfNotExists(channel, getParentDirectory(directory)); - channel.mkdir(directory); - log.debug("Created remote directory: {}", directory); - } - } - - // 获取路径的父目录 - private String getParentDirectory(String path) { - int lastSlash = path.lastIndexOf('/'); - return lastSlash > 0 ? path.substring(0, lastSlash) : ""; - } - - // 断开SFTP通道 - private void disconnectChannel(Channel channel) { - if (channel != null && channel.isConnected()) { - try { - channel.disconnect(); - log.debug("SFTP channel disconnected"); - } catch (Exception e) { - log.warn("Error disconnecting SFTP channel: {}", e.getMessage()); - } - } - } - - // 尝试删除不完整的文件 - private void tryDeleteIncompleteFile(String remoteFilePath) { - ChannelSftp channel = null; - try { - channel = (ChannelSftp) sshSession.openChannel("sftp"); - channel.connect(CONNECTION_TIMEOUT); - channel.rm(remoteFilePath); - log.info("Deleted incomplete file: {}", remoteFilePath); - } catch (Exception e) { - log.warn("Failed to delete incomplete file {}: {}", remoteFilePath, e.getMessage()); - } finally { - disconnectChannel(channel); - } - } - - // 增强的进度监控器 - private static class ProgressMonitorAdapter implements SftpProgressMonitor { - private final long totalBytes; - private long bytesWritten = 0; - private int lastProgress = 0; - private final long startTime = System.currentTimeMillis(); - - public ProgressMonitorAdapter(long totalBytes) { - this.totalBytes = totalBytes; - } - - @Override - public boolean count(long count) { - bytesWritten += count; - - // 计算进度百分比 - int progress = (int) ((bytesWritten * 100) / totalBytes); - - // 每10%或每秒更新一次日志 - long elapsedTime = System.currentTimeMillis() - startTime; - if (progress - lastProgress >= 10 || elapsedTime >= 1000) { - double speed = bytesWritten / (elapsedTime / 1000.0); - String speedStr = formatTransferSpeed(speed); - - log.debug("Upload progress: {}% ({}/{} bytes, {})", - progress, bytesWritten, totalBytes, speedStr); - lastProgress = progress; - } - - return true; // 返回true继续传输,返回false中断传输 - } - - @Override - public void end() { - long elapsedTime = System.currentTimeMillis() - startTime; - double speed = totalBytes / (elapsedTime / 1000.0); - String speedStr = formatTransferSpeed(speed); - - log.info("Upload completed: {} bytes in {} ms (avg speed: {})", - totalBytes, elapsedTime, speedStr); - } - - @Override - public void init(int op, String src, String dest, long max) { - log.info("Starting upload: {} -> {} ({} bytes)", src, dest, max); - } - - // 格式化传输速度 - private String formatTransferSpeed(double bytesPerSecond) { - String[] units = {"B/s", "KB/s", "MB/s", "GB/s"}; - int unitIndex = 0; - - while (bytesPerSecond >= 1024 && unitIndex < units.length - 1) { - bytesPerSecond /= 1024; - unitIndex++; - } - - return String.format("%.2f %s", bytesPerSecond, units[unitIndex]); - } - } - - - /** - * 检查连接状态 - */ - public ConnectionStatus getStatus() { - if (status == ConnectionStatus.CONNECTED && - (sshSession == null || !sshSession.isConnected())) { - status = ConnectionStatus.DISCONNECTED; - } - return status; - } - - /** - * 检查是否已连接 - */ - public boolean isConnected() { - return status == ConnectionStatus.CONNECTED && - sshSession != null && - sshSession.isConnected(); - } - - - private String getPrivateKeyContent(Long secretKeyId) { - if (secretKeyId == null) { - return null; - } - SecretKeyVO secretKey = secretKeyService.getSecretKey(secretKeyId); - byte[] content; - try { - content = aliYunOssClient.getContent(secretKey.getPath().substring(secretKey.getPath().lastIndexOf("/") + 1)); - } catch (Exception e) { - throw new RuntimeException(e); - } - - //改为S3FileClient读取 - InputStream read = new ByteArrayInputStream(content); - - try { - return StreamUtils.copyToString(read, StandardCharsets.UTF_8); - } catch (IOException e) { - log.error("读取私钥文件失败", e); - throw new RuntimeException(e); - } - - - } -} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/handler/MachineWebSocketHandler.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/handler/MachineWebSocketHandler.java new file mode 100644 index 00000000..acd353ea --- /dev/null +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/handler/MachineWebSocketHandler.java @@ -0,0 +1,67 @@ +package cd.casic.module.machine.handler; + +import cd.casic.module.machine.component.WebSocketConnection; +import cd.casic.module.machine.component.WebSocketSessionManager; +import cd.casic.module.machine.enums.ConnectionStatus; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; + +import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception; +import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*; + +//WebSocket处理器 +@Component("machineWebSocketHandler") +public class MachineWebSocketHandler extends TextWebSocketHandler { + + @Override + public void afterConnectionEstablished(@NotNull WebSocketSession webSocketSession) { + Long machineId = (Long) webSocketSession.getAttributes().get("machineId"); + //保存webSocketSession信息 + WebSocketSessionManager.addWebSocketSession(webSocketSession.getId(), webSocketSession); + WebSocketSessionManager.addWebSocketConnection(webSocketSession.getId(), WebSocketSessionManager.getWebSocketConnectionByMachineId(machineId)); + try { + webSocketSession.sendMessage(new TextMessage("欢迎连接到 WebSocket 服务器!")); + } catch (IOException e) { + throw exception(WEBSOCKET_SEND_MESSAGE_ERROR); + } + } + + // 处理文本消息 + @Override + protected void handleTextMessage(WebSocketSession webSocketSession, TextMessage message) { + String payload = message.getPayload(); + String sessionId = webSocketSession.getId(); + // 从管理器获取连接 + WebSocketConnection webSocketConnection = WebSocketSessionManager.getWebSocketConnection(sessionId); + + if (webSocketConnection != null && ConnectionStatus.CONNECTING.equals(webSocketConnection.getConnectionStatus())) { + // 转发消息到远程机器 + webSocketConnection.executeCommand(webSocketSession, payload); + } else if (webSocketConnection != null) { + webSocketConnection.sendErrorMessage(webSocketSession, "连接已断开,无法发送消息"); + } else { + throw exception(WEBSOCKET_SEND_MESSAGE_ERROR); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession webSocketSession, @NotNull CloseStatus status) { + String sessionId = webSocketSession.getId(); + // 获取并关闭相关的 WebSocketConnection + WebSocketConnection webSocketConnection = WebSocketSessionManager.getWebSocketConnection(sessionId); + if (webSocketConnection != null) { + webSocketConnection.disconnect(); + } + // 从管理器中移除会话和连接 + WebSocketSessionManager.removeWebSocketSession(sessionId); + WebSocketSessionManager.removeWebSocketConnection(sessionId); + Long machineInfoId = (Long) webSocketSession.getAttributes().get("machineId"); + WebSocketSessionManager.removeWebSocketConnectionByMachineId(machineInfoId); + } +} diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/MachineInfoService.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/MachineInfoService.java index b86538e0..6b54e02a 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/MachineInfoService.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/MachineInfoService.java @@ -5,15 +5,21 @@ import cd.casic.module.machine.controller.vo.MachineInfoVO; import cd.casic.module.machine.dal.dataobject.MachineInfoDO; import cd.casic.module.machine.enums.ConnectionStatus; import jakarta.validation.Valid; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; import java.util.List; import java.util.Map; public interface MachineInfoService { + /** + * 新增机器 + * @return 新增机器的id + */ Long createMachine(MachineInfoVO MachineInfoVO); + /** + * 查看机器列表 + * @return 分院 + */ PageResult listMachineInfo(@Valid MachineInfoVO MachineInfoVO); void updateMachineInfo(@Valid MachineInfoVO machineInfoVO); @@ -35,63 +41,61 @@ public interface MachineInfoService { */ boolean testConnection(Long id); + /** + * 连接远程机器 + * + * @param id 机器id + * @return 连接后端文件树 + */ + Map connect(Long id); + /** * 获取机器连接状态 * - * @param machineName 机器名称 * @return 连接状态 */ - ConnectionStatus getConnectionStatus(String machineName); + ConnectionStatus getConnectionStatus(Long id); /** * 获取所有连接状态 * * @return 机器名称到连接状态的映射 */ - Map getAllConnectionStatus(); + Map getAllConnectionStatus(); + +// /** +// * 上传文件到远程机器 +// * +// * @param sessionId 会话ID +// * @param localFilePath 本地文件路径 +// * @param remoteFilePath 远程文件路径 +// * @return 操作结果 +// */ +// boolean uploadFile(String sessionId, String localFilePath, String remoteFilePath); +// +// /** +// * 从远程机器下载文件 +// * +// * @param sessionId 会话ID +// * @param remoteFilePath 远程文件路径 +// * @param localFilePath 本地文件路径 +// * @return 操作结果 +// */ +// boolean downloadFile(String sessionId, String remoteFilePath, String localFilePath); /** - * 建立机器连接 + * 校验机器是否存在 * - * @param machineInfoDO 机器信息 - * @return 连接会话ID + * @param id 机器id */ - String connect(MachineInfoDO machineInfoDO); + MachineInfoDO validateMachineInfoExists(Long id); /** - * 断开机器连接 + * 根据路径获得远程文件树 * - * @param sessionId 会话ID - * @return 操作结果 + * @param machineId 机器id + * @param path 文件夹路径 + * @return 远程文件树 */ - boolean disconnect(String sessionId); - - /** - * 执行远程命令 - * - * @param sessionId 会话ID - * @param command 命令 - * @return 命令执行结果 - */ - String executeCommand(String sessionId, String command); - - /** - * 上传文件到远程机器 - * - * @param sessionId 会话ID - * @param localFilePath 本地文件路径 - * @param remoteFilePath 远程文件路径 - * @return 操作结果 - */ - boolean uploadFile(String sessionId, String localFilePath, String remoteFilePath); - - /** - * 从远程机器下载文件 - * - * @param sessionId 会话ID - * @param remoteFilePath 远程文件路径 - * @param localFilePath 本地文件路径 - * @return 操作结果 - */ - boolean downloadFile(String sessionId, String remoteFilePath, String localFilePath); + Map fileTreeNode(Long machineId, String path); } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/SecretKeyService.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/SecretKeyService.java index 0307cb28..98bd5009 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/SecretKeyService.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/SecretKeyService.java @@ -1,9 +1,12 @@ package cd.casic.module.machine.service; import cd.casic.framework.commons.pojo.PageResult; +import cd.casic.module.machine.dal.dataobject.MachineInfoDO; import cd.casic.module.machine.dal.dataobject.SecretKeyDO; import cd.casic.module.machine.controller.vo.SecretKeyVO; import jakarta.validation.Valid; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.ResponseEntity; import java.util.List; @@ -20,6 +23,14 @@ public interface SecretKeyService { SecretKeyVO getSecretKey(Long id); + void deleteSecretKey(Long id); + void unbindMachine(@Valid SecretKeyVO secretKeyVO); + + ResponseEntity downloadSecretFile(Long id); + + List getBindMachine(Long secretKeyId); + + String getKeyContent(Long secretKeyId); } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/MachineInfoServiceImpl.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/MachineInfoServiceImpl.java index 69b397be..9f8f430a 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/MachineInfoServiceImpl.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/MachineInfoServiceImpl.java @@ -1,18 +1,21 @@ package cd.casic.module.machine.service.impl; import cd.casic.framework.commons.pojo.PageResult; +import cd.casic.module.machine.component.FileTreeComponent; +import cd.casic.module.machine.component.WebSocketSessionManager; import cd.casic.module.machine.controller.vo.SecretKeyVO; import cd.casic.module.machine.enums.AuthenticationType; import cd.casic.module.machine.enums.MachineInfoType; -import cd.casic.module.machine.handler.ConnectionSession; import cd.casic.module.machine.dal.mysql.MachineInfoMapper; import cd.casic.module.machine.controller.vo.MachineInfoVO; import cd.casic.module.machine.dal.dataobject.MachineInfoDO; import cd.casic.module.machine.enums.ConnectionStatus; import cd.casic.module.machine.enums.MachineInfoStatus; +import cd.casic.module.machine.component.WebSocketConnection; import cd.casic.module.machine.service.MachineInfoService; import cd.casic.module.machine.service.SecretKeyService; import com.google.common.annotations.VisibleForTesting; +import com.jcraft.jsch.Session; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import cd.casic.framework.commons.util.object.BeanUtils; @@ -20,8 +23,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception; import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*; @@ -35,24 +37,12 @@ public class MachineInfoServiceImpl implements MachineInfoService { @Resource private SecretKeyService secretKeyService; + @Resource private MachineInfoMapper machineInfoMapper; + @Resource - private ConnectionSession connectionSession; - /** - * 会话ID生成器 - */ - private final AtomicInteger sessionIdGenerator = new AtomicInteger(1000); - - /** - * 会话管理:会话ID -> 连接会话 - */ - private final Map sessions = new ConcurrentHashMap<>(); - - /** - * 机器名称 -> 会话ID - */ - private final Map machineSessionMapping = new ConcurrentHashMap<>(); + private FileTreeComponent fileTreeComponent; @Override public Long createMachine(MachineInfoVO machineInfoVO) { @@ -124,162 +114,68 @@ public class MachineInfoServiceImpl implements MachineInfoService { MachineInfoDO machineInfoDO = validateMachineInfoExists(id); validateMachineUnEnable(machineInfoDO); log.info("测试机器连接: {}", machineInfoDO.getHostIp()); - connectionSession.setMachineInfo(machineInfoDO); - try { - connectionSession.connect(); - return true; - } catch (Exception e) { - log.error("机器连接测试失败: {}", e.getMessage(), e); - return false; - } + WebSocketConnection webSocketConnection = createWebSocketConnection(machineInfoDO); + webSocketConnection.initConnection(machineInfoDO); + return true; } @Override - public ConnectionStatus getConnectionStatus(String machineName) { - String sessionId = machineSessionMapping.get(machineName); - if (sessionId == null) { - return ConnectionStatus.DISCONNECTED; - } - - ConnectionSession session = sessions.get(sessionId); - return session != null ? session.getStatus() : ConnectionStatus.DISCONNECTED; + public Map connect(Long id) { + //todo使用代理机器的情况还未完成 + WebSocketConnection webSocketConnection = new WebSocketConnection(this.secretKeyService); + MachineInfoDO machineInfoDO = validateMachineInfoExists(id); + //初始化连接 + webSocketConnection.initConnection(machineInfoDO); + WebSocketSessionManager.addWebSocketConnectionByMachineId(id, webSocketConnection); + return fileTreeComponent.getRemoteFileTree(webSocketConnection.getSshSession(), "/"); } @Override - public Map getAllConnectionStatus() { - Map result = new HashMap<>(); - - machineSessionMapping.forEach((machineName, sessionId) -> { - ConnectionSession session = sessions.get(sessionId); - result.put(machineName, session != null ? session.getStatus() : ConnectionStatus.DISCONNECTED); - }); - - return result; + public ConnectionStatus getConnectionStatus(Long id) { + validateMachineInfoExists(id); + WebSocketConnection webSocketConnection = WebSocketSessionManager.getWebSocketConnectionByMachineId(id); + return webSocketConnection == null ? ConnectionStatus.DISCONNECTED : webSocketConnection.getConnectionStatus(); } @Override - public String connect(MachineInfoDO machineInfoDO) { - //先查询机器是否存在,在判断机器可用性 - validateMachineUnEnable(machineInfoDO); - log.info("建立机器连接: {}", machineInfoDO.getHostIp()); - - // 检查是否已连接 - String existingSessionId = machineSessionMapping.get(machineInfoDO.getName()); - if (existingSessionId != null) { - ConnectionSession existingSession = sessions.get(existingSessionId); - if (existingSession != null && existingSession.getStatus() == ConnectionStatus.CONNECTED) { - log.info("机器已连接,返回现有会话: {}", machineInfoDO.getHostIp()); - return existingSessionId; - } - } - - try { - connectionSession.setMachineInfo(machineInfoDO); - - connectionSession.connect(); - - // 生成会话ID - String sessionId = generateSessionId(); - - // 保存会话 - sessions.put(sessionId, connectionSession); - machineSessionMapping.put(machineInfoDO.getName(), sessionId); - - log.info("机器连接成功: {}, 会话ID: {}", machineInfoDO.getHostIp(), sessionId); - return sessionId; - } catch (Exception e) { - log.error("机器连接失败: {}", e.getMessage(), e); - throw new RuntimeException("机器连接失败: " + e.getMessage(), e); - } + public Map getAllConnectionStatus() { + return WebSocketSessionManager.getAllWebSocketConnections().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().getConnectionStatus() + )); } - @Override - public boolean disconnect(String sessionId) { - log.info("断开机器连接: {}", sessionId); +// @Override +// public boolean uploadFile(String sessionId, String localFilePath, String remoteFilePath) { +// log.info("上传文件: {} -> {}, 会话ID: {}", localFilePath, remoteFilePath, sessionId); +// WebSocketConnection webSocketConnection = sessionConnectionMap.get(sessionId); +// if (webSocketConnection == null||webSocketConnection.getConnectionStatus() != ConnectionStatus.CONNECTED) { +// throw exception(SESSION_NOT_CONNECT); +// } +// try { +// return webSocketConnection.uploadFile(localFilePath, remoteFilePath); +// } catch (Exception e) { +// log.error("文件上传失败: {}", e.getMessage(), e); +// throw exception(FILE_UPLOAD_ERROR); +// } +// } - ConnectionSession session = sessions.get(sessionId); - if (session == null) { - log.warn("会话不存在: {}", sessionId); - return false; - } - - try { - session.disconnect(); - - // 清理会话 - sessions.remove(sessionId); - machineSessionMapping.entrySet().removeIf(entry -> entry.getValue().equals(sessionId)); - - log.info("机器连接已断开: {}", sessionId); - return true; - } catch (Exception e) { - log.error("断开连接失败: {}", e.getMessage(), e); - return false; - } - } - - @Override - public String executeCommand(String sessionId, String command) { - log.info("执行命令: {}, 会话ID: {}", command, sessionId); - - ConnectionSession session = sessions.get(sessionId); - if (session == null) { - throw new RuntimeException("会话不存在: " + sessionId); - } - - if (session.getStatus() != ConnectionStatus.CONNECTED) { - throw new RuntimeException("会话未连接: " + sessionId); - } - - try { - return session.executeCommand(command); - } catch (Exception e) { - log.error("命令执行失败: {}", e.getMessage(), e); - throw new RuntimeException("命令执行失败: " + e.getMessage(), e); - } - } - - @Override - public boolean uploadFile(String sessionId, String localFilePath, String remoteFilePath) { - log.info("上传文件: {} -> {}, 会话ID: {}", localFilePath, remoteFilePath, sessionId); - - ConnectionSession session = sessions.get(sessionId); - if (session == null) { - throw new RuntimeException("会话不存在: " + sessionId); - } - - if (session.getStatus() != ConnectionStatus.CONNECTED) { - throw new RuntimeException("会话未连接: " + sessionId); - } - - try { - return session.uploadFile(localFilePath, remoteFilePath); - } catch (Exception e) { - log.error("文件上传失败: {}", e.getMessage(), e); - throw new RuntimeException("文件上传失败: " + e.getMessage(), e); - } - } - - @Override - public boolean downloadFile(String sessionId, String remoteFilePath, String localFilePath) { - log.info("下载文件: {} -> {}, 会话ID: {}", remoteFilePath, localFilePath, sessionId); - - ConnectionSession session = sessions.get(sessionId); - if (session == null) { - throw new RuntimeException("会话不存在: " + sessionId); - } - - if (session.getStatus() != ConnectionStatus.CONNECTED) { - throw new RuntimeException("会话未连接: " + sessionId); - } - - try { - return session.downloadFile(remoteFilePath, localFilePath); - } catch (Exception e) { - log.error("文件下载失败: {}", e.getMessage(), e); - throw new RuntimeException("文件下载失败: " + e.getMessage(), e); - } - } +// @Override +// public boolean downloadFile(String sessionId, String remoteFilePath, String localFilePath) { +// log.info("下载文件: {} -> {}, 会话ID: {}", remoteFilePath, localFilePath, sessionId); +// +// WebSocketConnection webSocketConnection = sessionConnectionMap.get(sessionId); +// if (webSocketConnection == null||webSocketConnection.getConnectionStatus() != ConnectionStatus.CONNECTED) { +// throw new RuntimeException("会话不存在: " + sessionId); +// } +// try { +// return webSocketConnection.downloadFile(remoteFilePath, localFilePath); +// } catch (Exception e) { +// log.error("文件下载失败: {}", e.getMessage(), e); +// throw exception(FILE_DOWNLOAD_ERROR); +// } +// } @VisibleForTesting void validateMachineEnvAdd(MachineInfoVO machineInfoVO) { @@ -333,8 +229,8 @@ public class MachineInfoServiceImpl implements MachineInfoService { } } - @VisibleForTesting - MachineInfoDO validateMachineInfoExists(Long id) { + @Override + public MachineInfoDO validateMachineInfoExists(Long id) { if (id == null) { throw exception(MACHINE_INFO_NULL); } @@ -345,6 +241,13 @@ public class MachineInfoServiceImpl implements MachineInfoService { return machineInfoDO; } + @Override + public Map fileTreeNode(Long machineId, String path) { + validateMachineInfoExists(machineId); + Session sshSession = WebSocketSessionManager.getWebSocketConnectionByMachineId(machineId).getSshSession(); + return fileTreeComponent.getRemoteFileTree(sshSession, path); + } + @VisibleForTesting void validateMachineEnable(MachineInfoDO machineInfoDO) { if (machineInfoDO.getStatus() == MachineInfoStatus.ENABLE.getCode()) { @@ -360,10 +263,12 @@ public class MachineInfoServiceImpl implements MachineInfoService { } } - /** - * 生成会话ID - */ - private String generateSessionId() { - return "session-" + sessionIdGenerator.incrementAndGet(); + @VisibleForTesting + WebSocketConnection createWebSocketConnection(MachineInfoDO machineInfoDO) { + if (WebSocketSessionManager.containsMachineId(machineInfoDO.getId())) { + return WebSocketSessionManager.getWebSocketConnectionByMachineId((machineInfoDO.getId())); + } else { + return new WebSocketConnection(this.secretKeyService); + } } } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/MachineProxyServiceImpl.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/MachineProxyServiceImpl.java index 3f3fc764..84bd5e2e 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/MachineProxyServiceImpl.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/MachineProxyServiceImpl.java @@ -14,7 +14,6 @@ import cd.casic.framework.commons.util.object.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -import org.springframework.web.bind.annotation.RequestParam; import java.util.*; import java.util.function.Function; @@ -38,7 +37,6 @@ public class MachineProxyServiceImpl implements MachineProxyService { validateMachineProxyAdd(machineProxyVO); // 创建代理记录 MachineProxyDO machineProxyDO = BeanUtils.toBean(machineProxyVO, MachineProxyDO.class); - ; save(machineProxyDO); return machineProxyDO.getId(); } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/SecretKeyServiceImpl.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/SecretKeyServiceImpl.java index 1e37d57e..4fa6203c 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/SecretKeyServiceImpl.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/SecretKeyServiceImpl.java @@ -3,7 +3,9 @@ package cd.casic.module.machine.service.impl; import cd.casic.framework.commons.pojo.PageResult; import cd.casic.framework.commons.util.object.BeanUtils; import cd.casic.module.machine.controller.vo.SecretKeyVO; +import cd.casic.module.machine.dal.dataobject.MachineInfoDO; import cd.casic.module.machine.dal.dataobject.SecretKeyDO; +import cd.casic.module.machine.dal.mysql.MachineInfoMapper; import cd.casic.module.machine.dal.mysql.SecretKeyMapper; import cd.casic.module.machine.service.MachineInfoService; import cn.hutool.core.io.resource.ResourceUtil; @@ -12,9 +14,18 @@ import cd.casic.module.machine.utils.AliYunOssClient; import cd.casic.module.machine.service.SecretKeyService; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StreamUtils; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.List; import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception; @@ -23,6 +34,7 @@ import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*; /** * 密钥服务实现类 */ +@Slf4j @Service("secretKeyService") public class SecretKeyServiceImpl implements SecretKeyService { @@ -35,6 +47,8 @@ public class SecretKeyServiceImpl implements SecretKeyService { @Resource private SecretKeyMapper secretKeyMapper; + @Resource + private MachineInfoMapper machineInfoMapper; @Override public SecretKeyVO getSecretKey(Long id) { @@ -42,33 +56,62 @@ public class SecretKeyServiceImpl implements SecretKeyService { return BeanUtils.toBean(secretKeyDO, SecretKeyVO.class); } + @Override + public void deleteSecretKey(Long id) { + validateSecretKeyExists(id); + secretKeyMapper.deleteById(id); + } + @Override public void unbindMachine(SecretKeyVO secretKeyVO) { - machineInfoService.bindingSecretKey(secretKeyVO.getMachineInfoIds(),null); + machineInfoService.bindingSecretKey(secretKeyVO.getMachineInfoIds(), null); + } + + @Override + public ResponseEntity downloadSecretFile(Long id) { + // 获取私钥内容 + String keyContent = getKeyContent(id); + if (keyContent == null || keyContent.isEmpty()) { + throw exception(SECRET_KEY_NULL); + } + // 将字符串转换为输入流 + ByteArrayInputStream inputStream = new ByteArrayInputStream(keyContent.getBytes(StandardCharsets.UTF_8)); + // 设置响应头 + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=private_key.pem"); + headers.add(HttpHeaders.CONTENT_TYPE, "application/octet-stream"); + headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(keyContent.getBytes(StandardCharsets.UTF_8).length)); + // 返回带有文件流的响应实体 + return ResponseEntity.ok() + .headers(headers) + .body(new InputStreamResource(inputStream)); + } + + @Override + public List getBindMachine(Long secretKeyId) { + return machineInfoMapper.selectBindMachineBySecretKey(secretKeyId); } @Override public Long createSecretKey(SecretKeyVO secretKeyVO) { validateSecretKeyAdd(secretKeyVO); - String ossPath = upLoadSecretKey(secretKeyVO.getPath()); + String localPath = secretKeyVO.getPath(); + //将反斜杠替换为双反斜杠(Windows路径转义) + localPath = localPath.replace("\\", "\\\\"); + String ossPath = upLoadSecretKey(localPath); //检查得到的oss路径是否为空 validateSecretKeyPath(ossPath); secretKeyVO.setPath(ossPath); SecretKeyDO secretKeyDO = BeanUtils.toBean(secretKeyVO, SecretKeyDO.class); - //todo检查密钥合法 - secretKeyMapper.insert(secretKeyDO); return secretKeyDO.getId(); - - } @Override public void updateSecretKey(SecretKeyVO secretKeyVO) { SecretKeyDO secretKeyDO = validateSecretKeyExists(secretKeyVO.getId()); - //如果路径改变==改变密钥 + //路径改变==改变密钥 if (!secretKeyDO.getPath().equals(secretKeyVO.getPath())) { - //todo检查密钥合法 String ossPath = upLoadSecretKey(secretKeyVO.getPath()); BeanUtils.copyProperties(secretKeyVO, secretKeyDO); secretKeyDO.setPath(ossPath); @@ -104,8 +147,7 @@ public class SecretKeyServiceImpl implements SecretKeyService { ); //绑定的机器全部设置为空 - machineInfoService.bindingSecretKey(ids,null); - + machineInfoService.bindingSecretKey(ids, null); secretKeyMapper.deleteBatchIds(ids); } @@ -160,4 +202,25 @@ public class SecretKeyServiceImpl implements SecretKeyService { return secretKeyDO; } + @Override + public String getKeyContent(Long secretKeyId) { + if (secretKeyId == null) { + return null; + } + SecretKeyVO secretKey = getSecretKey(secretKeyId); + byte[] content; + try { + content = aliYunOssClient.getContent(secretKey.getPath().substring(secretKey.getPath().lastIndexOf("/") + 1)); + } catch (Exception e) { + log.error("读取密钥文件失败", e); + throw exception(READ_SECRET_CONTENT_ERROR); + } + //改为S3FileClient读取 + InputStream read = new ByteArrayInputStream(content); + try { + return StreamUtils.copyToString(read, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/utils/AliYunOssClient.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/utils/AliYunOssClient.java index fff03244..dbe11d4f 100644 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/utils/AliYunOssClient.java +++ b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/utils/AliYunOssClient.java @@ -4,9 +4,8 @@ import cd.casic.module.infra.framework.file.core.client.s3.S3FileClient; import cd.casic.module.infra.framework.file.core.client.s3.S3FileClientConfig; -public class AliYunOssClient extends S3FileClient{ +public class AliYunOssClient extends S3FileClient { public AliYunOssClient(Long id, S3FileClientConfig config) { super(id, config); } - } diff --git a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/utils/PropertyUtils.java b/modules/module-ci-machine/src/main/java/cd/casic/module/machine/utils/PropertyUtils.java deleted file mode 100644 index 9919ea7a..00000000 --- a/modules/module-ci-machine/src/main/java/cd/casic/module/machine/utils/PropertyUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -package cd.casic.module.machine.utils; - -import io.micrometer.common.util.StringUtils; - -import java.util.function.Consumer; - -/** - * 属性操作工具类 - */ -public class PropertyUtils { - - /** - * 如果不为空则设置值 - * - * @param value 值 - * @param setter 具体set操作 - */ - public static void setIfNotBlank(String value, Consumer setter) { - if (StringUtils.isNotBlank(value)) { - setter.accept(value); - } - } - - /** - * 如果不为空则设置值 - * - * @param value 值 - * @param setter 具体set操作 - */ - public static void setIfNotNull(T value, Consumer setter) { - if (value != null) { - setter.accept(value); - } - } -}