// 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; import java.io.IOException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; import com.cloud.api.IdentityService; import com.cloud.exception.PermissionDeniedException; import com.cloud.host.HostVO; import com.cloud.server.ManagementServer; import com.cloud.storage.GuestOSVO; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.User; import com.cloud.uservm.UserVm; import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.component.ComponentLocator; import com.cloud.utils.db.Transaction; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineManager; /** * Thumbnail access : /console?cmd=thumbnail&vm=xxx&w=xxx&h=xxx * Console access : /conosole?cmd=access&vm=xxx * Authentication : /console?cmd=auth&vm=xxx&sid=xxx */ public class ConsoleProxyServlet extends HttpServlet { private static final long serialVersionUID = -5515382620323808168L; public static final Logger s_logger = Logger.getLogger(ConsoleProxyServlet.class.getName()); private static final int DEFAULT_THUMBNAIL_WIDTH = 144; private static final int DEFAULT_THUMBNAIL_HEIGHT = 110; 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 ManagementServer _ms = (ManagementServer)ComponentLocator.getComponent(ManagementServer.Name); private final static IdentityService _identityService = (IdentityService)ComponentLocator.getLocator(ManagementServer.Name).getManager(IdentityService.class); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { doGet(req, resp); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { try { if(_accountMgr == null || _vmMgr == null || _ms == null) { sendResponse(resp, "Service is not ready"); return; } if(_ms.getHashKey() == null) { s_logger.debug("Console/thumbnail access denied. Ticket service is not ready yet"); sendResponse(resp, "Service is not ready"); return; } String userId = null; String account = null; Account accountObj = null; Map<String, Object[]> params = new HashMap<String, Object[]>(); params.putAll(req.getParameterMap()); HttpSession session = req.getSession(false); if(session == null) { if(verifyRequest(params)) { userId = (String)params.get("userid")[0]; account = (String)params.get("account")[0]; accountObj = (Account)params.get("accountobj")[0]; } else { s_logger.debug("Invalid web session or API key in request, reject console/thumbnail access"); sendResponse(resp, "Access denied. Invalid web session or API key in request"); return; } } else { // adjust to latest API refactoring changes if(session.getAttribute("userid") != null) { userId = ((Long)session.getAttribute("userid")).toString(); } accountObj = (Account)session.getAttribute("accountobj"); if(accountObj != null) { account = "" + accountObj.getId(); } } // Do a sanity check here to make sure the user hasn't already been deleted if ((userId == null) || (account == null) || (accountObj == null) || !verifyUser(Long.valueOf(userId))) { s_logger.debug("Invalid user/account, reject console/thumbnail access"); sendResponse(resp, "Access denied. Invalid or inconsistent account is found"); return; } String cmd = req.getParameter("cmd"); if(cmd == null || !isValidCmd(cmd)) { s_logger.debug("invalid console servlet command: " + cmd); sendResponse(resp, ""); return; } String vmIdString = req.getParameter("vm"); Long vmId = _identityService.getIdentityId("vm_instance", vmIdString); if(vmId == null) { s_logger.info("invalid console servlet command parameter: " + vmIdString); sendResponse(resp, ""); return; } if(!checkSessionPermision(req, vmId, accountObj)) { sendResponse(resp, "Permission denied"); return; } if(cmd.equalsIgnoreCase("thumbnail")) { handleThumbnailRequest(req, resp, vmId); } else if(cmd.equalsIgnoreCase("access")) { handleAccessRequest(req, resp, vmId); } else { handleAuthRequest(req, resp, vmId); } } catch (Throwable e) { s_logger.error("Unexepected exception in ConsoleProxyServlet", e); sendResponse(resp, "Server Internal Error"); } } private void handleThumbnailRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) { VMInstanceVO vm = _vmMgr.findById(vmId); if(vm == null) { s_logger.warn("VM " + vmId + " does not exist, sending blank response for thumbnail request"); sendResponse(resp, ""); return; } if(vm.getHostId() == null) { s_logger.warn("VM " + vmId + " lost host info, sending blank response for thumbnail request"); sendResponse(resp, ""); return; } HostVO host = _ms.getHostBy(vm.getHostId()); if(host == null) { s_logger.warn("VM " + vmId + "'s host does not exist, sending blank response for thumbnail request"); sendResponse(resp, ""); return; } String rootUrl = _ms.getConsoleAccessUrlRoot(vmId); if(rootUrl == null) { sendResponse(resp, ""); return; } int w = DEFAULT_THUMBNAIL_WIDTH; int h = DEFAULT_THUMBNAIL_HEIGHT; String value = req.getParameter("w"); try { w = Integer.parseInt(value); } catch(NumberFormatException e) { } value = req.getParameter("h"); try { h = Integer.parseInt(value); } catch(NumberFormatException e) { } try { resp.sendRedirect(composeThumbnailUrl(rootUrl, vm, host, w, h)); } catch (IOException e) { if(s_logger.isInfoEnabled()) { s_logger.info("Client may already close the connection"); } } } private void handleAccessRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) { VMInstanceVO vm = _vmMgr.findById(vmId); if(vm == null) { s_logger.warn("VM " + vmId + " does not exist, sending blank response for console access request"); sendResponse(resp, ""); return; } if(vm.getHostId() == null) { s_logger.warn("VM " + vmId + " lost host info, sending blank response for console access request"); sendResponse(resp, ""); return; } HostVO host = _ms.getHostBy(vm.getHostId()); if(host == null) { s_logger.warn("VM " + vmId + "'s host does not exist, sending blank response for console access request"); sendResponse(resp, ""); return; } String rootUrl = _ms.getConsoleAccessUrlRoot(vmId); if(rootUrl == null) { sendResponse(resp, "<html><body><p>Console access will be ready in a few minutes. Please try it again later.</p></body></html>"); return; } String vmName = vm.getHostName(); if(vm.getType() == VirtualMachine.Type.User) { UserVm userVm = (UserVm)_vmMgr.findByIdAndType(VirtualMachine.Type.User, vmId); String displayName = userVm.getDisplayName(); if(displayName != null && !displayName.isEmpty() && !displayName.equals(vmName)) { vmName += "(" + displayName + ")"; } } StringBuffer sb = new StringBuffer(); sb.append("<html><title>").append(escapeHTML(vmName)).append("</title><frameset><frame src=\"").append(composeConsoleAccessUrl(rootUrl, vm, host)); sb.append("\"></frame></frameset></html>"); s_logger.debug("the console url is :: " + sb.toString()); sendResponse(resp, sb.toString()); } private void handleAuthRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) { // TODO authentication channel between console proxy VM and management server needs to be secured, // the data is now being sent through private network, but this is apparently not enough VMInstanceVO vm = _vmMgr.findById(vmId); if(vm == null) { s_logger.warn("VM " + vmId + " does not exist, sending failed response for authentication request from console proxy"); sendResponse(resp, "failed"); return; } if(vm.getHostId() == null) { s_logger.warn("VM " + vmId + " lost host info, failed response for authentication request from console proxy"); sendResponse(resp, "failed"); return; } HostVO host = _ms.getHostBy(vm.getHostId()); if(host == null) { s_logger.warn("VM " + vmId + "'s host does not exist, sending failed response for authentication request from console proxy"); sendResponse(resp, "failed"); return; } String sid = req.getParameter("sid"); if(sid == null || !sid.equals(vm.getVncPassword())) { s_logger.warn("sid " + sid + " in url does not match stored sid " + vm.getVncPassword()); sendResponse(resp, "failed"); return; } sendResponse(resp, "success"); } // put the ugly stuff here static public Ternary<String, String, String> parseHostInfo(String hostInfo) { String host = null; String tunnelUrl = null; String tunnelSession = null; s_logger.info("Parse host info returned from executing GetVNCPortCommand. host info: " + hostInfo); if(hostInfo != null && hostInfo.startsWith("consoleurl")) { String tokens[] = hostInfo.split("&"); if(hostInfo.length() > 19 && hostInfo.indexOf('/', 19) > 19) { host = hostInfo.substring(19, hostInfo.indexOf('/', 19)).trim(); tunnelUrl = tokens[0].substring("consoleurl=".length()); tunnelSession = tokens[1].split("=")[1]; } else { host = ""; } } else { host = hostInfo; } return new Ternary<String, String, String>(host, tunnelUrl, tunnelSession); } private String composeThumbnailUrl(String rootUrl, VMInstanceVO vm, HostVO hostVo, int w, int h) { StringBuffer sb = new StringBuffer(rootUrl); String host = hostVo.getPrivateIpAddress(); Pair<String, Integer> portInfo = _ms.getVncPort(vm); Ternary<String, String, String> parsedHostInfo = parseHostInfo(portInfo.first()); String sid = vm.getVncPassword(); String tag = String.valueOf(vm.getId()); tag = _identityService.getIdentityUuid("vm_instance", tag); String ticket = genAccessTicket(host, String.valueOf(portInfo.second()), sid, tag); 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) { 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()); } return sb.toString(); } private String composeConsoleAccessUrl(String rootUrl, VMInstanceVO vm, HostVO hostVo) { StringBuffer sb = new StringBuffer(rootUrl); String host = hostVo.getPrivateIpAddress(); Pair<String, Integer> portInfo = _ms.getVncPort(vm); if(s_logger.isDebugEnabled()) s_logger.debug("Port info " + portInfo.first()); Ternary<String, String, String> parsedHostInfo = parseHostInfo(portInfo.first()); String sid = vm.getVncPassword(); String tag = String.valueOf(vm.getId()); tag = _identityService.getIdentityUuid("vm_instance", tag); String ticket = genAccessTicket(host, String.valueOf(portInfo.second()), sid, tag); 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) { 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); if(guestOsVo.getCategoryId() == 6) sb.append("&guest=windows"); if(s_logger.isDebugEnabled()) { s_logger.debug("Compose console url: " + sb.toString()); } return sb.toString(); } public static String genAccessTicket(String host, String port, String sid, String tag) { return genAccessTicket(host, port, sid, tag, new Date()); } public static String genAccessTicket(String host, String port, String sid, String tag, Date normalizedHashTime) { String params = "host=" + host + "&port=" + port + "&sid=" + sid + "&tag=" + tag; try { Mac mac = Mac.getInstance("HmacSHA1"); long ts = normalizedHashTime.getTime(); ts = ts/60000; // round up to 1 minute String secretKey = _ms.getHashKey(); SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1"); mac.init(keySpec); mac.update(params.getBytes()); mac.update(String.valueOf(ts).getBytes()); byte[] encryptedBytes = mac.doFinal(); return Base64.encodeBase64URLSafeString(encryptedBytes); } catch(Exception e) { s_logger.error("Unexpected exception ", e); } return ""; } private void sendResponse(HttpServletResponse resp, String content) { try { resp.setContentType("text/html"); resp.getWriter().print(content); } catch(IOException e) { if(s_logger.isInfoEnabled()) { s_logger.info("Client may already close the connection"); } } } private boolean checkSessionPermision(HttpServletRequest req, long vmId, Account accountObj) { VMInstanceVO vm = _vmMgr.findById(vmId); if(vm == null) { s_logger.debug("Console/thumbnail access denied. VM " + vmId + " does not exist in system any more"); return false; } // root admin can access anything if(accountObj.getType() == Account.ACCOUNT_TYPE_ADMIN) return true; switch(vm.getType()) { case User : try { _accountMgr.checkAccess(accountObj, null, true, vm); } catch (PermissionDeniedException ex) { if (accountObj.getType() == Account.ACCOUNT_TYPE_NORMAL) { if (s_logger.isDebugEnabled()) { s_logger.debug("VM access is denied. VM owner account " + vm.getAccountId() + " does not match the account id in session " + accountObj.getId() + " and caller is a normal user"); } } else if(accountObj.getType() == Account.ACCOUNT_TYPE_DOMAIN_ADMIN || accountObj.getType() == Account.ACCOUNT_TYPE_READ_ONLY_ADMIN) { if(s_logger.isDebugEnabled()) { s_logger.debug("VM access is denied. VM owner account " + vm.getAccountId() + " does not match the account id in session " + accountObj.getId() + " and the domain-admin caller does not manage the target domain"); } } return false; } break; case DomainRouter: case ConsoleProxy : case SecondaryStorageVm: return false; default : s_logger.warn("Unrecoginized virtual machine type, deny access by default. type: " + vm.getType()); return false; } return true; } private boolean isValidCmd(String cmd) { if(cmd.equalsIgnoreCase("thumbnail") || cmd.equalsIgnoreCase("access") || cmd.equalsIgnoreCase("auth")) { return true; } return false; } public boolean verifyUser(Long userId) { // copy from ApiServer.java, a bit ugly here User user = _accountMgr.getUserIncludingRemoved(userId); Account account = null; if (user != null) { account = _accountMgr.getAccount(user.getAccountId()); } if ((user == null) || (user.getRemoved() != null) || !user.getState().equals(Account.State.enabled) || (account == null) || !account.getState().equals(Account.State.enabled)) { s_logger.warn("Deleted/Disabled/Locked user with id=" + userId + " attempting to access public API"); return false; } return true; } // copied and modified from ApiServer.java. // TODO need to replace the whole servlet with a API command private boolean verifyRequest(Map<String, Object[]> requestParameters) { try { String apiKey = null; String secretKey = null; String signature = null; String unsignedRequest = null; // - build a request string with sorted params, make sure it's all lowercase // - sign the request, verify the signature is the same List<String> parameterNames = new ArrayList<String>(); for (Object paramNameObj : requestParameters.keySet()) { parameterNames.add((String)paramNameObj); // put the name in a list that we'll sort later } Collections.sort(parameterNames); for (String paramName : parameterNames) { // parameters come as name/value pairs in the form String/String[] String paramValue = ((String[])requestParameters.get(paramName))[0]; if ("signature".equalsIgnoreCase(paramName)) { signature = paramValue; } else { if ("apikey".equalsIgnoreCase(paramName)) { apiKey = paramValue; } if (unsignedRequest == null) { unsignedRequest = paramName + "=" + URLEncoder.encode(paramValue, "UTF-8").replaceAll("\\+", "%20"); } else { unsignedRequest = unsignedRequest + "&" + paramName + "=" + URLEncoder.encode(paramValue, "UTF-8").replaceAll("\\+", "%20"); } } } // if api/secret key are passed to the parameters if ((signature == null) || (apiKey == null)) { if (s_logger.isDebugEnabled()) { s_logger.debug("expired session, missing signature, or missing apiKey -- ignoring request...sig: " + signature + ", apiKey: " + apiKey); } return false; // no signature, bad request } Transaction txn = Transaction.open(Transaction.CLOUD_DB); txn.close(); User user = null; // verify there is a user with this api key Pair<User, Account> userAcctPair = _accountMgr.findUserByApiKey(apiKey); if (userAcctPair == null) { s_logger.debug("apiKey does not map to a valid user -- ignoring request, apiKey: " + apiKey); return false; } user = userAcctPair.first(); Account account = userAcctPair.second(); if (!user.getState().equals(Account.State.enabled) || !account.getState().equals(Account.State.enabled)) { s_logger.debug("disabled or locked user accessing the api, userid = " + user.getId() + "; name = " + user.getUsername() + "; state: " + user.getState() + "; accountState: " + account.getState()); return false; } // verify secret key exists secretKey = user.getSecretKey(); if (secretKey == null) { s_logger.debug("User does not have a secret key associated with the account -- ignoring request, username: " + user.getUsername()); return false; } unsignedRequest = unsignedRequest.toLowerCase(); Mac mac = Mac.getInstance("HmacSHA1"); SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1"); mac.init(keySpec); mac.update(unsignedRequest.getBytes()); byte[] encryptedBytes = mac.doFinal(); String computedSignature = com.cloud.utils.encoding.Base64.encodeBytes(encryptedBytes); boolean equalSig = signature.equals(computedSignature); if (!equalSig) { s_logger.debug("User signature: " + signature + " is not equaled to computed signature: " + computedSignature); } if(equalSig) { requestParameters.put("userid", new Object[] {String.valueOf(user.getId())}); requestParameters.put("account", new Object[] {account.getAccountName()}); requestParameters.put("accountobj", new Object[] { account }); } return equalSig; } catch (Exception ex) { s_logger.error("unable to verifty request signature", ex); } return false; } public static final String escapeHTML(String content){ if(content == null || content.isEmpty()) return content; StringBuffer sb = new StringBuffer(); for (int i = 0; i < content.length(); i++) { char c = content.charAt(i); switch (c) { case '<': sb.append("<"); break; case '>': sb.append(">"); break; case '&': sb.append("&"); break; case '"': sb.append("""); break; case ' ': sb.append(" ");break; default: sb.append(c); break; } } return sb.toString(); } }