From c5083787c2c3b9cfd92c3d6ec29d0a4964d26985 Mon Sep 17 00:00:00 2001 From: Kelven Yang Date: Thu, 19 Apr 2012 12:10:33 -0700 Subject: [PATCH] Hardening console proxy AJAX protocol to address security concerns --- .../consoleproxy/ConsoleProxyResource.java | 34 ++++- ...rtConsoleProxyAgentHttpHandlerCommand.java | 13 +- .../com/cloud/consoleproxy/ConsoleProxy.java | 42 +++++- .../consoleproxy/ConsoleProxyAjaxHandler.java | 30 +--- .../ConsoleProxyAjaxImageHandler.java | 14 +- .../consoleproxy/ConsoleProxyClientBase.java | 25 ++-- .../consoleproxy/ConsoleProxyClientParam.java | 10 ++ .../ConsoleProxyHttpHandlerHelper.java | 70 +++++++++ .../ConsoleProxyPasswordBasedEncryptor.java | 139 ++++++++++++++++++ .../consoleproxy/ConsoleProxyManagerImpl.java | 16 +- .../servlet/ConsoleProxyClientParam.java | 102 +++++++++++++ .../ConsoleProxyPasswordBasedEncryptor.java | 121 +++++++++++++++ .../cloud/servlet/ConsoleProxyServlet.java | 68 +++------ 13 files changed, 573 insertions(+), 111 deletions(-) create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyPasswordBasedEncryptor.java create mode 100644 server/src/com/cloud/servlet/ConsoleProxyClientParam.java create mode 100644 server/src/com/cloud/servlet/ConsoleProxyPasswordBasedEncryptor.java diff --git a/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java index 21863c1ab2c..a0c9b2257c3 100644 --- a/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java +++ b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java @@ -106,7 +106,7 @@ public class ConsoleProxyResource extends ServerResourceBase implements } private Answer execute(StartConsoleProxyAgentHttpHandlerCommand cmd) { - launchConsoleProxy(cmd.getKeystoreBits(), cmd.getKeystorePassword()); + launchConsoleProxy(cmd.getKeystoreBits(), cmd.getKeystorePassword(), cmd.getEncryptorPassword()); return new Answer(cmd); } @@ -347,7 +347,7 @@ public class ConsoleProxyResource extends ServerResourceBase implements return _name; } - private void launchConsoleProxy(final byte[] ksBits, final String ksPassword) { + private void launchConsoleProxy(final byte[] ksBits, final String ksPassword, final String encryptorPassword) { final Object resource = this; if (_consoleProxyMain == null) { _consoleProxyMain = new Thread(new Runnable() { @@ -355,6 +355,10 @@ public class ConsoleProxyResource extends ServerResourceBase implements try { Class consoleProxyClazz = Class.forName("com.cloud.consoleproxy.ConsoleProxy"); try { + Method methodSetup = consoleProxyClazz.getMethod( + "setEncryptorPassword", String.class); + methodSetup.invoke(null, encryptorPassword); + Method method = consoleProxyClazz.getMethod( "startWithContext", Properties.class, Object.class, byte[].class, String.class); @@ -385,7 +389,31 @@ public class ConsoleProxyResource extends ServerResourceBase implements _consoleProxyMain.setDaemon(true); _consoleProxyMain.start(); } else { - s_logger.error("com.cloud.consoleproxy.ConsoleProxy is already running"); + s_logger.info("com.cloud.consoleproxy.ConsoleProxy is already running"); + + try { + Class consoleProxyClazz = Class.forName("com.cloud.consoleproxy.ConsoleProxy"); + Method methodSetup = consoleProxyClazz.getMethod("setEncryptorPassword", String.class); + methodSetup.invoke(null, encryptorPassword); + } catch (SecurityException e) { + s_logger.error("Unable to launch console proxy due to SecurityException"); + System.exit(ExitStatus.Error.value()); + } catch (NoSuchMethodException e) { + s_logger.error("Unable to launch console proxy due to NoSuchMethodException"); + System.exit(ExitStatus.Error.value()); + } catch (IllegalArgumentException e) { + s_logger.error("Unable to launch console proxy due to IllegalArgumentException"); + System.exit(ExitStatus.Error.value()); + } catch (IllegalAccessException e) { + s_logger.error("Unable to launch console proxy due to IllegalAccessException"); + System.exit(ExitStatus.Error.value()); + } catch (InvocationTargetException e) { + s_logger.error("Unable to launch console proxy due to InvocationTargetException"); + System.exit(ExitStatus.Error.value()); + } catch (final ClassNotFoundException e) { + s_logger.error("Unable to launch console proxy due to ClassNotFoundException"); + System.exit(ExitStatus.Error.value()); + } } } diff --git a/api/src/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java b/api/src/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java index 132fc520457..c3b0479bdc7 100644 --- a/api/src/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java +++ b/api/src/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java @@ -16,13 +16,14 @@ import com.cloud.agent.api.Command; import com.cloud.agent.api.LogLevel.Log4jLevel; import com.cloud.agent.api.LogLevel; - public class StartConsoleProxyAgentHttpHandlerCommand extends Command { @LogLevel(Log4jLevel.Off) private byte[] keystoreBits; @LogLevel(Log4jLevel.Off) private String keystorePassword; - + @LogLevel(Log4jLevel.Off) + private String encryptorPassword; + public StartConsoleProxyAgentHttpHandlerCommand() { super(); } @@ -52,4 +53,12 @@ public class StartConsoleProxyAgentHttpHandlerCommand extends Command { public void setKeystorePassword(String keystorePassword) { this.keystorePassword = keystorePassword; } + + public String getEncryptorPassword() { + return encryptorPassword; + } + + public void setEncryptorPassword(String encryptorPassword) { + this.encryptorPassword = encryptorPassword; + } } diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java index 11a3d2aa9ad..96c57e40ba7 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java @@ -19,11 +19,14 @@ 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; @@ -47,8 +50,8 @@ public class ConsoleProxy { // 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 String ksPassword; + public static Method authMethod; public static Method reportMethod; public static Method ensureRouteMethod; @@ -60,7 +63,24 @@ public class ConsoleProxy { static int readTimeoutSeconds = 90; static int keyboardType = KEYBOARD_RAW; static String factoryClzName; - static boolean standaloneStart = false; + 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"); @@ -89,8 +109,8 @@ public class ConsoleProxy { } 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()) { @@ -211,7 +231,7 @@ public class ConsoleProxy { } 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"); @@ -440,11 +460,19 @@ public class ConsoleProxy { throw new AuthenticationException("External authenticator failed request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword()); } + } + + 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/ConsoleProxyAjaxHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java index 8f44b7f777e..a4542db6a6e 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java @@ -62,8 +62,8 @@ public class ConsoleProxyAjaxHandler implements HttpHandler { if(s_logger.isTraceEnabled()) s_logger.trace("Handle AJAX request: " + queries); - Map queryMap = getQueryMap(queries); - + Map queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(queries); + String host = queryMap.get("host"); String portStr = queryMap.get("port"); String sid = queryMap.get("sid"); @@ -75,8 +75,8 @@ public class ConsoleProxyAjaxHandler implements HttpHandler { String console_host_session = queryMap.get("sessionref"); if(tag == null) - tag = ""; - + tag = ""; + long ajaxSessionId = 0; int event = 0; @@ -179,28 +179,6 @@ public class ConsoleProxyAjaxHandler implements HttpHandler { } } - public static Map getQueryMap(String query) { - String[] params = query.split("&"); - Map map = new HashMap(); - for (String param : params) { - String[] paramTokens = param.split("="); - if(paramTokens != null && paramTokens.length == 2) { - String name = param.split("=")[0]; - String value = param.split("=")[1]; - map.put(name, value); - } else if (paramTokens.length == 3) { - // very ugly, added for Xen tunneling url - String name = paramTokens[0]; - String value = paramTokens[1] + "=" + paramTokens[2]; - map.put(name, value); - } else { - if(s_logger.isDebugEnabled()) - s_logger.debug("Invalid paramemter in URL found. param: " + param); - } - } - return map; - } - private static String convertStreamToString(InputStream is, boolean closeStreamAfterRead) { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder sb = new StringBuilder(); diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java index 5aceb68c77f..bc01ed72474 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java @@ -14,7 +14,6 @@ package com.cloud.consoleproxy; import java.io.IOException; import java.io.OutputStream; -import java.util.HashMap; import java.util.Map; import com.cloud.consoleproxy.util.Logger; @@ -54,7 +53,7 @@ public class ConsoleProxyAjaxImageHandler implements HttpHandler { private void doHandle(HttpExchange t) throws Exception, IllegalArgumentException { String queries = t.getRequestURI().getQuery(); - Map queryMap = getQueryMap(queries); + Map queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(queries); String host = queryMap.get("host"); String portStr = queryMap.get("port"); @@ -110,15 +109,4 @@ public class ConsoleProxyAjaxImageHandler implements HttpHandler { t.sendResponseHeaders(404, -1); } } - - public static Map getQueryMap(String query) { - String[] params = query.split("&"); - Map map = new HashMap(); - for (String param : params) { - String name = param.split("=")[0]; - String value = param.split("=")[1]; - map.put(name, value); - } - return map; - } } diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java index 68d2750ebb0..b3925556bb1 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java @@ -44,14 +44,8 @@ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, Cons protected TileTracker tracker; protected AjaxFIFOImageCache ajaxImageCache = new AjaxFIFOImageCache(2); -/* - protected String host; - protected int port; - protected String passwordParam; - protected String tag = ""; - protected String ticket = ""; -*/ protected ConsoleProxyClientParam clientParam; + protected String clientToken; protected long createTime = System.currentTimeMillis(); protected long lastFrontEndActivityTime = System.currentTimeMillis(); @@ -192,11 +186,12 @@ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, Cons s_logger.trace("Generated jpeg image size: " + imgBits.length); } - int key = ajaxImageCache.putImage(imgBits); - StringBuffer sb = new StringBuffer("/ajaximg?host="); - sb.append(getClientHostAddress()).append("&port=").append(getClientHostPort()).append("&sid=").append(getClientHostPassword()); - sb.append("&key=").append(key).append("&tag=").append(this.getClientTag()); - sb.append("&ts=").append(System.currentTimeMillis()); + 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(); } @@ -208,9 +203,7 @@ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, Cons } StringBuffer sb = new StringBuffer(); - - sb.append("/ajax?host=").append(getClientHostAddress()).append("&port=").append(getClientHostPort()); - sb.append("&sid=").append(getClientHostPassword()).append("&tag=").append(getClientTag()).append("&sess=").append(ajaxSessionId); + sb.append("/ajax?token=").append(clientToken).append("&sess=").append(ajaxSessionId); return sb.toString(); } @@ -455,5 +448,7 @@ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, Cons 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 01a053df969..0b29d23cddf 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java @@ -28,6 +28,8 @@ public class ConsoleProxyClientParam { private String clientTunnelUrl; private String clientTunnelSession; + private String ajaxSessionId; + public ConsoleProxyClientParam() { clientHostPort = 0; } @@ -87,6 +89,14 @@ public class ConsoleProxyClientParam { 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()) diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java new file mode 100644 index 00000000000..105537ea2a6 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java @@ -0,0 +1,70 @@ +// 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.util.HashMap; +import java.util.Map; + +import com.cloud.consoleproxy.util.Logger; + +public class ConsoleProxyHttpHandlerHelper { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyHttpHandlerHelper.class); + + public static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap(); + for (String param : params) { + String[] paramTokens = param.split("="); + if(paramTokens != null && paramTokens.length == 2) { + String name = param.split("=")[0]; + String value = param.split("=")[1]; + map.put(name, value); + } else if (paramTokens.length == 3) { + // very ugly, added for Xen tunneling url + String name = paramTokens[0]; + String value = paramTokens[1] + "=" + paramTokens[2]; + map.put(name, value); + } else { + if(s_logger.isDebugEnabled()) + s_logger.debug("Invalid paramemter in URL found. param: " + param); + } + } + + // This is a ugly solution for now. We will do encryption/decryption translation + // here to make it transparent to rest of the code. + if(map.get("token") != null) { + ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor( + ConsoleProxy.getEncryptorPassword()); + + ConsoleProxyClientParam param = encryptor.decryptObject(ConsoleProxyClientParam.class, map.get("token")); + if(param != null) { + if(param.getClientHostAddress() != null) + map.put("host", param.getClientHostAddress()); + if(param.getClientHostPort() != 0) + map.put("port", String.valueOf(param.getClientHostPort())); + if(param.getClientTag() != null) + map.put("tag", param.getClientTag()); + if(param.getClientHostPassword() != null) + map.put("sid", param.getClientHostPassword()); + if(param.getClientTunnelUrl() != null) + map.put("consoleurl", param.getClientTunnelUrl()); + if(param.getClientTunnelSession() != null) + map.put("sessionref", param.getClientTunnelSession()); + if(param.getTicket() != null) + map.put("ticket", param.getTicket()); + } + } + + return map; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyPasswordBasedEncryptor.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyPasswordBasedEncryptor.java new file mode 100644 index 00000000000..8e1f7b46fd8 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyPasswordBasedEncryptor.java @@ -0,0 +1,139 @@ +// 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.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 = Cipher.getMaxAllowedKeyLength("DES") / 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 = Cipher.getMaxAllowedKeyLength("DES") / 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/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index c5438229457..612d2b69797 100755 --- a/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.UUID; import javax.ejb.Local; import javax.naming.ConfigurationException; @@ -89,6 +90,7 @@ import com.cloud.resource.ResourceManager; import com.cloud.resource.ResourceStateAdapter; import com.cloud.resource.ServerResource; import com.cloud.resource.UnableDeleteHostException; +import com.cloud.server.ManagementServer; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.servlet.ConsoleProxyServlet; @@ -229,7 +231,6 @@ public class ConsoleProxyManagerImpl implements ConsoleProxyManager, ConsoleProx private int _capacityPerProxy = ConsoleProxyManager.DEFAULT_PROXY_CAPACITY; private int _standbyCapacity = ConsoleProxyManager.DEFAULT_STANDBY_CAPACITY; - private boolean _use_lvm; private boolean _use_storage_vm; private boolean _disable_rp_filter = false; @@ -243,6 +244,8 @@ public class ConsoleProxyManagerImpl implements ConsoleProxyManager, ConsoleProx private Map _zoneHostInfoMap; // map private Map _zoneProxyCountMap; // map private Map _zoneVmCountMap; // map + + private String _hashKey; private final GlobalLock _allocProxyLock = GlobalLock.getInternLock(getAllocProxyLockName()); @@ -1664,8 +1667,10 @@ public class ConsoleProxyManagerImpl implements ConsoleProxyManager, ConsoleProx s_logger.error("Could not find and construct a valid SSL certificate"); } cmd = new StartConsoleProxyAgentHttpHandlerCommand(ksBits, storePassword); + cmd.setEncryptorPassword(getHashKey()); } else { cmd = new StartConsoleProxyAgentHttpHandlerCommand(); + cmd.setEncryptorPassword(getHashKey()); } try { @@ -1906,4 +1911,13 @@ public class ConsoleProxyManagerImpl implements ConsoleProxyManager, ConsoleProx sc.addAnd(sc.getEntity().getName(), Op.EQ, name); return sc.find(); } + + public String getHashKey() { + // although we may have race conditioning here, database transaction serialization should + // give us the same key + if (_hashKey == null) { + _hashKey = _configDao.getValueAndInitIfNotExist(Config.HashKey.key(), Config.HashKey.getCategory(), UUID.randomUUID().toString()); + } + return _hashKey; + } } diff --git a/server/src/com/cloud/servlet/ConsoleProxyClientParam.java b/server/src/com/cloud/servlet/ConsoleProxyClientParam.java new file mode 100644 index 00000000000..a53ab398148 --- /dev/null +++ b/server/src/com/cloud/servlet/ConsoleProxyClientParam.java @@ -0,0 +1,102 @@ +// 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.servlet; + +// To maintain independency of console proxy project, we duplicate this class from console proxy project +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/server/src/com/cloud/servlet/ConsoleProxyPasswordBasedEncryptor.java b/server/src/com/cloud/servlet/ConsoleProxyPasswordBasedEncryptor.java new file mode 100644 index 00000000000..2d1b6f529c6 --- /dev/null +++ b/server/src/com/cloud/servlet/ConsoleProxyPasswordBasedEncryptor.java @@ -0,0 +1,121 @@ +package com.cloud.servlet; + +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; + +// To maintain independency of console proxy project, we duplicate this class from console proxy project +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 = Cipher.getMaxAllowedKeyLength("DES") / 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 = Cipher.getMaxAllowedKeyLength("DES") / 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/server/src/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/com/cloud/servlet/ConsoleProxyServlet.java index 0362a2cf70b..d400d51ec60 100644 --- a/server/src/com/cloud/servlet/ConsoleProxyServlet.java +++ b/server/src/com/cloud/servlet/ConsoleProxyServlet.java @@ -64,7 +64,6 @@ public class ConsoleProxyServlet extends HttpServlet { private final static AccountManager _accountMgr = ComponentLocator.getLocator(ManagementServer.Name).getManager(AccountManager.class); private final static VirtualMachineManager _vmMgr = ComponentLocator.getLocator(ManagementServer.Name).getManager(VirtualMachineManager.class); - private final static DomainManager _domainMgr = ComponentLocator.getLocator(ManagementServer.Name).getManager(DomainManager.class); private final static ManagementServer _ms = (ManagementServer)ComponentLocator.getComponent(ManagementServer.Name); private final static IdentityService _identityService = (IdentityService)ComponentLocator.getLocator(ManagementServer.Name).getManager(IdentityService.class); @@ -317,31 +316,22 @@ public class ConsoleProxyServlet extends HttpServlet { String tag = String.valueOf(vm.getId()); tag = _identityService.getIdentityUuid("vm_instance", tag); String ticket = genAccessTicket(host, String.valueOf(portInfo.second()), sid, tag); - String consoleurl = null; - String sessionref= null; - - sb.append("/getscreen?host=").append(parsedHostInfo.first()); - sb.append("&port=").append(portInfo.second()); - sb.append("&sid=").append(sid); - sb.append("&w=").append(w).append("&h=").append(h); - sb.append("&tag=").append(tag); - sb.append("&ticket=").append(ticket); - + + ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(_ms.getHashKey()); + ConsoleProxyClientParam param = new ConsoleProxyClientParam(); + param.setClientHostAddress(parsedHostInfo.first()); + param.setClientHostPort(portInfo.second()); + param.setClientHostPassword(sid); + param.setClientTag(tag); + param.setTicket(ticket); if(parsedHostInfo.second() != null && parsedHostInfo.third() != null) { - - try { - - consoleurl = URLEncoder.encode(parsedHostInfo.second(), "UTF-8"); - sessionref = URLEncoder.encode(parsedHostInfo.third(), "UTF-8"); - sb.append("&").append("consoleurl=").append(URLDecoder.decode(consoleurl, "UTF-8")); - sb.append("&").append("sessionref=").append(URLDecoder.decode(sessionref, "UTF-8")); - - } catch (UnsupportedEncodingException e) { - s_logger.error("Unexpected exception ", e); - } - + param.setClientTunnelUrl(parsedHostInfo.second()); + param.setClientTunnelSession(parsedHostInfo.third()); } + sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param)); + sb.append("&w=").append(w).append("&h=").append(h); + if(s_logger.isDebugEnabled()) { s_logger.debug("Compose thumbnail url: " + sb.toString()); } @@ -362,30 +352,20 @@ public class ConsoleProxyServlet extends HttpServlet { String tag = String.valueOf(vm.getId()); tag = _identityService.getIdentityUuid("vm_instance", tag); String ticket = genAccessTicket(host, String.valueOf(portInfo.second()), sid, tag); - String consoleurl = null; - String sessionref= null; - - sb.append("/ajax?host=").append(parsedHostInfo.first()); - sb.append("&port=").append(portInfo.second()); - sb.append("&sid=").append(sid); - sb.append("&tag=").append(tag); - sb.append("&ticket=").append(ticket); - + ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(_ms.getHashKey()); + ConsoleProxyClientParam param = new ConsoleProxyClientParam(); + param.setClientHostAddress(parsedHostInfo.first()); + param.setClientHostPort(portInfo.second()); + param.setClientHostPassword(sid); + param.setClientTag(tag); + param.setTicket(ticket); if(parsedHostInfo.second() != null && parsedHostInfo.third() != null) { - - try { - - consoleurl = URLEncoder.encode(parsedHostInfo.second(), "UTF-8"); - sessionref = URLEncoder.encode(parsedHostInfo.third(), "UTF-8"); - sb.append("&").append("consoleurl=").append(URLDecoder.decode(consoleurl, "UTF-8")); - sb.append("&").append("sessionref=").append(URLDecoder.decode(sessionref, "UTF-8")); - - } catch (UnsupportedEncodingException e) { - s_logger.error("Unexpected exception ", e); - } - + param.setClientTunnelUrl(parsedHostInfo.second()); + param.setClientTunnelSession(parsedHostInfo.third()); } + sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param)); + // for console access, we need guest OS type to help implement keyboard long guestOs = vm.getGuestOSId(); GuestOSVO guestOsVo = _ms.getGuestOs(guestOs);