| // 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.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.commons.codec.binary.Base64; | |
| import org.apache.log4j.xml.DOMConfigurator; | |
| import com.google.gson.Gson; | |
| import com.sun.net.httpserver.HttpServer; | |
| import com.cloud.consoleproxy.util.Logger; | |
| import com.cloud.utils.PropertiesUtil; | |
| /** | |
| * | |
| * 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 = genDefaultEncryptorPassword(); | |
| private static String genDefaultEncryptorPassword() { | |
| try { | |
| SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); | |
| byte[] randomBytes = new byte[16]; | |
| random.nextBytes(randomBytes); | |
| return Base64.encodeBase64String(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) { | |
| 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(); | |
| } | |
| } | |
| } |