This is an automated email from the ASF dual-hosted git repository.
nvazquez pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.20 by this push:
new e6f193db80b Backport: Honour consoleproxy.session.timeout for noVNC
console sessions (#13058)
e6f193db80b is described below
commit e6f193db80b5f355e01a22414a3658687a9b0986
Author: Dheeraj Bansal <[email protected]>
AuthorDate: Tue Nov 10 02:16:01 2026 +0530
Backport: Honour consoleproxy.session.timeout for noVNC console sessions
(#13058)
* Fix console proxy idle timeout and noVNC session handling
* Honor consoleproxy.session.timeout in console proxy GC
* Apply suggestion
* Apply suggestion
* Apply suggestions from code review
Co-authored-by: Copilot <[email protected]>
Co-authored-by: dahn <[email protected]>
* Apply suggestion
* Apply suggestions
Co-authored-by: dahn <[email protected]>
* half the white lines removed
Co-authored-by: dahn <[email protected]>
* white lines
Co-authored-by: dahn <[email protected]>
* more white lines
Co-authored-by: dahn <[email protected]>
* Apply suggestions from code review
Co-authored-by: dahn <[email protected]>
* Forward console proxy session timeout to CPVM
* Fix idle timeout tracking for noVNC sessions
* Fix noVNC console session activity tracking
* Add test for noVNC activity timestamp
* Close noVNC WebSocket when console session times out
---------
Co-authored-by: dahn <[email protected]>
Co-authored-by: Copilot <[email protected]>
---
.../consoleproxy/ConsoleProxyManagerImpl.java | 1 +
.../java/com/cloud/consoleproxy/ConsoleProxy.java | 148 +++++++++++++--------
.../cloud/consoleproxy/ConsoleProxyGCThread.java | 41 +++---
.../consoleproxy/ConsoleProxyNoVNCHandler.java | 62 +++++++--
.../consoleproxy/ConsoleProxyNoVncClient.java | 16 ++-
.../consoleproxy/ConsoleProxyNoVncClientTest.java | 27 ++++
systemvm/agent/noVNC/core/rfb.js | 4 +
systemvm/agent/noVNC/core/websock.js | 15 +++
8 files changed, 224 insertions(+), 90 deletions(-)
diff --git
a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
index f1e422ec36c..f9b2912b83d 100644
--- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
+++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
@@ -1259,6 +1259,7 @@ public class ConsoleProxyManagerImpl extends ManagerBase
implements ConsoleProxy
buf.append(" pod=").append(dest.getPod().getId());
buf.append(" guid=Proxy.").append(profile.getId());
buf.append(" proxy_vm=").append(profile.getId());
+ buf.append("
consoleproxy.session.timeout=").append(proxySessionTimeoutValue);
if (disableRpFilter) {
buf.append(" disable_rp_filter=true");
}
diff --git
a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java
b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java
index 0befe392b4e..99e9e30e092 100644
---
a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java
+++
b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java
@@ -36,6 +36,8 @@ import java.util.concurrent.Executor;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;
import org.eclipse.jetty.websocket.api.Session;
@@ -43,8 +45,6 @@ import com.cloud.utils.PropertiesUtil;
import com.google.gson.Gson;
import com.sun.net.httpserver.HttpServer;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
/**
*
@@ -58,6 +58,11 @@ public class ConsoleProxy {
public static final int VIEWER_LINGER_SECONDS = 180;
+ // New: default and effective session timeout (milliseconds) honoured from
consoleproxy.session.timeout
+ public static final long DEFAULT_SESSION_TIMEOUT_MILLIS = 300000;
+ public static volatile long sessionTimeoutMillis =
DEFAULT_SESSION_TIMEOUT_MILLIS;
+
+
public static Object context;
// this has become more ugly, to store keystore info passed from
management server (we now use management server managed keystore to support
@@ -82,7 +87,8 @@ public class ConsoleProxy {
static boolean standaloneStart = false;
static String encryptorPassword = "Dummy";
- static final String[] skipProperties = new String[]{"certificate",
"cacertificate", "keystore_password", "privatekey"};
+ static final String[] skipProperties = new String[] {"certificate",
"cacertificate", "keystore_password", "privatekey"};
+
static Set<String> allowedSessions = new HashSet<>();
@@ -93,11 +99,13 @@ public class ConsoleProxy {
private static void configLog4j() {
final ClassLoader loader =
Thread.currentThread().getContextClassLoader();
URL configUrl = loader.getResource("/conf/log4j-cloud.xml");
- if (configUrl == null)
+ if (configUrl == null) {
configUrl = ClassLoader.getSystemResource("log4j-cloud.xml");
+ }
- if (configUrl == null)
+ if (configUrl == null) {
configUrl = ClassLoader.getSystemResource("conf/log4j-cloud.xml");
+ }
if (configUrl != null) {
try {
@@ -114,7 +122,6 @@ public class ConsoleProxy {
} catch (URISyntaxException e) {
System.out.println("Unable to convert log4j configuration Url
to URI");
}
- // DOMConfigurator.configure(configUrl);
} else {
System.out.println("Configure log4j with default properties");
}
@@ -122,20 +129,21 @@ public class ConsoleProxy {
private static void configProxy(Properties conf) {
LOGGER.info("Configure console proxy...");
- for (Object key : conf.keySet()) {
- LOGGER.info("Property " + (String)key + ": " +
conf.getProperty((String)key));
- if (!ArrayUtils.contains(skipProperties, key)) {
- LOGGER.info("Property " + (String)key + ": " +
conf.getProperty((String)key));
+ if (conf != null) {
+ for (Object key : conf.keySet()) {
+ if (!ArrayUtils.contains(skipProperties, key)) {
+ LOGGER.info("Property " + (String) key + ": " +
conf.getProperty((String) key));
+ }
}
}
- String s = conf.getProperty("consoleproxy.httpListenPort");
+ String s = conf != null ?
conf.getProperty("consoleproxy.httpListenPort") : null;
if (s != null) {
httpListenPort = Integer.parseInt(s);
LOGGER.info("Setting httpListenPort=" + s);
}
- s = conf.getProperty("premium");
+ s = conf != null ? conf.getProperty("premium") : null;
if (s != null && s.equalsIgnoreCase("true")) {
LOGGER.info("Premium setting will override settings from
consoleproxy.properties, listen at port 443");
httpListenPort = 443;
@@ -144,36 +152,55 @@ public class ConsoleProxy {
factoryClzName = ConsoleProxyBaseServerFactoryImpl.class.getName();
}
- s = conf.getProperty("consoleproxy.httpCmdListenPort");
+ s = conf != null ? conf.getProperty("consoleproxy.httpCmdListenPort")
: null;
if (s != null) {
httpCmdListenPort = Integer.parseInt(s);
LOGGER.info("Setting httpCmdListenPort=" + s);
}
- s = conf.getProperty("consoleproxy.reconnectMaxRetry");
+ s = conf != null ? conf.getProperty("consoleproxy.reconnectMaxRetry")
: null;
if (s != null) {
reconnectMaxRetry = Integer.parseInt(s);
LOGGER.info("Setting reconnectMaxRetry=" + reconnectMaxRetry);
}
- s = conf.getProperty("consoleproxy.readTimeoutSeconds");
+ s = conf != null ? conf.getProperty("consoleproxy.readTimeoutSeconds")
: null;
if (s != null) {
readTimeoutSeconds = Integer.parseInt(s);
LOGGER.info("Setting readTimeoutSeconds=" + readTimeoutSeconds);
}
- s = conf.getProperty("consoleproxy.defaultBufferSize");
+ s = conf != null ? conf.getProperty("consoleproxy.defaultBufferSize")
: null;
if (s != null) {
defaultBufferSize = Integer.parseInt(s);
LOGGER.info("Setting defaultBufferSize=" + defaultBufferSize);
}
+
+ s = conf != null ? conf.getProperty("consoleproxy.session.timeout") :
null;
+ if (s != null) {
+ try {
+ long value = Long.parseLong(s);
+ if (value <= 0) {
+ LOGGER.warn("consoleproxy.session.timeout={} is <= 0,
using default {} ms",
+ value, DEFAULT_SESSION_TIMEOUT_MILLIS);
+ sessionTimeoutMillis = DEFAULT_SESSION_TIMEOUT_MILLIS;
+ } else {
+ sessionTimeoutMillis = value;
+ }
+ } catch (NumberFormatException e) {
+ LOGGER.warn("Invalid value for consoleproxy.session.timeout:
{}, using default {} ms",
+ s, DEFAULT_SESSION_TIMEOUT_MILLIS, e);
+ sessionTimeoutMillis = DEFAULT_SESSION_TIMEOUT_MILLIS;
+ }
+ }
+ LOGGER.info("Effective consoleproxy.session.timeout={} ms",
sessionTimeoutMillis);
}
public static ConsoleProxyServerFactory getHttpServerFactory() {
try {
Class<?> clz = Class.forName(factoryClzName);
try {
- ConsoleProxyServerFactory factory =
(ConsoleProxyServerFactory)clz.newInstance();
+ ConsoleProxyServerFactory factory =
(ConsoleProxyServerFactory) clz.newInstance();
factory.init(ConsoleProxy.ksBits, ConsoleProxy.ksPassword);
return factory;
} catch (InstantiationException e) {
@@ -197,12 +224,11 @@ public class ConsoleProxy {
authResult.setHost(param.getClientHostAddress());
authResult.setPort(param.getClientHostPort());
- if
(org.apache.commons.lang3.StringUtils.isNotBlank(param.getExtraSecurityToken()))
{
+ if (StringUtils.isNotBlank(param.getExtraSecurityToken())) {
String extraToken = param.getExtraSecurityToken();
String clientProvidedToken =
param.getClientProvidedExtraSecurityToken();
- LOGGER.debug(String.format("Extra security validation for the
console access, provided %s " +
- "to validate against %s", clientProvidedToken,
extraToken));
-
+ LOGGER.debug(String.format("Extra security validation for the
console access, provided %s to validate against %s",
+ clientProvidedToken, extraToken));
if (!extraToken.equals(clientProvidedToken)) {
LOGGER.error("The provided extra token does not match the
expected value for this console endpoint");
authResult.setSuccess(false);
@@ -233,20 +259,21 @@ public class ConsoleProxy {
Object result;
try {
result =
- authMethod.invoke(ConsoleProxy.context,
param.getClientHostAddress(), String.valueOf(param.getClientHostPort()),
param.getClientTag(),
- param.getClientHostPassword(),
param.getTicket(), reauthentication, param.getSessionUuid());
+ authMethod.invoke(ConsoleProxy.context,
param.getClientHostAddress(), String.valueOf(param.getClientHostPort()),
+ param.getClientTag(),
param.getClientHostPassword(), param.getTicket(), reauthentication,
+ param.getSessionUuid());
} catch (IllegalAccessException e) {
- LOGGER.error("Unable to invoke authenticateConsoleAccess due
to IllegalAccessException" + " for vm: " + param.getClientTag(), e);
+ LOGGER.error("Unable to invoke authenticateConsoleAccess due
to IllegalAccessException for vm: " + param.getClientTag(), e);
authResult.setSuccess(false);
return authResult;
} catch (InvocationTargetException e) {
- LOGGER.error("Unable to invoke authenticateConsoleAccess due
to InvocationTargetException " + " for vm: " + param.getClientTag(), e);
+ LOGGER.error("Unable to invoke authenticateConsoleAccess due
to InvocationTargetException for vm: " + param.getClientTag(), e);
authResult.setSuccess(false);
return authResult;
}
if (result != null && result instanceof String) {
- authResult = new Gson().fromJson((String)result,
ConsoleProxyAuthenticationResult.class);
+ authResult = new Gson().fromJson((String) result,
ConsoleProxyAuthenticationResult.class);
} else {
LOGGER.error("Invalid authentication return object " + result
+ " for vm: " + param.getClientTag() + ", decline the access");
authResult.setSuccess(false);
@@ -286,7 +313,8 @@ public class ConsoleProxy {
}
}
- public static void startWithContext(Properties conf, Object context,
byte[] ksBits, String ksPassword, String password, Boolean
isSourceIpCheckEnabled) {
+ public static void startWithContext(Properties conf, Object context,
byte[] ksBits, String ksPassword,
+ String password, Boolean
isSourceIpCheckEnabled) {
setEncryptorPassword(password);
configLog4j();
LOGGER.info("Start console proxy with context");
@@ -326,35 +354,40 @@ public class ConsoleProxy {
Properties props = new Properties();
if (confs == null) {
final File file =
PropertiesUtil.findConfigFile("consoleproxy.properties");
- if (file == null)
+ if (file == null) {
LOGGER.info("Can't load consoleproxy.properties from
classpath, will use default configuration");
- else
+ } else {
try {
confs = new FileInputStream(file);
} catch (FileNotFoundException e) {
LOGGER.info("Ignoring file not found exception and using
defaults");
}
+ }
}
if (confs != null) {
try {
props.load(confs);
+ if (conf == null) {
+ conf = new Properties();
+ }
for (Object key : props.keySet()) {
// give properties passed via context high priority, treat
properties from consoleproxy.properties
// as default values
- if (conf.get(key) == null)
+ if (conf.get(key) == null) {
conf.put(key, props.get(key));
+ }
}
} catch (Exception e) {
LOGGER.error(e.toString(), e);
+ } finally {
+ try {
+ confs.close();
+ } catch (IOException e) {
+ LOGGER.error("Failed to close consoleproxy.properties : "
+ e.toString(), e);
+ }
}
}
- try {
- confs.close();
- } catch (IOException e) {
- LOGGER.error("Failed to close consolepropxy.properties : " +
e.toString(), e);
- }
-
start(conf);
}
@@ -474,8 +507,8 @@ public class ConsoleProxy {
LOGGER.info("The rfb thread died, reinitializing the viewer "
+ viewer);
viewer.initClient(param);
} else if
(!param.getClientHostPassword().equals(viewer.getClientHostPassword())) {
- LOGGER.warn("Bad sid detected(VNC port may be reused). sid in
session: " + viewer.getClientHostPassword() + ", sid in request: " +
- param.getClientHostPassword());
+ LOGGER.warn("Bad sid detected(VNC port may be reused). sid in
session: " + viewer.getClientHostPassword() +
+ ", sid in request: " + param.getClientHostPassword());
viewer.initClient(param);
}
}
@@ -484,8 +517,9 @@ public class ConsoleProxy {
ConsoleProxyClientStatsCollector statsCollector =
getStatsCollector();
String loadInfo = statsCollector.getStatsReport();
reportLoadInfo(loadInfo);
- if (LOGGER.isDebugEnabled())
+ if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Report load change : " + loadInfo);
+ }
}
return viewer;
@@ -509,13 +543,15 @@ public class ConsoleProxy {
// protected against malicious attack by modifying URL content
if (ajaxSession != null) {
long ajaxSessionIdFromUrl = Long.parseLong(ajaxSession);
- if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId())
+ if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId()) {
throw new AuthenticationException("Cannot use the
existing viewer " + viewer + ": modified AJAX session id");
+ }
}
- if (param.getClientHostPassword() == null ||
param.getClientHostPassword().isEmpty() ||
-
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
+ if (param.getClientHostPassword() == null ||
param.getClientHostPassword().isEmpty()
+ ||
!param.getClientHostPassword().equals(viewer.getClientHostPassword())) {
throw new AuthenticationException("Cannot use the existing
viewer " + viewer + ": bad sid");
+ }
if (!viewer.isFrontEndAlive()) {
@@ -529,8 +565,9 @@ public class ConsoleProxy {
ConsoleProxyClientStatsCollector statsCollector =
getStatsCollector();
String loadInfo = statsCollector.getStatsReport();
reportLoadInfo(loadInfo);
- if (LOGGER.isDebugEnabled())
+ if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Report load change : " + loadInfo);
+ }
}
return viewer;
}
@@ -566,9 +603,11 @@ public class ConsoleProxy {
ConsoleProxyAuthenticationResult authResult =
authenticateConsoleAccess(param, false);
if (authResult == null || !authResult.isSuccess()) {
- LOGGER.warn("External authenticator failed authentication request
for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword());
+ LOGGER.warn("External authenticator failed authentication request
for vm " + param.getClientTag()
+ + " with sid " + param.getClientHostPassword());
- throw new AuthenticationException("External authenticator failed
request for vm " + param.getClientTag() + " with sid " +
param.getClientHostPassword());
+ throw new AuthenticationException("External authenticator failed
request for vm " + param.getClientTag()
+ + " with sid " + param.getClientHostPassword());
}
}
@@ -596,9 +635,11 @@ public class ConsoleProxy {
}
public static ConsoleProxyNoVncClient
getNoVncViewer(ConsoleProxyClientParam param, String ajaxSession,
- Session session) throws AuthenticationException {
+ Session session)
throws AuthenticationException {
boolean reportLoadChange = false;
String clientKey = param.getClientMapKey();
+ LOGGER.debug("Getting NoVNC viewer for {}. Client tag: {}. session
UUID: {}",
+ clientKey, param.getClientTag(), param.getSessionUuid());
synchronized (connectionMap) {
ConsoleProxyClient viewer = connectionMap.get(clientKey);
if (viewer == null || viewer.getClass() !=
ConsoleProxyNoVncClient.class) {
@@ -609,19 +650,19 @@ public class ConsoleProxy {
connectionMap.put(clientKey, viewer);
reportLoadChange = true;
} else {
- if (param.getClientHostPassword() == null ||
param.getClientHostPassword().isEmpty() ||
-
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
+ if (param.getClientHostPassword() == null ||
param.getClientHostPassword().isEmpty()
+ ||
!param.getClientHostPassword().equals(viewer.getClientHostPassword())) {
throw new AuthenticationException("Cannot use the existing
viewer " + viewer + ": bad sid");
-
+ }
try {
authenticationExternally(param);
} catch (Exception e) {
- LOGGER.error("Authentication failed for param: " + param);
+ LOGGER.error("Authentication failed for param: {}", param,
e);
return null;
}
LOGGER.info("Initializing new novnc client and disconnecting
existing session");
try {
-
((ConsoleProxyNoVncClient)viewer).getSession().disconnect();
+ ((ConsoleProxyNoVncClient)
viewer).getSession().disconnect();
} catch (IOException e) {
LOGGER.error("Exception while disconnect session of novnc
viewer object: " + viewer, e);
}
@@ -636,10 +677,11 @@ public class ConsoleProxy {
ConsoleProxyClientStatsCollector statsCollector =
getStatsCollector();
String loadInfo = statsCollector.getStatsReport();
reportLoadInfo(loadInfo);
- if (LOGGER.isDebugEnabled())
+ if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Report load change : " + loadInfo);
+ }
}
- return (ConsoleProxyNoVncClient)viewer;
+ return (ConsoleProxyNoVncClient) viewer;
}
}
}
diff --git
a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java
b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java
index 0e8f576cf6d..3dea97e89e8 100644
---
a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java
+++
b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java
@@ -18,7 +18,7 @@ package com.cloud.consoleproxy;
import java.io.File;
import java.util.ArrayList;
-import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -32,9 +32,8 @@ import org.apache.logging.log4j.LogManager;
* management software
*/
public class ConsoleProxyGCThread extends Thread {
- protected Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class);
+ private static final Logger logger =
LogManager.getLogger(ConsoleProxyGCThread.class);
- private final static int MAX_SESSION_IDLE_SECONDS = 180;
private final Map<String, ConsoleProxyClient> connMap;
private final Set<String> removedSessionsSet;
@@ -46,21 +45,20 @@ public class ConsoleProxyGCThread extends Thread {
}
private void cleanupLogging() {
- if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan <
3600000)
+ if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan <
3600000) {
return;
-
+ }
lastLogScan = System.currentTimeMillis();
File logDir = new File("./logs");
- File files[] = logDir.listFiles();
+ File[] files = logDir.listFiles();
if (files != null) {
for (File file : files) {
if (System.currentTimeMillis() - file.lastModified() >=
86400000L) {
try {
file.delete();
} catch (Throwable e) {
- logger.info("[ignored]"
- + "failed to delete file: " +
e.getLocalizedMessage());
+ logger.info("[ignored] failed to delete file: " +
e.getLocalizedMessage());
}
}
}
@@ -78,21 +76,25 @@ public class ConsoleProxyGCThread extends Thread {
bReportLoad = false;
if (logger.isDebugEnabled()) {
- logger.debug(String.format("connMap=%s, removedSessions=%s",
connMap, removedSessionsSet));
+ logger.debug(String.format("ConsoleProxyGCThread loop:
connMap=%s, removedSessions=%s", connMap, removedSessionsSet));
}
- Set<String> e = connMap.keySet();
- Iterator<String> iterator = e.iterator();
- while (iterator.hasNext()) {
- String key;
+ List<String> keys;
+ synchronized (connMap) {
+ keys = new ArrayList<>(connMap.keySet());
+ }
+ for (String key : keys) {
ConsoleProxyClient client;
synchronized (connMap) {
- key = iterator.next();
client = connMap.get(key);
}
- long seconds_unused = (System.currentTimeMillis() -
client.getClientLastFrontEndActivityTime()) / 1000;
- if (seconds_unused < MAX_SESSION_IDLE_SECONDS) {
+ if (client == null) {
+ continue;
+ }
+
+ long millisecondsUnused = System.currentTimeMillis() -
client.getClientLastFrontEndActivityTime();
+ if (millisecondsUnused < ConsoleProxy.sessionTimeoutMillis) {
continue;
}
@@ -102,12 +104,13 @@ public class ConsoleProxyGCThread extends Thread {
}
// close the server connection
- logger.info("Dropping " + client + " which has not been used
for " + seconds_unused + " seconds");
+ logger.info("Dropping " + client + " which has not been used
for " + millisecondsUnused
+ + " ms (configured timeout: " +
ConsoleProxy.sessionTimeoutMillis + " ms)");
client.closeClient();
}
if (bReportLoad || System.currentTimeMillis() - lastReportTick >
5000) {
- // report load changes
+ // report load changes, including removed sessions since last
report
ConsoleProxyClientStatsCollector collector = new
ConsoleProxyClientStatsCollector(connMap);
collector.setRemovedSessions(new
ArrayList<>(removedSessionsSet));
String loadInfo = collector.getStatsReport();
@@ -125,7 +128,7 @@ public class ConsoleProxyGCThread extends Thread {
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
- logger.debug("[ignored] Console proxy was interrupted during
GC.");
+ logger.debug("[ignored] Console proxy GC thread interrupted.",
ex);
}
}
}
diff --git
a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java
b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java
index a9639d0b32e..bff3f7c4376 100644
---
a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java
+++
b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java
@@ -17,6 +17,8 @@
package com.cloud.consoleproxy;
import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.util.Map;
import javax.servlet.ServletException;
@@ -34,14 +36,18 @@ import
org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.Frame.Type;
import org.eclipse.jetty.websocket.server.WebSocketHandler;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@WebSocket
public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
+ private static final Logger logger =
LogManager.getLogger(ConsoleProxyNoVNCHandler.class);
+
+ private static final String ACTIVITY_SIGNAL = "CLOUDSTACK_ACTIVITY";
+
private ConsoleProxyNoVncClient viewer = null;
- protected Logger logger = LogManager.getLogger(getClass());
public ConsoleProxyNoVNCHandler() {
super();
@@ -73,6 +79,10 @@ public class ConsoleProxyNoVNCHandler extends
WebSocketHandler {
@OnWebSocketConnect
public void onConnect(final Session session) throws IOException,
InterruptedException {
+ session.setIdleTimeout(ConsoleProxy.sessionTimeoutMillis);
+ logger.debug("Set WebSocket idle timeout to {} ms for session from
{}.",
+ ConsoleProxy.sessionTimeoutMillis, session.getRemoteAddress());
+
String queries = session.getUpgradeRequest().getQueryString();
Map<String, String> queryMap =
ConsoleProxyHttpHandlerHelper.getQueryMap(queries);
@@ -94,14 +104,14 @@ public class ConsoleProxyNoVNCHandler extends
WebSocketHandler {
String sessionUuid = queryMap.get("sessionUuid");
String clientIp =
session.getRemoteAddress().getAddress().getHostAddress();
- if (tag == null)
+ if (tag == null) {
tag = "";
+ }
- long ajaxSessionId = 0;
int port;
-
- if (host == null || portStr == null || sid == null)
- throw new IllegalArgumentException();
+ if (host == null || portStr == null || sid == null) {
+ throw new IllegalArgumentException("Missing required console
connection parameters");
+ }
try {
port = Integer.parseInt(portStr);
@@ -112,7 +122,7 @@ public class ConsoleProxyNoVNCHandler extends
WebSocketHandler {
if (ajaxSessionIdStr != null) {
try {
- ajaxSessionId = Long.parseLong(ajaxSessionIdStr);
+ Long.parseLong(ajaxSessionIdStr);
} catch (NumberFormatException e) {
logger.error("Invalid ajaxSessionId (sess) value in query
string: {}. Expected a number.", ajaxSessionIdStr, e);
throw new IllegalArgumentException(e);
@@ -151,7 +161,7 @@ public class ConsoleProxyNoVNCHandler extends
WebSocketHandler {
viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr,
session);
logger.info("Viewer has been created successfully [session UUID:
{}, client IP: {}].", sessionUuid, clientIp);
} catch (Exception e) {
- logger.error("Failed to create viewer [session UUID: {}, client
IP: {}] due to {}.", sessionUuid, clientIp, e.getMessage(), e);
+ logger.error("Failed to create viewer [session UUID: {}, client
IP: {}].", sessionUuid, clientIp, e);
return;
} finally {
if (viewer == null) {
@@ -160,7 +170,7 @@ public class ConsoleProxyNoVNCHandler extends
WebSocketHandler {
}
}
- private boolean checkSessionSourceIp(final Session session, final String
sourceIP, String sessionSourceIP) throws IOException {
+ private boolean checkSessionSourceIp(final Session session, final String
sourceIP, final String sessionSourceIP) throws IOException {
logger.info("Verifying session source IP {} from WebSocket connection
request.", sessionSourceIP);
if (ConsoleProxy.isSourceIpCheckEnabled && (sessionSourceIP == null ||
!sessionSourceIP.equals(sourceIP))) {
logger.warn("Failed to access console as the source IP to request
the console is {}.", sourceIP);
@@ -174,7 +184,7 @@ public class ConsoleProxyNoVNCHandler extends
WebSocketHandler {
@OnWebSocketClose
public void onClose(Session session, int statusCode, String reason) throws
IOException, InterruptedException {
String sessionSourceIp =
session.getRemoteAddress().getAddress().getHostAddress();
- logger.debug("Closing WebSocket session [source IP: {}, status code:
{}].", sessionSourceIp, statusCode);
+ logger.debug("Closing WebSocket session [source IP: {}, status code:
{}, reason: {}].", sessionSourceIp, statusCode, reason);
if (viewer != null) {
ConsoleProxy.removeViewer(viewer);
}
@@ -183,12 +193,40 @@ public class ConsoleProxyNoVNCHandler extends
WebSocketHandler {
@OnWebSocketFrame
public void onFrame(Frame f) throws IOException {
- logger.trace("Sending client [ID: {}] frame of {} bytes.",
viewer.getClientId(), f.getPayloadLength());
+ if (viewer == null) {
+ logger.debug("Ignoring WebSocket frame because viewer is not
initialized yet.");
+ return;
+ }
+
+ if (f.getType() == Type.TEXT) {
+ ByteBuffer payload = f.getPayload().slice();
+ byte[] data = new byte[payload.remaining()];
+ payload.get(data);
+
+ String message = new String(data, StandardCharsets.UTF_8);
+ if (ACTIVITY_SIGNAL.equals(message)) {
+ viewer.updateFrontEndActivityTime();
+ } else {
+ logger.debug("Ignoring unexpected text WebSocket frame.");
+ }
+ return;
+ }
+
+ if (f.getType() != Type.BINARY) {
+ return;
+ }
+
+ logger.trace("Sending client [ID: {}] frame of {} bytes.",
+ viewer.getClientId(), f.getPayloadLength());
viewer.sendClientFrame(f);
}
@OnWebSocketError
public void onError(Throwable cause) {
- logger.error("Error on WebSocket [client ID: {}, session UUID: {}].",
cause, viewer.getClientId(), viewer.getSessionUuid());
+ if (viewer != null) {
+ logger.error("Error on WebSocket [client ID: {}, session UUID:
{}].", viewer.getClientId(), viewer.getSessionUuid(), cause);
+ } else {
+ logger.error("Error on WebSocket before viewer initialization.",
cause);
+ }
}
}
diff --git
a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java
b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java
index 85a2e5c541f..9c3cba0bbb1 100644
---
a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java
+++
b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java
@@ -118,9 +118,6 @@ public class ConsoleProxyNoVncClient implements
ConsoleProxyClient {
while (connectionAlive) {
logger.trace("Connection with client [{}] [IP: {}] is
alive.", clientId, clientSourceIp);
if (client.isVncOverWebSocketConnection()) {
- if (client.isVncOverWebSocketConnectionOpen()) {
- updateFrontEndActivityTime();
- }
connectionAlive = session.isOpen();
sleepTime = 1;
} else if (client.isVncOverNioSocket()) {
@@ -128,7 +125,6 @@ public class ConsoleProxyNoVncClient implements
ConsoleProxyClient {
int bytesRead =
client.readAvailableDataIntoBuffer(buffer, buffer.remaining());
if (bytesRead > 0) {
- updateFrontEndActivityTime();
consecutiveZeroReads = 0; // Reset counter on
successful read
sleepTime = 0; // Still no sleep to catch any
remaining data quickly
@@ -185,7 +181,6 @@ public class ConsoleProxyNoVncClient implements
ConsoleProxyClient {
try {
// Buffer is already prepared for reading by flip()
session.getRemote().sendBytes(buffer);
- updateFrontEndActivityTime();
} catch (WebSocketException | IOException e) {
logger.error("VNC server connection exception.", e);
return false;
@@ -367,7 +362,16 @@ public class ConsoleProxyNoVncClient implements
ConsoleProxyClient {
this.connectionAlive = false;
// Clear buffer reference to allow GC when client disconnects
this.readBuffer = null;
- ConsoleProxy.removeViewer(this);
+
+ try {
+ if (session != null && session.isOpen()) {
+ session.disconnect();
+ }
+ } catch (IOException e) {
+ logger.warn("Failed to disconnect WebSocket session for client
[{}].", clientId, e);
+ } finally {
+ ConsoleProxy.removeViewer(this);
+ }
}
@Override
diff --git
a/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java
b/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java
index 56a87373711..d805031caad 100644
---
a/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java
+++
b/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java
@@ -16,8 +16,10 @@
// under the License.
package com.cloud.consoleproxy;
+import org.eclipse.jetty.websocket.api.Session;
import org.junit.Assert;
import org.junit.Test;
+import org.mockito.Mockito;
public class ConsoleProxyNoVncClientTest {
@Test
@@ -29,4 +31,29 @@ public class ConsoleProxyNoVncClientTest {
byte[] expectedBytes = new byte[]{4, 0, 3, 0, 32, 24, 0, 1, 0, -1, 0,
-1, 0, -1, 16, 8, 0, 0, 0, 0, 0, 0, 0, 22, 115, 101, 114, 118, 101, 114, 49,
50, 51, 44, 32, 98, 97, 99, 107, 101, 110, 100, 58, 84, 76, 83};
Assert.assertArrayEquals(newServerInit, expectedBytes);
}
+
+ @Test
+ public void updateFrontEndActivityTimeUpdatesTimestamp() throws
InterruptedException {
+ ConsoleProxyNoVncClient client = new ConsoleProxyNoVncClient(null);
+
+ long previousActivityTime = client.getClientLastFrontEndActivityTime();
+
+ Thread.sleep(10);
+ client.updateFrontEndActivityTime();
+
+ long updatedActivityTime = client.getClientLastFrontEndActivityTime();
+ Assert.assertTrue(updatedActivityTime > previousActivityTime);
+ }
+
+ @Test
+ public void closeClientDisconnectsOpenWebSocketSession() throws Exception {
+ Session session = Mockito.mock(Session.class);
+ Mockito.when(session.isOpen()).thenReturn(true);
+
+ ConsoleProxyNoVncClient client = new ConsoleProxyNoVncClient(session);
+
+ client.closeClient();
+
+ Mockito.verify(session).disconnect();
+ }
}
diff --git a/systemvm/agent/noVNC/core/rfb.js b/systemvm/agent/noVNC/core/rfb.js
index 228340a2dec..113dca230aa 100644
--- a/systemvm/agent/noVNC/core/rfb.js
+++ b/systemvm/agent/noVNC/core/rfb.js
@@ -516,6 +516,8 @@ export default class RFB extends EventTargetMixin {
return;
}
+ this._sock.sendActivity();
+
const scancode = XtScancode[code];
if (keysym === KeyTable.XK_Shift_L || keysym === KeyTable.XK_Shift_R) {
@@ -1210,6 +1212,8 @@ export default class RFB extends EventTargetMixin {
if (this._rfbConnectionState !== 'connected') { return; }
if (this._viewOnly) { return; } // View only, skip mouse events
+ this._sock.sendActivity();
+
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
this._display.absY(y), mask);
}
diff --git a/systemvm/agent/noVNC/core/websock.js
b/systemvm/agent/noVNC/core/websock.js
index 37b33fcc585..dddf280cc5e 100644
--- a/systemvm/agent/noVNC/core/websock.js
+++ b/systemvm/agent/noVNC/core/websock.js
@@ -203,6 +203,21 @@ export default class Websock {
this.flush();
}
+ sendActivity() {
+ if (this._websocket === null || this.readyState !== 'open') {
+ return;
+ }
+
+ const now = Date.now();
+ if (this._lastActivitySignal &&
+ now - this._lastActivitySignal < 500) {
+ return;
+ }
+
+ this._lastActivitySignal = now;
+ this._websocket.send("CLOUDSTACK_ACTIVITY");
+ }
+
sendString(str) {
this.send(str.split('').map(chr => chr.charCodeAt(0)));
}