Skip to content

Commit 15c2f36

Browse files
committed
WIP: console proxy timeout changes
1 parent 9315f98 commit 15c2f36

3 files changed

Lines changed: 99 additions & 34 deletions

File tree

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ public class ConsoleProxy {
8080
static String factoryClzName;
8181
static boolean standaloneStart = false;
8282

83+
/**
84+
* Session timeout in milliseconds, default 300000 (5 minutes).
85+
*/
86+
public static int sessionTimeoutMillis = 300000;
87+
8388
static String encryptorPassword = "Dummy";
8489
static final String[] skipProperties = new String[]{"certificate", "cacertificate", "keystore_password", "privatekey"};
8590

@@ -92,11 +97,13 @@ public static void addAllowedSession(String sessionUuid) {
9297
private static void configLog4j() {
9398
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
9499
URL configUrl = loader.getResource("/conf/log4j-cloud.xml");
95-
if (configUrl == null)
100+
if (configUrl == null) {
96101
configUrl = ClassLoader.getSystemResource("log4j-cloud.xml");
102+
}
97103

98-
if (configUrl == null)
104+
if (configUrl == null) {
99105
configUrl = ClassLoader.getSystemResource("conf/log4j-cloud.xml");
106+
}
100107

101108
if (configUrl != null) {
102109
try {
@@ -121,9 +128,8 @@ private static void configLog4j() {
121128
private static void configProxy(Properties conf) {
122129
LOGGER.info("Configure console proxy...");
123130
for (Object key : conf.keySet()) {
124-
LOGGER.info("Property " + (String)key + ": " + conf.getProperty((String)key));
125131
if (!ArrayUtils.contains(skipProperties, key)) {
126-
LOGGER.info("Property " + (String)key + ": " + conf.getProperty((String)key));
132+
LOGGER.info("Property " + (String) key + ": " + conf.getProperty((String) key));
127133
}
128134
}
129135

@@ -165,13 +171,31 @@ private static void configProxy(Properties conf) {
165171
defaultBufferSize = Integer.parseInt(s);
166172
LOGGER.info("Setting defaultBufferSize=" + defaultBufferSize);
167173
}
174+
175+
// Read consoleproxy.session.timeout in milliseconds.
176+
s = conf.getProperty("consoleproxy.session.timeout");
177+
if (s != null) {
178+
try {
179+
int parsedTimeout = Integer.parseInt(s);
180+
if (parsedTimeout < 1000) {
181+
LOGGER.warn("Invalid value for consoleproxy.session.timeout: " + s
182+
+ " ms, must be >= 1000 ms, keeping default " + sessionTimeoutMillis + " ms");
183+
} else {
184+
sessionTimeoutMillis = parsedTimeout;
185+
LOGGER.info("Setting consoleproxy.session.timeout=" + sessionTimeoutMillis + " ms");
186+
}
187+
} catch (NumberFormatException e) {
188+
LOGGER.warn("Invalid value for consoleproxy.session.timeout: " + s
189+
+ ", keeping default " + sessionTimeoutMillis + " ms", e);
190+
}
191+
}
168192
}
169193

170194
public static ConsoleProxyServerFactory getHttpServerFactory() {
171195
try {
172196
Class<?> clz = Class.forName(factoryClzName);
173197
try {
174-
ConsoleProxyServerFactory factory = (ConsoleProxyServerFactory)clz.newInstance();
198+
ConsoleProxyServerFactory factory = (ConsoleProxyServerFactory) clz.newInstance();
175199
factory.init(ConsoleProxy.ksBits, ConsoleProxy.ksPassword);
176200
return factory;
177201
} catch (InstantiationException e) {
@@ -243,7 +267,7 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console
243267
}
244268

245269
if (result != null && result instanceof String) {
246-
authResult = new Gson().fromJson((String)result, ConsoleProxyAuthenticationResult.class);
270+
authResult = new Gson().fromJson((String) result, ConsoleProxyAuthenticationResult.class);
247271
} else {
248272
LOGGER.error("Invalid authentication return object " + result + " for vm: " + param.getClientTag() + ", decline the access");
249273
authResult.setSuccess(false);
@@ -318,19 +342,25 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi
318342
LOGGER.error("Unable to setup private channel due to ClassNotFoundException", e);
319343
}
320344

345+
// ensure we have a Properties object before merging defaults
346+
if (conf == null) {
347+
conf = new Properties();
348+
}
349+
321350
// merge properties from conf file
322351
InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties");
323352
Properties props = new Properties();
324353
if (confs == null) {
325354
final File file = PropertiesUtil.findConfigFile("consoleproxy.properties");
326-
if (file == null)
355+
if (file == null) {
327356
LOGGER.info("Can't load consoleproxy.properties from classpath, will use default configuration");
328-
else
357+
} else {
329358
try {
330359
confs = new FileInputStream(file);
331360
} catch (FileNotFoundException e) {
332361
LOGGER.info("Ignoring file not found exception and using defaults");
333362
}
363+
}
334364
}
335365
if (confs != null) {
336366
try {
@@ -339,15 +369,18 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi
339369
for (Object key : props.keySet()) {
340370
// give properties passed via context high priority, treat properties from consoleproxy.properties
341371
// as default values
342-
if (conf.get(key) == null)
372+
if (conf.get(key) == null) {
343373
conf.put(key, props.get(key));
374+
}
344375
}
345376
} catch (Exception e) {
346377
LOGGER.error(e.toString(), e);
347378
}
348379
}
349380
try {
350-
confs.close();
381+
if (confs != null) {
382+
confs.close();
383+
}
351384
} catch (IOException e) {
352385
LOGGER.error("Failed to close consolepropxy.properties : " + e.toString(), e);
353386
}
@@ -481,8 +514,9 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr
481514
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
482515
String loadInfo = statsCollector.getStatsReport();
483516
reportLoadInfo(loadInfo);
484-
if (LOGGER.isDebugEnabled())
517+
if (LOGGER.isDebugEnabled()) {
485518
LOGGER.debug("Report load change : " + loadInfo);
519+
}
486520
}
487521

488522
return viewer;
@@ -506,13 +540,15 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param,
506540
// protected against malicious attack by modifying URL content
507541
if (ajaxSession != null) {
508542
long ajaxSessionIdFromUrl = Long.parseLong(ajaxSession);
509-
if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId())
543+
if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId()) {
510544
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": modified AJAX session id");
545+
}
511546
}
512547

513548
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() ||
514-
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
549+
!param.getClientHostPassword().equals(viewer.getClientHostPassword())) {
515550
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid");
551+
}
516552

517553
if (!viewer.isFrontEndAlive()) {
518554

@@ -526,8 +562,9 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param,
526562
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
527563
String loadInfo = statsCollector.getStatsReport();
528564
reportLoadInfo(loadInfo);
529-
if (LOGGER.isDebugEnabled())
565+
if (LOGGER.isDebugEnabled()) {
530566
LOGGER.debug("Report load change : " + loadInfo);
567+
}
531568
}
532569
return viewer;
533570
}
@@ -593,7 +630,7 @@ public void execute(Runnable r) {
593630
}
594631

595632
public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam param, String ajaxSession,
596-
Session session) throws AuthenticationException {
633+
Session session) throws AuthenticationException {
597634
boolean reportLoadChange = false;
598635
String clientKey = param.getClientMapKey();
599636
LOGGER.debug("Getting NoVNC viewer for {}. Session requires new viewer: {}, client tag: {}. session UUID: {}",
@@ -609,8 +646,9 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par
609646
reportLoadChange = true;
610647
} else {
611648
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() ||
612-
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
649+
!param.getClientHostPassword().equals(viewer.getClientHostPassword())) {
613650
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid");
651+
}
614652

615653
try {
616654
authenticationExternally(param);
@@ -620,7 +658,7 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par
620658
}
621659
LOGGER.info("Initializing new novnc client and disconnecting existing session");
622660
try {
623-
((ConsoleProxyNoVncClient)viewer).getSession().disconnect();
661+
((ConsoleProxyNoVncClient) viewer).getSession().disconnect();
624662
} catch (IOException e) {
625663
LOGGER.error("Exception while disconnect session of novnc viewer object: " + viewer, e);
626664
}
@@ -635,10 +673,11 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par
635673
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
636674
String loadInfo = statsCollector.getStatsReport();
637675
reportLoadInfo(loadInfo);
638-
if (LOGGER.isDebugEnabled())
676+
if (LOGGER.isDebugEnabled()) {
639677
LOGGER.debug("Report load change : " + loadInfo);
678+
}
640679
}
641-
return (ConsoleProxyNoVncClient)viewer;
680+
return (ConsoleProxyNoVncClient) viewer;
642681
}
643682
}
644683
}

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import java.util.Map;
2323
import java.util.Set;
2424

