From 5b10cdc21950ed59c2029b6df6e3531cb4b3993e Mon Sep 17 00:00:00 2001 From: Aleksandr Skoblikov Date: Tue, 17 Dec 2024 17:47:31 +0400 Subject: [PATCH] dbeaver/pro#3807 jakarta websocket --- .../model/session/WebHttpRequestInfo.java | 20 ++- .../src/io/cloudbeaver/server/CBPlatform.java | 2 +- .../server/jetty/CBJettyServer.java | 33 ++-- .../server/servlets/CBStaticServlet.java | 4 +- .../service/session/CBSessionManager.java | 29 ++-- .../cloudbeaver/server/BaseWebPlatform.java | 5 +- .../server/WebAppSessionManager.java | 11 +- .../server/jetty/CBJettyWebSocketContext.java | 28 ++-- .../websockets/CBAbstractWebSocket.java | 49 ++++-- .../server/websockets/CBEventsWebSocket.java | 146 +++++++++--------- .../websockets/CBExpiredSessionWebSocket.java | 29 ---- .../websockets/CBJettyWebSocketManager.java | 81 ++-------- .../CBWebSocketServerConfigurator.java | 108 +++++++++++++ .../websockets/WebSocketPingPongCallback.java | 7 +- .../websockets/WebSocketPingPongJob.java | 10 +- .../service/DBWWebSocketContext.java | 7 +- 16 files changed, 307 insertions(+), 262 deletions(-) delete mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBWebSocketServerConfigurator.java diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java index a6dde6469d..7aeb977db7 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java @@ -17,22 +17,33 @@ package io.cloudbeaver.model.session; import jakarta.servlet.http.HttpServletRequest; +import org.jkiss.code.Nullable; public class WebHttpRequestInfo { + public static final String USER_AGENT = "User-Agent"; + @Nullable private final String id; + @Nullable private final Object locale; + @Nullable private final String lastRemoteAddress; + @Nullable private final String lastRemoteUserAgent; public WebHttpRequestInfo(HttpServletRequest request) { - this.id = request.getSession().getId(); + this.id = request.getSession() == null ? null : request.getSession().getId(); this.locale = request.getAttribute("locale"); this.lastRemoteAddress = request.getRemoteAddr(); - this.lastRemoteUserAgent = request.getHeader("User-Agent"); + this.lastRemoteUserAgent = request.getHeader(USER_AGENT); } - public WebHttpRequestInfo(String id, Object locale, String lastRemoteAddress, String lastRemoteUserAgent) { + public WebHttpRequestInfo( + @Nullable String id, + @Nullable Object locale, + @Nullable String lastRemoteAddress, + @Nullable String lastRemoteUserAgent + ) { this.id = id; this.locale = locale; this.lastRemoteAddress = lastRemoteAddress; @@ -43,14 +54,17 @@ public String getId() { return id; } + @Nullable public Object getLocale() { return locale; } + @Nullable public String getLastRemoteAddress() { return lastRemoteAddress; } + @Nullable public String getLastRemoteUserAgent() { return lastRemoteUserAgent; } diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java index 1fd68e3073..3adab6265c 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java @@ -21,7 +21,6 @@ import io.cloudbeaver.server.jobs.SessionStateJob; import io.cloudbeaver.server.jobs.WebDataSourceMonitorJob; import io.cloudbeaver.server.jobs.WebSessionMonitorJob; -import io.cloudbeaver.service.session.CBSessionManager; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.jkiss.code.NotNull; @@ -84,6 +83,7 @@ protected synchronized void initialize() { } protected void scheduleServerJobs() { + super.scheduleServerJobs(); new WebSessionMonitorJob(this, application.getSessionManager()) .scheduleMonitor(); diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBJettyServer.java index 9d0f86ec72..8a59df8ee8 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -20,24 +20,22 @@ import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBConstants; -import io.cloudbeaver.server.WebApplication; import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.server.servlets.CBImageServlet; import io.cloudbeaver.server.servlets.CBStaticServlet; import io.cloudbeaver.server.servlets.WebStatusServlet; -import io.cloudbeaver.server.websockets.CBJettyWebSocketManager; +import io.cloudbeaver.server.websockets.CBEventsWebSocket; +import io.cloudbeaver.server.websockets.CBWebSocketServerConfigurator; import io.cloudbeaver.service.DBWServiceBindingServlet; import io.cloudbeaver.service.DBWServiceBindingWebSocket; +import jakarta.websocket.server.ServerEndpointConfig; import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.ee10.servlet.ServletMapping; +import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; import org.eclipse.jetty.server.*; -import org.eclipse.jetty.session.DefaultSessionCache; -import org.eclipse.jetty.session.DefaultSessionIdManager; -import org.eclipse.jetty.session.NullSessionDataStore; import org.eclipse.jetty.util.resource.ResourceFactory; -import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; import org.eclipse.jetty.xml.XmlConfiguration; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -48,7 +46,6 @@ import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Duration; import java.util.Arrays; public class CBJettyServer { @@ -137,7 +134,7 @@ public void runServer() { } CBJettyWebSocketContext webSocketContext = new CBJettyWebSocketContext(server, servletContextHandler); - for (DBWServiceBindingWebSocket wsb : WebServiceRegistry.getInstance() + for (DBWServiceBindingWebSocket wsb : WebServiceRegistry.getInstance() .getWebServices(DBWServiceBindingWebSocket.class) ) { if (wsb.isApplicable(this.application)) { @@ -149,16 +146,16 @@ public void runServer() { } } - WebSocketUpgradeHandler webSocketHandler = WebSocketUpgradeHandler.from(server, servletContextHandler, (wsContainer) -> { - wsContainer.setIdleTimeout(Duration.ofMinutes(5)); - // Add websockets - wsContainer.addMapping( - serverConfiguration.getServicesURI() + "ws", - new CBJettyWebSocketManager(this.application.getSessionManager()) - ); - } - ); - servletContextHandler.insertHandler(webSocketHandler); + JakartaWebSocketServletContainerInitializer.configure(servletContextHandler, (context, container) -> { + // Add echo endpoint to server container + ServerEndpointConfig eventWsEnpoint = ServerEndpointConfig.Builder + .create( + CBEventsWebSocket.class, + serverConfiguration.getServicesURI() + "ws" + ).configurator(new CBWebSocketServerConfigurator(application.getSessionManager())) + .build(); + container.addEndpoint(eventWsEnpoint); + }); JettyUtils.initSessionManager( this.application.getMaxSessionIdleTime(), diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/servlets/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/servlets/CBStaticServlet.java index 3c8f643b4b..a191e146dd 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/servlets/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/servlets/CBStaticServlet.java @@ -29,7 +29,6 @@ import io.cloudbeaver.registry.WebHandlerRegistry; import io.cloudbeaver.registry.WebServletHandlerDescriptor; import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBPlatform; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServletRequest; @@ -49,7 +48,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -227,7 +225,7 @@ private void patchStaticContentIfNeeded(HttpServletRequest request, HttpServletR response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); response.setHeader(HttpHeader.CONTENT_TYPE.toString(), MimeTypes.TEXT_HTML); response.setHeader(HttpHeader.EXPIRES.toString(), "0"); - response.getOutputStream().write(ByteBuffer.wrap(indexBytes)); + response.getOutputStream().write(indexBytes); } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java index cc36ba849d..c58b49eaf7 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java @@ -22,22 +22,18 @@ import io.cloudbeaver.registry.WebHandlerRegistry; import io.cloudbeaver.registry.WebSessionHandlerDescriptor; import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.server.WebAppSessionManager; import io.cloudbeaver.server.events.WSWebUtils; import io.cloudbeaver.service.DBWSessionHandler; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Session; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.security.user.SMAuthPermissions; -import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.model.websocket.event.WSUserDeletedEvent; import org.jkiss.dbeaver.model.websocket.event.session.WSSessionStateEvent; import org.jkiss.utils.CommonUtils; @@ -163,15 +159,12 @@ public WebSession getWebSession( * @return WebSession object or null, if session expired or invalid */ @Nullable - public WebSession getOrRestoreSession(@NotNull Request request) { - var sessionIdCookie = Request.getCookies(request).stream().filter( - c -> c.getName().equals(CBConstants.CB_SESSION_COOKIE_NAME) - ).findAny().orElse(null); - if (sessionIdCookie == null) { + public WebSession getOrRestoreWebSession(@NotNull WebHttpRequestInfo requestInfo) { + final var sessionId = requestInfo.getId(); + if (sessionId == null) { log.debug("Http session is null. No Web Session returned"); return null; } - var sessionId = sessionIdCookie.getValue(); WebSession webSession; synchronized (sessionMap) { if (sessionMap.containsKey(sessionId)) { @@ -189,12 +182,7 @@ public WebSession getOrRestoreSession(@NotNull Request request) { return null; } - webSession = createWebSessionImpl(new WebHttpRequestInfo( - request.getId(), - request.getAttribute("locale"), - Request.getRemoteAddr(request), - request.getHeaders().get("User-Agent") - )); + webSession = createWebSessionImpl(requestInfo); restorePreviousUserSession(webSession, oldAuthInfo); sessionMap.put(sessionId, webSession); @@ -301,15 +289,18 @@ public Collection getAllActiveSessions() { } @Nullable - public WebHeadlessSession getHeadlessSession(Request request, Session session, boolean create) throws DBException { - String smAccessToken = request.getHeaders().get(WSConstants.WS_AUTH_HEADER); + public WebHeadlessSession getHeadlessSession( + @Nullable String smAccessToken, + @NotNull WebHttpRequestInfo requestInfo, + boolean create + ) throws DBException { if (CommonUtils.isEmpty(smAccessToken)) { return null; } synchronized (sessionMap) { var tempCredProvider = new SMTokenCredentialProvider(smAccessToken); SMAuthPermissions authPermissions = application.createSecurityController(tempCredProvider).getTokenPermissions(); - var sessionId = session != null ? session.getId() + var sessionId = requestInfo.getId() != null ? requestInfo.getId() : authPermissions.getSessionId(); var existSession = sessionMap.get(sessionId); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java index 9d8cca6ad2..d12d76f740 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java @@ -17,6 +17,7 @@ package io.cloudbeaver.server; import io.cloudbeaver.DBWConstants; +import io.cloudbeaver.server.websockets.WebSocketPingPongJob; import org.eclipse.core.runtime.Plugin; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; @@ -132,7 +133,9 @@ private void initTempFolder(@NotNull DBRProgressMonitor monitor) { @NotNull public abstract WebApplication getApplication(); - protected abstract void scheduleServerJobs(); + protected void scheduleServerJobs() { + new WebSocketPingPongJob(WebAppUtils.getWebPlatform()).scheduleMonitor(); + } @Override public synchronized void dispose() { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java index d6465fb05c..9e54be6766 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java @@ -19,11 +19,10 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebHeadlessSession; +import io.cloudbeaver.model.session.WebHttpRequestInfo; import io.cloudbeaver.model.session.WebSession; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Session; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -56,9 +55,13 @@ WebSession getWebSession( Collection getAllActiveSessions(); - WebSession getOrRestoreSession(Request httpRequest); + WebSession getOrRestoreWebSession(WebHttpRequestInfo httpRequest); - WebHeadlessSession getHeadlessSession(Request request, Session session, boolean create) throws DBException; + WebHeadlessSession getHeadlessSession( + @Nullable String smAccessToken, + @NotNull WebHttpRequestInfo requestInfo, + boolean create + ) throws DBException; boolean touchSession(HttpServletRequest request, HttpServletResponse response) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java index 8a19513a84..dd7330972c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java @@ -17,36 +17,34 @@ package io.cloudbeaver.server.jetty; import io.cloudbeaver.service.DBWWebSocketContext; +import jakarta.websocket.server.ServerEndpointConfig; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.websocket.api.Configurable; -import org.eclipse.jetty.websocket.server.WebSocketCreator; -import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; import org.jkiss.code.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; public class CBJettyWebSocketContext implements DBWWebSocketContext { private final List mappings = new ArrayList<>(); private final Server server; - private final ContextHandler handler; + private final ServletContextHandler servletContextHandler; - public CBJettyWebSocketContext(@NotNull Server server, @NotNull ContextHandler handler) { + public CBJettyWebSocketContext(@NotNull Server server, @NotNull ServletContextHandler servletContextHandler) { this.server = server; - this.handler = handler; + this.servletContextHandler = servletContextHandler; } + @Override - public void addWebSocket(@NotNull String mapping, @NotNull Function configurator) { - handler.insertHandler(WebSocketUpgradeHandler.from( - server, - handler, - container -> container.addMapping(mapping, configurator.apply(container)) - )); - mappings.add(mapping); + public void addWebSocket(@NotNull ServerEndpointConfig endpointConfig) { + // Add jakarta.websocket support + JakartaWebSocketServletContainerInitializer.configure(servletContextHandler, (context, container) -> { + container.addEndpoint(endpointConfig); + this.mappings.add(endpointConfig.getPath()); + }); } @NotNull diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java index 7af0f380c4..413fcdd165 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java @@ -17,28 +17,41 @@ package io.cloudbeaver.server.websockets; import com.google.gson.Gson; -import org.eclipse.jetty.websocket.api.Callback; -import org.eclipse.jetty.websocket.api.Session; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.WSUtils; import org.jkiss.dbeaver.model.websocket.event.WSEvent; -public class CBAbstractWebSocket extends Session.Listener.AbstractAutoDemanding { +public abstract class CBAbstractWebSocket extends Endpoint { private static final Log log = Log.getLog(CBAbstractWebSocket.class); protected static final Gson gson = WSUtils.clientGson; + @Nullable + private Session webSocketSession; + + @Override + public void onOpen(Session session, EndpointConfig config) { + this.webSocketSession = session; + } + public void handleEvent(WSEvent event) { if (!isOpen()) { return; } - Session session = getSession(); - session.sendText(gson.toJson(event), new Callback() { - @Override - public void fail(Throwable e) { - handleEventException(e); - } - }); + try { + webSocketSession.getBasicRemote().sendText( + gson.toJson(event) + ); + } catch (Exception e) { + handleEventException(e); + } + } + protected boolean isOpen() { + return webSocketSession != null && webSocketSession.isOpen(); } protected void handleEventException(Throwable e) { @@ -46,10 +59,18 @@ protected void handleEventException(Throwable e) { } public void close() { - var session = getSession(); - // the socket may not be connected to the client - if (session != null) { - getSession().close(); + if (isOpen()) { + try { + getSession().close(); + } catch (Exception e) { + log.error("Failed to close websocket", e); + } } } + + @Nullable + public Session getSession() { + return webSocketSession; + } + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java index 55bbc7ce2b..ddfe292bfd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java @@ -20,107 +20,115 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.websocket.CBWebSessionEventHandler; -import org.eclipse.jetty.websocket.api.Callback; -import org.eclipse.jetty.websocket.api.Session; -import org.jkiss.code.NotNull; +import jakarta.websocket.*; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.event.WSClientEvent; import org.jkiss.dbeaver.model.websocket.event.WSClientEventType; import org.jkiss.dbeaver.model.websocket.event.WSEvent; import org.jkiss.dbeaver.model.websocket.event.client.WSUpdateActiveProjectsClientEvent; +import org.jkiss.dbeaver.model.websocket.event.session.WSAccessTokenExpiredEvent; import org.jkiss.dbeaver.model.websocket.event.session.WSSocketConnectedEvent; +import org.jkiss.utils.CommonUtils; public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessionEventHandler { private static final Log log = Log.getLog(CBEventsWebSocket.class); - @NotNull - private final BaseWebSession webSession; - @NotNull - private final Callback callback; - - public CBEventsWebSocket(@NotNull BaseWebSession webSession) { - this.webSession = webSession; - - callback = new WebSocketPingPongCallback(webSession); - } + @Nullable + private BaseWebSession webSession; @Override - public void onWebSocketOpen(Session session) { - super.onWebSocketOpen(session); - this.webSession.addEventHandler(this); - handleEvent(new WSSocketConnectedEvent(webSession.getApplication().getApplicationRunId())); - log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session"); - } + public void onOpen(Session session, EndpointConfig config) { + super.onOpen(session, config); + if (session.getUserProperties().containsKey(CBWebSocketServerConfigurator.PROP_TOKEN_EXPIRED)) { + handleEvent(new WSAccessTokenExpiredEvent()); + close(); + } else { + this.webSession = (BaseWebSession) session.getUserProperties() + .get(CBWebSocketServerConfigurator.PROP_WEB_SESSION); + this.webSession.addEventHandler(this); + handleEvent(new WSSocketConnectedEvent(webSession.getApplication().getApplicationRunId())); + log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session"); + session.addMessageHandler(String.class, new FromUserEventHandler()); + session.addMessageHandler(PongMessage.class, new WebSocketPingPongCallback(webSession)); - @Override - public void onWebSocketText(String message) { - super.onWebSocketText(message); - var clientEvent = CBAbstractWebSocket.gson.fromJson(message, WSClientEvent.class); - var clientEventType = WSClientEventType.valueById(clientEvent.getId()); - if (clientEventType == null) { - webSession.addSessionError( - new DBWebException("Invalid websocket event: " + message) - ); - return; + CBJettyWebSocketManager.registerWebSocket(webSession.getSessionId(), this); } - switch (clientEventType) { - case TOPIC_SUBSCRIBE: { - this.webSession.getEventsFilter().subscribeOnEventTopic(clientEvent.getTopicId()); - break; + } + + private class FromUserEventHandler implements MessageHandler.Whole { + @Override + public void onMessage(String message) { + if (CommonUtils.isEmpty(message)) { + return; } - case TOPIC_UNSUBSCRIBE: { - this.webSession.getEventsFilter().unsubscribeFromEventTopic(clientEvent.getTopicId()); - break; + var clientEvent = CBAbstractWebSocket.gson.fromJson(message, WSClientEvent.class); + var clientEventType = WSClientEventType.valueById(clientEvent.getId()); + if (webSession == null) { + log.warn("No web session for browser event"); + return; } - case ACTIVE_PROJECTS: { - var projectEvent = (WSUpdateActiveProjectsClientEvent) clientEvent; - this.webSession.getEventsFilter().setSubscribedProjects(projectEvent.getProjectIds()); - break; + if (clientEventType == null) { + webSession.addSessionError( + new DBWebException("Invalid websocket event: " + message) + ); + return; } - case SESSION_PING: { - if (webSession instanceof WebSession session) { - session.updateInfo(true); + switch (clientEventType) { + case TOPIC_SUBSCRIBE: { + webSession.getEventsFilter().subscribeOnEventTopic(clientEvent.getTopicId()); + break; + } + case TOPIC_UNSUBSCRIBE: { + webSession.getEventsFilter().unsubscribeFromEventTopic(clientEvent.getTopicId()); + break; + } + case ACTIVE_PROJECTS: { + var projectEvent = (WSUpdateActiveProjectsClientEvent) clientEvent; + webSession.getEventsFilter().setSubscribedProjects(projectEvent.getProjectIds()); + break; } - break; + case SESSION_PING: { + if (webSession instanceof WebSession session) { + session.updateInfo(true); + } + break; + } + default: + var e = new DBWebException("Unknown websocket client event: " + clientEvent.getId()); + log.error(e.getMessage(), e); + webSession.addSessionError(e); } - default: - var e = new DBWebException("Unknown websocket client event: " + clientEvent.getId()); - log.error(e.getMessage(), e); - webSession.addSessionError(e); } } @Override - public void onWebSocketClose(int statusCode, String reason) { - super.onWebSocketClose(statusCode, reason); - this.webSession.removeEventHandler(this); - log.debug("Socket Closed: [" + statusCode + "] " + reason); + public void onClose(Session session, CloseReason closeReason) { + super.onClose(session, closeReason); + log.debug("Socket Closed: [" + closeReason.getCloseCode() + "] " + closeReason.getReasonPhrase()); + if (webSession != null) { + this.webSession.removeEventHandler(this); + } } @Override - public void onWebSocketError(Throwable cause) { - super.onWebSocketError(cause); - log.error(cause.getMessage(), cause); - webSession.addSessionError(cause); + public void handleWebSessionEvent(WSEvent event) { + super.handleEvent(event); } @Override - public void handleWebSessionEvent(WSEvent event) { - super.handleEvent(event); + public void onError(Session session, Throwable thr) { + if (webSession != null) { + webSession.addSessionError(thr); + } + log.trace("Error in websocket session: " + thr.getMessage(), thr); } + @Override protected void handleEventException(Throwable e) { super.handleEventException(e); - webSession.addSessionError(e); - } - - @NotNull - public BaseWebSession getWebSession() { - return webSession; - } - - @NotNull - public Callback getCallback() { - return callback; + if (webSession != null) { + webSession.addSessionError(e); + } } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java deleted file mode 100644 index 643197d7e3..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.cloudbeaver.server.websockets; - -import org.eclipse.jetty.websocket.api.Session; -import org.jkiss.dbeaver.model.websocket.event.session.WSAccessTokenExpiredEvent; - -public class CBExpiredSessionWebSocket extends CBAbstractWebSocket { - @Override - public void onWebSocketOpen(Session session) { - super.onWebSocketOpen(session); - handleEvent(new WSAccessTokenExpiredEvent()); - close(); - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java index 99641357ba..bfe6712f86 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java @@ -16,21 +16,10 @@ */ package io.cloudbeaver.server.websockets; -import io.cloudbeaver.model.session.BaseWebSession; -import io.cloudbeaver.model.session.WebHeadlessSession; -import io.cloudbeaver.model.session.WebHttpRequestInfo; import io.cloudbeaver.server.WebAppSessionManager; import io.cloudbeaver.server.WebAppUtils; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.websocket.server.ServerUpgradeRequest; -import org.eclipse.jetty.websocket.server.ServerUpgradeResponse; -import org.eclipse.jetty.websocket.server.WebSocketCreator; import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -39,71 +28,20 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -public class CBJettyWebSocketManager implements WebSocketCreator { +public class CBJettyWebSocketManager { private static final Log log = Log.getLog(CBJettyWebSocketManager.class); - private final Map> socketBySessionId = new ConcurrentHashMap<>(); - private final WebAppSessionManager webSessionManager; + private static final Map> socketBySessionId = new ConcurrentHashMap<>(); - public CBJettyWebSocketManager(@NotNull WebAppSessionManager webSessionManager) { - this.webSessionManager = webSessionManager; - - new WebSocketPingPongJob(WebAppUtils.getWebPlatform(), this).scheduleMonitor(); - } - - @Nullable - @Override - public Object createWebSocket(@NotNull ServerUpgradeRequest request, ServerUpgradeResponse resp, Callback callback) { - var webSession = webSessionManager.getOrRestoreSession(request); - var requestInfo = new WebHttpRequestInfo( - request.getId(), - request.getAttribute("locale"), - Request.getRemoteAddr(request), - request.getHeaders().get("User-Agent") - ); - if (webSession != null) { - webSession.updateSessionParameters(requestInfo); - // web client session - return createNewEventsWebSocket(webSession); - } - // possible desktop client session - try { - var headlessSession = createHeadlessSession(request); - if (headlessSession == null) { - log.debug("Couldn't create headless session"); - return null; - } - return createNewEventsWebSocket(headlessSession); - } catch (SMAccessTokenExpiredException e) { - return new CBExpiredSessionWebSocket(); - } catch (DBException e) { - log.error("Error resolve websocket session", e); - return null; - } - } - - @NotNull - private CBEventsWebSocket createNewEventsWebSocket(@NotNull BaseWebSession webSession) { - var sessionId = webSession.getSessionId(); - var newWebSocket = new CBEventsWebSocket(webSession); - socketBySessionId.computeIfAbsent(sessionId, key -> new CopyOnWriteArrayList<>()) - .add(newWebSocket); - log.info("Websocket created for session: " + sessionId); - return newWebSocket; + private CBJettyWebSocketManager() { } - @Nullable - private WebHeadlessSession createHeadlessSession(@NotNull Request request) throws DBException { - var requestSession = request.getSession(false); - if (requestSession == null) { - log.debug("CloudBeaver web session not exist, try to create headless session"); - } else { - log.debug("CloudBeaver session not found with id " + requestSession.getId() + ", try to create headless session"); - } - return webSessionManager.getHeadlessSession(request, requestSession, true); + public static void registerWebSocket(@NotNull String webSessionId, @NotNull CBEventsWebSocket webSocket) { + socketBySessionId.computeIfAbsent(webSessionId, key -> new CopyOnWriteArrayList<>()).add(webSocket); } - public void sendPing() { + public static void sendPing() { //remove expired sessions + WebAppSessionManager webSessionManager = WebAppUtils.getWebApplication().getSessionManager(); socketBySessionId.entrySet() .removeIf(entry -> { entry.getValue().removeIf(ws -> !ws.isOpen()); @@ -121,9 +59,8 @@ public void sendPing() { var webSockets = entry.getValue(); for (CBEventsWebSocket webSocket : webSockets) { try { - webSocket.getSession().sendPing( - ByteBuffer.wrap("cb-ping".getBytes(StandardCharsets.UTF_8)), - webSocket.getCallback() + webSocket.getSession().getBasicRemote().sendPing( + ByteBuffer.wrap("cb-ping".getBytes(StandardCharsets.UTF_8)) ); } catch (Exception e) { log.error("Failed to send ping in web socket: " + sessionId); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBWebSocketServerConfigurator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBWebSocketServerConfigurator.java new file mode 100644 index 0000000000..7f7dc8b2e8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBWebSocketServerConfigurator.java @@ -0,0 +1,108 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.websockets; + +import io.cloudbeaver.model.session.WebHeadlessSession; +import io.cloudbeaver.model.session.WebHttpRequestInfo; +import io.cloudbeaver.server.WebAppSessionManager; +import jakarta.servlet.http.HttpSession; +import jakarta.websocket.HandshakeResponse; +import jakarta.websocket.server.HandshakeRequest; +import jakarta.websocket.server.ServerEndpointConfig; +import org.eclipse.jetty.ee10.websocket.jakarta.server.internal.JakartaWebSocketCreator; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException; +import org.jkiss.dbeaver.model.websocket.WSConstants; +import org.jkiss.utils.CommonUtils; + +import java.util.List; + +public class CBWebSocketServerConfigurator extends ServerEndpointConfig.Configurator { + private static final Log log = Log.getLog(CBWebSocketServerConfigurator.class); + + public static final String PROP_WEB_SESSION = "cb-session"; + public static final String PROP_TOKEN_EXPIRED = "cb-token-expired"; + + @NotNull + private final WebAppSessionManager webSessionManager; + + public CBWebSocketServerConfigurator(@NotNull WebAppSessionManager sessionManager) { + this.webSessionManager = sessionManager; + } + + @Override + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + String sessionId = request.getHttpSession() instanceof HttpSession httpSession ? httpSession.getId() : null; + + String userAgentHeader = request.getHeaders() + .get(WebHttpRequestInfo.USER_AGENT) + .stream() + .findFirst() + .orElse(null); + + WebHttpRequestInfo requestInfo = new WebHttpRequestInfo( + sessionId, + sec.getUserProperties().get(JakartaWebSocketCreator.PROP_LOCALES), + CommonUtils.toString(sec.getUserProperties().get(JakartaWebSocketCreator.PROP_REMOTE_ADDRESS)), + userAgentHeader + ); + + var webSession = webSessionManager.getOrRestoreWebSession(requestInfo); + + if (webSession != null) { + webSession.updateSessionParameters(requestInfo); + // web client session + sec.getUserProperties().put(PROP_WEB_SESSION, webSession); + } + // possible desktop client session + try { + var headlessSession = createHeadlessSession(requestInfo, request); + if (headlessSession == null) { + log.debug("Couldn't create headless session"); + return; + } + sec.getUserProperties().put(PROP_WEB_SESSION, headlessSession); + } catch (SMAccessTokenExpiredException e) { + sec.getUserProperties().put(PROP_TOKEN_EXPIRED, true); + } catch (DBException e) { + log.error("Error resolve websocket session", e); + throw new RuntimeException(e.getMessage(), e); + } + } + + + @Nullable + private WebHeadlessSession createHeadlessSession( + @NotNull WebHttpRequestInfo requestInfo, + @NotNull HandshakeRequest request + ) throws DBException { + if (request.getHeaders() == null) { + return null; + } + List tokenHeaders = request.getHeaders().get(WSConstants.WS_AUTH_HEADER); + if (CommonUtils.isEmpty(tokenHeaders)) { + return null; + } + String smAccessToken = tokenHeaders.stream() + .findFirst() + .orElse(null); + return webSessionManager.getHeadlessSession(smAccessToken, requestInfo, true); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java index b19741df9a..79e875f238 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java @@ -18,10 +18,11 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebHeadlessSession; -import org.eclipse.jetty.websocket.api.Callback; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.PongMessage; import org.jkiss.code.NotNull; -public class WebSocketPingPongCallback implements Callback { +public class WebSocketPingPongCallback implements MessageHandler.Whole { @NotNull private final BaseWebSession webSession; @@ -30,7 +31,7 @@ public WebSocketPingPongCallback(@NotNull BaseWebSession webSession) { } @Override - public void succeed() { + public void onMessage(PongMessage message) { if (webSession instanceof WebHeadlessSession) { webSession.touchSession(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java index 6409f09d0e..2973f48914 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java @@ -25,17 +25,15 @@ /** * WebSessionMonitorJob */ -class WebSocketPingPongJob extends AbstractJob { +public class WebSocketPingPongJob extends AbstractJob { private static final int INTERVAL = 1000 * 60 * 1; // once per 1 min private final BaseWebPlatform platform; - private final CBJettyWebSocketManager webSocketManager; - public WebSocketPingPongJob(BaseWebPlatform platform, CBJettyWebSocketManager webSocketManager) { + public WebSocketPingPongJob(BaseWebPlatform platform) { super("WebSocket monitor"); this.platform = platform; setUser(false); setSystem(true); - this.webSocketManager = webSocketManager; } @Override @@ -44,7 +42,7 @@ protected IStatus run(DBRProgressMonitor monitor) { return Status.OK_STATUS; } - webSocketManager.sendPing(); + CBJettyWebSocketManager.sendPing(); if (!platform.isShuttingDown()) { scheduleMonitor(); @@ -52,7 +50,7 @@ protected IStatus run(DBRProgressMonitor monitor) { return Status.OK_STATUS; } - void scheduleMonitor() { + public void scheduleMonitor() { schedule(INTERVAL); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java index 4da0e61f05..8b9a050ea5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java @@ -16,13 +16,10 @@ */ package io.cloudbeaver.service; -import org.eclipse.jetty.websocket.api.Configurable; -import org.eclipse.jetty.websocket.server.WebSocketCreator; +import jakarta.websocket.server.ServerEndpointConfig; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; -import java.util.function.Function; - public interface DBWWebSocketContext { - void addWebSocket(@NotNull String mapping, @NotNull Function configurator) throws DBException; + void addWebSocket(@NotNull ServerEndpointConfig endpointConfig) throws DBException; }