diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java index 94f1ef0ac0f..9f1304f627f 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java @@ -14,487 +14,486 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.consoleproxy; - -import java.io.File; -import java.io.InputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.InetSocketAddress; -import java.net.URISyntaxException; -import java.net.URL; +package com.cloud.consoleproxy; + +import java.io.File; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.URISyntaxException; +import java.net.URL; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.Hashtable; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.Executor; - -import org.apache.axis.encoding.Base64; -import org.apache.log4j.xml.DOMConfigurator; - -import com.cloud.consoleproxy.util.Logger; -import com.google.gson.Gson; -import com.sun.net.httpserver.HttpServer; - -/** - * - * @author Kelven Yang - * ConsoleProxy, singleton class that manages overall activities in console proxy process. To make legacy code work, we still - */ -public class ConsoleProxy { - private static final Logger s_logger = Logger.getLogger(ConsoleProxy.class); - - public static final int KEYBOARD_RAW = 0; - public static final int KEYBOARD_COOKED = 1; - - public static int VIEWER_LINGER_SECONDS = 180; - - 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 Method authMethod; - public static Method reportMethod; - public static Method ensureRouteMethod; - - static Hashtable connectionMap = new Hashtable(); - static int httpListenPort = 80; - static int httpCmdListenPort = 8001; - static int reconnectMaxRetry = 5; - static int readTimeoutSeconds = 90; - static int keyboardType = KEYBOARD_RAW; - static String factoryClzName; - static boolean standaloneStart = false; - - static String encryptorPassword = genDefaultEncryptorPassword(); - - private static String genDefaultEncryptorPassword() { - try { - SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); - - byte[] randomBytes = new byte[16]; - random.nextBytes(randomBytes); - return Base64.encode(randomBytes); - } catch (NoSuchAlgorithmException e) { - s_logger.error("Unexpected exception ", e); - assert(false); - } - - return "Dummy"; - } - - private static void configLog4j() { - URL configUrl = System.class.getResource("/conf/log4j-cloud.xml"); - if(configUrl == null) - configUrl = ClassLoader.getSystemResource("log4j-cloud.xml"); - - if(configUrl == null) - configUrl = ClassLoader.getSystemResource("conf/log4j-cloud.xml"); - - if(configUrl != null) { - try { - System.out.println("Configure log4j using " + configUrl.toURI().toString()); - } catch (URISyntaxException e1) { - e1.printStackTrace(); - } - - try { - File file = new File(configUrl.toURI()); - - System.out.println("Log4j configuration from : " + file.getAbsolutePath()); - DOMConfigurator.configureAndWatch(file.getAbsolutePath(), 10000); - } 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) { - s_logger.info("Configure console proxy..."); - for(Object key : conf.keySet()) { - s_logger.info("Property " + (String)key + ": " + conf.getProperty((String)key)); - } - - String s = conf.getProperty("consoleproxy.httpListenPort"); - if (s!=null) { - httpListenPort = Integer.parseInt(s); - s_logger.info("Setting httpListenPort=" + s); - } - - s = conf.getProperty("premium"); - if(s != null && s.equalsIgnoreCase("true")) { - s_logger.info("Premium setting will override settings from consoleproxy.properties, listen at port 443"); - httpListenPort = 443; - factoryClzName = "com.cloud.consoleproxy.ConsoleProxySecureServerFactoryImpl"; - } else { - factoryClzName = ConsoleProxyBaseServerFactoryImpl.class.getName(); - } - - s = conf.getProperty("consoleproxy.httpCmdListenPort"); - if (s!=null) { - httpCmdListenPort = Integer.parseInt(s); - s_logger.info("Setting httpCmdListenPort=" + s); - } - - s = conf.getProperty("consoleproxy.reconnectMaxRetry"); - if (s!=null) { - reconnectMaxRetry = Integer.parseInt(s); - s_logger.info("Setting reconnectMaxRetry=" + reconnectMaxRetry); - } - - s = conf.getProperty("consoleproxy.readTimeoutSeconds"); - if (s!=null) { - readTimeoutSeconds = Integer.parseInt(s); - s_logger.info("Setting readTimeoutSeconds=" + readTimeoutSeconds); - } - } - - public static ConsoleProxyServerFactory getHttpServerFactory() { - try { - Class clz = Class.forName(factoryClzName); - try { - ConsoleProxyServerFactory factory = (ConsoleProxyServerFactory)clz.newInstance(); - factory.init(ConsoleProxy.ksBits, ConsoleProxy.ksPassword); - return factory; - } catch (InstantiationException e) { - s_logger.error(e.getMessage(), e); - return null; - } catch (IllegalAccessException e) { - s_logger.error(e.getMessage(), e); - return null; - } - } catch (ClassNotFoundException e) { - s_logger.warn("Unable to find http server factory class: " + factoryClzName); - return new ConsoleProxyBaseServerFactoryImpl(); - } - } - - public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(ConsoleProxyClientParam param, boolean reauthentication) { +import java.util.Hashtable; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; - ConsoleProxyAuthenticationResult authResult = new ConsoleProxyAuthenticationResult(); - authResult.setSuccess(true); - authResult.setReauthentication(reauthentication); - authResult.setHost(param.getClientHostAddress()); - authResult.setPort(param.getClientHostPort()); - - if(standaloneStart) { - return authResult; - } - - if(authMethod != null) { - Object result; - try { - result = authMethod.invoke(ConsoleProxy.context, - param.getClientHostAddress(), - String.valueOf(param.getClientHostPort()), - param.getClientTag(), - param.getClientHostPassword(), - param.getTicket(), - new Boolean(reauthentication)); - } catch (IllegalAccessException e) { - s_logger.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + param.getClientTag(), e); - authResult.setSuccess(false); - return authResult; - } catch (InvocationTargetException e) { - s_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); - } else { - s_logger.error("Invalid authentication return object " + result + " for vm: " + param.getClientTag() + ", decline the access"); - authResult.setSuccess(false); - } - } else { - s_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 { - reportMethod.invoke(ConsoleProxy.context, gsonLoadInfo); - } catch (IllegalAccessException e) { - s_logger.error("Unable to invoke reportLoadInfo due to " + e.getMessage()); - } catch (InvocationTargetException e) { - s_logger.error("Unable to invoke reportLoadInfo due to " + e.getMessage()); - } - } else { - s_logger.warn("Private channel towards management server is not setup. Switch to offline mode and ignore load report"); - } - } - - public static void ensureRoute(String address) { - if(ensureRouteMethod != null) { - try { - ensureRouteMethod.invoke(ConsoleProxy.context, address); - } catch (IllegalAccessException e) { - s_logger.error("Unable to invoke ensureRoute due to " + e.getMessage()); - } catch (InvocationTargetException e) { - s_logger.error("Unable to invoke ensureRoute due to " + e.getMessage()); - } - } else { - s_logger.warn("Unable to find ensureRoute method, console proxy agent is not up to date"); - } - } - - public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword) { - s_logger.info("Start console proxy with context"); - if(conf != null) { - for(Object key : conf.keySet()) { - s_logger.info("Context property " + (String)key + ": " + conf.getProperty((String)key)); - } - } - - configLog4j(); - Logger.setFactory(new ConsoleProxyLoggerFactory()); - - // Using reflection to setup private/secure communication channel towards management server - ConsoleProxy.context = context; - ConsoleProxy.ksBits = ksBits; - ConsoleProxy.ksPassword = ksPassword; - try { - Class contextClazz = Class.forName("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource"); - authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class, String.class, String.class, String.class, Boolean.class); - reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class); - ensureRouteMethod = contextClazz.getDeclaredMethod("ensureRoute", String.class); - } catch (SecurityException e) { - s_logger.error("Unable to setup private channel due to SecurityException", e); - } catch (NoSuchMethodException e) { - s_logger.error("Unable to setup private channel due to NoSuchMethodException", e); - } catch (IllegalArgumentException e) { - s_logger.error("Unable to setup private channel due to IllegalArgumentException", e); - } catch(ClassNotFoundException e) { - s_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) { - s_logger.info("Can't load consoleproxy.properties from classpath, will use default configuration"); - } else { - try { - props.load(confs); - - 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) - conf.put(key, props.get(key)); - } - } catch (Exception e) { - s_logger.error(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) { - s_logger.error("Unable to load console proxy server factory"); - System.exit(1); - } - - if(httpListenPort != 0) { - startupHttpMain(); - } else { - s_logger.error("A valid HTTP server port is required to be specified, please check your consoleproxy.httpListenPort settings"); - System.exit(1); - } - - if(httpCmdListenPort > 0) { - startupHttpCmdPort(); - } else { - s_logger.info("HTTP command port is disabled"); - } - - ConsoleProxyGCThread cthread = new ConsoleProxyGCThread(connectionMap); - cthread.setName("Console Proxy GC Thread"); - cthread.start(); - } - - private static void startupHttpMain() { - try { - ConsoleProxyServerFactory factory = getHttpServerFactory(); - if(factory == null) { - s_logger.error("Unable to load HTTP server factory"); - System.exit(1); - } - - HttpServer server = factory.createHttpServerInstance(httpListenPort); - server.createContext("/getscreen", new ConsoleProxyThumbnailHandler()); - server.createContext("/resource/", new ConsoleProxyResourceHandler()); - server.createContext("/ajax", new ConsoleProxyAjaxHandler()); - server.createContext("/ajaximg", new ConsoleProxyAjaxImageHandler()); - server.setExecutor(new ThreadExecutor()); // creates a default executor - server.start(); - } catch(Exception e) { - s_logger.error(e.getMessage(), e); - System.exit(1); - } - } - - private static void startupHttpCmdPort() { - try { - s_logger.info("Listening for HTTP CMDs on port " + httpCmdListenPort); - HttpServer cmdServer = HttpServer.create(new InetSocketAddress(httpCmdListenPort), 2); - cmdServer.createContext("/cmd", new ConsoleProxyCmdHandler()); - cmdServer.setExecutor(new ThreadExecutor()); // creates a default executor - cmdServer.start(); - } catch(Exception e) { - s_logger.error(e.getMessage(), e); - System.exit(1); - } - } - - public static void main(String[] argv) { - standaloneStart = true; - configLog4j(); - Logger.setFactory(new ConsoleProxyLoggerFactory()); - - InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties"); - Properties conf = new Properties(); - if (confs == null) { - s_logger.info("Can't load consoleproxy.properties from classpath, will use default configuration"); - } else { - try { - conf.load(confs); - } catch (Exception e) { - s_logger.error(e.toString(), e); - } - } - start(conf); - } - - public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) throws Exception { - ConsoleProxyClient viewer = null; - - boolean reportLoadChange = false; - String clientKey = param.getClientMapKey(); - synchronized (connectionMap) { - viewer = connectionMap.get(clientKey); - if (viewer == null) { - viewer = new ConsoleProxyVncClient(); - viewer.initClient(param); - connectionMap.put(clientKey, viewer); - s_logger.info("Added viewer object " + viewer); - - reportLoadChange = true; - } else if (!viewer.isFrontEndAlive()) { - s_logger.info("The rfb thread died, reinitializing the viewer " + viewer); - viewer.initClient(param); - } else if (!param.getClientHostPassword().equals(viewer.getClientHostPassword())) { - s_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(s_logger.isDebugEnabled()) - s_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) { - ConsoleProxyClient viewer = connectionMap.get(clientKey); - if (viewer == null) { - viewer = new ConsoleProxyVncClient(); - viewer.initClient(param); - - connectionMap.put(clientKey, viewer); - s_logger.info("Added viewer object " + viewer); - reportLoadChange = true; - } else if (!viewer.isFrontEndAlive()) { - s_logger.info("The rfb thread died, reinitializing the viewer " + viewer); - viewer.initClient(param); - } else if (!param.getClientHostPassword().equals(viewer.getClientHostPassword())) { - s_logger.warn("Bad sid detected(VNC port may be reused). sid in session: " - + viewer.getClientHostPassword() + ", sid in request: " + param.getClientHostPassword()); - viewer.initClient(param); - } else { - if(ajaxSession == null || ajaxSession.isEmpty()) - authenticationExternally(param); - } - - if(reportLoadChange) { - ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); - String loadInfo = statsCollector.getStatsReport(); - reportLoadInfo(loadInfo); - if(s_logger.isDebugEnabled()) - s_logger.debug("Report load change : " + loadInfo); - } - return viewer; - } - } - - public static void removeViewer(ConsoleProxyClient viewer) { - synchronized (connectionMap) { - for(Map.Entry entry : connectionMap.entrySet()) { - if(entry.getValue() == viewer) { - connectionMap.remove(entry.getKey()); - return; - } - } - } - } - - public static ConsoleProxyClientStatsCollector getStatsCollector() { - return new ConsoleProxyClientStatsCollector(connectionMap); - } - - public static void authenticationExternally(ConsoleProxyClientParam param) throws AuthenticationException { - ConsoleProxyAuthenticationResult authResult = authenticateConsoleAccess(param, false); - - if(authResult == null || !authResult.isSuccess()) { - s_logger.warn("External authenticator failed authencation 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; - } - - static class ThreadExecutor implements Executor { - public void execute(Runnable r) { - new Thread(r).start(); - } - } -} +import org.apache.axis.encoding.Base64; +import org.apache.log4j.xml.DOMConfigurator; + +import com.cloud.consoleproxy.util.Logger; +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpServer; + +/** + * + * ConsoleProxy, singleton class that manages overall activities in console proxy process. To make legacy code work, we still + */ +public class ConsoleProxy { + private static final Logger s_logger = Logger.getLogger(ConsoleProxy.class); + + public static final int KEYBOARD_RAW = 0; + public static final int KEYBOARD_COOKED = 1; + + public static int VIEWER_LINGER_SECONDS = 180; + + 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 Method authMethod; + public static Method reportMethod; + public static Method ensureRouteMethod; + + static Hashtable connectionMap = new Hashtable(); + static int httpListenPort = 80; + static int httpCmdListenPort = 8001; + static int reconnectMaxRetry = 5; + static int readTimeoutSeconds = 90; + static int keyboardType = KEYBOARD_RAW; + static String factoryClzName; + static boolean standaloneStart = false; + + static String encryptorPassword = genDefaultEncryptorPassword(); + + private static String genDefaultEncryptorPassword() { + try { + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + + byte[] randomBytes = new byte[16]; + random.nextBytes(randomBytes); + return Base64.encode(randomBytes); + } catch (NoSuchAlgorithmException e) { + s_logger.error("Unexpected exception ", e); + assert(false); + } + + return "Dummy"; + } + + private static void configLog4j() { + URL configUrl = System.class.getResource("/conf/log4j-cloud.xml"); + if(configUrl == null) + configUrl = ClassLoader.getSystemResource("log4j-cloud.xml"); + + if(configUrl == null) + configUrl = ClassLoader.getSystemResource("conf/log4j-cloud.xml"); + + if(configUrl != null) { + try { + System.out.println("Configure log4j using " + configUrl.toURI().toString()); + } catch (URISyntaxException e1) { + e1.printStackTrace(); + } + + try { + File file = new File(configUrl.toURI()); + + System.out.println("Log4j configuration from : " + file.getAbsolutePath()); + DOMConfigurator.configureAndWatch(file.getAbsolutePath(), 10000); + } 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) { + s_logger.info("Configure console proxy..."); + for(Object key : conf.keySet()) { + s_logger.info("Property " + (String)key + ": " + conf.getProperty((String)key)); + } + + String s = conf.getProperty("consoleproxy.httpListenPort"); + if (s!=null) { + httpListenPort = Integer.parseInt(s); + s_logger.info("Setting httpListenPort=" + s); + } + + s = conf.getProperty("premium"); + if(s != null && s.equalsIgnoreCase("true")) { + s_logger.info("Premium setting will override settings from consoleproxy.properties, listen at port 443"); + httpListenPort = 443; + factoryClzName = "com.cloud.consoleproxy.ConsoleProxySecureServerFactoryImpl"; + } else { + factoryClzName = ConsoleProxyBaseServerFactoryImpl.class.getName(); + } + + s = conf.getProperty("consoleproxy.httpCmdListenPort"); + if (s!=null) { + httpCmdListenPort = Integer.parseInt(s); + s_logger.info("Setting httpCmdListenPort=" + s); + } + + s = conf.getProperty("consoleproxy.reconnectMaxRetry"); + if (s!=null) { + reconnectMaxRetry = Integer.parseInt(s); + s_logger.info("Setting reconnectMaxRetry=" + reconnectMaxRetry); + } + + s = conf.getProperty("consoleproxy.readTimeoutSeconds"); + if (s!=null) { + readTimeoutSeconds = Integer.parseInt(s); + s_logger.info("Setting readTimeoutSeconds=" + readTimeoutSeconds); + } + } + + public static ConsoleProxyServerFactory getHttpServerFactory() { + try { + Class clz = Class.forName(factoryClzName); + try { + ConsoleProxyServerFactory factory = (ConsoleProxyServerFactory)clz.newInstance(); + factory.init(ConsoleProxy.ksBits, ConsoleProxy.ksPassword); + return factory; + } catch (InstantiationException e) { + s_logger.error(e.getMessage(), e); + return null; + } catch (IllegalAccessException e) { + s_logger.error(e.getMessage(), e); + return null; + } + } catch (ClassNotFoundException e) { + s_logger.warn("Unable to find http server factory class: " + factoryClzName); + return new ConsoleProxyBaseServerFactoryImpl(); + } + } + + 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(standaloneStart) { + return authResult; + } + + if(authMethod != null) { + Object result; + try { + result = authMethod.invoke(ConsoleProxy.context, + param.getClientHostAddress(), + String.valueOf(param.getClientHostPort()), + param.getClientTag(), + param.getClientHostPassword(), + param.getTicket(), + new Boolean(reauthentication)); + } catch (IllegalAccessException e) { + s_logger.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + param.getClientTag(), e); + authResult.setSuccess(false); + return authResult; + } catch (InvocationTargetException e) { + s_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); + } else { + s_logger.error("Invalid authentication return object " + result + " for vm: " + param.getClientTag() + ", decline the access"); + authResult.setSuccess(false); + } + } else { + s_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 { + reportMethod.invoke(ConsoleProxy.context, gsonLoadInfo); + } catch (IllegalAccessException e) { + s_logger.error("Unable to invoke reportLoadInfo due to " + e.getMessage()); + } catch (InvocationTargetException e) { + s_logger.error("Unable to invoke reportLoadInfo due to " + e.getMessage()); + } + } else { + s_logger.warn("Private channel towards management server is not setup. Switch to offline mode and ignore load report"); + } + } + + public static void ensureRoute(String address) { + if(ensureRouteMethod != null) { + try { + ensureRouteMethod.invoke(ConsoleProxy.context, address); + } catch (IllegalAccessException e) { + s_logger.error("Unable to invoke ensureRoute due to " + e.getMessage()); + } catch (InvocationTargetException e) { + s_logger.error("Unable to invoke ensureRoute due to " + e.getMessage()); + } + } else { + s_logger.warn("Unable to find ensureRoute method, console proxy agent is not up to date"); + } + } + + public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword) { + s_logger.info("Start console proxy with context"); + if(conf != null) { + for(Object key : conf.keySet()) { + s_logger.info("Context property " + (String)key + ": " + conf.getProperty((String)key)); + } + } + + configLog4j(); + Logger.setFactory(new ConsoleProxyLoggerFactory()); + + // Using reflection to setup private/secure communication channel towards management server + ConsoleProxy.context = context; + ConsoleProxy.ksBits = ksBits; + ConsoleProxy.ksPassword = ksPassword; + try { + Class contextClazz = Class.forName("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource"); + authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class, String.class, String.class, String.class, Boolean.class); + reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class); + ensureRouteMethod = contextClazz.getDeclaredMethod("ensureRoute", String.class); + } catch (SecurityException e) { + s_logger.error("Unable to setup private channel due to SecurityException", e); + } catch (NoSuchMethodException e) { + s_logger.error("Unable to setup private channel due to NoSuchMethodException", e); + } catch (IllegalArgumentException e) { + s_logger.error("Unable to setup private channel due to IllegalArgumentException", e); + } catch(ClassNotFoundException e) { + s_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) { + s_logger.info("Can't load consoleproxy.properties from classpath, will use default configuration"); + } else { + try { + props.load(confs); + + 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) + conf.put(key, props.get(key)); + } + } catch (Exception e) { + s_logger.error(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) { + s_logger.error("Unable to load console proxy server factory"); + System.exit(1); + } + + if(httpListenPort != 0) { + startupHttpMain(); + } else { + s_logger.error("A valid HTTP server port is required to be specified, please check your consoleproxy.httpListenPort settings"); + System.exit(1); + } + + if(httpCmdListenPort > 0) { + startupHttpCmdPort(); + } else { + s_logger.info("HTTP command port is disabled"); + } + + ConsoleProxyGCThread cthread = new ConsoleProxyGCThread(connectionMap); + cthread.setName("Console Proxy GC Thread"); + cthread.start(); + } + + private static void startupHttpMain() { + try { + ConsoleProxyServerFactory factory = getHttpServerFactory(); + if(factory == null) { + s_logger.error("Unable to load HTTP server factory"); + System.exit(1); + } + + HttpServer server = factory.createHttpServerInstance(httpListenPort); + server.createContext("/getscreen", new ConsoleProxyThumbnailHandler()); + server.createContext("/resource/", new ConsoleProxyResourceHandler()); + server.createContext("/ajax", new ConsoleProxyAjaxHandler()); + server.createContext("/ajaximg", new ConsoleProxyAjaxImageHandler()); + server.setExecutor(new ThreadExecutor()); // creates a default executor + server.start(); + } catch(Exception e) { + s_logger.error(e.getMessage(), e); + System.exit(1); + } + } + + private static void startupHttpCmdPort() { + try { + s_logger.info("Listening for HTTP CMDs on port " + httpCmdListenPort); + HttpServer cmdServer = HttpServer.create(new InetSocketAddress(httpCmdListenPort), 2); + cmdServer.createContext("/cmd", new ConsoleProxyCmdHandler()); + cmdServer.setExecutor(new ThreadExecutor()); // creates a default executor + cmdServer.start(); + } catch(Exception e) { + s_logger.error(e.getMessage(), e); + System.exit(1); + } + } + + public static void main(String[] argv) { + standaloneStart = true; + configLog4j(); + Logger.setFactory(new ConsoleProxyLoggerFactory()); + + InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties"); + Properties conf = new Properties(); + if (confs == null) { + s_logger.info("Can't load consoleproxy.properties from classpath, will use default configuration"); + } else { + try { + conf.load(confs); + } catch (Exception e) { + s_logger.error(e.toString(), e); + } + } + start(conf); + } + + public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) throws Exception { + ConsoleProxyClient viewer = null; + + boolean reportLoadChange = false; + String clientKey = param.getClientMapKey(); + synchronized (connectionMap) { + viewer = connectionMap.get(clientKey); + if (viewer == null) { + viewer = new ConsoleProxyVncClient(); + viewer.initClient(param); + connectionMap.put(clientKey, viewer); + s_logger.info("Added viewer object " + viewer); + + reportLoadChange = true; + } else if (!viewer.isFrontEndAlive()) { + s_logger.info("The rfb thread died, reinitializing the viewer " + viewer); + viewer.initClient(param); + } else if (!param.getClientHostPassword().equals(viewer.getClientHostPassword())) { + s_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(s_logger.isDebugEnabled()) + s_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) { + ConsoleProxyClient viewer = connectionMap.get(clientKey); + if (viewer == null) { + viewer = new ConsoleProxyVncClient(); + viewer.initClient(param); + + connectionMap.put(clientKey, viewer); + s_logger.info("Added viewer object " + viewer); + reportLoadChange = true; + } else if (!viewer.isFrontEndAlive()) { + s_logger.info("The rfb thread died, reinitializing the viewer " + viewer); + viewer.initClient(param); + } else if (!param.getClientHostPassword().equals(viewer.getClientHostPassword())) { + s_logger.warn("Bad sid detected(VNC port may be reused). sid in session: " + + viewer.getClientHostPassword() + ", sid in request: " + param.getClientHostPassword()); + viewer.initClient(param); + } else { + if(ajaxSession == null || ajaxSession.isEmpty()) + authenticationExternally(param); + } + + if(reportLoadChange) { + ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); + String loadInfo = statsCollector.getStatsReport(); + reportLoadInfo(loadInfo); + if(s_logger.isDebugEnabled()) + s_logger.debug("Report load change : " + loadInfo); + } + return viewer; + } + } + + public static void removeViewer(ConsoleProxyClient viewer) { + synchronized (connectionMap) { + for(Map.Entry entry : connectionMap.entrySet()) { + if(entry.getValue() == viewer) { + connectionMap.remove(entry.getKey()); + return; + } + } + } + } + + public static ConsoleProxyClientStatsCollector getStatsCollector() { + return new ConsoleProxyClientStatsCollector(connectionMap); + } + + public static void authenticationExternally(ConsoleProxyClientParam param) throws AuthenticationException { + ConsoleProxyAuthenticationResult authResult = authenticateConsoleAccess(param, false); + + if(authResult == null || !authResult.isSuccess()) { + s_logger.warn("External authenticator failed authencation 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; + } + + static class ThreadExecutor implements Executor { + public void execute(Runnable r) { + new Thread(r).start(); + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClient.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClient.java index 594b2aedb8b..8a0be051903 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClient.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClient.java @@ -14,57 +14,56 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.consoleproxy; - -import java.awt.Image; -import java.util.List; - -/** - * @author Kelven Yang - * ConsoleProxyClient defines an standard interface that a console client should implement, - * - * ConsoleProxyClient maintains a session towards the target host, it glues the session - * to a AJAX front-end viewer - */ -public interface ConsoleProxyClient { - int getClientId(); - - // - // Quick status - // - boolean isHostConnected(); - boolean isFrontEndAlive(); - - // - // AJAX viewer - // - long getAjaxSessionId(); - AjaxFIFOImageCache getAjaxImageCache(); - Image getClientScaledImage(int width, int height); // client thumbnail support - - String onAjaxClientStart(String title, List languages, String guest); - String onAjaxClientUpdate(); - String onAjaxClientKickoff(); - - // - // Input handling - // - void sendClientRawKeyboardEvent(InputEventType event, int code, int modifiers); - void sendClientMouseEvent(InputEventType event, int x, int y, int code, int modifiers); - - // - // Info/Stats - // - long getClientCreateTime(); - long getClientLastFrontEndActivityTime(); - String getClientHostAddress(); - int getClientHostPort(); - String getClientHostPassword(); - String getClientTag(); - - // - // Setup/house-keeping - // - void initClient(ConsoleProxyClientParam param); - void closeClient(); -} +package com.cloud.consoleproxy; + +import java.awt.Image; +import java.util.List; + +/** + * ConsoleProxyClient defines an standard interface that a console client should implement, + * + * ConsoleProxyClient maintains a session towards the target host, it glues the session + * to a AJAX front-end viewer + */ +public interface ConsoleProxyClient { + int getClientId(); + + // + // Quick status + // + boolean isHostConnected(); + boolean isFrontEndAlive(); + + // + // AJAX viewer + // + long getAjaxSessionId(); + AjaxFIFOImageCache getAjaxImageCache(); + Image getClientScaledImage(int width, int height); // client thumbnail support + + String onAjaxClientStart(String title, List languages, String guest); + String onAjaxClientUpdate(); + String onAjaxClientKickoff(); + + // + // Input handling + // + void sendClientRawKeyboardEvent(InputEventType event, int code, int modifiers); + void sendClientMouseEvent(InputEventType event, int x, int y, int code, int modifiers); + + // + // Info/Stats + // + long getClientCreateTime(); + long getClientLastFrontEndActivityTime(); + String getClientHostAddress(); + int getClientHostPort(); + String getClientHostPassword(); + String getClientTag(); + + // + // Setup/house-keeping + // + void initClient(ConsoleProxyClientParam param); + void closeClient(); +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java index ac38f5fd6f1..e4f47270e4e 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java @@ -14,445 +14,443 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.consoleproxy; - -import java.awt.Image; -import java.awt.Rectangle; -import java.util.List; - -import org.apache.log4j.Logger; - -import com.cloud.consoleproxy.util.TileInfo; -import com.cloud.consoleproxy.util.TileTracker; -import com.cloud.consoleproxy.vnc.FrameBufferCanvas; - -/** - * - * @author Kelven Yang - * - * an instance of specialized console protocol implementation, such as VNC or RDP - * - * It mainly implements the features needed by front-end AJAX viewer - * - */ -public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, ConsoleProxyClientListener { - private static final Logger s_logger = Logger.getLogger(ConsoleProxyClientBase.class); - - private static int s_nextClientId = 0; - protected int clientId = getNextClientId(); - - protected long ajaxSessionId = 0; - - protected boolean dirtyFlag = false; - protected Object tileDirtyEvent = new Object(); - protected TileTracker tracker; - protected AjaxFIFOImageCache ajaxImageCache = new AjaxFIFOImageCache(2); +package com.cloud.consoleproxy; - protected ConsoleProxyClientParam clientParam; - protected String clientToken; - - protected long createTime = System.currentTimeMillis(); - protected long lastFrontEndActivityTime = System.currentTimeMillis(); - - protected boolean framebufferResized = false; - protected int resizedFramebufferWidth; - protected int resizedFramebufferHeight; - - public ConsoleProxyClientBase() { - tracker = new TileTracker(); - tracker.initTracking(64, 64, 800, 600); - } - - // - // interface ConsoleProxyClient - // - @Override - public int getClientId() { - return clientId; - } - - public abstract boolean isHostConnected(); - public abstract boolean isFrontEndAlive(); - - @Override - public long getAjaxSessionId() { - return this.ajaxSessionId; - } - - @Override - public AjaxFIFOImageCache getAjaxImageCache() { - return ajaxImageCache; - } - - public Image getClientScaledImage(int width, int height) { - FrameBufferCanvas canvas = getFrameBufferCavas(); - if(canvas != null) - return canvas.getFrameBufferScaledImage(width, height); - - return null; - } - - public abstract void sendClientRawKeyboardEvent(InputEventType event, int code, int modifiers); - public abstract void sendClientMouseEvent(InputEventType event, int x, int y, int code, int modifiers); - - @Override - public long getClientCreateTime() { - return createTime; - } - - @Override - public long getClientLastFrontEndActivityTime() { - return lastFrontEndActivityTime; - } - - @Override - public String getClientHostAddress() { - return clientParam.getClientHostAddress(); - } - - @Override - public int getClientHostPort() { - return clientParam.getClientHostPort(); - } - - @Override - public String getClientHostPassword() { - return clientParam.getClientHostPassword(); - } - - @Override - public String getClientTag() { - if(clientParam.getClientTag() != null) - return clientParam.getClientTag(); - return ""; - } - - @Override - public abstract void initClient(ConsoleProxyClientParam param); - - @Override - public abstract void closeClient(); - - // - // interface FrameBufferEventListener - // - @Override - public void onFramebufferSizeChange(int w, int h) { - tracker.resize(w, h); - - synchronized(this) { - framebufferResized = true; - resizedFramebufferWidth = w; - resizedFramebufferHeight = h; - } - - signalTileDirtyEvent(); - } - - @Override - public void onFramebufferUpdate(int x, int y, int w, int h) { - if(s_logger.isTraceEnabled()) - s_logger.trace("Frame buffer update {" + x + "," + y + "," + w + "," + h + "}"); - tracker.invalidate(new Rectangle(x, y, w, h)); - - signalTileDirtyEvent(); - } - - // - // AJAX Image manipulation - // - public byte[] getFrameBufferJpeg() { - FrameBufferCanvas canvas = getFrameBufferCavas(); - if(canvas != null) - return canvas.getFrameBufferJpeg(); - - return null; - } - - public byte[] getTilesMergedJpeg(List tileList, int tileWidth, int tileHeight) { - FrameBufferCanvas canvas = getFrameBufferCavas(); - if(canvas != null) - return canvas.getTilesMergedJpeg(tileList, tileWidth, tileHeight); - return null; - } - - private String prepareAjaxImage(List tiles, boolean init) { - byte[] imgBits; - if(init) - imgBits = getFrameBufferJpeg(); - else - imgBits = getTilesMergedJpeg(tiles, tracker.getTileWidth(), tracker.getTileHeight()); - - if(imgBits == null) { - s_logger.warn("Unable to generate jpeg image"); - } else { - if(s_logger.isTraceEnabled()) - s_logger.trace("Generated jpeg image size: " + imgBits.length); - } - - int key = ajaxImageCache.putImage(imgBits); - StringBuffer sb = new StringBuffer(); - sb.append("/ajaximg?token=").append(clientToken); - sb.append("&key=").append(key); - sb.append("&ts=").append(System.currentTimeMillis()); - - return sb.toString(); - } - - private String prepareAjaxSession(boolean init) { - if(init) { - synchronized(this) { - ajaxSessionId++; - } - } - - StringBuffer sb = new StringBuffer(); - sb.append("/ajax?token=").append(clientToken).append("&sess=").append(ajaxSessionId); - return sb.toString(); - } - - @Override - public String onAjaxClientKickoff() { - return "onKickoff();"; - } - - private boolean waitForViewerReady() { - long startTick = System.currentTimeMillis(); - while(System.currentTimeMillis() - startTick < 5000) { - if(getFrameBufferCavas() != null) - return true; - - try { - Thread.sleep(100); - } catch (InterruptedException e) { - } - } - return false; - } - - private String onAjaxClientConnectFailed() { - return "

" + - "Unable to start console session as connection is refused by the machine you are accessing" + - "

"; - } - - @Override - public String onAjaxClientStart(String title, List languages, String guest) { - updateFrontEndActivityTime(); - - if(!waitForViewerReady()) - return onAjaxClientConnectFailed(); - - synchronized(this) { - ajaxSessionId++; - framebufferResized = false; - } - - int tileWidth = tracker.getTileWidth(); - int tileHeight = tracker.getTileHeight(); - int width = tracker.getTrackWidth(); - int height = tracker.getTrackHeight(); - - if(s_logger.isTraceEnabled()) - s_logger.trace("Ajax client start, frame buffer w: " + width + ", " + height); - - int retry = 0; - tracker.initCoverageTest(); - while(!tracker.hasFullCoverage() && retry < 10) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - } - retry++; - } - - List tiles = tracker.scan(true); - String imgUrl = prepareAjaxImage(tiles, true); - String updateUrl = prepareAjaxSession(true); - - StringBuffer sbTileSequence = new StringBuffer(); - int i = 0; - for(TileInfo tile : tiles) { - sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]"); - if(i < tiles.size() - 1) - sbTileSequence.append(","); - - i++; - } - - return getAjaxViewerPageContent(sbTileSequence.toString(), imgUrl, - updateUrl, width, height, tileWidth, tileHeight, title, - ConsoleProxy.keyboardType == ConsoleProxy.KEYBOARD_RAW, - languages, guest); - } - - private String getAjaxViewerPageContent(String tileSequence, String imgUrl, String updateUrl, int width, - int height, int tileWidth, int tileHeight, String title, boolean rawKeyboard, List languages, String guest) { - - StringBuffer sbLanguages = new StringBuffer(""); - if(languages != null) { - for(String lang : languages) { - if(sbLanguages.length() > 0) { - sbLanguages.append(","); - } - sbLanguages.append(lang); - } - } - - String[] content = new String[] { - "", - "", - "", - "", - "", - "", - "", - "", - "" + title + "", - "", - "", - "
", - "", - "", - "
", - "
", - "", - "", - "" - }; - - StringBuffer sb = new StringBuffer(); - for(int i = 0; i < content.length; i++) - sb.append(content[i]); - - return sb.toString(); - } - - public String onAjaxClientDisconnected() { - return "onDisconnect();"; - } - - @Override - public String onAjaxClientUpdate() { - updateFrontEndActivityTime(); - if(!waitForViewerReady()) - return onAjaxClientDisconnected(); - - synchronized(tileDirtyEvent) { - if(!dirtyFlag) { - try { - tileDirtyEvent.wait(3000); - } catch(InterruptedException e) { - } - } - } - - boolean doResize = false; - synchronized(this) { - if(framebufferResized) { - framebufferResized = false; - doResize = true; - } - } - - List tiles; - - if(doResize) - tiles = tracker.scan(true); - else - tiles = tracker.scan(false); - dirtyFlag = false; - - String imgUrl = prepareAjaxImage(tiles, false); - StringBuffer sbTileSequence = new StringBuffer(); - int i = 0; - for(TileInfo tile : tiles) { - sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]"); - if(i < tiles.size() - 1) - sbTileSequence.append(","); - - i++; - } - - return getAjaxViewerUpdatePageContent(sbTileSequence.toString(), imgUrl, doResize, - resizedFramebufferWidth, resizedFramebufferHeight, - tracker.getTileWidth(), tracker.getTileHeight()); - } - - private String getAjaxViewerUpdatePageContent(String tileSequence, String imgUrl, boolean resized, int width, - int height, int tileWidth, int tileHeight) { - - String[] content = new String[] { - "tileMap = [ " + tileSequence + " ];", - resized ? "ajaxViewer.resize('main_panel', " + width + ", " + height + " , " + tileWidth + ", " + tileHeight + ");" : "", - "ajaxViewer.refresh('" + imgUrl + "', tileMap, false);" - }; - - StringBuffer sb = new StringBuffer(); - for(int i = 0; i < content.length; i++) - sb.append(content[i]); - - return sb.toString(); - } - - // - // Helpers - // - private synchronized static int getNextClientId() { - return ++s_nextClientId; - } - - private void signalTileDirtyEvent() { - synchronized(tileDirtyEvent) { - dirtyFlag = true; - tileDirtyEvent.notifyAll(); - } - } - - public void updateFrontEndActivityTime() { - lastFrontEndActivityTime = System.currentTimeMillis(); - } - - protected abstract FrameBufferCanvas getFrameBufferCavas(); +import java.awt.Image; +import java.awt.Rectangle; +import java.util.List; - public ConsoleProxyClientParam getClientParam() { - return clientParam; - } +import org.apache.log4j.Logger; - public void setClientParam(ConsoleProxyClientParam clientParam) { - this.clientParam = clientParam; - ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(ConsoleProxy.getEncryptorPassword()); - this.clientToken = encryptor.encryptObject(ConsoleProxyClientParam.class, clientParam); - } -} +import com.cloud.consoleproxy.util.TileInfo; +import com.cloud.consoleproxy.util.TileTracker; +import com.cloud.consoleproxy.vnc.FrameBufferCanvas; + +/** + * + * an instance of specialized console protocol implementation, such as VNC or RDP + * + * It mainly implements the features needed by front-end AJAX viewer + * + */ +public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, ConsoleProxyClientListener { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyClientBase.class); + + private static int s_nextClientId = 0; + protected int clientId = getNextClientId(); + + protected long ajaxSessionId = 0; + + protected boolean dirtyFlag = false; + protected Object tileDirtyEvent = new Object(); + protected TileTracker tracker; + protected AjaxFIFOImageCache ajaxImageCache = new AjaxFIFOImageCache(2); + + protected ConsoleProxyClientParam clientParam; + protected String clientToken; + + protected long createTime = System.currentTimeMillis(); + protected long lastFrontEndActivityTime = System.currentTimeMillis(); + + protected boolean framebufferResized = false; + protected int resizedFramebufferWidth; + protected int resizedFramebufferHeight; + + public ConsoleProxyClientBase() { + tracker = new TileTracker(); + tracker.initTracking(64, 64, 800, 600); + } + + // + // interface ConsoleProxyClient + // + @Override + public int getClientId() { + return clientId; + } + + public abstract boolean isHostConnected(); + public abstract boolean isFrontEndAlive(); + + @Override + public long getAjaxSessionId() { + return this.ajaxSessionId; + } + + @Override + public AjaxFIFOImageCache getAjaxImageCache() { + return ajaxImageCache; + } + + public Image getClientScaledImage(int width, int height) { + FrameBufferCanvas canvas = getFrameBufferCavas(); + if(canvas != null) + return canvas.getFrameBufferScaledImage(width, height); + + return null; + } + + public abstract void sendClientRawKeyboardEvent(InputEventType event, int code, int modifiers); + public abstract void sendClientMouseEvent(InputEventType event, int x, int y, int code, int modifiers); + + @Override + public long getClientCreateTime() { + return createTime; + } + + @Override + public long getClientLastFrontEndActivityTime() { + return lastFrontEndActivityTime; + } + + @Override + public String getClientHostAddress() { + return clientParam.getClientHostAddress(); + } + + @Override + public int getClientHostPort() { + return clientParam.getClientHostPort(); + } + + @Override + public String getClientHostPassword() { + return clientParam.getClientHostPassword(); + } + + @Override + public String getClientTag() { + if(clientParam.getClientTag() != null) + return clientParam.getClientTag(); + return ""; + } + + @Override + public abstract void initClient(ConsoleProxyClientParam param); + + @Override + public abstract void closeClient(); + + // + // interface FrameBufferEventListener + // + @Override + public void onFramebufferSizeChange(int w, int h) { + tracker.resize(w, h); + + synchronized(this) { + framebufferResized = true; + resizedFramebufferWidth = w; + resizedFramebufferHeight = h; + } + + signalTileDirtyEvent(); + } + + @Override + public void onFramebufferUpdate(int x, int y, int w, int h) { + if(s_logger.isTraceEnabled()) + s_logger.trace("Frame buffer update {" + x + "," + y + "," + w + "," + h + "}"); + tracker.invalidate(new Rectangle(x, y, w, h)); + + signalTileDirtyEvent(); + } + + // + // AJAX Image manipulation + // + public byte[] getFrameBufferJpeg() { + FrameBufferCanvas canvas = getFrameBufferCavas(); + if(canvas != null) + return canvas.getFrameBufferJpeg(); + + return null; + } + + public byte[] getTilesMergedJpeg(List tileList, int tileWidth, int tileHeight) { + FrameBufferCanvas canvas = getFrameBufferCavas(); + if(canvas != null) + return canvas.getTilesMergedJpeg(tileList, tileWidth, tileHeight); + return null; + } + + private String prepareAjaxImage(List tiles, boolean init) { + byte[] imgBits; + if(init) + imgBits = getFrameBufferJpeg(); + else + imgBits = getTilesMergedJpeg(tiles, tracker.getTileWidth(), tracker.getTileHeight()); + + if(imgBits == null) { + s_logger.warn("Unable to generate jpeg image"); + } else { + if(s_logger.isTraceEnabled()) + s_logger.trace("Generated jpeg image size: " + imgBits.length); + } + + int key = ajaxImageCache.putImage(imgBits); + StringBuffer sb = new StringBuffer(); + sb.append("/ajaximg?token=").append(clientToken); + sb.append("&key=").append(key); + sb.append("&ts=").append(System.currentTimeMillis()); + + return sb.toString(); + } + + private String prepareAjaxSession(boolean init) { + if(init) { + synchronized(this) { + ajaxSessionId++; + } + } + + StringBuffer sb = new StringBuffer(); + sb.append("/ajax?token=").append(clientToken).append("&sess=").append(ajaxSessionId); + return sb.toString(); + } + + @Override + public String onAjaxClientKickoff() { + return "onKickoff();"; + } + + private boolean waitForViewerReady() { + long startTick = System.currentTimeMillis(); + while(System.currentTimeMillis() - startTick < 5000) { + if(getFrameBufferCavas() != null) + return true; + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + return false; + } + + private String onAjaxClientConnectFailed() { + return "

