增加ssh链接框架
This commit is contained in:
parent
80c40bb6ab
commit
24f62c5719
@ -5,28 +5,34 @@ import com.fasterxml.jackson.annotation.JsonValue;
|
||||
/**
|
||||
* SSH WebSocket 消息类型枚举(Framework层)
|
||||
*
|
||||
* 用于标识不同类型的SSH WebSocket消息
|
||||
* 定义标准的SSH WebSocket消息类型
|
||||
* 使用 @JsonValue 注解实现JSON序列化为小写字符串
|
||||
*/
|
||||
public enum SSHMessageType {
|
||||
/**
|
||||
* 用户输入(前端 → 后端)
|
||||
* 用户输入消息(前端 → 后端)
|
||||
*/
|
||||
INPUT("input"),
|
||||
|
||||
/**
|
||||
* 终端输出(后端 → 前端)
|
||||
* SSH输出消息(后端 → 前端)
|
||||
*/
|
||||
OUTPUT("output"),
|
||||
|
||||
/**
|
||||
* 连接状态(后端 → 前端)
|
||||
* 连接状态消息(后端 → 前端)
|
||||
*/
|
||||
STATUS("status"),
|
||||
|
||||
/**
|
||||
* 错误信息(后端 → 前端)
|
||||
* 错误消息(后端 → 前端)
|
||||
*/
|
||||
ERROR("error");
|
||||
ERROR("error"),
|
||||
|
||||
/**
|
||||
* 终端尺寸调整消息(前端 → 后端)
|
||||
*/
|
||||
RESIZE("resize");
|
||||
|
||||
private final String value;
|
||||
|
||||
|
||||
@ -310,28 +310,84 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
SSHWebSocketMessage msg = JsonUtils.fromJson(message.getPayload(), SSHWebSocketMessage.class);
|
||||
|
||||
if (msg.getType() == SSHMessageType.INPUT) {
|
||||
// 用户输入命令
|
||||
// 用户输入命令(使用强类型Request)
|
||||
com.qqchen.deploy.backend.framework.ssh.websocket.request.SSHInputRequest request =
|
||||
msg.getRequest(com.qqchen.deploy.backend.framework.ssh.websocket.request.SSHInputRequest.class);
|
||||
|
||||
if (request == null || !request.isValid()) {
|
||||
log.warn("INPUT消息格式错误或为空: sessionId={}", sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
Session.Shell shell = sshShells.get(sessionId);
|
||||
if (shell != null) {
|
||||
OutputStream outputStream = shell.getOutputStream();
|
||||
outputStream.write(msg.getData().getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.write(request.getCommand().getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.flush();
|
||||
|
||||
// 触发命令事件
|
||||
SSHTarget target = sessionTargets.get(sessionId);
|
||||
SSHEventData eventData = SSHEventData.builder()
|
||||
.sessionId(sessionId)
|
||||
.command(msg.getData())
|
||||
.command(request.getCommand())
|
||||
.target(target)
|
||||
.build();
|
||||
onEvent(SSHEvent.ON_COMMAND, eventData);
|
||||
}
|
||||
} else if (msg.getType() == SSHMessageType.RESIZE) {
|
||||
// 终端尺寸调整
|
||||
handleResizeMessage(sessionId, msg);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("处理WebSocket消息失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理终端尺寸调整消息(使用强类型Request)
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param msg 消息对象
|
||||
*/
|
||||
private void handleResizeMessage(String sessionId, SSHWebSocketMessage msg) {
|
||||
try {
|
||||
// 使用强类型提取request
|
||||
com.qqchen.deploy.backend.framework.ssh.websocket.request.SSHResizeRequest request =
|
||||
msg.getRequest(com.qqchen.deploy.backend.framework.ssh.websocket.request.SSHResizeRequest.class);
|
||||
|
||||
if (request == null) {
|
||||
log.warn("RESIZE消息缺少request或格式错误: sessionId={}", sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if (!request.isValid()) {
|
||||
log.warn("RESIZE消息参数无效: sessionId={}, rows={}, cols={}",
|
||||
sessionId, request.getRows(), request.getCols());
|
||||
return;
|
||||
}
|
||||
|
||||
// 调整SSH PTY尺寸
|
||||
Session.Shell shell = sshShells.get(sessionId);
|
||||
if (shell != null) {
|
||||
// 计算像素尺寸(标准字体:8x16像素)
|
||||
int widthPixels = request.getCols() * 8;
|
||||
int heightPixels = request.getRows() * 16;
|
||||
|
||||
shell.changeWindowDimensions(request.getCols(), request.getRows(),
|
||||
widthPixels, heightPixels);
|
||||
|
||||
log.debug("SSH终端尺寸已调整: sessionId={}, cols={}, rows={}",
|
||||
sessionId, request.getCols(), request.getRows());
|
||||
} else {
|
||||
log.warn("未找到SSH Shell,无法调整尺寸: sessionId={}", sessionId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理RESIZE消息失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||
String sessionId = getSessionId(session);
|
||||
@ -624,7 +680,7 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送输出消息到前端
|
||||
* 发送输出消息到前端(使用强类型Response)
|
||||
*
|
||||
* @param session WebSocket会话
|
||||
* @param output 输出内容
|
||||
@ -640,8 +696,16 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(" ├─ 创建SSHWebSocketMessage: sessionId={}", sessionId);
|
||||
SSHWebSocketMessage msg = SSHWebSocketMessage.output(output);
|
||||
// 创建强类型Response对象
|
||||
com.qqchen.deploy.backend.framework.ssh.websocket.response.SSHOutputResponse response =
|
||||
new com.qqchen.deploy.backend.framework.ssh.websocket.response.SSHOutputResponse(output);
|
||||
|
||||
// 包装成 {"response": SSHOutputResponse}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("response", response);
|
||||
|
||||
// 创建消息
|
||||
SSHWebSocketMessage msg = new SSHWebSocketMessage(SSHMessageType.OUTPUT, data);
|
||||
|
||||
log.debug(" ├─ 准备调用session.sendMessage: sessionId={}", sessionId);
|
||||
session.sendMessage(new TextMessage(JsonUtils.toJson(msg)));
|
||||
@ -655,11 +719,25 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送状态消息到前端
|
||||
* 发送状态消息到前端(使用强类型Response)
|
||||
*/
|
||||
protected void sendStatus(WebSocketSession session, SSHStatusEnum status) {
|
||||
try {
|
||||
SSHWebSocketMessage msg = SSHWebSocketMessage.status(status);
|
||||
if (!session.isOpen()) {
|
||||
log.debug("WebSocket已关闭,跳过发送状态: sessionId={}", session.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建强类型Response对象
|
||||
com.qqchen.deploy.backend.framework.ssh.websocket.response.SSHStatusResponse response =
|
||||
new com.qqchen.deploy.backend.framework.ssh.websocket.response.SSHStatusResponse(status);
|
||||
|
||||
// 包装成 {"response": SSHStatusResponse}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("response", response);
|
||||
|
||||
// 创建消息
|
||||
SSHWebSocketMessage msg = new SSHWebSocketMessage(SSHMessageType.STATUS, data);
|
||||
session.sendMessage(new TextMessage(JsonUtils.toJson(msg)));
|
||||
} catch (IOException e) {
|
||||
log.error("发送状态消息失败: sessionId={}", session.getId(), e);
|
||||
@ -667,7 +745,7 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误消息到前端
|
||||
* 发送错误消息到前端(使用强类型Response)
|
||||
*/
|
||||
protected void sendError(WebSocketSession session, String error) {
|
||||
try {
|
||||
@ -675,7 +753,17 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
log.debug("WebSocket已关闭,跳过发送错误消息: sessionId={}", session.getId());
|
||||
return;
|
||||
}
|
||||
SSHWebSocketMessage msg = SSHWebSocketMessage.error(error);
|
||||
|
||||
// 创建强类型Response对象
|
||||
com.qqchen.deploy.backend.framework.ssh.websocket.response.SSHErrorResponse response =
|
||||
new com.qqchen.deploy.backend.framework.ssh.websocket.response.SSHErrorResponse(error);
|
||||
|
||||
// 包装成 {"response": SSHErrorResponse}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("response", response);
|
||||
|
||||
// 创建消息
|
||||
SSHWebSocketMessage msg = new SSHWebSocketMessage(SSHMessageType.ERROR, data);
|
||||
session.sendMessage(new TextMessage(JsonUtils.toJson(msg)));
|
||||
} catch (IOException e) {
|
||||
if (session.isOpen()) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.qqchen.deploy.backend.framework.ssh.websocket;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHMessageType;
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHStatusEnum;
|
||||
import lombok.Data;
|
||||
@ -30,13 +31,23 @@ public class SSHWebSocketMessage {
|
||||
private SSHMessageType type;
|
||||
|
||||
/**
|
||||
* 消息内容(字符串)
|
||||
* 消息内容(可以是字符串或对象)
|
||||
*
|
||||
* 字符串类型(后端→前端):
|
||||
* - type=OUTPUT: 终端输出内容
|
||||
* - type=INPUT: 用户输入内容
|
||||
* - type=STATUS: 状态值(connecting/connected/reconnecting/disconnected/error)
|
||||
* - type=ERROR: 错误描述信息
|
||||
*
|
||||
* 字符串类型(前端→后端):
|
||||
* - type=INPUT: 用户输入内容
|
||||
*
|
||||
* 对象类型(前端→后端):
|
||||
* - type=RESIZE: {"request": {"rows": 40, "cols": 150}}
|
||||
*
|
||||
* 对象类型(后端→前端,预留):
|
||||
* - 未来可能的response: {"response": {...}}
|
||||
*/
|
||||
private String data;
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 消息时间戳(Unix毫秒)
|
||||
@ -46,14 +57,15 @@ public class SSHWebSocketMessage {
|
||||
|
||||
/**
|
||||
* 可选元数据
|
||||
* 通常为null,特殊场景下使用
|
||||
*
|
||||
* 用于特殊场景的扩展字段,通常为null
|
||||
*/
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
/**
|
||||
* 构造函数(自动填充时间戳)
|
||||
*/
|
||||
public SSHWebSocketMessage(SSHMessageType type, String data) {
|
||||
public SSHWebSocketMessage(SSHMessageType type, Object data) {
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
@ -94,4 +106,40 @@ public class SSHWebSocketMessage {
|
||||
public static SSHWebSocketMessage input(String data) {
|
||||
return new SSHWebSocketMessage(SSHMessageType.INPUT, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息中提取request对象(强类型转换)
|
||||
*
|
||||
* @param requestClass 请求类型
|
||||
* @return 请求对象,如果解析失败返回null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getRequest(Class<T> requestClass) {
|
||||
try {
|
||||
if (data instanceof Map) {
|
||||
Map<String, Object> dataMap = (Map<String, Object>) data;
|
||||
Object requestObj = dataMap.get("request");
|
||||
|
||||
if (requestObj != null) {
|
||||
// 如果已经是目标类型,直接返回
|
||||
if (requestClass.isInstance(requestObj)) {
|
||||
return (T) requestObj;
|
||||
}
|
||||
// 否则使用ObjectMapper转换(Map -> POJO)
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.convertValue(requestObj, requestClass);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 转换失败,返回null
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查data是否包含request
|
||||
*/
|
||||
public boolean hasRequest() {
|
||||
return data instanceof Map && ((Map<?, ?>) data).containsKey("request");
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package com.qqchen.deploy.backend.framework.ssh.websocket.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHMessageType;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* SSH WebSocket请求基类(Framework层)
|
||||
*
|
||||
* 所有前端发送到后端的请求消息都应该继承此类
|
||||
* 使用Jackson的@JsonTypeInfo和@JsonSubTypes实现多态反序列化
|
||||
*/
|
||||
@Data
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
property = "type",
|
||||
visible = true
|
||||
)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = SSHInputRequest.class, name = "input"),
|
||||
@JsonSubTypes.Type(value = SSHResizeRequest.class, name = "resize")
|
||||
})
|
||||
public abstract class SSHBaseRequest {
|
||||
/**
|
||||
* 请求类型(使用枚举保证类型安全)
|
||||
*/
|
||||
private SSHMessageType type;
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.qqchen.deploy.backend.framework.ssh.websocket.request;
|
||||
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHMessageType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* SSH用户输入请求(Framework层)
|
||||
*
|
||||
* 前端发送格式:
|
||||
* {
|
||||
* "type": "input",
|
||||
* "data": {
|
||||
* "request": {
|
||||
* "type": "input",
|
||||
* "command": "ls -la"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SSHInputRequest extends SSHBaseRequest {
|
||||
|
||||
/**
|
||||
* 用户输入命令(终端命令或字符)
|
||||
*/
|
||||
private String command;
|
||||
|
||||
public SSHInputRequest() {
|
||||
setType(SSHMessageType.INPUT);
|
||||
}
|
||||
|
||||
public SSHInputRequest(String command) {
|
||||
this();
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证参数有效性
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return command != null && !command.isEmpty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.qqchen.deploy.backend.framework.ssh.websocket.request;
|
||||
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHMessageType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* SSH终端尺寸调整请求(Framework层)
|
||||
*
|
||||
* 前端发送格式:
|
||||
* {
|
||||
* "type": "resize",
|
||||
* "data": {
|
||||
* "request": {
|
||||
* "rows": 40,
|
||||
* "cols": 150
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SSHResizeRequest extends SSHBaseRequest {
|
||||
|
||||
/**
|
||||
* 终端行数
|
||||
*/
|
||||
private Integer rows;
|
||||
|
||||
/**
|
||||
* 终端列数
|
||||
*/
|
||||
private Integer cols;
|
||||
|
||||
public SSHResizeRequest() {
|
||||
setType(SSHMessageType.RESIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证参数有效性
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return rows != null && cols != null && rows > 0 && cols > 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.qqchen.deploy.backend.framework.ssh.websocket.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHMessageType;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* SSH WebSocket响应基类(Framework层)
|
||||
*
|
||||
* 所有后端发送到前端的响应消息都应该继承此类
|
||||
* 使用Jackson的@JsonTypeInfo和@JsonSubTypes实现多态序列化
|
||||
*/
|
||||
@Data
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
property = "type",
|
||||
visible = true
|
||||
)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = SSHOutputResponse.class, name = "output"),
|
||||
@JsonSubTypes.Type(value = SSHStatusResponse.class, name = "status"),
|
||||
@JsonSubTypes.Type(value = SSHErrorResponse.class, name = "error")
|
||||
})
|
||||
public abstract class SSHBaseResponse {
|
||||
/**
|
||||
* 响应类型(使用枚举保证类型安全)
|
||||
*/
|
||||
private SSHMessageType type;
|
||||
|
||||
/**
|
||||
* 响应数据(统一字段)
|
||||
* - OUTPUT: 终端输出内容
|
||||
* - STATUS: 状态值(connected/disconnected等)
|
||||
* - ERROR: 错误消息
|
||||
*/
|
||||
private String data;
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.qqchen.deploy.backend.framework.ssh.websocket.response;
|
||||
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHMessageType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* SSH错误响应(Framework层)
|
||||
*
|
||||
* 后端发送格式:
|
||||
* {
|
||||
* "type": "error",
|
||||
* "data": {
|
||||
* "response": {
|
||||
* "type": "error",
|
||||
* "data": "Connection failed"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SSHErrorResponse extends SSHBaseResponse {
|
||||
|
||||
public SSHErrorResponse() {
|
||||
setType(SSHMessageType.ERROR);
|
||||
}
|
||||
|
||||
public SSHErrorResponse(String message) {
|
||||
this();
|
||||
setType(SSHMessageType.ERROR);
|
||||
setData(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.qqchen.deploy.backend.framework.ssh.websocket.response;
|
||||
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHMessageType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* SSH终端输出响应(Framework层)
|
||||
*
|
||||
* 后端发送格式:
|
||||
* {
|
||||
* "type": "output",
|
||||
* "data": {
|
||||
* "response": {
|
||||
* "type": "output",
|
||||
* "data": "terminal output text"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SSHOutputResponse extends SSHBaseResponse {
|
||||
|
||||
public SSHOutputResponse() {
|
||||
setType(SSHMessageType.OUTPUT);
|
||||
}
|
||||
|
||||
public SSHOutputResponse(String data) {
|
||||
this();
|
||||
setType(SSHMessageType.OUTPUT);
|
||||
setData(data);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.qqchen.deploy.backend.framework.ssh.websocket.response;
|
||||
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHMessageType;
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHStatusEnum;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* SSH连接状态响应(Framework层)
|
||||
*
|
||||
* 后端发送格式:
|
||||
* {
|
||||
* "type": "status",
|
||||
* "data": {
|
||||
* "response": {
|
||||
* "type": "status",
|
||||
* "data": "connected"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SSHStatusResponse extends SSHBaseResponse {
|
||||
|
||||
public SSHStatusResponse() {
|
||||
setType(SSHMessageType.STATUS);
|
||||
}
|
||||
|
||||
public SSHStatusResponse(SSHStatusEnum status) {
|
||||
this();
|
||||
setType(SSHMessageType.STATUS);
|
||||
setData(status.name().toLowerCase());
|
||||
}
|
||||
|
||||
public SSHStatusResponse(String status) {
|
||||
this();
|
||||
setType(SSHMessageType.STATUS);
|
||||
setData(status);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user