25-
import org.apache.logging.log4j.Logger;
2625
import org.apache.logging.log4j.LogManager;
26+
import org.apache.logging.log4j.Logger;
2727

2828
/**
2929
*
@@ -34,7 +34,7 @@
3434
public class ConsoleProxyGCThread extends Thread {
3535
protected Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class);
3636

37-
private final static int MAX_SESSION_IDLE_SECONDS = 180;
37+
private static final int DEFAULT_MAX_SESSION_IDLE_SECONDS = 180;
3838

3939
private final Map<String, ConsoleProxyClient> connMap;
4040
private final Set<String> removedSessionsSet;
@@ -45,22 +45,30 @@ public ConsoleProxyGCThread(Map<String, ConsoleProxyClient> connMap, Set<String>
4545
this.removedSessionsSet = removedSet;
4646
}
4747

48+
private int getMaxSessionIdleSeconds() {
49+
if (ConsoleProxy.sessionTimeoutMillis <= 0) {
50+
return DEFAULT_MAX_SESSION_IDLE_SECONDS;
51+
}
52+
53+
return Math.max(1, ConsoleProxy.sessionTimeoutMillis / 1000);
54+
}
55+
4856
private void cleanupLogging() {
49-
if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000)
57+
if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000) {
5058
return;
59+
}
5160

5261
lastLogScan = System.currentTimeMillis();
5362

5463
File logDir = new File("./logs");
55-
File files[] = logDir.listFiles();
64+
File[] files = logDir.listFiles();
5665
if (files != null) {
5766
for (File file : files) {
5867
if (System.currentTimeMillis() - file.lastModified() >= 86400000L) {
5968
try {
6069
file.delete();
6170
} catch (Throwable e) {
62-
logger.info("[ignored]"
63-
+ "failed to delete file: " + e.getLocalizedMessage());
71+
logger.info("[ignored]failed to delete file: " + e.getLocalizedMessage());
6472
}
6573
}
6674
}
@@ -69,7 +77,6 @@ private void cleanupLogging() {
6977

7078
@Override
7179
public void run() {
72-
7380
boolean bReportLoad = false;
7481
long lastReportTick = System.currentTimeMillis();
7582

@@ -80,6 +87,7 @@ public void run() {
8087
if (logger.isDebugEnabled()) {
8188
logger.debug(String.format("connMap=%s, removedSessions=%s", connMap, removedSessionsSet));
8289
}
90+
8391
Set<String> e = connMap.keySet();
8492
Iterator<String> iterator = e.iterator();
8593
while (iterator.hasNext()) {
@@ -91,8 +99,8 @@ public void run() {
9199
client = connMap.get(key);
92100
}
93101

94-
long seconds_unused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000;
95-
if (seconds_unused < MAX_SESSION_IDLE_SECONDS) {
102+
long secondsUnused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000;
103+
if (secondsUnused < getMaxSessionIdleSeconds()) {
96104
continue;
97105
}
98106

@@ -101,18 +109,17 @@ public void run() {
101109
bReportLoad = true;
102110
}
103111

104-
// close the server connection
105-
logger.info("Dropping " + client + " which has not been used for " + seconds_unused + " seconds");
112+
logger.info("Dropping " + client + " which has not been used for " + secondsUnused + " seconds");
106113
client.closeClient();
107114
}
108115

109116
if (bReportLoad || System.currentTimeMillis() - lastReportTick > 5000) {
110-
// report load changes
111117
ConsoleProxyClientStatsCollector collector = new ConsoleProxyClientStatsCollector(connMap);
112118
collector.setRemovedSessions(new ArrayList<>(removedSessionsSet));
113119
String loadInfo = collector.getStatsReport();
114120
ConsoleProxy.reportLoadInfo(loadInfo);
115121
lastReportTick = System.currentTimeMillis();
122+
116123
synchronized (removedSessionsSet) {
117124
removedSessionsSet.clear();
118125
}

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,16 @@ public void onConnect(final Session session) throws IOException, InterruptedExce
9595
String clientIp = session.getRemoteAddress().getAddress().getHostAddress();
9696
boolean sessionRequiresNewViewer = Boolean.parseBoolean(queryMap.get("sessionRequiresNewViewer"));
9797

98-
if (tag == null)
98+
if (tag == null) {
9999
tag = "";
100+
}
100101

101102
long ajaxSessionId = 0;
102103
int port;
103104

104-
if (host == null || portStr == null || sid == null)
105+
if (host == null || portStr == null || sid == null) {
105106
throw new IllegalArgumentException();
107+
}
106108

107109
try {
108110
port = Integer.parseInt(portStr);
@@ -125,6 +127,14 @@ public void onConnect(final Session session) throws IOException, InterruptedExce
125127
}
126128

127129
try {
130+
if (ConsoleProxy.sessionTimeoutMillis > 0) {
131+
session.setIdleTimeout(ConsoleProxy.sessionTimeoutMillis);
132+
logger.debug("Set noVNC WebSocket idle timeout to {} ms for session UUID {}.",
133+
ConsoleProxy.sessionTimeoutMillis, sessionUuid);
134+
} else {
135+
logger.debug("Using default noVNC WebSocket idle timeout for session UUID {}.", sessionUuid);
136+
}
137+
128138
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
129139
param.setClientHostAddress(host);
130140
param.setClientHostPort(port);
@@ -185,12 +195,21 @@ public void onClose(Session session, int statusCode, String reason) throws IOExc
185195

186196
@OnWebSocketFrame
187197
public void onFrame(Frame f) throws IOException {
198+
if (viewer == null) {
199+
logger.warn("Ignoring WebSocket frame because viewer is not initialized yet.");
200+
return;
201+
}
188202
logger.trace("Sending client [ID: {}] frame of {} bytes.", viewer.getClientId(), f.getPayloadLength());
189203
viewer.sendClientFrame(f);
190204
}
191205

192206
@OnWebSocketError
193207
public void onError(Throwable cause) {
194-
logger.error("Error on WebSocket [client ID: {}, session UUID: {}].", cause, viewer.getClientId(), viewer.getSessionUuid());
208+
if (viewer != null) {
209+
logger.error("Error on WebSocket [client ID: {}, session UUID: {}].",
210+
viewer.getClientId(), viewer.getSessionUuid(), cause);
211+
} else {
212+
logger.error("Error on WebSocket before viewer initialization.", cause);
213+
}
195214
}
196215
}

0 commit comments

Comments
 (0)