| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with 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. |
| package com.cloud.consoleproxy; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| 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.util.Hashtable; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.concurrent.Executor; |
| |
| import org.apache.log4j.xml.DOMConfigurator; |
| |
| import com.cloud.consoleproxy.util.Logger; |
| import com.cloud.utils.PropertiesUtil; |
| import com.cloud.utils.ReflectUtil; |
| 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 final 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 = "Dummy"; |
| |
| private static void configLog4j() { |
| final ClassLoader loader = ReflectUtil.getClassLoaderForName("conf"); |
| URL configUrl = loader.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, String password) { |
| setEncryptorPassword(password); |
| configLog4j(); |
| Logger.setFactory(new ConsoleProxyLoggerFactory()); |
| 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)); |
| } |
| } |
| |
| // Using reflection to setup private/secure communication channel towards management server |
| ConsoleProxy.context = context; |
| ConsoleProxy.ksBits = ksBits; |
| ConsoleProxy.ksPassword = ksPassword; |
| try { |
| final ClassLoader loader = ReflectUtil.getClassLoaderForName("agent"); |
| Class<?> contextClazz = loader.loadClass("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) { |
| final File file = PropertiesUtil.findConfigFile("consoleproxy.properties"); |
| if (file == null) |
| s_logger.info("Can't load consoleproxy.properties from classpath, will use default configuration"); |
| else |
| try { |
| confs = new FileInputStream(file); |
| } catch (FileNotFoundException e) { |
| s_logger.info("Ignoring file not found exception and using defaults"); |
| } |
| } |
| if (confs != null) { |
| 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); |
| } |
| } |
| try { |
| confs.close(); |
| } catch (IOException e) { |
| s_logger.error("Failed to close consolepropxy.properties : " + e.toString(), e); |
| } |
| |
| start(conf); |
| } |
| |
| public static void start(Properties conf) { |
| System.setProperty("java.awt.headless", "true"); |
| |
| configProxy(conf); |
| |
| ConsoleProxyServerFactory factory = getHttpServerFactory(); |
| if (factory == null) { |
| 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); |
| } finally { |
| try { |
| confs.close(); |
| } catch (IOException ioex) { |
| s_logger.error(ioex.toString(), ioex); |
| } |
| } |
| } |
| 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 = getClient(param); |
| 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) { |
| authenticationExternally(param); |
| viewer = getClient(param); |
| viewer.initClient(param); |
| |
| connectionMap.put(clientKey, viewer); |
| s_logger.info("Added viewer object " + viewer); |
| reportLoadChange = true; |
| } else { |
| // protected against malicous attack by modifying URL content |
| if (ajaxSession != null) { |
| long ajaxSessionIdFromUrl = Long.parseLong(ajaxSession); |
| if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId()) |
| throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": modified AJAX session id"); |
| } |
| |
| if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() || |
| !param.getClientHostPassword().equals(viewer.getClientHostPassword())) |
| throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); |
| |
| if (!viewer.isFrontEndAlive()) { |
| |
| authenticationExternally(param); |
| viewer.initClient(param); |
| reportLoadChange = true; |
| } |
| } |
| |
| if (reportLoadChange) { |
| ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); |
| String loadInfo = statsCollector.getStatsReport(); |
| reportLoadInfo(loadInfo); |
| if (s_logger.isDebugEnabled()) |
| s_logger.debug("Report load change : " + loadInfo); |
| } |
| return viewer; |
| } |
| } |
| |
| private static ConsoleProxyClient getClient(ConsoleProxyClientParam param) { |
| if (param.getHypervHost() != null) { |
| return new ConsoleProxyRdpClient(); |
| } else { |
| return new ConsoleProxyVncClient(); |
| } |
| } |
| |
| 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() { |
| synchronized (connectionMap) { |
| 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 { |
| @Override |
| public void execute(Runnable r) { |
| new Thread(r).start(); |
| } |
| } |
| } |