// Copyright 2012 Citrix Systems, Inc. Licensed under the // Apache License, Version 2.0 (the "License"); you may not use this // file except in compliance with the License. Citrix Systems, Inc. // reserves all rights not expressly granted by the License. // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Automatically generated by addcopyright.py at 04/03/2012 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<String, ConsoleProxyClient> connectionMap = new Hashtable<String, ConsoleProxyClient>(); 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<String, ConsoleProxyClient> 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(); } } }