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 0befe392b4e3..9f06cc836887 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 @@ -16,6 +16,7 @@ // under the License. package com.cloud.consoleproxy; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -34,17 +35,19 @@ import java.util.concurrent.ConcurrentHashMap; 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; + 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; /** * @@ -53,23 +56,33 @@ public class ConsoleProxy { protected static Logger LOGGER = LogManager.getLogger(ConsoleProxy.class); + public static final int KEYBOARD_RAW = 0; public static final int KEYBOARD_COOKED = 1; + public static final int VIEWER_LINGER_SECONDS = 180; + // New: default and effective session timeout (milliseconds) honoured from consoleproxy.session.timeout + public static final int DEFAULT_SESSION_TIMEOUT_MILLIS = 300000; + public static volatile int 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 // dynamically changing to customer supplied certificate) public static byte[] ksBits; public static String ksPassword; public static Boolean isSourceIpCheckEnabled; + public static Method authMethod; public static Method reportMethod; public static Method ensureRouteMethod; + static Hashtable connectionMap = new Hashtable(); static Set removedSessionsSet = ConcurrentHashMap.newKeySet(); static int httpListenPort = 80; @@ -81,23 +94,31 @@ public class ConsoleProxy { static String factoryClzName; 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 allowedSessions = new HashSet<>(); + public static void addAllowedSession(String sessionUuid) { allowedSessions.add(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 { @@ -106,36 +127,41 @@ private static void configLog4j() { e1.printStackTrace(); } + try { File file = new File(configUrl.toURI()); + System.out.println("Log4j configuration from : " + file.getAbsolutePath()); Configurator.initialize(null, file.getAbsolutePath()); } 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"); } } + 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 +170,61 @@ private static void configProxy(Properties conf) { 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); } + + // New: read consoleproxy.session.timeout (milliseconds) + s = conf != null ? conf.getProperty("consoleproxy.session.timeout") : null; + if (s != null) { + try { + int value = Integer.parseInt(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) { @@ -189,19 +240,23 @@ public static ConsoleProxyServerFactory getHttpServerFactory() { } } + public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(ConsoleProxyClientParam param, boolean reauthentication) { + ConsoleProxyAuthenticationResult authResult = new ConsoleProxyAuthenticationResult(); authResult.setSuccess(true); authResult.setReauthentication(reauthentication); 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"); @@ -210,6 +265,7 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console } } + String sessionUuid = param.getSessionUuid(); if (allowedSessions.contains(sessionUuid)) { LOGGER.debug("Acquiring the session " + sessionUuid + " not available for future use"); @@ -220,33 +276,39 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console return authResult; } + String websocketUrl = param.getWebsocketUrl(); if (StringUtils.isNotBlank(websocketUrl)) { return authResult; } + if (standaloneStart) { return authResult; } + if (authMethod != null) { Object result; try { + // 4.20: authenticateConsoleAccess(host, port, vmId, sid, ticket, isReauthentication, sessionToken) 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); @@ -255,9 +317,11 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console LOGGER.warn("Private channel towards management server is not setup. Switch to offline mode and allow access to vm: " + param.getClientTag()); } + return authResult; } + public static void reportLoadInfo(String gsonLoadInfo) { if (reportMethod != null) { try { @@ -272,6 +336,7 @@ public static void reportLoadInfo(String gsonLoadInfo) { } } + public static void ensureRoute(String address) { if (ensureRouteMethod != null) { try { @@ -286,11 +351,14 @@ public static void ensureRoute(String address) { } } - 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"); + if (conf != null) { for (Object key : conf.keySet()) { if (!ArrayUtils.contains(skipProperties, key)) { @@ -299,6 +367,7 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi } } + // Using reflection to setup private/secure communication channel towards management server ConsoleProxy.context = context; ConsoleProxy.ksBits = ksBits; @@ -307,6 +376,7 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi try { final ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class contextClazz = loader.loadClass("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource"); + // 4.20 signature: 7 parameters authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class, String.class, String.class, String.class, Boolean.class, String.class); reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class); @@ -321,54 +391,67 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi LOGGER.error("Unable to setup private channel due to ClassNotFoundException", e); } + // 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 { 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); } + public static void start(Properties conf) { System.setProperty("java.awt.headless", "true"); + configProxy(conf); + ConsoleProxyServerFactory factory = getHttpServerFactory(); if (factory == null) { LOGGER.error("Unable to load console proxy server factory"); System.exit(1); } + if (httpListenPort != 0) { startupHttpMain(); } else { @@ -376,17 +459,20 @@ public static void start(Properties conf) { System.exit(1); } + if (httpCmdListenPort > 0) { startupHttpCmdPort(); } else { LOGGER.info("HTTP command port is disabled"); } + ConsoleProxyGCThread cthread = new ConsoleProxyGCThread(connectionMap, removedSessionsSet); cthread.setName("Console Proxy GC Thread"); cthread.start(); } + private static void startupHttpMain() { try { ConsoleProxyServerFactory factory = getHttpServerFactory(); @@ -395,6 +481,7 @@ private static void startupHttpMain() { System.exit(1); } + HttpServer server = factory.createHttpServerInstance(httpListenPort); server.createContext("/getscreen", new ConsoleProxyThumbnailHandler()); server.createContext("/resource/", new ConsoleProxyResourceHandler()); @@ -403,15 +490,18 @@ private static void startupHttpMain() { server.setExecutor(new ThreadExecutor()); // creates a default executor server.start(); + ConsoleProxyNoVNCServer noVNCServer = getNoVNCServer(); noVNCServer.start(); + } catch (Exception e) { LOGGER.error(e.getMessage(), e); System.exit(1); } } + private static ConsoleProxyNoVNCServer getNoVNCServer() { int vncPort = ConsoleProxyNoVNCServer.getVNCPort(); return vncPort == ConsoleProxyNoVNCServer.WSS_PORT ? @@ -419,6 +509,7 @@ private static ConsoleProxyNoVNCServer getNoVNCServer() { new ConsoleProxyNoVNCServer(); } + private static void startupHttpCmdPort() { try { LOGGER.info("Listening for HTTP CMDs on port " + httpCmdListenPort); @@ -432,10 +523,12 @@ private static void startupHttpCmdPort() { } } + public static void main(String[] argv) { standaloneStart = true; configLog4j(); + InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties"); Properties conf = new Properties(); if (confs == null) { @@ -456,9 +549,11 @@ public static void main(String[] argv) { start(conf); } + public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) throws Exception { ConsoleProxyClient viewer = null; + boolean reportLoadChange = false; String clientKey = param.getClientMapKey(); synchronized (connectionMap) { @@ -469,30 +564,36 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr connectionMap.put(clientKey, viewer); LOGGER.info("Added viewer object " + viewer); + reportLoadChange = true; } else if (!viewer.isFrontEndAlive()) { 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); } } + if (reportLoadChange) { ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); String loadInfo = statsCollector.getStatsReport(); reportLoadInfo(loadInfo); - if (LOGGER.isDebugEnabled()) + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Report load change : " + loadInfo); + } } + return viewer; } + public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, String ajaxSession) throws Exception { + boolean reportLoadChange = false; String clientKey = param.getClientMapKey(); synchronized (connectionMap) { @@ -502,6 +603,7 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, viewer = getClient(param); viewer.initClient(param); + connectionMap.put(clientKey, viewer); LOGGER.info("Added viewer object " + viewer); reportLoadChange = true; @@ -509,33 +611,41 @@ 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())) + + 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()) { + authenticationExternally(param); viewer.initClient(param); reportLoadChange = true; } } + if (reportLoadChange) { ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); String loadInfo = statsCollector.getStatsReport(); reportLoadInfo(loadInfo); - if (LOGGER.isDebugEnabled()) + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Report load change : " + loadInfo); + } } return viewer; } } + private static ConsoleProxyClient getClient(ConsoleProxyClientParam param) { if (param.getHypervHost() != null) { return new ConsoleProxyRdpClient(); @@ -544,6 +654,7 @@ private static ConsoleProxyClient getClient(ConsoleProxyClientParam param) { } } + public static void removeViewer(ConsoleProxyClient viewer) { synchronized (connectionMap) { for (Map.Entry entry : connectionMap.entrySet()) { @@ -556,38 +667,49 @@ public static void removeViewer(ConsoleProxyClient viewer) { } } + public static ConsoleProxyClientStatsCollector getStatsCollector() { synchronized (connectionMap) { return new ConsoleProxyClientStatsCollector(connectionMap); } } + public static void authenticationExternally(ConsoleProxyClientParam param) throws AuthenticationException { 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()); } } + public static ConsoleProxyAuthenticationResult reAuthenticationExternally(ConsoleProxyClientParam param) { return authenticateConsoleAccess(param, true); } + public static String getEncryptorPassword() { return encryptorPassword; } + public static void setEncryptorPassword(String password) { encryptorPassword = password; } + public static void setIsSourceIpCheckEnabled(Boolean isEnabled) { isSourceIpCheckEnabled = isEnabled; } + static class ThreadExecutor implements Executor { @Override public void execute(Runnable r) { @@ -595,10 +717,13 @@ 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 {}. Client tag: {}. session UUID: {}", + clientKey, param.getClientTag(), param.getSessionUuid()); synchronized (connectionMap) { ConsoleProxyClient viewer = connectionMap.get(clientKey); if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) { @@ -606,22 +731,25 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par viewer = new ConsoleProxyNoVncClient(session); viewer.initClient(param); + 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); } @@ -632,14 +760,16 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par reportLoadChange = true; } + if (reportLoadChange) { 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..a69f29706db9 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 @@ -16,15 +16,18 @@ // under the License. package com.cloud.consoleproxy; + import java.io.File; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; import java.util.Set; + import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; + /** * * ConsoleProxyGCThread does house-keeping work for the process, it helps cleanup log files, @@ -32,82 +35,100 @@ * 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 connMap; private final Set removedSessionsSet; private long lastLogScan = 0; + public ConsoleProxyGCThread(Map connMap, Set removedSet) { this.connMap = connMap; this.removedSessionsSet = removedSet; } + 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()); } } } } } + @Override public void run() { + boolean bReportLoad = false; long lastReportTick = System.currentTimeMillis(); + while (true) { cleanupLogging(); 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 e = connMap.keySet(); - Iterator iterator = e.iterator(); + Set keys = connMap.keySet(); + Iterator iterator = keys.iterator(); while (iterator.hasNext()) { String key; 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; } + synchronized (connMap) { connMap.remove(key); 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 " + 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(); @@ -117,15 +138,17 @@ public void run() { removedSessionsSet.clear(); } + if (logger.isDebugEnabled()) { logger.debug("Report load change : " + loadInfo); } } + 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 a9639d0b32e3..c0172fe816c2 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 @@ -40,8 +40,9 @@ @WebSocket public class ConsoleProxyNoVNCHandler extends WebSocketHandler { + private static final Logger logger = LogManager.getLogger(ConsoleProxyNoVNCHandler.class); + private ConsoleProxyNoVncClient viewer = null; - protected Logger logger = LogManager.getLogger(getClass()); public ConsoleProxyNoVNCHandler() { super(); @@ -93,15 +94,16 @@ public void onConnect(final Session session) throws IOException, InterruptedExce String websocketUrl = queryMap.get("websocketUrl"); String sessionUuid = queryMap.get("sessionUuid"); 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) - 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 +114,7 @@ public void onConnect(final Session session) throws IOException, InterruptedExce 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); @@ -148,10 +150,11 @@ public void onConnect(final Session session) throws IOException, InterruptedExce if (queryMap.containsKey("extra")) { param.setClientProvidedExtraSecurityToken(queryMap.get("extra")); } + 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 +163,7 @@ public void onConnect(final Session session) throws IOException, InterruptedExce } } - 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 +177,7 @@ private boolean checkSessionSourceIp(final Session session, final String sourceI @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 +186,20 @@ public void onClose(Session session, int statusCode, String reason) throws IOExc @OnWebSocketFrame public void onFrame(Frame f) throws IOException { + if (viewer == null) { + logger.debug("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); + } } }