From 08b149a6da8cbdb4e6de7208e62c5b43a4998507 Mon Sep 17 00:00:00 2001 From: Kelven Yang Date: Tue, 17 Aug 2010 16:17:43 -0700 Subject: [PATCH] Let console proxy servlet support session-less authentication throught API key signed request --- .../cloud/servlet/ConsoleProxyServlet.java | 600 +++++++++++------- 1 file changed, 362 insertions(+), 238 deletions(-) diff --git a/server/src/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/com/cloud/servlet/ConsoleProxyServlet.java index 1901f43cad3..0ed772a960e 100644 --- a/server/src/com/cloud/servlet/ConsoleProxyServlet.java +++ b/server/src/com/cloud/servlet/ConsoleProxyServlet.java @@ -16,56 +16,81 @@ * */ -package com.cloud.servlet; - -import java.io.IOException; - -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +package com.cloud.servlet; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +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.log4j.Logger; - + +import org.apache.log4j.Logger; + +import com.cloud.api.BaseCmd; import com.cloud.host.HostVO; import com.cloud.server.ManagementServer; import com.cloud.user.Account; import com.cloud.user.User; +import com.cloud.utils.Pair; import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.encoding.Base64; import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; - -/** - * 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 ManagementServer _ms = (ManagementServer)ComponentLocator.getComponent(ManagementServer.Name); - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) { - doGet(req, resp); - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + +/** + * 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 ManagementServer _ms = (ManagementServer)ComponentLocator.getComponent(ManagementServer.Name); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + doGet(req, resp); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { try { + String userId = null; + String account = null; + Account accountObj = null; + + Map params = new HashMap(); + params.putAll(req.getParameterMap()); + HttpSession session = req.getSession(false); if(session == null) { - s_logger.info("Invalid web session, reject console/thumbnail access"); - sendResponse(resp, "Access denied. You haven't logged in or your web session has timed out"); - return; + if(verifyRequest(params)) { + userId = (String)params.get(BaseCmd.Properties.USER_ID.getName())[0]; + account = (String)params.get(BaseCmd.Properties.ACCOUNT.getName())[0]; + accountObj = (Account)params.get(BaseCmd.Properties.ACCOUNT_OBJ.getName())[0]; + } else { + s_logger.info("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 { + userId = (String)session.getAttribute(BaseCmd.Properties.USER_ID.getName()); + account = (String)session.getAttribute(BaseCmd.Properties.ACCOUNT.getName()); + accountObj = (Account)session.getAttribute(BaseCmd.Properties.ACCOUNT_OBJ.getName()); } - - String userId = (String)session.getAttribute("userId"); - String account = (String)session.getAttribute("account"); - Account accountObj = (Account)session.getAttribute("accountobj"); // 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))) { @@ -73,202 +98,199 @@ public class ConsoleProxyServlet extends HttpServlet { sendResponse(resp, "Access denied. Invalid or inconsistent account is found"); return; } - - String cmd = req.getParameter("cmd"); - if(cmd == null || !isValidCmd(cmd)) { - s_logger.info("invalid console servlet command: " + cmd); - sendResponse(resp, ""); - return; - } - - String vmIdString = req.getParameter("vm"); - long vmId = 0; - try { - vmId = Long.parseLong(vmIdString); - } catch(NumberFormatException e) { - s_logger.info("invalid console servlet command parameter: " + vmIdString); - sendResponse(resp, ""); - return; - } - - if(!checkSessionPermision(req, vmId)) { - 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, ""); - } - } - - private void handleThumbnailRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) { - VMInstanceVO vm = _ms.findVMInstanceById(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 = _ms.findVMInstanceById(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, "

Console access will be ready in a few minutes. Please try it again later.

"); - return; - } + + String cmd = req.getParameter("cmd"); + if(cmd == null || !isValidCmd(cmd)) { + s_logger.info("invalid console servlet command: " + cmd); + sendResponse(resp, ""); + return; + } + + String vmIdString = req.getParameter("vm"); + long vmId = 0; + try { + vmId = Long.parseLong(vmIdString); + } catch(NumberFormatException e) { + 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 = _ms.findVMInstanceById(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 = _ms.findVMInstanceById(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, "

Console access will be ready in a few minutes. Please try it again later.

"); + return; + } String vmName = vm.getName(); if(vmName == null) vmName = vm.getInstanceName(); - - StringBuffer sb = new StringBuffer(); - sb.append("").append(vmName).append(""); - 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 = _ms.findVMInstanceById(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"); - } - - private String composeThumbnailUrl(String rootUrl, VMInstanceVO vm, HostVO host, int w, int h) { - StringBuffer sb = new StringBuffer(rootUrl); - sb.append("/getscreen?host=").append(host.getPrivateIpAddress()); - sb.append("&port=").append(_ms.getVncPort(vm)); - sb.append("&sid=").append(vm.getVncPassword()); - sb.append("&w=").append(w).append("&h=").append(h); - sb.append("&tag=").append(vm.getId()); - - if(s_logger.isInfoEnabled()) - s_logger.info("Compose thumbnail url: " + sb.toString()); - return sb.toString(); - } - - private String composeConsoleAccessUrl(String rootUrl, VMInstanceVO vm, HostVO host) { - StringBuffer sb = new StringBuffer(rootUrl); - sb.append("/ajax?host=").append(host.getPrivateIpAddress()); - sb.append("&port=").append(_ms.getVncPort(vm)); - sb.append("&sid=").append(vm.getVncPassword()); - sb.append("&tag=").append(vm.getId()); - - if(s_logger.isInfoEnabled()) - s_logger.info("Compose console url: " + sb.toString()); - return sb.toString(); - } - - private void sendResponse(HttpServletResponse resp, String content) { - try { - 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) { + + StringBuffer sb = new StringBuffer(); + sb.append("").append(vmName).append(""); + 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 = _ms.findVMInstanceById(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"); + } + + private String composeThumbnailUrl(String rootUrl, VMInstanceVO vm, HostVO host, int w, int h) { + StringBuffer sb = new StringBuffer(rootUrl); + sb.append("/getscreen?host=").append(host.getPrivateIpAddress()); + sb.append("&port=").append(_ms.getVncPort(vm)); + sb.append("&sid=").append(vm.getVncPassword()); + sb.append("&w=").append(w).append("&h=").append(h); + sb.append("&tag=").append(vm.getId()); + + if(s_logger.isInfoEnabled()) + s_logger.info("Compose thumbnail url: " + sb.toString()); + return sb.toString(); + } + + private String composeConsoleAccessUrl(String rootUrl, VMInstanceVO vm, HostVO host) { + StringBuffer sb = new StringBuffer(rootUrl); + sb.append("/ajax?host=").append(host.getPrivateIpAddress()); + sb.append("&port=").append(_ms.getVncPort(vm)); + sb.append("&sid=").append(vm.getVncPassword()); + sb.append("&tag=").append(vm.getId()); + + if(s_logger.isInfoEnabled()) + s_logger.info("Compose console url: " + sb.toString()); + return sb.toString(); + } + + private void sendResponse(HttpServletResponse resp, String content) { + try { + 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) { - HttpSession session = req.getSession(false); - Account accountObj = (Account)session.getAttribute("accountobj"); - VMInstanceVO vm = _ms.findVMInstanceById(vmId); UserVmVO userVm; switch(vm.getType()) @@ -295,14 +317,14 @@ public class ConsoleProxyServlet extends HttpServlet { break; } - return true; - } - - private boolean isValidCmd(String cmd) { - if(cmd.equalsIgnoreCase("thumbnail") || cmd.equalsIgnoreCase("access") || cmd.equalsIgnoreCase("auth")) - return true; - - 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) { @@ -320,4 +342,106 @@ public class ConsoleProxyServlet extends HttpServlet { } return true; } -} + + // copied and modified from ApiServer.java. + // TODO need to replace the whole servlet with a API command + private boolean verifyRequest(Map 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 parameterNames = new ArrayList(); + + 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.info("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 userAcctPair = _ms.findUserByApiKey(apiKey); + if (userAcctPair == null) { + s_logger.info("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.ACCOUNT_STATE_ENABLED) || !account.getState().equals(Account.ACCOUNT_STATE_ENABLED)) { + s_logger.info("disabled or locked user accessing the api, userid = " + user.getId() + "; name = " + user.getUsername() + "; state: " + user.getState() + "; accountState: " + account.getState()); + return false; + } + + if (account.getType() == Account.ACCOUNT_TYPE_NORMAL) { + requestParameters.put(BaseCmd.Properties.USER_ID.getName(), new String[] { user.getId().toString() }); + requestParameters.put(BaseCmd.Properties.ACCOUNT.getName(), new String[] { account.getAccountName() }); + requestParameters.put(BaseCmd.Properties.DOMAIN_ID.getName(), new String[] { account.getDomainId().toString() }); + requestParameters.put(BaseCmd.Properties.ACCOUNT_OBJ.getName(), new Object[] { account }); + } else { + requestParameters.put(BaseCmd.Properties.USER_ID.getName(), new String[] { user.getId().toString() }); + requestParameters.put(BaseCmd.Properties.ACCOUNT.getName(), new String[] { account.getAccountName() }); + requestParameters.put(BaseCmd.Properties.ACCOUNT_OBJ.getName(), new Object[] { account }); + } + + // verify secret key exists + secretKey = user.getSecretKey(); + if (secretKey == null) { + s_logger.info("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 = Base64.encodeBytes(encryptedBytes); + boolean equalSig = signature.equals(computedSignature); + if (!equalSig) { + s_logger.info("User signature: " + signature + " is not equaled to computed signature: " + computedSignature); + } + return equalSig; + } catch (Exception ex) { + s_logger.error("unable to verifty request signature", ex); + } + return false; + } +}