清除机器连接,文件管理部分
This commit is contained in:
parent
de817f1060
commit
d30f11a47a
@ -34,24 +34,11 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.aliyun.oss</groupId>
|
|
||||||
<artifactId>aliyun-sdk-oss</artifactId>
|
|
||||||
<version>3.15.1</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 机器连接-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.jcraft</groupId>
|
|
||||||
<artifactId>jsch</artifactId>
|
|
||||||
<version>0.1.55</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.antherd</groupId>
|
<groupId>com.antherd</groupId>
|
||||||
<artifactId>sm-crypto</artifactId>
|
<artifactId>sm-crypto</artifactId>
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
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<String, Object> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,340 +0,0 @@
|
|||||||
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 jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
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<String, FileNode> directoryCache = new ConcurrentHashMap<>();
|
|
||||||
private static final long CACHE_EXPIRE_TIME = 60 * 1000; // 缓存过期时间:1分钟
|
|
||||||
private final Map<String, Long> cacheTimeStamp = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
// 将文件树转换为JSON友好的Map结构(仅一级目录)
|
|
||||||
public static Map<String, Object> convertToMap(FileNode node) {
|
|
||||||
Map<String, Object> 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<Map<String, Object>> children = node.getChildren().stream()
|
|
||||||
.map(FileTreeComponent::convertToMap)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
map.put("children", children);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取指定路径的直接子项(不递归)
|
|
||||||
public List<ChannelSftp.LsEntry> listDirectChildren(String path) {
|
|
||||||
if (sftp == null || !isSftpConnected()) {
|
|
||||||
log.error("SFTP连接无效,无法列出文件");
|
|
||||||
throw exception(CONNECTION_LOST);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ChannelSftp.LsEntry> 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.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<String, Object> 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<ChannelSftp.LsEntry> 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<String, Object> sortFileInfo(Map<String, Object> fileInfoMap) {
|
|
||||||
// 检查是否包含子节点
|
|
||||||
if (fileInfoMap.containsKey("children")) {
|
|
||||||
List<Map<String, Object>> children = (List<Map<String, Object>>) 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传到远程服务器
|
|
||||||
public void uploadToRemoteServer(Path tempFilePath, String safeFilename, String remoteFilePath) {
|
|
||||||
// 上传文件(使用原始文件名)
|
|
||||||
try {
|
|
||||||
sftp.put(tempFilePath.toString(), remoteFilePath + safeFilename);
|
|
||||||
} catch (SftpException e) {
|
|
||||||
throw exception(UPLOAD_REMOTE_FILE_ERROR);
|
|
||||||
} finally {
|
|
||||||
// 清理临时文件
|
|
||||||
cleanupTempFile(tempFilePath);
|
|
||||||
}
|
|
||||||
log.info("文件上传成功: {} -> {}{}", tempFilePath, remoteFilePath, safeFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
//下载文件
|
|
||||||
public void downloadFile(String remoteFilePath, HttpServletResponse httpServletResponse) {
|
|
||||||
InputStream inputStream = null;
|
|
||||||
OutputStream outputStream = null;
|
|
||||||
try {
|
|
||||||
// 获取文件信息并判断是否为文件夹
|
|
||||||
SftpATTRS attrs = sftp.lstat(remoteFilePath);
|
|
||||||
if (attrs.isDir()) {
|
|
||||||
httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
|
||||||
log.error("无法下载文件夹: {}", remoteFilePath);
|
|
||||||
throw exception(DOWNLOAD_FOLDER_NOT_ALLOWED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理文件下载逻辑
|
|
||||||
String fileName = Paths.get(remoteFilePath).getFileName().toString();
|
|
||||||
|
|
||||||
// 设置响应头
|
|
||||||
httpServletResponse.setContentType("application/octet-stream");
|
|
||||||
httpServletResponse.setHeader("Content-Disposition", "attachment; filename=\"" + encodeFileName(fileName) + "\"");
|
|
||||||
httpServletResponse.setContentLengthLong(attrs.getSize());
|
|
||||||
|
|
||||||
// 流式传输文件
|
|
||||||
inputStream = sftp.get(remoteFilePath);
|
|
||||||
outputStream = httpServletResponse.getOutputStream();
|
|
||||||
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
||||||
outputStream.write(buffer, 0, bytesRead);
|
|
||||||
}
|
|
||||||
outputStream.flush();
|
|
||||||
} catch (SftpException e) {
|
|
||||||
httpServletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
|
||||||
log.error("远程文件不存在: {}", remoteFilePath, e);
|
|
||||||
throw exception(PATH_NOT_EXISTS);
|
|
||||||
} catch (IOException e) {
|
|
||||||
httpServletResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
||||||
log.error("远程文件传输失败:{}", e.getMessage());
|
|
||||||
throw exception(REMOTE_FILE_TRANSFER_FAIL);
|
|
||||||
} finally {
|
|
||||||
// 关闭资源
|
|
||||||
closeQuietly(outputStream);
|
|
||||||
closeQuietly(inputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//删除远程机器文件
|
|
||||||
public void deleteRemoteFile(String remoteFilePath) {
|
|
||||||
try {
|
|
||||||
// 检查文件是否存在
|
|
||||||
if (checkFileExists(remoteFilePath, sftp)) {
|
|
||||||
sftp.rm(remoteFilePath);
|
|
||||||
log.info("文件删除成功: {}", remoteFilePath);
|
|
||||||
} else {
|
|
||||||
log.error("文件不存在,无法删除");
|
|
||||||
throw exception(PATH_NOT_EXISTS);
|
|
||||||
}
|
|
||||||
} catch (SftpException e) {
|
|
||||||
log.error("删除文件时出错: {}", e.getMessage());
|
|
||||||
throw exception(DELETE_REMOTE_FILE_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理临时文件
|
|
||||||
private void cleanupTempFile(Path filePath) {
|
|
||||||
if (filePath != null && Files.exists(filePath)) {
|
|
||||||
try {
|
|
||||||
Files.delete(filePath);
|
|
||||||
log.debug("临时文件已删除: {}", filePath);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.warn("临时文件删除失败: {}", filePath, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String encodeFileName(String fileName) {
|
|
||||||
return URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void closeQuietly(AutoCloseable closeable) {
|
|
||||||
if (closeable != null) {
|
|
||||||
try {
|
|
||||||
closeable.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("关闭资源失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 辅助方法:检查文件是否存在
|
|
||||||
private boolean checkFileExists(String filePath, ChannelSftp sftp) {
|
|
||||||
try {
|
|
||||||
sftp.lstat(filePath);
|
|
||||||
return true;
|
|
||||||
} catch (SftpException e) {
|
|
||||||
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,228 +0,0 @@
|
|||||||
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 cd.casic.module.machine.utils.CryptogramUtil;
|
|
||||||
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 javax.script.ScriptException;
|
|
||||||
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.*;
|
|
||||||
|
|
||||||
@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;
|
|
||||||
try {
|
|
||||||
pubKeyContent = CryptogramUtil.doDecrypt(secretKeyService.getPublicKeyContent(machineInfo.getSecretKeyId()));
|
|
||||||
} catch (ScriptException e) {
|
|
||||||
throw exception(ENCRYPT_OR_DECRYPT_FAIL);
|
|
||||||
}
|
|
||||||
// 验证秘钥格式
|
|
||||||
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 { // 密钥认证
|
|
||||||
// 保持默认认证顺序(公钥优先)
|
|
||||||
config.put("PreferredAuthentications", "publicKey,password,keyboard-interactive");
|
|
||||||
}
|
|
||||||
config.put("ServerAliveInterval", "30"); // 每30秒发送一次心跳
|
|
||||||
config.put("ServerAliveCountMax", "3"); // 允许3次心跳失败
|
|
||||||
session.setConfig(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
package cd.casic.module.machine.component;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
@Component("machineWebSocketSessionManger")
|
|
||||||
//管理webSocketSession
|
|
||||||
public class WebSocketSessionManager {
|
|
||||||
|
|
||||||
//webSocketSessionId - WebSocketConnection 与远程机器的会话管理
|
|
||||||
private static final ConcurrentHashMap<String, WebSocketConnection> sessionConnectionMap = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
//机器id - WebSocketConnection
|
|
||||||
private static final ConcurrentHashMap<Long, WebSocketConnection> webSocketSessionConnectionMap = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有 WebSocketConnection
|
|
||||||
*/
|
|
||||||
public static ConcurrentHashMap<Long, WebSocketConnection> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package cd.casic.module.machine.configuration;
|
|
||||||
|
|
||||||
import cd.casic.module.infra.framework.file.core.client.s3.S3FileClientConfig;
|
|
||||||
import cd.casic.module.machine.utils.AliYunOssClient;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class AliYunOssConfig extends S3FileClientConfig{
|
|
||||||
@Value("${aliyun.oss.endpoint}")
|
|
||||||
private String endpoint;
|
|
||||||
@Value("${aliyun.oss.accessKeyId}")
|
|
||||||
private String accessKey;
|
|
||||||
@Value("${aliyun.oss.accessKeySecret}")
|
|
||||||
private String accessSecret;
|
|
||||||
@Value("${aliyun.oss.bucketName}")
|
|
||||||
private String bucket;
|
|
||||||
// 定义 S3 客户端 Bean
|
|
||||||
@Bean
|
|
||||||
public AliYunOssClient aliYunClient() {
|
|
||||||
// 创建配置对象
|
|
||||||
S3FileClientConfig config = new AliYunOssConfig();
|
|
||||||
config.setEndpoint(endpoint);
|
|
||||||
config.setAccessKey(accessKey);
|
|
||||||
config.setAccessSecret(accessSecret);
|
|
||||||
config.setBucket(bucket);
|
|
||||||
AliYunOssClient aliYunOssClient = new AliYunOssClient(1L, config);
|
|
||||||
// 创建并返回客户端实例
|
|
||||||
aliYunOssClient.init();
|
|
||||||
return aliYunOssClient;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,7 +17,6 @@ public interface MachineErrorCodeConstants {
|
|||||||
ErrorCode MACHINE_INFO_AUTHENTICATION_TYPE_NULL = new ErrorCode(1_003_000_007, "机器认证类型为空");
|
ErrorCode MACHINE_INFO_AUTHENTICATION_TYPE_NULL = new ErrorCode(1_003_000_007, "机器认证类型为空");
|
||||||
ErrorCode MACHINE_INFO_AUTHENTICATION_TYPE_NOT_EXISTS = new ErrorCode(1_003_000_008, "机器认证类型不存在");
|
ErrorCode MACHINE_INFO_AUTHENTICATION_TYPE_NOT_EXISTS = new ErrorCode(1_003_000_008, "机器认证类型不存在");
|
||||||
ErrorCode MACHINE_ENABLE = new ErrorCode(1_003_000_009, "机器启用中");
|
ErrorCode MACHINE_ENABLE = new ErrorCode(1_003_000_009, "机器启用中");
|
||||||
ErrorCode MACHINE_UN_ENABLE = new ErrorCode(1_003_000_010, "机器不可用");
|
|
||||||
|
|
||||||
// ========== 机器环境变量模块 1-003-002-000 ==========
|
// ========== 机器环境变量模块 1-003-002-000 ==========
|
||||||
ErrorCode MACHINE_ENV_NULL = new ErrorCode(1_003_002_000, "机器环境变量为空");
|
ErrorCode MACHINE_ENV_NULL = new ErrorCode(1_003_002_000, "机器环境变量为空");
|
||||||
@ -34,32 +33,5 @@ public interface MachineErrorCodeConstants {
|
|||||||
// ========== 密钥模块 1-003-004-000 ==========
|
// ========== 密钥模块 1-003-004-000 ==========
|
||||||
ErrorCode SECRET_KEY_NULL = new ErrorCode(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_NOT_EXISTS = new ErrorCode(1_003_004_001, "密钥不存在");
|
||||||
ErrorCode INVALID_kEY_FORMAT = new ErrorCode(1_003_004_002, "无效的密钥格式");
|
ErrorCode ENCRYPT_OR_DECRYPT_FAIL = new ErrorCode(1_003_004_002, "加密/解密失败");
|
||||||
ErrorCode READ_SECRET_CONTENT_ERROR = new ErrorCode(1_003_004_003, "读取密钥加载失败");
|
|
||||||
ErrorCode ENCRYPT_OR_DECRYPT_FAIL = new ErrorCode(1_003_004_004, "加密/解密失败");
|
|
||||||
|
|
||||||
//========== 会话连接模块 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 EXECUTE_COMMAND_FAIL = new ErrorCode(1_003_006_003, "命令执行失败");
|
|
||||||
ErrorCode PASSWORD_NOT_EXISTS = new ErrorCode(1_003_006_004, "密码不存在");
|
|
||||||
ErrorCode NOT_SUPPORT_AUTHENTICATION_TYPE = new ErrorCode(1_003_006_005, "认证类型不支持");
|
|
||||||
ErrorCode CREATE_SESSION_ERROR = new ErrorCode(1_003_006_006, "创建会话失败");
|
|
||||||
ErrorCode WEBSOCKET_SEND_MESSAGE_ERROR = new ErrorCode(1_003_006_007, "websocket发送消息失败");
|
|
||||||
ErrorCode CREATE_CHANEL_ERROR = new ErrorCode(1_003_006_008, "执行通道创建失败");
|
|
||||||
ErrorCode CHANEL_CONNECT_FAIL = new ErrorCode(1_003_006_009, "通道连接失败");
|
|
||||||
ErrorCode FAILED_TO_PARSE_URL_PARAMETERS = new ErrorCode(1_003_006_010, "解析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, "无路径访问权限");
|
|
||||||
ErrorCode INVALID_FILE_NAME = new ErrorCode(1_003_007_006, "无效的文件名");
|
|
||||||
ErrorCode CREATE_TEMP_FILE_ERROR = new ErrorCode(1_003_007_007, "创建临时文件失败");
|
|
||||||
ErrorCode UPLOAD_REMOTE_FILE_ERROR = new ErrorCode(1_003_007_008, "上传文件到远程机器出错");
|
|
||||||
ErrorCode DOWNLOAD_FOLDER_NOT_ALLOWED = new ErrorCode(1_003_007_009, "无法下载文件夹");
|
|
||||||
ErrorCode REMOTE_FILE_TRANSFER_FAIL = new ErrorCode(1_003_007_010, "远程文件传输失败");
|
|
||||||
ErrorCode DELETE_REMOTE_FILE_ERROR = new ErrorCode(1_003_007_011, "删除文件时出错");
|
|
||||||
}
|
}
|
||||||
|
@ -5,20 +5,15 @@ import cd.casic.framework.commons.pojo.PageResult;
|
|||||||
import cd.casic.framework.commons.util.object.BeanUtils;
|
import cd.casic.framework.commons.util.object.BeanUtils;
|
||||||
import cd.casic.module.machine.controller.vo.SecretKeyVO;
|
import cd.casic.module.machine.controller.vo.SecretKeyVO;
|
||||||
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
|
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
|
||||||
import cd.casic.module.machine.enums.ConnectionStatus;
|
|
||||||
import cd.casic.module.machine.service.MachineInfoService;
|
import cd.casic.module.machine.service.MachineInfoService;
|
||||||
import cd.casic.module.machine.controller.vo.MachineInfoVO;
|
import cd.casic.module.machine.controller.vo.MachineInfoVO;
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static cd.casic.framework.commons.pojo.CommonResult.success;
|
import static cd.casic.framework.commons.pojo.CommonResult.success;
|
||||||
|
|
||||||
@ -85,55 +80,4 @@ public class MachineInfoController {
|
|||||||
public void bindingSecretKey(@RequestBody SecretKeyVO secretKeyVO) {
|
public void bindingSecretKey(@RequestBody SecretKeyVO secretKeyVO) {
|
||||||
machineInfoService.bindingSecretKey(secretKeyVO);
|
machineInfoService.bindingSecretKey(secretKeyVO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/test")
|
|
||||||
@Operation(summary = "测试机器连接")
|
|
||||||
public CommonResult<Boolean> testConnection(@RequestParam("id") Long id) {
|
|
||||||
return success(machineInfoService.testConnection(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/status")
|
|
||||||
@Operation(summary = "获取机器连接状态")
|
|
||||||
public CommonResult<ConnectionStatus> getConnectionStatus(@RequestParam Long id) {
|
|
||||||
return success(machineInfoService.getConnectionStatus(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/status/all")
|
|
||||||
@Operation(summary = "获取所有机器连接状态")
|
|
||||||
public CommonResult<Map<Long, ConnectionStatus>> getAllConnectionStatus() {
|
|
||||||
return success(machineInfoService.getAllConnectionStatus());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/connect")
|
|
||||||
@Operation(summary = "建立连接")
|
|
||||||
public CommonResult<Map<String, Object>> connect(@RequestParam Long id) {
|
|
||||||
return success(machineInfoService.connect(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/fileTreeNode")
|
|
||||||
@Operation(summary = "获得文件树")
|
|
||||||
public CommonResult<Map<String, Object>> fileTreeNode(
|
|
||||||
@RequestParam Long machineId,
|
|
||||||
@RequestParam(required = false, defaultValue = "/") String path
|
|
||||||
) {
|
|
||||||
return success(machineInfoService.fileTreeNode(machineId, path));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/upload")
|
|
||||||
@Operation(summary = "上传文件到远程机器")
|
|
||||||
public CommonResult<Boolean> uploadFile(@RequestParam MultipartFile file, @RequestParam String remoteFilePath) {
|
|
||||||
return success(machineInfoService.uploadFile(file, remoteFilePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/download")
|
|
||||||
@Operation(summary = "从远程机器下载文件")
|
|
||||||
public CommonResult<Boolean> downloadFile(@RequestParam String remoteFilePath, HttpServletResponse httpServletResponse) {
|
|
||||||
return success(machineInfoService.downloadFile(remoteFilePath, httpServletResponse));
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/deleteRemoteFile")
|
|
||||||
@Operation(summary = "删除远程机器选定文件")
|
|
||||||
public CommonResult<Boolean> deleteRemoteFile(@RequestParam String remoteFilePath) {
|
|
||||||
return success(machineInfoService.deleteRemoteFile(remoteFilePath));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
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<FileNode> 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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,13 +4,9 @@ import cd.casic.framework.commons.pojo.PageResult;
|
|||||||
import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
|
import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
import cd.casic.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import cd.casic.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import cd.casic.module.machine.controller.vo.SecretKeyVO;
|
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.dataobject.SecretKeyDO;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface SecretKeyMapper extends BaseMapperX<SecretKeyDO> {
|
public interface SecretKeyMapper extends BaseMapperX<SecretKeyDO> {
|
||||||
//查询列表
|
//查询列表
|
||||||
@ -19,10 +15,4 @@ public interface SecretKeyMapper extends BaseMapperX<SecretKeyDO> {
|
|||||||
.likeIfPresent(SecretKeyDO::getName, secretKeyVO.getName())
|
.likeIfPresent(SecretKeyDO::getName, secretKeyVO.getName())
|
||||||
.likeIfPresent(SecretKeyDO::getDescription, secretKeyVO.getDescription()));
|
.likeIfPresent(SecretKeyDO::getDescription, secretKeyVO.getDescription()));
|
||||||
}
|
}
|
||||||
|
|
||||||
default void bindingMachine(Long machineInfoId, List<Long> secretKeyId) {
|
|
||||||
UpdateWrapper<SecretKeyDO> set = new UpdateWrapper<>();
|
|
||||||
set.eq("id", secretKeyId).set("machineInfoId", machineInfoId);
|
|
||||||
this.update(null, set);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
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 implements IntArrayValuable {
|
|
||||||
DISCONNECTED(1, "断开连接"),
|
|
||||||
CONNECTING(2, "正在连接"),
|
|
||||||
CONNECTED(3, "已连接"),
|
|
||||||
AUTH_FAILED(4, "认证失败"),
|
|
||||||
CONNECTION_TIMEOUT(5, "连接超时"),
|
|
||||||
CONNECTION_ERROR(6, "连接错误"),
|
|
||||||
CLOSED(7, "已关闭");
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
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.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.removeWebSocketConnection(sessionId);
|
|
||||||
Long machineInfoId = (Long) webSocketSession.getAttributes().get("machineId");
|
|
||||||
WebSocketSessionManager.removeWebSocketConnectionByMachineId(machineInfoId);
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,23 +4,21 @@ import cd.casic.framework.commons.pojo.PageResult;
|
|||||||
import cd.casic.module.machine.controller.vo.MachineInfoVO;
|
import cd.casic.module.machine.controller.vo.MachineInfoVO;
|
||||||
import cd.casic.module.machine.controller.vo.SecretKeyVO;
|
import cd.casic.module.machine.controller.vo.SecretKeyVO;
|
||||||
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
|
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
|
||||||
import cd.casic.module.machine.enums.ConnectionStatus;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public interface MachineInfoService {
|
public interface MachineInfoService {
|
||||||
/**
|
/**
|
||||||
* 新增机器
|
* 新增机器
|
||||||
|
*
|
||||||
* @return 新增机器的id
|
* @return 新增机器的id
|
||||||
*/
|
*/
|
||||||
Long createMachine(MachineInfoVO MachineInfoVO);
|
Long createMachine(MachineInfoVO MachineInfoVO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查看机器列表
|
* 查看机器列表
|
||||||
|
*
|
||||||
* @return 机器列表
|
* @return 机器列表
|
||||||
*/
|
*/
|
||||||
PageResult<MachineInfoDO> listMachineInfo(@Valid MachineInfoVO MachineInfoVO);
|
PageResult<MachineInfoDO> listMachineInfo(@Valid MachineInfoVO MachineInfoVO);
|
||||||
@ -42,89 +40,30 @@ public interface MachineInfoService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量删除机器
|
* 批量删除机器
|
||||||
|
*
|
||||||
* @param machineInfoIds 机器id列表
|
* @param machineInfoIds 机器id列表
|
||||||
*/
|
*/
|
||||||
void deleteMachineInfoList(String machineInfoIds);
|
void deleteMachineInfoList(String machineInfoIds);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除机器信息
|
* 删除机器信息
|
||||||
|
*
|
||||||
* @param machineInfoId 机器id
|
* @param machineInfoId 机器id
|
||||||
*/
|
*/
|
||||||
void deleteMachineInfo(Long machineInfoId);
|
void deleteMachineInfo(Long machineInfoId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取绑定的密钥
|
* 获取绑定的密钥
|
||||||
|
*
|
||||||
* @param secretKeyId 密钥id
|
* @param secretKeyId 密钥id
|
||||||
* @return 绑定的机器列表
|
* @return 绑定的机器列表
|
||||||
*/
|
*/
|
||||||
List<MachineInfoDO>selectBindMachineBySecretKey(Long secretKeyId);
|
List<MachineInfoDO> selectBindMachineBySecretKey(Long secretKeyId);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试机器连接
|
* 验证机器是否存在
|
||||||
* @param id 机器id
|
|
||||||
* @return 连接是否成功
|
|
||||||
*/
|
|
||||||
boolean testConnection(Long id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接远程机器
|
|
||||||
*
|
|
||||||
* @param id 机器id
|
|
||||||
* @return 连接后端文件树
|
|
||||||
*/
|
|
||||||
Map<String, Object> connect(Long id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取机器连接状态
|
|
||||||
*
|
|
||||||
* @return 连接状态
|
|
||||||
*/
|
|
||||||
ConnectionStatus getConnectionStatus(Long id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有连接状态
|
|
||||||
*
|
|
||||||
* @return 机器名称到连接状态的映射
|
|
||||||
*/
|
|
||||||
Map<Long, ConnectionStatus> getAllConnectionStatus();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传文件到远程机器
|
|
||||||
*
|
|
||||||
* @param file 上传文件
|
|
||||||
* @param remoteFilePath 远程文件路径
|
|
||||||
* @return 操作结果
|
|
||||||
*/
|
|
||||||
boolean uploadFile(MultipartFile file, String remoteFilePath);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从远程机器下载文件
|
|
||||||
*
|
|
||||||
* @param remoteFilePath 远程文件路径
|
|
||||||
* @return 操作结果
|
|
||||||
*/
|
|
||||||
boolean downloadFile(String remoteFilePath, HttpServletResponse httpServletResponse);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验机器是否存在
|
|
||||||
*
|
*
|
||||||
* @param id 机器id
|
* @param id 机器id
|
||||||
*/
|
*/
|
||||||
MachineInfoDO validateMachineInfoExists(Long id);
|
MachineInfoDO validateMachineInfoExists(Long id);
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据路径获得远程文件树
|
|
||||||
*
|
|
||||||
* @param machineId 机器id
|
|
||||||
* @param path 文件夹路径
|
|
||||||
* @return 远程文件树
|
|
||||||
*/
|
|
||||||
Map<String, Object> fileTreeNode(Long machineId, String path);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除远程文件路径
|
|
||||||
* @param remoteFilePath 远程文件路径
|
|
||||||
*/
|
|
||||||
boolean deleteRemoteFile(String remoteFilePath);
|
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,23 @@
|
|||||||
package cd.casic.module.machine.service.impl;
|
package cd.casic.module.machine.service.impl;
|
||||||
|
|
||||||
import cd.casic.framework.commons.pojo.PageResult;
|
import cd.casic.framework.commons.pojo.PageResult;
|
||||||
import cd.casic.module.machine.component.FileTreeComponent;
|
|
||||||
import cd.casic.module.machine.component.WebSocketConnection;
|
|
||||||
import cd.casic.module.machine.component.WebSocketSessionManager;
|
|
||||||
import cd.casic.module.machine.controller.vo.SecretKeyVO;
|
import cd.casic.module.machine.controller.vo.SecretKeyVO;
|
||||||
import cd.casic.module.machine.enums.AuthenticationType;
|
import cd.casic.module.machine.enums.AuthenticationType;
|
||||||
import cd.casic.module.machine.enums.MachineInfoType;
|
import cd.casic.module.machine.enums.MachineInfoType;
|
||||||
import cd.casic.module.machine.dal.mysql.MachineInfoMapper;
|
import cd.casic.module.machine.dal.mysql.MachineInfoMapper;
|
||||||
import cd.casic.module.machine.controller.vo.MachineInfoVO;
|
import cd.casic.module.machine.controller.vo.MachineInfoVO;
|
||||||
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
|
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.enums.MachineInfoStatus;
|
||||||
import cd.casic.module.machine.service.MachineInfoService;
|
import cd.casic.module.machine.service.MachineInfoService;
|
||||||
import cd.casic.module.machine.service.SecretKeyService;
|
import cd.casic.module.machine.service.SecretKeyService;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.jcraft.jsch.Session;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import cd.casic.framework.commons.util.object.BeanUtils;
|
import cd.casic.framework.commons.util.object.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
|
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
|
||||||
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
|
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
|
||||||
@ -47,12 +35,6 @@ public class MachineInfoServiceImpl implements MachineInfoService {
|
|||||||
@Resource
|
@Resource
|
||||||
private MachineInfoMapper machineInfoMapper;
|
private MachineInfoMapper machineInfoMapper;
|
||||||
|
|
||||||
@Resource
|
|
||||||
private FileTreeComponent fileTreeComponent;
|
|
||||||
|
|
||||||
//todo,部署后更改
|
|
||||||
private static final Path TEMP_DIR = Paths.get("D:\\桌面\\work\\ops-pro111\\temp");
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long createMachine(MachineInfoVO machineInfoVO) {
|
public Long createMachine(MachineInfoVO machineInfoVO) {
|
||||||
validateMachineEnvAdd(machineInfoVO);
|
validateMachineEnvAdd(machineInfoVO);
|
||||||
@ -95,7 +77,7 @@ public class MachineInfoServiceImpl implements MachineInfoService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bindingSecretKey(SecretKeyVO secretKeyVO) {
|
public void bindingSecretKey(SecretKeyVO secretKeyVO) {
|
||||||
machineInfoMapper.bindingSecretKey(secretKeyVO.getMachineInfoIds(), secretKeyVO.getId(),secretKeyVO.getEnableBind());
|
machineInfoMapper.bindingSecretKey(secretKeyVO.getMachineInfoIds(), secretKeyVO.getId(), secretKeyVO.getEnableBind());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -121,71 +103,6 @@ public class MachineInfoServiceImpl implements MachineInfoService {
|
|||||||
return machineInfoMapper.selectBindMachineBySecretKey(secretKeyId);
|
return machineInfoMapper.selectBindMachineBySecretKey(secretKeyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean testConnection(Long id) {
|
|
||||||
//先查询机器是否存在,在判断机器可用性
|
|
||||||
MachineInfoDO machineInfoDO = validateMachineInfoExists(id);
|
|
||||||
validateMachineUnEnable(machineInfoDO);
|
|
||||||
log.info("测试机器连接: {}", machineInfoDO.getHostIp());
|
|
||||||
WebSocketConnection webSocketConnection = createWebSocketConnection(machineInfoDO);
|
|
||||||
webSocketConnection.initConnection(machineInfoDO);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> 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 ConnectionStatus getConnectionStatus(Long id) {
|
|
||||||
validateMachineInfoExists(id);
|
|
||||||
WebSocketConnection webSocketConnection = WebSocketSessionManager.getWebSocketConnectionByMachineId(id);
|
|
||||||
return webSocketConnection == null ? ConnectionStatus.DISCONNECTED : webSocketConnection.getConnectionStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<Long, ConnectionStatus> getAllConnectionStatus() {
|
|
||||||
return WebSocketSessionManager.getAllWebSocketConnections().entrySet().stream()
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
Map.Entry::getKey,
|
|
||||||
entry -> entry.getValue().getConnectionStatus()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean uploadFile(MultipartFile file, String remoteFilePath) {
|
|
||||||
Path tempFilePath;
|
|
||||||
// 保存上传的文件到临时位置
|
|
||||||
String originalFilename = file.getOriginalFilename();
|
|
||||||
if (originalFilename == null || originalFilename.isEmpty()) {
|
|
||||||
throw exception(INVALID_FILE_NAME);
|
|
||||||
}
|
|
||||||
// 安全过滤文件名,防止路径遍历攻击
|
|
||||||
String safeFilename = sanitizeFilename(originalFilename);
|
|
||||||
try {
|
|
||||||
tempFilePath = Files.createTempFile(TEMP_DIR, "upload-", safeFilename);
|
|
||||||
file.transferTo(tempFilePath.toFile());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw exception(CREATE_TEMP_FILE_ERROR);
|
|
||||||
}
|
|
||||||
fileTreeComponent.uploadToRemoteServer(tempFilePath, safeFilename, remoteFilePath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean downloadFile(String remoteFilePath, HttpServletResponse httpServletResponse) {
|
|
||||||
fileTreeComponent.downloadFile(remoteFilePath, httpServletResponse);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateMachineEnvAdd(MachineInfoVO machineInfoVO) {
|
void validateMachineEnvAdd(MachineInfoVO machineInfoVO) {
|
||||||
if (machineInfoVO.getHostIp().isEmpty()) {
|
if (machineInfoVO.getHostIp().isEmpty()) {
|
||||||
@ -250,53 +167,10 @@ public class MachineInfoServiceImpl implements MachineInfoService {
|
|||||||
return machineInfoDO;
|
return machineInfoDO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> fileTreeNode(Long machineId, String path) {
|
|
||||||
validateMachineInfoExists(machineId);
|
|
||||||
Session sshSession = WebSocketSessionManager.getWebSocketConnectionByMachineId(machineId).getSshSession();
|
|
||||||
return fileTreeComponent.getRemoteFileTree(sshSession, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean deleteRemoteFile(String remoteFilePath) {
|
|
||||||
fileTreeComponent.deleteRemoteFile(remoteFilePath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateMachineEnable(MachineInfoDO machineInfoDO) {
|
void validateMachineEnable(MachineInfoDO machineInfoDO) {
|
||||||
if (machineInfoDO.getStatus() == MachineInfoStatus.ENABLE.getCode()) {
|
if (machineInfoDO.getStatus() == MachineInfoStatus.ENABLE.getCode()) {
|
||||||
throw exception(MACHINE_ENABLE);
|
throw exception(MACHINE_ENABLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
void validateMachineUnEnable(MachineInfoDO machineInfoDO) {
|
|
||||||
|
|
||||||
if (machineInfoDO.getStatus() == MachineInfoStatus.UN_ENABLE.getCode()) {
|
|
||||||
throw exception(MACHINE_UN_ENABLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
WebSocketConnection createWebSocketConnection(MachineInfoDO machineInfoDO) {
|
|
||||||
if (WebSocketSessionManager.containsMachineId(machineInfoDO.getId())) {
|
|
||||||
return WebSocketSessionManager.getWebSocketConnectionByMachineId((machineInfoDO.getId()));
|
|
||||||
} else {
|
|
||||||
return new WebSocketConnection(this.secretKeyService);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
// 安全过滤文件名,防止路径遍历攻击
|
|
||||||
private String sanitizeFilename(String filename) {
|
|
||||||
// 移除路径相关字符,只保留文件名和扩展名
|
|
||||||
filename = filename.replaceAll("[\\\\/:*?\"<>|]", "_");
|
|
||||||
// 限制最大长度
|
|
||||||
if (filename.length() > 255) {
|
|
||||||
filename = filename.substring(0, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
package cd.casic.module.machine.utils;
|
|
||||||
|
|
||||||
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 AliYunOssClient(Long id, S3FileClientConfig config) {
|
|
||||||
super(id, config);
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,11 +16,11 @@ public class CryptogramUtil {
|
|||||||
/**
|
/**
|
||||||
* 加密方法(Sm2 的专门针对前后端分离,非对称秘钥对的方式,暴露出去的公钥,对传输过程中的密码加个密)
|
* 加密方法(Sm2 的专门针对前后端分离,非对称秘钥对的方式,暴露出去的公钥,对传输过程中的密码加个密)
|
||||||
*
|
*
|
||||||
* @author yubaoshan
|
|
||||||
* @param str 待加密数据
|
* @param str 待加密数据
|
||||||
* @return 加密后的密文
|
* @return 加密后的密文
|
||||||
|
* @author yubaoshan
|
||||||
*/
|
*/
|
||||||
public static String doSm2Encrypt (String str) throws ScriptException {
|
public static String doSm2Encrypt(String str) throws ScriptException {
|
||||||
return Sm2.doEncrypt(str, Keypair.PUBLIC_KEY);
|
return Sm2.doEncrypt(str, Keypair.PUBLIC_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,11 +28,11 @@ public class CryptogramUtil {
|
|||||||
* 解密方法
|
* 解密方法
|
||||||
* 如果采用加密机的方法,用try catch 捕捉异常,返回原文值即可
|
* 如果采用加密机的方法,用try catch 捕捉异常,返回原文值即可
|
||||||
*
|
*
|
||||||
* @author yubaoshan
|
|
||||||
* @param str 密文
|
* @param str 密文
|
||||||
* @return 解密后的明文
|
* @return 解密后的明文
|
||||||
|
* @author yubaoshan
|
||||||
*/
|
*/
|
||||||
public static String doSm2Decrypt (String str) throws ScriptException {
|
public static String doSm2Decrypt(String str) throws ScriptException {
|
||||||
// 解密
|
// 解密
|
||||||
return Sm2.doDecrypt(str, Keypair.PRIVATE_KEY);
|
return Sm2.doDecrypt(str, Keypair.PRIVATE_KEY);
|
||||||
}
|
}
|
||||||
@ -40,11 +40,11 @@ public class CryptogramUtil {
|
|||||||
/**
|
/**
|
||||||
* 加密方法
|
* 加密方法
|
||||||
*
|
*
|
||||||
* @author yubaoshan
|
|
||||||
* @param str 待加密数据
|
* @param str 待加密数据
|
||||||
* @return 加密后的密文
|
* @return 加密后的密文
|
||||||
|
* @author yubaoshan
|
||||||
*/
|
*/
|
||||||
public static String doEncrypt (String str) throws ScriptException {
|
public static String doEncrypt(String str) throws ScriptException {
|
||||||
// SM4 加密 cbc模式
|
// SM4 加密 cbc模式
|
||||||
Sm4Options sm4Options4 = new Sm4Options();
|
Sm4Options sm4Options4 = new Sm4Options();
|
||||||
sm4Options4.setMode("cbc");
|
sm4Options4.setMode("cbc");
|
||||||
@ -56,11 +56,11 @@ public class CryptogramUtil {
|
|||||||
* 解密方法
|
* 解密方法
|
||||||
* 如果采用加密机的方法,用try catch 捕捉异常,返回原文值即可
|
* 如果采用加密机的方法,用try catch 捕捉异常,返回原文值即可
|
||||||
*
|
*
|
||||||
* @author yubaoshan
|
|
||||||
* @param str 密文
|
* @param str 密文
|
||||||
* @return 解密后的明文
|
* @return 解密后的明文
|
||||||
|
* @author yubaoshan
|
||||||
*/
|
*/
|
||||||
public static String doDecrypt (String str) throws ScriptException {
|
public static String doDecrypt(String str) throws ScriptException {
|
||||||
// 解密,cbc 模式,输出 utf8 字符串
|
// 解密,cbc 模式,输出 utf8 字符串
|
||||||
Sm4Options sm4Options8 = new Sm4Options();
|
Sm4Options sm4Options8 = new Sm4Options();
|
||||||
sm4Options8.setMode("cbc");
|
sm4Options8.setMode("cbc");
|
||||||
@ -77,34 +77,34 @@ public class CryptogramUtil {
|
|||||||
/**
|
/**
|
||||||
* 纯签名
|
* 纯签名
|
||||||
*
|
*
|
||||||
* @author yubaoshan
|
|
||||||
* @param str 待签名数据
|
* @param str 待签名数据
|
||||||
* @return 签名结果
|
* @return 签名结果
|
||||||
|
* @author yubaoshan
|
||||||
*/
|
*/
|
||||||
public static String doSignature (String str) throws ScriptException {
|
public static String doSignature(String str) throws ScriptException {
|
||||||
return Sm2.doSignature(str, Keypair.PRIVATE_KEY);
|
return Sm2.doSignature(str, Keypair.PRIVATE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证签名结果
|
* 验证签名结果
|
||||||
*
|
*
|
||||||
* @author yubaoshan
|
|
||||||
* @param originalStr 签名原文数据
|
* @param originalStr 签名原文数据
|
||||||
* @param str 签名结果
|
* @param str 签名结果
|
||||||
* @return 是否通过
|
* @return 是否通过
|
||||||
|
* @author yubaoshan
|
||||||
*/
|
*/
|
||||||
public static boolean doVerifySignature (String originalStr, String str) throws ScriptException {
|
public static boolean doVerifySignature(String originalStr, String str) throws ScriptException {
|
||||||
return Sm2.doVerifySignature(originalStr, str, Keypair.PUBLIC_KEY);
|
return Sm2.doVerifySignature(originalStr, str, Keypair.PUBLIC_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过杂凑算法取得hash值,用于做数据完整性保护
|
* 通过杂凑算法取得hash值,用于做数据完整性保护
|
||||||
*
|
*
|
||||||
* @author yubaoshan
|
|
||||||
* @param str 字符串
|
* @param str 字符串
|
||||||
* @return hash 值
|
* @return hash 值
|
||||||
|
* @author yubaoshan
|
||||||
*/
|
*/
|
||||||
public static String doHashValue (String str) throws ScriptException {
|
public static String doHashValue(String str) throws ScriptException {
|
||||||
return Sm3.sm3(str);
|
return Sm3.sm3(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user