" + + "Unable to start console session as connection is refused by the machine you are accessing" + + "

"; + } + + @Override + public String onAjaxClientStart(String title, List languages, String guest) { + updateFrontEndActivityTime(); + + if(!waitForViewerReady()) + return onAjaxClientConnectFailed(); + + synchronized(this) { + ajaxSessionId++; + framebufferResized = false; + } + + int tileWidth = tracker.getTileWidth(); + int tileHeight = tracker.getTileHeight(); + int width = tracker.getTrackWidth(); + int height = tracker.getTrackHeight(); + + if(s_logger.isTraceEnabled()) + s_logger.trace("Ajax client start, frame buffer w: " + width + ", " + height); + + int retry = 0; + tracker.initCoverageTest(); + while(!tracker.hasFullCoverage() && retry < 10) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + retry++; + } + + List tiles = tracker.scan(true); + String imgUrl = prepareAjaxImage(tiles, true); + String updateUrl = prepareAjaxSession(true); + + StringBuffer sbTileSequence = new StringBuffer(); + int i = 0; + for(TileInfo tile : tiles) { + sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]"); + if(i < tiles.size() - 1) + sbTileSequence.append(","); + + i++; + } + + return getAjaxViewerPageContent(sbTileSequence.toString(), imgUrl, + updateUrl, width, height, tileWidth, tileHeight, title, + ConsoleProxy.keyboardType == ConsoleProxy.KEYBOARD_RAW, + languages, guest); + } + + private String getAjaxViewerPageContent(String tileSequence, String imgUrl, String updateUrl, int width, + int height, int tileWidth, int tileHeight, String title, boolean rawKeyboard, List languages, String guest) { + + StringBuffer sbLanguages = new StringBuffer(""); + if(languages != null) { + for(String lang : languages) { + if(sbLanguages.length() > 0) { + sbLanguages.append(","); + } + sbLanguages.append(lang); + } + } + + String[] content = new String[] { + "", + "", + "", + "", + "", + "", + "", + "", + "" + title + "", + "", + "", + "
", + "", + "", + "
", + "
", + "", + "", + "" + }; + + StringBuffer sb = new StringBuffer(); + for(int i = 0; i < content.length; i++) + sb.append(content[i]); + + return sb.toString(); + } + + public String onAjaxClientDisconnected() { + return "onDisconnect();"; + } + + @Override + public String onAjaxClientUpdate() { + updateFrontEndActivityTime(); + if(!waitForViewerReady()) + return onAjaxClientDisconnected(); + + synchronized(tileDirtyEvent) { + if(!dirtyFlag) { + try { + tileDirtyEvent.wait(3000); + } catch(InterruptedException e) { + } + } + } + + boolean doResize = false; + synchronized(this) { + if(framebufferResized) { + framebufferResized = false; + doResize = true; + } + } + + List tiles; + + if(doResize) + tiles = tracker.scan(true); + else + tiles = tracker.scan(false); + dirtyFlag = false; + + String imgUrl = prepareAjaxImage(tiles, false); + StringBuffer sbTileSequence = new StringBuffer(); + int i = 0; + for(TileInfo tile : tiles) { + sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]"); + if(i < tiles.size() - 1) + sbTileSequence.append(","); + + i++; + } + + return getAjaxViewerUpdatePageContent(sbTileSequence.toString(), imgUrl, doResize, + resizedFramebufferWidth, resizedFramebufferHeight, + tracker.getTileWidth(), tracker.getTileHeight()); + } + + private String getAjaxViewerUpdatePageContent(String tileSequence, String imgUrl, boolean resized, int width, + int height, int tileWidth, int tileHeight) { + + String[] content = new String[] { + "tileMap = [ " + tileSequence + " ];", + resized ? "ajaxViewer.resize('main_panel', " + width + ", " + height + " , " + tileWidth + ", " + tileHeight + ");" : "", + "ajaxViewer.refresh('" + imgUrl + "', tileMap, false);" + }; + + StringBuffer sb = new StringBuffer(); + for(int i = 0; i < content.length; i++) + sb.append(content[i]); + + return sb.toString(); + } + + // + // Helpers + // + private synchronized static int getNextClientId() { + return ++s_nextClientId; + } + + private void signalTileDirtyEvent() { + synchronized(tileDirtyEvent) { + dirtyFlag = true; + tileDirtyEvent.notifyAll(); + } + } + + public void updateFrontEndActivityTime() { + lastFrontEndActivityTime = System.currentTimeMillis(); + } + + protected abstract FrameBufferCanvas getFrameBufferCavas(); + + public ConsoleProxyClientParam getClientParam() { + return clientParam; + } + + public void setClientParam(ConsoleProxyClientParam clientParam) { + this.clientParam = clientParam; + ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(ConsoleProxy.getEncryptorPassword()); + this.clientToken = encryptor.encryptObject(ConsoleProxyClientParam.class, clientParam); + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java index 8dc576fc036..8de4955d4b7 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java @@ -14,98 +14,97 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.consoleproxy; - -/** - * - * @author Kelven Yang - * Data object to store parameter info needed by client to connect to its host - */ -public class ConsoleProxyClientParam { - - private String clientHostAddress; - private int clientHostPort; - private String clientHostPassword; - private String clientTag; - private String ticket; - - private String clientTunnelUrl; - private String clientTunnelSession; - - private String ajaxSessionId; - - public ConsoleProxyClientParam() { - clientHostPort = 0; - } - - public String getClientHostAddress() { - return clientHostAddress; - } - - public void setClientHostAddress(String clientHostAddress) { - this.clientHostAddress = clientHostAddress; - } - - public int getClientHostPort() { - return clientHostPort; - } - - public void setClientHostPort(int clientHostPort) { - this.clientHostPort = clientHostPort; - } - - public String getClientHostPassword() { - return clientHostPassword; - } - - public void setClientHostPassword(String clientHostPassword) { - this.clientHostPassword = clientHostPassword; - } - - public String getClientTag() { - return clientTag; - } - - public void setClientTag(String clientTag) { - this.clientTag = clientTag; - } - - public String getTicket() { - return ticket; - } - - public void setTicket(String ticket) { - this.ticket = ticket; - } - - public String getClientTunnelUrl() { - return clientTunnelUrl; - } - - public void setClientTunnelUrl(String clientTunnelUrl) { - this.clientTunnelUrl = clientTunnelUrl; - } - - public String getClientTunnelSession() { - return clientTunnelSession; - } - - public void setClientTunnelSession(String clientTunnelSession) { - this.clientTunnelSession = clientTunnelSession; - } - - public String getAjaxSessionId() { - return this.ajaxSessionId; - } - - public void setAjaxSessionId(String ajaxSessionId) { - this.ajaxSessionId = ajaxSessionId; - } - - public String getClientMapKey() { - if(clientTag != null && !clientTag.isEmpty()) - return clientTag; - - return clientHostAddress + ":" + clientHostPort; - } -} +package com.cloud.consoleproxy; + +/** + * + * Data object to store parameter info needed by client to connect to its host + */ +public class ConsoleProxyClientParam { + + private String clientHostAddress; + private int clientHostPort; + private String clientHostPassword; + private String clientTag; + private String ticket; + + private String clientTunnelUrl; + private String clientTunnelSession; + + private String ajaxSessionId; + + public ConsoleProxyClientParam() { + clientHostPort = 0; + } + + public String getClientHostAddress() { + return clientHostAddress; + } + + public void setClientHostAddress(String clientHostAddress) { + this.clientHostAddress = clientHostAddress; + } + + public int getClientHostPort() { + return clientHostPort; + } + + public void setClientHostPort(int clientHostPort) { + this.clientHostPort = clientHostPort; + } + + public String getClientHostPassword() { + return clientHostPassword; + } + + public void setClientHostPassword(String clientHostPassword) { + this.clientHostPassword = clientHostPassword; + } + + public String getClientTag() { + return clientTag; + } + + public void setClientTag(String clientTag) { + this.clientTag = clientTag; + } + + public String getTicket() { + return ticket; + } + + public void setTicket(String ticket) { + this.ticket = ticket; + } + + public String getClientTunnelUrl() { + return clientTunnelUrl; + } + + public void setClientTunnelUrl(String clientTunnelUrl) { + this.clientTunnelUrl = clientTunnelUrl; + } + + public String getClientTunnelSession() { + return clientTunnelSession; + } + + public void setClientTunnelSession(String clientTunnelSession) { + this.clientTunnelSession = clientTunnelSession; + } + + public String getAjaxSessionId() { + return this.ajaxSessionId; + } + + public void setAjaxSessionId(String ajaxSessionId) { + this.ajaxSessionId = ajaxSessionId; + } + + public String getClientMapKey() { + if(clientTag != null && !clientTag.isEmpty()) + return clientTag; + + return clientHostAddress + ":" + clientHostPort; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientStatsCollector.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientStatsCollector.java index 82bceafcfb2..15cf451ca2c 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientStatsCollector.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientStatsCollector.java @@ -14,76 +14,75 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.consoleproxy; - -import java.io.OutputStreamWriter; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Hashtable; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -/** - * - * @author Kelven Yang - * ConsoleProxyClientStatsCollector collects client stats for console proxy agent to report - */ -public class ConsoleProxyClientStatsCollector { - - ArrayList connections; - - public ConsoleProxyClientStatsCollector() { - } - - public ConsoleProxyClientStatsCollector(Hashtable connMap) { - setConnections(connMap); - } - - public String getStatsReport() { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - return gson.toJson(this); - } - - public void getStatsReport(OutputStreamWriter os) { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - gson.toJson(this, os); - } - - private void setConnections(Hashtable connMap) { - - ArrayList conns = new ArrayList(); - Enumeration e = connMap.keys(); - while (e.hasMoreElements()) { - synchronized (connMap) { - String key = e.nextElement(); - ConsoleProxyClient client = connMap.get(key); - - ConsoleProxyConnection conn = new ConsoleProxyConnection(); - - conn.id = client.getClientId(); - conn.clientInfo = ""; - conn.host = client.getClientHostAddress(); - conn.port = client.getClientHostPort(); - conn.tag = client.getClientTag(); - conn.createTime = client.getClientCreateTime(); - conn.lastUsedTime = client.getClientLastFrontEndActivityTime(); - conns.add(conn); - } - } - connections = conns; - } - - public static class ConsoleProxyConnection { - public int id; - public String clientInfo; - public String host; - public int port; - public String tag; - public long createTime; - public long lastUsedTime; - - public ConsoleProxyConnection() { - } - } -} +package com.cloud.consoleproxy; + +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Hashtable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * + * ConsoleProxyClientStatsCollector collects client stats for console proxy agent to report + */ +public class ConsoleProxyClientStatsCollector { + + ArrayList connections; + + public ConsoleProxyClientStatsCollector() { + } + + public ConsoleProxyClientStatsCollector(Hashtable connMap) { + setConnections(connMap); + } + + public String getStatsReport() { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + return gson.toJson(this); + } + + public void getStatsReport(OutputStreamWriter os) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + gson.toJson(this, os); + } + + private void setConnections(Hashtable connMap) { + + ArrayList conns = new ArrayList(); + Enumeration e = connMap.keys(); + while (e.hasMoreElements()) { + synchronized (connMap) { + String key = e.nextElement(); + ConsoleProxyClient client = connMap.get(key); + + ConsoleProxyConnection conn = new ConsoleProxyConnection(); + + conn.id = client.getClientId(); + conn.clientInfo = ""; + conn.host = client.getClientHostAddress(); + conn.port = client.getClientHostPort(); + conn.tag = client.getClientTag(); + conn.createTime = client.getClientCreateTime(); + conn.lastUsedTime = client.getClientLastFrontEndActivityTime(); + conns.add(conn); + } + } + connections = conns; + } + + public static class ConsoleProxyConnection { + public int id; + public String clientInfo; + public String host; + public int port; + public String tag; + public long createTime; + public long lastUsedTime; + + public ConsoleProxyConnection() { + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyGCThread.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyGCThread.java index f14cd5a8493..158f9fec49f 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyGCThread.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyGCThread.java @@ -14,96 +14,95 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.consoleproxy; - -import java.io.File; -import java.util.Enumeration; -import java.util.Hashtable; - -import org.apache.log4j.Logger; - -/** - * - * @author Kelven Yang - * ConsoleProxyGCThread does house-keeping work for the process, it helps cleanup log files, - * recycle idle client sessions without front-end activities and report client stats to external - * management software - */ -public class ConsoleProxyGCThread extends Thread { - private static final Logger s_logger = Logger.getLogger(ConsoleProxyGCThread.class); - - private final static int MAX_SESSION_IDLE_SECONDS = 180; - - private Hashtable connMap; - private long lastLogScan = 0; - - public ConsoleProxyGCThread(Hashtable connMap) { - this.connMap = connMap; - } - - private void cleanupLogging() { - if(lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000) - return; - - lastLogScan = System.currentTimeMillis(); - - File logDir = new File("./logs"); - File files[] = logDir.listFiles(); - if(files != null) { - for(File file : files) { - if(System.currentTimeMillis() - file.lastModified() >= 86400000L) { - try { - file.delete(); - } catch(Throwable e) { - } - } - } - } - } - - @Override +package com.cloud.consoleproxy; + +import java.io.File; +import java.util.Enumeration; +import java.util.Hashtable; + +import org.apache.log4j.Logger; + +/** + * + * ConsoleProxyGCThread does house-keeping work for the process, it helps cleanup log files, + * recycle idle client sessions without front-end activities and report client stats to external + * management software + */ +public class ConsoleProxyGCThread extends Thread { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyGCThread.class); + + private final static int MAX_SESSION_IDLE_SECONDS = 180; + + private Hashtable connMap; + private long lastLogScan = 0; + + public ConsoleProxyGCThread(Hashtable connMap) { + this.connMap = connMap; + } + + private void cleanupLogging() { + if(lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000) + return; + + lastLogScan = System.currentTimeMillis(); + + File logDir = new File("./logs"); + File files[] = logDir.listFiles(); + if(files != null) { + for(File file : files) { + if(System.currentTimeMillis() - file.lastModified() >= 86400000L) { + try { + file.delete(); + } catch(Throwable e) { + } + } + } + } + } + + @Override public void run() { - - boolean bReportLoad = false; - while (true) { - cleanupLogging(); - bReportLoad = false; - - if(s_logger.isDebugEnabled()) - s_logger.debug("connMap=" + connMap); - Enumeration e = connMap.keys(); - while (e.hasMoreElements()) { - String key; - ConsoleProxyClient client; - - synchronized (connMap) { - key = e.nextElement(); - client = connMap.get(key); - } - - long seconds_unused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000; - if (seconds_unused < MAX_SESSION_IDLE_SECONDS) { - continue; - } - - synchronized (connMap) { - connMap.remove(key); - } - - // close the server connection - s_logger.info("Dropping " + client + " which has not been used for " + seconds_unused + " seconds"); - client.closeClient(); - } - - if(bReportLoad) { - // report load changes - String loadInfo = new ConsoleProxyClientStatsCollector(connMap).getStatsReport(); - ConsoleProxy.reportLoadInfo(loadInfo); - if(s_logger.isDebugEnabled()) - s_logger.debug("Report load change : " + loadInfo); - } - - try { Thread.sleep(5000); } catch (InterruptedException ex) {} - } - } -} + + boolean bReportLoad = false; + while (true) { + cleanupLogging(); + bReportLoad = false; + + if(s_logger.isDebugEnabled()) + s_logger.debug("connMap=" + connMap); + Enumeration e = connMap.keys(); + while (e.hasMoreElements()) { + String key; + ConsoleProxyClient client; + + synchronized (connMap) { + key = e.nextElement(); + client = connMap.get(key); + } + + long seconds_unused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000; + if (seconds_unused < MAX_SESSION_IDLE_SECONDS) { + continue; + } + + synchronized (connMap) { + connMap.remove(key); + } + + // close the server connection + s_logger.info("Dropping " + client + " which has not been used for " + seconds_unused + " seconds"); + client.closeClient(); + } + + if(bReportLoad) { + // report load changes + String loadInfo = new ConsoleProxyClientStatsCollector(connMap).getStatsReport(); + ConsoleProxy.reportLoadInfo(loadInfo); + if(s_logger.isDebugEnabled()) + s_logger.debug("Report load change : " + loadInfo); + } + + try { Thread.sleep(5000); } catch (InterruptedException ex) {} + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyPasswordBasedEncryptor.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyPasswordBasedEncryptor.java index 3ef22c5ff90..29826f0ea92 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyPasswordBasedEncryptor.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyPasswordBasedEncryptor.java @@ -14,130 +14,129 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.consoleproxy; - -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.SecretKeySpec; - -import org.apache.commons.codec.binary.Base64; -import org.apache.log4j.Logger; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -/** - * - * @author Kelven Yang - * A simple password based encyrptor based on DES. It can serialize simple POJO object into URL safe string - * and deserialize it back. - * - */ -public class ConsoleProxyPasswordBasedEncryptor { - private static final Logger s_logger = Logger.getLogger(ConsoleProxyPasswordBasedEncryptor.class); - - private String password; - private Gson gson; - - public ConsoleProxyPasswordBasedEncryptor(String password) { - this.password = password; - gson = new GsonBuilder().create(); - } - - public String encryptText(String text) { - if(text == null || text.isEmpty()) - return text; - - assert(password != null); - assert(!password.isEmpty()); - - try { - Cipher cipher = Cipher.getInstance("DES"); - int maxKeySize = 8; - SecretKeySpec keySpec = new SecretKeySpec(normalizeKey(password.getBytes(), maxKeySize), "DES"); - cipher.init(Cipher.ENCRYPT_MODE, keySpec); - byte[] encryptedBytes = cipher.doFinal(text.getBytes()); - return Base64.encodeBase64URLSafeString(encryptedBytes); - } catch (NoSuchAlgorithmException e) { - s_logger.error("Unexpected exception ", e); - return null; - } catch (NoSuchPaddingException e) { - s_logger.error("Unexpected exception ", e); - return null; - } catch (IllegalBlockSizeException e) { - s_logger.error("Unexpected exception ", e); - return null; - } catch (BadPaddingException e) { - s_logger.error("Unexpected exception ", e); - return null; - } catch (InvalidKeyException e) { - s_logger.error("Unexpected exception ", e); - return null; - } - } - - public String decryptText(String encryptedText) { - if(encryptedText == null || encryptedText.isEmpty()) - return encryptedText; - - assert(password != null); - assert(!password.isEmpty()); - - try { - Cipher cipher = Cipher.getInstance("DES"); - int maxKeySize = 8; - SecretKeySpec keySpec = new SecretKeySpec(normalizeKey(password.getBytes(), maxKeySize), "DES"); - cipher.init(Cipher.DECRYPT_MODE, keySpec); - - byte[] encryptedBytes = Base64.decodeBase64(encryptedText); - return new String(cipher.doFinal(encryptedBytes)); - } catch (NoSuchAlgorithmException e) { - s_logger.error("Unexpected exception ", e); - return null; - } catch (NoSuchPaddingException e) { - s_logger.error("Unexpected exception ", e); - return null; - } catch (IllegalBlockSizeException e) { - s_logger.error("Unexpected exception ", e); - return null; - } catch (BadPaddingException e) { - s_logger.error("Unexpected exception ", e); - return null; - } catch (InvalidKeyException e) { - s_logger.error("Unexpected exception ", e); - return null; - } - } - - public String encryptObject(Class clz, T obj) { - if(obj == null) - return null; - - String json = gson.toJson(obj); - return encryptText(json); - } - - @SuppressWarnings("unchecked") - public T decryptObject(Class clz, String encrypted) { - if(encrypted == null || encrypted.isEmpty()) - return null; - - String json = decryptText(encrypted); - return (T)gson.fromJson(json, clz); - } - - private static byte[] normalizeKey(byte[] keyBytes, int keySize) { - assert(keySize > 0); - byte[] key = new byte[keySize]; - - for(int i = 0; i < keyBytes.length; i++) - key[i%keySize] ^= keyBytes[i]; - - return key; - } -} +package com.cloud.consoleproxy; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.commons.codec.binary.Base64; +import org.apache.log4j.Logger; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * + * A simple password based encyrptor based on DES. It can serialize simple POJO object into URL safe string + * and deserialize it back. + * + */ +public class ConsoleProxyPasswordBasedEncryptor { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyPasswordBasedEncryptor.class); + + private String password; + private Gson gson; + + public ConsoleProxyPasswordBasedEncryptor(String password) { + this.password = password; + gson = new GsonBuilder().create(); + } + + public String encryptText(String text) { + if(text == null || text.isEmpty()) + return text; + + assert(password != null); + assert(!password.isEmpty()); + + try { + Cipher cipher = Cipher.getInstance("DES"); + int maxKeySize = 8; + SecretKeySpec keySpec = new SecretKeySpec(normalizeKey(password.getBytes(), maxKeySize), "DES"); + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + byte[] encryptedBytes = cipher.doFinal(text.getBytes()); + return Base64.encodeBase64URLSafeString(encryptedBytes); + } catch (NoSuchAlgorithmException e) { + s_logger.error("Unexpected exception ", e); + return null; + } catch (NoSuchPaddingException e) { + s_logger.error("Unexpected exception ", e); + return null; + } catch (IllegalBlockSizeException e) { + s_logger.error("Unexpected exception ", e); + return null; + } catch (BadPaddingException e) { + s_logger.error("Unexpected exception ", e); + return null; + } catch (InvalidKeyException e) { + s_logger.error("Unexpected exception ", e); + return null; + } + } + + public String decryptText(String encryptedText) { + if(encryptedText == null || encryptedText.isEmpty()) + return encryptedText; + + assert(password != null); + assert(!password.isEmpty()); + + try { + Cipher cipher = Cipher.getInstance("DES"); + int maxKeySize = 8; + SecretKeySpec keySpec = new SecretKeySpec(normalizeKey(password.getBytes(), maxKeySize), "DES"); + cipher.init(Cipher.DECRYPT_MODE, keySpec); + + byte[] encryptedBytes = Base64.decodeBase64(encryptedText); + return new String(cipher.doFinal(encryptedBytes)); + } catch (NoSuchAlgorithmException e) { + s_logger.error("Unexpected exception ", e); + return null; + } catch (NoSuchPaddingException e) { + s_logger.error("Unexpected exception ", e); + return null; + } catch (IllegalBlockSizeException e) { + s_logger.error("Unexpected exception ", e); + return null; + } catch (BadPaddingException e) { + s_logger.error("Unexpected exception ", e); + return null; + } catch (InvalidKeyException e) { + s_logger.error("Unexpected exception ", e); + return null; + } + } + + public String encryptObject(Class clz, T obj) { + if(obj == null) + return null; + + String json = gson.toJson(obj); + return encryptText(json); + } + + @SuppressWarnings("unchecked") + public T decryptObject(Class clz, String encrypted) { + if(encrypted == null || encrypted.isEmpty()) + return null; + + String json = decryptText(encrypted); + return (T)gson.fromJson(json, clz); + } + + private static byte[] normalizeKey(byte[] keyBytes, int keySize) { + assert(keySize > 0); + byte[] key = new byte[keySize]; + + for(int i = 0; i < keyBytes.length; i++) + key[i%keySize] ^= keyBytes[i]; + + return key; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java index 77a9977c90b..6a473b5f007 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java @@ -14,223 +14,222 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.consoleproxy; - -import java.io.IOException; +package com.cloud.consoleproxy; + +import java.io.IOException; import java.net.URI; -import java.net.UnknownHostException; - -import org.apache.log4j.Logger; - -import com.cloud.consoleproxy.vnc.FrameBufferCanvas; -import com.cloud.consoleproxy.vnc.RfbConstants; -import com.cloud.consoleproxy.vnc.VncClient; - -/** - * - * @author Kelven Yang - * ConsoleProxyVncClient bridges a VNC engine with the front-end AJAX viewer - * - */ -public class ConsoleProxyVncClient extends ConsoleProxyClientBase { - private static final Logger s_logger = Logger.getLogger(ConsoleProxyVncClient.class); +import java.net.UnknownHostException; - private static final int SHIFT_KEY_MASK = 64; - private static final int CTRL_KEY_MASK = 128; - private static final int META_KEY_MASK = 256; - private static final int ALT_KEY_MASK = 512; - - private static final int X11_KEY_SHIFT = 0xffe1; - private static final int X11_KEY_CTRL = 0xffe3; - private static final int X11_KEY_ALT = 0xffe9; - private static final int X11_KEY_META = 0xffe7; - - private VncClient client; - private Thread worker; - private boolean workerDone = false; - - private int lastModifierStates = 0; - private int lastPointerMask = 0; - - public ConsoleProxyVncClient() { - } - - public boolean isHostConnected() { - if(client != null) - return client.isHostConnected(); - - return false; - } - - @Override - public boolean isFrontEndAlive() { - if(workerDone || System.currentTimeMillis() - getClientLastFrontEndActivityTime() > ConsoleProxy.VIEWER_LINGER_SECONDS*1000) { - s_logger.info("Front end has been idle for too long"); - return false; - } - return true; - } - - @Override - public void initClient(ConsoleProxyClientParam param) { - setClientParam(param); - - client = new VncClient(this); - worker = new Thread(new Runnable() { - public void run() { - String tunnelUrl = getClientParam().getClientTunnelUrl(); - String tunnelSession = getClientParam().getClientTunnelSession(); - - for(int i = 0; i < 15; i++) { - try { - if(tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null && !tunnelSession.isEmpty()) { - URI uri = new URI(tunnelUrl); - s_logger.info("Connect to VNC server via tunnel. url: " + tunnelUrl + ", session: " + tunnelSession); - - ConsoleProxy.ensureRoute(uri.getHost()); - client.connectTo( - uri.getHost(), uri.getPort(), - uri.getPath() + "?" + uri.getQuery(), - tunnelSession, "https".equalsIgnoreCase(uri.getScheme()), - getClientHostPassword()); - } else { - s_logger.info("Connect to VNC server directly. host: " + getClientHostAddress() + ", port: " + getClientHostPort()); - ConsoleProxy.ensureRoute(getClientHostAddress()); - client.connectTo(getClientHostAddress(), getClientHostPort(), getClientHostPassword()); - } - } catch (UnknownHostException e) { - s_logger.error("Unexpected exception (will retry until timeout)", e); - } catch (IOException e) { - s_logger.error("Unexpected exception (will retry until timeout) ", e); - } catch (Throwable e) { - s_logger.error("Unexpected exception (will retry until timeout) ", e); - } - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - } +import org.apache.log4j.Logger; - if(tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null && !tunnelSession.isEmpty()) { - ConsoleProxyAuthenticationResult authResult = ConsoleProxy.reAuthenticationExternally(getClientParam()); - if(authResult != null && authResult.isSuccess()) { - if(authResult.getTunnelUrl() != null && !authResult.getTunnelUrl().isEmpty() && - authResult.getTunnelSession() != null && !authResult.getTunnelSession().isEmpty()) { - tunnelUrl = authResult.getTunnelUrl(); - tunnelSession = authResult.getTunnelSession(); - - s_logger.info("Reset XAPI session. url: " + tunnelUrl + ", session: " + tunnelSession); - } - } - } - } +import com.cloud.consoleproxy.vnc.FrameBufferCanvas; +import com.cloud.consoleproxy.vnc.RfbConstants; +import com.cloud.consoleproxy.vnc.VncClient; - s_logger.info("Receiver thread stopped."); - workerDone = true; - client.getClientListener().onClientClose(); - } - }); - - worker.setDaemon(true); - worker.start(); - } - - @Override - public void closeClient() { - if(client != null) - client.shutdown(); - } - - @Override - public void onClientConnected() { - } - - public void onClientClose() { - s_logger.info("Received client close indication. remove viewer from map."); - - ConsoleProxy.removeViewer(this); - } - - @Override - public void onFramebufferUpdate(int x, int y, int w, int h) { - super.onFramebufferUpdate(x, y, w, h); - client.requestUpdate(false); - } - - public void sendClientRawKeyboardEvent(InputEventType event, int code, int modifiers) { - if(client == null) - return; - - updateFrontEndActivityTime(); - - switch(event) { - case KEY_DOWN : - sendModifierEvents(modifiers); - client.sendClientKeyboardEvent(RfbConstants.KEY_DOWN, code, 0); - break; - - case KEY_UP : - client.sendClientKeyboardEvent(RfbConstants.KEY_UP, code, 0); - sendModifierEvents(0); - break; - - case KEY_PRESS : - break; - - default : - assert(false); - break; - } - } - - public void sendClientMouseEvent(InputEventType event, int x, int y, int code, int modifiers) { - if(client == null) - return; - - updateFrontEndActivityTime(); - - if (event == InputEventType.MOUSE_DOWN) { - if (code == 2) { - lastPointerMask |= 4; - } else if (code == 0) { - lastPointerMask |= 1; - } - } - - if (event == InputEventType.MOUSE_UP) { - if (code == 2) { - lastPointerMask ^= 4; - } else if (code == 0) { - lastPointerMask ^= 1; - } - } - - sendModifierEvents(modifiers); - client.sendClientMouseEvent(lastPointerMask, x, y, code, modifiers); - if(lastPointerMask == 0) - sendModifierEvents(0); - } - - @Override - protected FrameBufferCanvas getFrameBufferCavas() { - if(client != null) - return client.getFrameBufferCanvas(); - return null; - } - - private void sendModifierEvents(int modifiers) { - if((modifiers & SHIFT_KEY_MASK) != (lastModifierStates & SHIFT_KEY_MASK)) - client.sendClientKeyboardEvent((modifiers & SHIFT_KEY_MASK) != 0 ? RfbConstants.KEY_DOWN : RfbConstants.KEY_UP, X11_KEY_SHIFT, 0); - - if((modifiers & CTRL_KEY_MASK) != (lastModifierStates & CTRL_KEY_MASK)) - client.sendClientKeyboardEvent((modifiers & CTRL_KEY_MASK) != 0 ? RfbConstants.KEY_DOWN : RfbConstants.KEY_UP, X11_KEY_CTRL, 0); +/** + * + * ConsoleProxyVncClient bridges a VNC engine with the front-end AJAX viewer + * + */ +public class ConsoleProxyVncClient extends ConsoleProxyClientBase { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyVncClient.class); - if((modifiers & META_KEY_MASK) != (lastModifierStates & META_KEY_MASK)) - client.sendClientKeyboardEvent((modifiers & META_KEY_MASK) != 0 ? RfbConstants.KEY_DOWN : RfbConstants.KEY_UP, X11_KEY_META, 0); - - if((modifiers & ALT_KEY_MASK) != (lastModifierStates & ALT_KEY_MASK)) - client.sendClientKeyboardEvent((modifiers & ALT_KEY_MASK) != 0 ? RfbConstants.KEY_DOWN : RfbConstants.KEY_UP, X11_KEY_ALT, 0); - - lastModifierStates = modifiers; - } -} + private static final int SHIFT_KEY_MASK = 64; + private static final int CTRL_KEY_MASK = 128; + private static final int META_KEY_MASK = 256; + private static final int ALT_KEY_MASK = 512; + + private static final int X11_KEY_SHIFT = 0xffe1; + private static final int X11_KEY_CTRL = 0xffe3; + private static final int X11_KEY_ALT = 0xffe9; + private static final int X11_KEY_META = 0xffe7; + + private VncClient client; + private Thread worker; + private boolean workerDone = false; + + private int lastModifierStates = 0; + private int lastPointerMask = 0; + + public ConsoleProxyVncClient() { + } + + public boolean isHostConnected() { + if(client != null) + return client.isHostConnected(); + + return false; + } + + @Override + public boolean isFrontEndAlive() { + if(workerDone || System.currentTimeMillis() - getClientLastFrontEndActivityTime() > ConsoleProxy.VIEWER_LINGER_SECONDS*1000) { + s_logger.info("Front end has been idle for too long"); + return false; + } + return true; + } + + @Override + public void initClient(ConsoleProxyClientParam param) { + setClientParam(param); + + client = new VncClient(this); + worker = new Thread(new Runnable() { + public void run() { + String tunnelUrl = getClientParam().getClientTunnelUrl(); + String tunnelSession = getClientParam().getClientTunnelSession(); + + for(int i = 0; i < 15; i++) { + try { + if(tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null && !tunnelSession.isEmpty()) { + URI uri = new URI(tunnelUrl); + s_logger.info("Connect to VNC server via tunnel. url: " + tunnelUrl + ", session: " + tunnelSession); + + ConsoleProxy.ensureRoute(uri.getHost()); + client.connectTo( + uri.getHost(), uri.getPort(), + uri.getPath() + "?" + uri.getQuery(), + tunnelSession, "https".equalsIgnoreCase(uri.getScheme()), + getClientHostPassword()); + } else { + s_logger.info("Connect to VNC server directly. host: " + getClientHostAddress() + ", port: " + getClientHostPort()); + ConsoleProxy.ensureRoute(getClientHostAddress()); + client.connectTo(getClientHostAddress(), getClientHostPort(), getClientHostPassword()); + } + } catch (UnknownHostException e) { + s_logger.error("Unexpected exception (will retry until timeout)", e); + } catch (IOException e) { + s_logger.error("Unexpected exception (will retry until timeout) ", e); + } catch (Throwable e) { + s_logger.error("Unexpected exception (will retry until timeout) ", e); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + + if(tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null && !tunnelSession.isEmpty()) { + ConsoleProxyAuthenticationResult authResult = ConsoleProxy.reAuthenticationExternally(getClientParam()); + if(authResult != null && authResult.isSuccess()) { + if(authResult.getTunnelUrl() != null && !authResult.getTunnelUrl().isEmpty() && + authResult.getTunnelSession() != null && !authResult.getTunnelSession().isEmpty()) { + tunnelUrl = authResult.getTunnelUrl(); + tunnelSession = authResult.getTunnelSession(); + + s_logger.info("Reset XAPI session. url: " + tunnelUrl + ", session: " + tunnelSession); + } + } + } + } + + s_logger.info("Receiver thread stopped."); + workerDone = true; + client.getClientListener().onClientClose(); + } + }); + + worker.setDaemon(true); + worker.start(); + } + + @Override + public void closeClient() { + if(client != null) + client.shutdown(); + } + + @Override + public void onClientConnected() { + } + + public void onClientClose() { + s_logger.info("Received client close indication. remove viewer from map."); + + ConsoleProxy.removeViewer(this); + } + + @Override + public void onFramebufferUpdate(int x, int y, int w, int h) { + super.onFramebufferUpdate(x, y, w, h); + client.requestUpdate(false); + } + + public void sendClientRawKeyboardEvent(InputEventType event, int code, int modifiers) { + if(client == null) + return; + + updateFrontEndActivityTime(); + + switch(event) { + case KEY_DOWN : + sendModifierEvents(modifiers); + client.sendClientKeyboardEvent(RfbConstants.KEY_DOWN, code, 0); + break; + + case KEY_UP : + client.sendClientKeyboardEvent(RfbConstants.KEY_UP, code, 0); + sendModifierEvents(0); + break; + + case KEY_PRESS : + break; + + default : + assert(false); + break; + } + } + + public void sendClientMouseEvent(InputEventType event, int x, int y, int code, int modifiers) { + if(client == null) + return; + + updateFrontEndActivityTime(); + + if (event == InputEventType.MOUSE_DOWN) { + if (code == 2) { + lastPointerMask |= 4; + } else if (code == 0) { + lastPointerMask |= 1; + } + } + + if (event == InputEventType.MOUSE_UP) { + if (code == 2) { + lastPointerMask ^= 4; + } else if (code == 0) { + lastPointerMask ^= 1; + } + } + + sendModifierEvents(modifiers); + client.sendClientMouseEvent(lastPointerMask, x, y, code, modifiers); + if(lastPointerMask == 0) + sendModifierEvents(0); + } + + @Override + protected FrameBufferCanvas getFrameBufferCavas() { + if(client != null) + return client.getFrameBufferCanvas(); + return null; + } + + private void sendModifierEvents(int modifiers) { + if((modifiers & SHIFT_KEY_MASK) != (lastModifierStates & SHIFT_KEY_MASK)) + client.sendClientKeyboardEvent((modifiers & SHIFT_KEY_MASK) != 0 ? RfbConstants.KEY_DOWN : RfbConstants.KEY_UP, X11_KEY_SHIFT, 0); + + if((modifiers & CTRL_KEY_MASK) != (lastModifierStates & CTRL_KEY_MASK)) + client.sendClientKeyboardEvent((modifiers & CTRL_KEY_MASK) != 0 ? RfbConstants.KEY_DOWN : RfbConstants.KEY_UP, X11_KEY_CTRL, 0); + + if((modifiers & META_KEY_MASK) != (lastModifierStates & META_KEY_MASK)) + client.sendClientKeyboardEvent((modifiers & META_KEY_MASK) != 0 ? RfbConstants.KEY_DOWN : RfbConstants.KEY_UP, X11_KEY_META, 0); + + if((modifiers & ALT_KEY_MASK) != (lastModifierStates & ALT_KEY_MASK)) + client.sendClientKeyboardEvent((modifiers & ALT_KEY_MASK) != 0 ? RfbConstants.KEY_DOWN : RfbConstants.KEY_UP, X11_KEY_ALT, 0); + + lastModifierStates = modifiers; + } +}