diff --git a/agent/conf/log4j-cloud.xml.in b/agent/conf/log4j-cloud.xml.in index 84957edca032..87cc9ae5424f 100644 --- a/agent/conf/log4j-cloud.xml.in +++ b/agent/conf/log4j-cloud.xml.in @@ -38,8 +38,13 @@ under the License. - - + + + + + + + @@ -50,17 +55,11 @@ under the License. - - - - - - @@ -69,6 +68,7 @@ under the License. + diff --git a/client/conf/log4j-cloud.xml.in b/client/conf/log4j-cloud.xml.in index 26da171269de..a1e73a412c37 100755 --- a/client/conf/log4j-cloud.xml.in +++ b/client/conf/log4j-cloud.xml.in @@ -21,7 +21,7 @@ under the License. - net.sf.cglib.proxy + net.sf.cglib.proxy @@ -37,7 +37,6 @@ under the License. - @@ -69,8 +68,13 @@ under the License. - - + + + + + + + @@ -78,7 +82,7 @@ under the License. - + @@ -102,7 +106,7 @@ under the License. - + @@ -115,6 +119,7 @@ under the License. + @@ -127,6 +132,7 @@ under the License. + diff --git a/server/conf/log4j-cloud.xml.in b/server/conf/log4j-cloud.xml.in index 9a8e5dc7bf33..06fa7527a52d 100755 --- a/server/conf/log4j-cloud.xml.in +++ b/server/conf/log4j-cloud.xml.in @@ -24,7 +24,6 @@ under the License. - @@ -34,7 +33,6 @@ under the License. - @@ -43,7 +41,6 @@ under the License. - @@ -56,7 +53,6 @@ under the License. - @@ -67,8 +63,13 @@ under the License. - - + + + + + + + @@ -101,6 +102,7 @@ under the License. + @@ -113,6 +115,7 @@ under the License. + 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 e75ccea90441..b0a2a1fca76c 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 @@ -80,6 +80,11 @@ public class ConsoleProxy { static String factoryClzName; static boolean standaloneStart = false; + /** + * Session timeout in milliseconds, default 300000 (5 minutes). + */ + public static int sessionTimeoutMillis = 300000; + static String encryptorPassword = "Dummy"; static final String[] skipProperties = new String[]{"certificate", "cacertificate", "keystore_password", "privatekey"}; @@ -92,11 +97,13 @@ public static void addAllowedSession(String sessionUuid) { 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 { @@ -121,9 +128,8 @@ private static void configLog4j() { 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)); + LOGGER.info("Property " + (String) key + ": " + conf.getProperty((String) key)); } } @@ -165,13 +171,31 @@ private static void configProxy(Properties conf) { defaultBufferSize = Integer.parseInt(s); LOGGER.info("Setting defaultBufferSize=" + defaultBufferSize); } + + // Read consoleproxy.session.timeout in milliseconds. + s = conf.getProperty("consoleproxy.session.timeout"); + if (s != null) { + try { + int parsedTimeout = Integer.parseInt(s); + if (parsedTimeout < 1000) { + LOGGER.warn("Invalid value for consoleproxy.session.timeout: " + s + + " ms, must be >= 1000 ms, keeping default " + sessionTimeoutMillis + " ms"); + } else { + sessionTimeoutMillis = parsedTimeout; + LOGGER.info("Setting consoleproxy.session.timeout=" + sessionTimeoutMillis + " ms"); + } + } catch (NumberFormatException e) { + LOGGER.warn("Invalid value for consoleproxy.session.timeout: " + s + + ", keeping default " + sessionTimeoutMillis + " ms", e); + } + } } 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) { @@ -243,7 +267,7 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console } 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); @@ -318,19 +342,25 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi LOGGER.error("Unable to setup private channel due to ClassNotFoundException", e); } + // ensure we have a Properties object before merging defaults + if (conf == null) { + conf = new Properties(); + } + // merge properties from conf file InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties"); 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 { @@ -339,15 +369,18 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi 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); } } try { - confs.close(); + if (confs != null) { + confs.close(); + } } catch (IOException e) { LOGGER.error("Failed to close consolepropxy.properties : " + e.toString(), e); } @@ -481,8 +514,9 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); String loadInfo = statsCollector.getStatsReport(); reportLoadInfo(loadInfo); - if (LOGGER.isDebugEnabled()) + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Report load change : " + loadInfo); + } } return viewer; @@ -506,13 +540,15 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, // 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())) + !param.getClientHostPassword().equals(viewer.getClientHostPassword())) { throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); + } if (!viewer.isFrontEndAlive()) { @@ -526,8 +562,9 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); String loadInfo = statsCollector.getStatsReport(); reportLoadInfo(loadInfo); - if (LOGGER.isDebugEnabled()) + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Report load change : " + loadInfo); + } } return viewer; } @@ -593,7 +630,7 @@ public void execute(Runnable r) { } 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 {}. Session requires new viewer: {}, client tag: {}. session UUID: {}", @@ -609,8 +646,9 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par reportLoadChange = true; } else { if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() || - !param.getClientHostPassword().equals(viewer.getClientHostPassword())) + !param.getClientHostPassword().equals(viewer.getClientHostPassword())) { throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); + } try { authenticationExternally(param); @@ -620,7 +658,7 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par } 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); } @@ -635,10 +673,11 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par 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 0e8f576cf6db..66d14e4da79a 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 @@ -22,8 +22,8 @@ import java.util.Map; import java.util.Set; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * @@ -34,7 +34,7 @@ public class ConsoleProxyGCThread extends Thread { protected Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class); - private final static int MAX_SESSION_IDLE_SECONDS = 180; + private static final int DEFAULT_MAX_SESSION_IDLE_SECONDS = 180; private final Map connMap; private final Set removedSessionsSet; @@ -45,22 +45,30 @@ public ConsoleProxyGCThread(Map connMap, Set this.removedSessionsSet = removedSet; } + private int getMaxSessionIdleSeconds() { + if (ConsoleProxy.sessionTimeoutMillis <= 0) { + return DEFAULT_MAX_SESSION_IDLE_SECONDS; + } + + return Math.max(1, ConsoleProxy.sessionTimeoutMillis / 1000); + } + 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()); } } } @@ -69,7 +77,6 @@ private void cleanupLogging() { @Override public void run() { - boolean bReportLoad = false; long lastReportTick = System.currentTimeMillis(); @@ -80,6 +87,7 @@ public void run() { if (logger.isDebugEnabled()) { logger.debug(String.format("connMap=%s, removedSessions=%s", connMap, removedSessionsSet)); } + Set e = connMap.keySet(); Iterator iterator = e.iterator(); while (iterator.hasNext()) { @@ -91,8 +99,8 @@ public void run() { client = connMap.get(key); } - long seconds_unused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000; - if (seconds_unused < MAX_SESSION_IDLE_SECONDS) { + long secondsUnused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000; + if (secondsUnused < getMaxSessionIdleSeconds()) { continue; } @@ -101,18 +109,17 @@ public void run() { bReportLoad = true; } - // 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 " + secondsUnused + " seconds"); client.closeClient(); } if (bReportLoad || System.currentTimeMillis() - lastReportTick > 5000) { - // report load changes ConsoleProxyClientStatsCollector collector = new ConsoleProxyClientStatsCollector(connMap); collector.setRemovedSessions(new ArrayList<>(removedSessionsSet)); String loadInfo = collector.getStatsReport(); ConsoleProxy.reportLoadInfo(loadInfo); lastReportTick = System.currentTimeMillis(); + synchronized (removedSessionsSet) { removedSessionsSet.clear(); } 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 a148b988e40d..0f8b54e078e4 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 @@ -95,14 +95,16 @@ public void onConnect(final Session session) throws IOException, InterruptedExce String clientIp = session.getRemoteAddress().getAddress().getHostAddress(); boolean sessionRequiresNewViewer = Boolean.parseBoolean(queryMap.get("sessionRequiresNewViewer")); - if (tag == null) + if (tag == null) { tag = ""; + } long ajaxSessionId = 0; int port; - if (host == null || portStr == null || sid == null) + if (host == null || portStr == null || sid == null) { throw new IllegalArgumentException(); + } try { port = Integer.parseInt(portStr); @@ -125,6 +127,14 @@ public void onConnect(final Session session) throws IOException, InterruptedExce } try { + if (ConsoleProxy.sessionTimeoutMillis > 0) { + session.setIdleTimeout(ConsoleProxy.sessionTimeoutMillis); + logger.debug("Set noVNC WebSocket idle timeout to {} ms for session UUID {}.", + ConsoleProxy.sessionTimeoutMillis, sessionUuid); + } else { + logger.debug("Using default noVNC WebSocket idle timeout for session UUID {}.", sessionUuid); + } + ConsoleProxyClientParam param = new ConsoleProxyClientParam(); param.setClientHostAddress(host); param.setClientHostPort(port); @@ -185,12 +195,21 @@ public void onClose(Session session, int statusCode, String reason) throws IOExc @OnWebSocketFrame public void onFrame(Frame f) throws IOException { + if (viewer == null) { + logger.warn("Ignoring WebSocket frame because viewer is not initialized yet."); + 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/usage/conf/log4j-cloud_usage.xml.in b/usage/conf/log4j-cloud_usage.xml.in index 871d6fb5a7a6..fb09b4374acb 100644 --- a/usage/conf/log4j-cloud_usage.xml.in +++ b/usage/conf/log4j-cloud_usage.xml.in @@ -20,19 +20,23 @@ under the License. - - - + + + - - - - + + + + + + + + - - - + + + @@ -44,16 +48,13 @@ under the License. - - - + + + - - - @@ -62,6 +63,7 @@ under the License. +