| // 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();
|
| }
|
| } |
| }
|