diff --git a/orion-ops-dependencies/pom.xml b/orion-ops-dependencies/pom.xml index db8caf02..96cee127 100644 --- a/orion-ops-dependencies/pom.xml +++ b/orion-ops-dependencies/pom.xml @@ -81,8 +81,23 @@ orion-ops-spring-boot-starter-mybatis ${revision} + + com.orion.ops + orion-ops-spring-boot-starter-job + ${revision} + + + com.orion.ops + orion-ops-spring-boot-starter-websocket + ${revision} + - + + + org.springframework.boot + spring-boot-starter-websocket + ${spring.boot.version} + diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/pom.xml b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/pom.xml new file mode 100644 index 00000000..a6756d40 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/pom.xml @@ -0,0 +1,36 @@ + + + + com.orion.ops + orion-ops-framework + ${revision} + + + 4.0.0 + orion-ops-spring-boot-starter-websocket + ${project.artifactId} + jar + + 项目 websocket 配置包 + https://github.com/lijiahangmax/orion-ops-pro + + + + com.orion.ops + orion-ops-common + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-websocket + + + + \ No newline at end of file diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/config/OrionWebsocketAutoConfiguration.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/config/OrionWebsocketAutoConfiguration.java new file mode 100644 index 00000000..b91cac44 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/config/OrionWebsocketAutoConfiguration.java @@ -0,0 +1,44 @@ +package com.orion.ops.framework.websocket.config; + +import com.orion.ops.framework.websocket.core.WebsocketContainerConfig; +import com.orion.ops.framework.websocket.interceptor.UserHandshakeInterceptor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.server.HandshakeInterceptor; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; + +/** + * websocket 配置类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/6/25 19:45 + */ +@EnableWebSocket +@AutoConfiguration +@EnableConfigurationProperties(WebsocketContainerConfig.class) +public class OrionWebsocketAutoConfiguration { + + /** + * @return websocket 缓冲区大小配置 + */ + @Bean + public ServletServerContainerFactoryBean servletServerContainerFactoryBean(WebsocketContainerConfig config) { + ServletServerContainerFactoryBean factory = new ServletServerContainerFactoryBean(); + factory.setMaxBinaryMessageBufferSize(config.getBinaryBufferSize()); + factory.setMaxTextMessageBufferSize(config.getBinaryBufferSize()); + factory.setMaxSessionIdleTimeout(config.getSessionIdleTimeout()); + return factory; + } + + /** + * @return 用户认证拦截器 按需注入 + */ + @Bean + public HandshakeInterceptor userHandshakeInterceptor() { + return new UserHandshakeInterceptor(); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsAttr.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsAttr.java new file mode 100644 index 00000000..2eb2ceb9 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsAttr.java @@ -0,0 +1,22 @@ +package com.orion.ops.framework.websocket.constant; + +/** + * websocket 属性 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/6/25 20:25 + */ +public interface WsAttr { + + String USER = "user"; + + String UID = "uid"; + + String TOKEN = "token"; + + String READONLY = "readonly"; + + String CONNECTED = "connected"; + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseCode.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseCode.java new file mode 100644 index 00000000..d279abd2 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseCode.java @@ -0,0 +1,137 @@ +package com.orion.ops.framework.websocket.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * ws服务端关闭code + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2021/6/16 15:18 + */ +@AllArgsConstructor +@Getter +public enum WsCloseCode { + + /** + * 未查询到token + */ + INCORRECT_TOKEN(4100, WsCloseReason.CLOSED_CONNECTION), + + /** + * 伪造token + */ + FORGE_TOKEN(4120, WsCloseReason.CLOSED_CONNECTION), + + /** + * token已被绑定 + */ + TOKEN_BIND(4125, WsCloseReason.CLOSED_CONNECTION), + + /** + * 未知的连接 + */ + UNKNOWN_CONNECT(4130, WsCloseReason.CLOSED_CONNECTION), + + /** + * 认证失败 id不匹配 + */ + IDENTITY_MISMATCH(4140, WsCloseReason.IDENTITY_MISMATCH), + + /** + * 认证信息不匹配 + */ + VALID(4150, WsCloseReason.AUTHENTICATION_FAILURE), + + /** + * 机器不合法 + */ + INVALID_MACHINE(4200, WsCloseReason.CLOSED_CONNECTION), + + /** + * 连接远程服务器连接超时 + */ + CONNECTION_TIMEOUT(4201, WsCloseReason.CONNECTION_TIMEOUT), + + /** + * 连接远程服务器失败 + */ + CONNECTION_FAILURE(4202, WsCloseReason.REMOTE_SERVER_UNREACHABLE), + + /** + * 远程服务器认证失败 + */ + CONNECTION_AUTH_FAILURE(4205, WsCloseReason.REMOTE_SERVER_AUTHENTICATION_FAILURE), + + /** + * 远程服务器认证出现异常 + */ + CONNECTION_EXCEPTION(4210, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER), + + /** + * 机器未启用 + */ + MACHINE_DISABLED(4215, WsCloseReason.MACHINE_DISABLED), + + /** + * 打开shell出现异常 + */ + OPEN_SHELL_EXCEPTION(4220, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER), + + /** + * 打开command出现异常 + */ + OPEN_COMMAND_EXCEPTION(4225, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER), + + /** + * 打开sftp出现异常 + */ + OPEN_SFTP_EXCEPTION(4230, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER), + + /** + * 服务出现异常 + */ + RUNTIME_EXCEPTION(4300, WsCloseReason.CLOSED_CONNECTION), + + /** + * 心跳结束 + */ + HEART_DOWN(4310, WsCloseReason.CLOSED_CONNECTION), + + /** + * 用户关闭 + */ + DISCONNECT(4320, WsCloseReason.CLOSED_CONNECTION), + + /** + * 结束 + */ + EOF(4330, WsCloseReason.CLOSED_CONNECTION), + + /** + * 读取失败 + */ + READ_EXCEPTION(4335, WsCloseReason.CLOSED_CONNECTION), + + /** + * 强制下线 + */ + FORCED_OFFLINE(4500, WsCloseReason.FORCED_OFFLINE), + + ; + + private final int code; + + private final String reason; + + public static WsCloseCode of(int code) { + for (WsCloseCode value : values()) { + if (value.code == code) { + return value; + } + } + return null; + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseReason.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseReason.java new file mode 100644 index 00000000..33d4da48 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseReason.java @@ -0,0 +1,30 @@ +package com.orion.ops.framework.websocket.constant; + +/** + * ws服务端关闭reason + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2021/6/16 15:21 + */ +public interface WsCloseReason { + + String CLOSED_CONNECTION = "closed connection..."; + + String IDENTITY_MISMATCH = "identity mismatch..."; + + String AUTHENTICATION_FAILURE = "authentication failure..."; + + String REMOTE_SERVER_UNREACHABLE = "remote server unreachable..."; + + String CONNECTION_TIMEOUT = "connection timeout..."; + + String REMOTE_SERVER_AUTHENTICATION_FAILURE = "remote server authentication failure..."; + + String MACHINE_DISABLED = "machine disabled..."; + + String UNABLE_TO_CONNECT_REMOTE_SERVER = "unable to connect remote server..."; + + String FORCED_OFFLINE = "forced offline..."; + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsProtocol.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsProtocol.java new file mode 100644 index 00000000..7fb23051 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsProtocol.java @@ -0,0 +1,79 @@ +package com.orion.ops.framework.websocket.constant; + +import com.orion.lang.utils.Exceptions; +import com.orion.lang.utils.Strings; +import com.orion.lang.utils.Valid; +import lombok.AllArgsConstructor; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * ws服务端响应常量 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2021/4/16 21:48 + */ +@AllArgsConstructor +public enum WsProtocol { + + /** + * 正常返回 + */ + OK("0"), + + /** + * 连接成功 + */ + CONNECTED("1"), + + /** + * ping + */ + PING("2"), + + /** + * pong + */ + PONG("3"), + + /** + * 未知操作 + */ + ERROR("4"), + + ; + + private final String code; + + /** + * 分隔符 + */ + public static final String SYMBOL = "|"; + + public byte[] get() { + return Strings.bytes(code); + } + + public byte[] msg(String body) { + Valid.notNull(body); + return this.msg(Strings.bytes(body)); + } + + public byte[] msg(byte[] body) { + return this.msg(body, 0, body.length); + } + + public byte[] msg(byte[] body, int offset, int len) { + Valid.notNull(body); + try (ByteArrayOutputStream o = new ByteArrayOutputStream()) { + o.write(Strings.bytes(code + SYMBOL)); + o.write(body, offset, len); + return o.toByteArray(); + } catch (IOException e) { + throw Exceptions.ioRuntime(e); + } + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/core/WebsocketContainerConfig.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/core/WebsocketContainerConfig.java new file mode 100644 index 00000000..e388766b --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/core/WebsocketContainerConfig.java @@ -0,0 +1,32 @@ +package com.orion.ops.framework.websocket.core; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * websocket 配置属性 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/6/25 19:57 + */ +@Data +@ConfigurationProperties("spring.websocket") +public class WebsocketContainerConfig { + + /** + * 二进制消息缓冲区大小 byte + */ + private Integer binaryBufferSize; + + /** + * 文本消息缓冲区大小 byte + */ + private Integer textBufferSize; + + /** + * session 最大超时时间 ms + */ + private Long sessionIdleTimeout; + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/interceptor/UserHandshakeInterceptor.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/interceptor/UserHandshakeInterceptor.java new file mode 100644 index 00000000..7550e8aa --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/interceptor/UserHandshakeInterceptor.java @@ -0,0 +1,34 @@ +package com.orion.ops.framework.websocket.interceptor; + +import com.orion.ops.framework.websocket.constant.WsAttr; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +/** + * 用户拦截器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/6/25 20:16 + */ +public class UserHandshakeInterceptor implements HandshakeInterceptor { + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) { + // TODO 获取当前用户 + attributes.put(WsAttr.USER, 1); + // if (user == null){ + // return false; + // } + return true; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/utils/WebSockets.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/utils/WebSockets.java new file mode 100644 index 00000000..4201ad05 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/utils/WebSockets.java @@ -0,0 +1,110 @@ +package com.orion.ops.framework.websocket.utils; + +import com.orion.lang.exception.AuthenticationException; +import com.orion.lang.exception.ConnectionRuntimeException; +import com.orion.lang.exception.DisabledException; +import com.orion.lang.exception.TimeoutException; +import com.orion.lang.utils.Exceptions; +import com.orion.lang.utils.Urls; +import com.orion.ops.framework.websocket.constant.WsCloseCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.IOException; +import java.util.Objects; + +/** + * websocket 工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2021/6/14 0:36 + */ +@Slf4j +public class WebSockets { + + private WebSockets() { + } + + /** + * 发送消息 忽略并发报错 + * + * @param session session + * @param message message + */ + public static void sendText(WebSocketSession session, byte[] message) { + if (!session.isOpen()) { + return; + } + try { + // 响应 + session.sendMessage(new TextMessage(message)); + } catch (IllegalStateException e) { + // 并发异常 + log.error("发送消息失败 {}", Exceptions.getDigest(e)); + } catch (IOException e) { + throw Exceptions.ioRuntime(e); + } + } + + /** + * 关闭会话 + * + * @param session session + * @param code code + */ + public static void close(WebSocketSession session, WsCloseCode code) { + if (!session.isOpen()) { + return; + } + try { + session.close(new CloseStatus(code.getCode(), code.getReason())); + } catch (Exception e) { + log.error("websocket close failure", e); + } + } + + /** + * 获取 urlToken + * + * @param request request + * @return token + */ + public static String getToken(ServerHttpRequest request) { + return Urls.getUrlSource(Objects.requireNonNull(request.getURI().toString())); + } + + /** + * 获取 urlToken + * + * @param session session + * @return token + */ + public static String getToken(WebSocketSession session) { + return Urls.getUrlSource(Objects.requireNonNull(session.getUri()).toString()); + } + + /** + * 打开 session 异常关闭 + * + * @param session session + * @param e e + */ + public static void openSessionStoreThrowClose(WebSocketSession session, Exception e) { + if (Exceptions.isCausedBy(e, TimeoutException.class)) { + close(session, WsCloseCode.CONNECTION_TIMEOUT); + } else if (Exceptions.isCausedBy(e, ConnectionRuntimeException.class)) { + close(session, WsCloseCode.CONNECTION_FAILURE); + } else if (Exceptions.isCausedBy(e, AuthenticationException.class)) { + close(session, WsCloseCode.CONNECTION_AUTH_FAILURE); + } else if (Exceptions.isCausedBy(e, DisabledException.class)) { + close(session, WsCloseCode.MACHINE_DISABLED); + } else { + close(session, WsCloseCode.CONNECTION_EXCEPTION); + } + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..23f104b6 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orion.ops.framework.websocket.config.OrionWebsocketAutoConfiguration \ No newline at end of file diff --git a/orion-ops-framework/pom.xml b/orion-ops-framework/pom.xml index b81d870e..7b9a53f9 100644 --- a/orion-ops-framework/pom.xml +++ b/orion-ops-framework/pom.xml @@ -22,6 +22,8 @@ orion-ops-spring-boot-starter-swagger orion-ops-spring-boot-starter-datasource orion-ops-spring-boot-starter-mybatis + orion-ops-spring-boot-starter-job + orion-ops-spring-boot-starter-websocket \ No newline at end of file diff --git a/orion-ops-server/pom.xml b/orion-ops-server/pom.xml index ee95d39e..39daf032 100644 --- a/orion-ops-server/pom.xml +++ b/orion-ops-server/pom.xml @@ -44,6 +44,14 @@ com.orion.ops orion-ops-spring-boot-starter-mybatis + + com.orion.ops + orion-ops-spring-boot-starter-job + + + com.orion.ops + orion-ops-spring-boot-starter-websocket + diff --git a/orion-ops-server/src/main/resources/application.yaml b/orion-ops-server/src/main/resources/application.yaml index 39d2142d..347ce1ef 100644 --- a/orion-ops-server/src/main/resources/application.yaml +++ b/orion-ops-server/src/main/resources/application.yaml @@ -19,6 +19,13 @@ spring: mvc: pathmatch: matching-strategy: ANT_PATH_MATCHER + websocket: + # 1MB + binary-buffer-size: 1048576 + # 1MB + text-buffer-size: 1048576 + # 30MIN + session-idle-timeout: 1800000 datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver