diff --git a/api/src/com/cloud/user/AccountService.java b/api/src/com/cloud/user/AccountService.java index b88338704d1..ff7c432d562 100644 --- a/api/src/com/cloud/user/AccountService.java +++ b/api/src/com/cloud/user/AccountService.java @@ -1,178 +1,181 @@ -/** - * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. - * - * This software is licensed under the GNU General Public License v3 or later. - * - * It is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.cloud.user; - -import java.util.List; - -import com.cloud.api.commands.CreateAccountCmd; -import com.cloud.api.commands.CreateUserCmd; -import com.cloud.api.commands.DeleteAccountCmd; -import com.cloud.api.commands.DeleteUserCmd; -import com.cloud.api.commands.DisableAccountCmd; -import com.cloud.api.commands.DisableUserCmd; -import com.cloud.api.commands.EnableAccountCmd; -import com.cloud.api.commands.EnableUserCmd; -import com.cloud.api.commands.ListResourceLimitsCmd; -import com.cloud.api.commands.LockUserCmd; -import com.cloud.api.commands.UpdateAccountCmd; -import com.cloud.api.commands.UpdateResourceLimitCmd; -import com.cloud.api.commands.UpdateUserCmd; -import com.cloud.configuration.ResourceLimit; -import com.cloud.domain.Domain; -import com.cloud.exception.ConcurrentOperationException; -import com.cloud.exception.ResourceUnavailableException; -import com.cloud.utils.Pair; - -public interface AccountService { - - /** - * Creates a new user, stores the password as is so encrypted passwords are recommended. - * - * @param cmd - * the create command that has the username, email, password, account name, domain, timezone, etc. for creating - * the user. - * @return the user if created successfully, null otherwise - */ - UserAccount createAccount(CreateAccountCmd cmd); - - /** - * Deletes a user by userId - * - * @param cmd - * - the delete command defining the id of the user to be deleted. - * @return true if delete was successful, false otherwise - */ - boolean deleteUserAccount(DeleteAccountCmd cmd); - - /** - * Disables a user by userId - * - * @param cmd - * the command wrapping the userId parameter - * @return UserAccount object - */ - UserAccount disableUser(DisableUserCmd cmd); - - /** - * Enables a user - * - * @param cmd - * - the command containing userId - * @return UserAccount object - */ - UserAccount enableUser(EnableUserCmd cmd); - - /** - * Locks a user by userId. A locked user cannot access the API, but will still have running VMs/IP addresses allocated/etc. - * - * @param userId - * @return UserAccount object - */ - UserAccount lockUser(LockUserCmd cmd); - - /** - * Update a user by userId - * - * @param userId - * @return UserAccount object - */ - UserAccount updateUser(UpdateUserCmd cmd); - - /** - * Disables an account by accountName and domainId - * - * @param disabled - * account if success - * @return true if disable was successful, false otherwise - */ - Account disableAccount(DisableAccountCmd cmd) throws ConcurrentOperationException, ResourceUnavailableException; - - /** - * Enables an account by accountId - * - * @param cmd - * - the enableAccount command defining the accountId to be deleted. - * @return account object - */ - Account enableAccount(EnableAccountCmd cmd); - - /** - * Locks an account by accountId. A locked account cannot access the API, but will still have running VMs/IP addresses - * allocated/etc. - * - * @param cmd - * - the LockAccount command defining the accountId to be locked. - * @return account object - */ - Account lockAccount(DisableAccountCmd cmd); - - /** - * Updates an account name - * - * @param cmd - * - the parameter containing accountId - * @return updated account object - */ - - Account updateAccount(UpdateAccountCmd cmd); - - /** - * Updates an existing resource limit with the specified details. If a limit doesn't exist, will create one. - * - * @param cmd - * the command that wraps the domainId, accountId, type, and max parameters - * @return the updated/created resource limit - */ - ResourceLimit updateResourceLimit(UpdateResourceLimitCmd cmd); - - /** - * Search for resource limits for the given id and/or account and/or type and/or domain. - * - * @param cmd - * the command wrapping the id, type, account, and domain - * @return a list of limits that match the criteria - */ - List searchForLimits(ListResourceLimitsCmd cmd); - - Account getSystemAccount(); - - User getSystemUser(); - - User createUser(CreateUserCmd cmd); - - boolean deleteUser(DeleteUserCmd deleteUserCmd); - - boolean isAdmin(short accountType); - - Account finalizeOwner(Account caller, String accountName, Long domainId); - - Pair finalizeAccountDomainForList(Account caller, String accountName, Long domainId); - - Account getActiveAccount(String accountName, Long domainId); - - Account getActiveAccount(Long accountId); - - Account getAccount(Long accountId); - - User getActiveUser(long userId); - - Domain getDomain(long id); - - boolean isRootAdmin(short accountType); - -} +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.user; + +import java.util.List; + +import com.cloud.api.commands.CreateAccountCmd; +import com.cloud.api.commands.CreateUserCmd; +import com.cloud.api.commands.DeleteAccountCmd; +import com.cloud.api.commands.DeleteUserCmd; +import com.cloud.api.commands.DisableAccountCmd; +import com.cloud.api.commands.DisableUserCmd; +import com.cloud.api.commands.EnableAccountCmd; +import com.cloud.api.commands.EnableUserCmd; +import com.cloud.api.commands.ListResourceLimitsCmd; +import com.cloud.api.commands.LockUserCmd; +import com.cloud.api.commands.UpdateAccountCmd; +import com.cloud.api.commands.UpdateResourceLimitCmd; +import com.cloud.api.commands.UpdateUserCmd; +import com.cloud.configuration.ResourceLimit; +import com.cloud.domain.Domain; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.Pair; + +public interface AccountService { + + /** + * Creates a new user, stores the password as is so encrypted passwords are recommended. + * + * @param cmd + * the create command that has the username, email, password, account name, domain, timezone, etc. for creating + * the user. + * @return the user if created successfully, null otherwise + */ + UserAccount createAccount(CreateAccountCmd cmd); + + /** + * Deletes a user by userId + * + * @param cmd + * - the delete command defining the id of the user to be deleted. + * @return true if delete was successful, false otherwise + */ + boolean deleteUserAccount(DeleteAccountCmd cmd); + + /** + * Disables a user by userId + * + * @param cmd + * the command wrapping the userId parameter + * @return UserAccount object + */ + UserAccount disableUser(DisableUserCmd cmd); + + /** + * Enables a user + * + * @param cmd + * - the command containing userId + * @return UserAccount object + */ + UserAccount enableUser(EnableUserCmd cmd); + + /** + * Locks a user by userId. A locked user cannot access the API, but will still have running VMs/IP addresses allocated/etc. + * + * @param userId + * @return UserAccount object + */ + UserAccount lockUser(LockUserCmd cmd); + + /** + * Update a user by userId + * + * @param userId + * @return UserAccount object + */ + UserAccount updateUser(UpdateUserCmd cmd); + + /** + * Disables an account by accountName and domainId + * + * @param disabled + * account if success + * @return true if disable was successful, false otherwise + */ + Account disableAccount(DisableAccountCmd cmd) throws ConcurrentOperationException, ResourceUnavailableException; + + /** + * Enables an account by accountId + * + * @param cmd + * - the enableAccount command defining the accountId to be deleted. + * @return account object + */ + Account enableAccount(EnableAccountCmd cmd); + + /** + * Locks an account by accountId. A locked account cannot access the API, but will still have running VMs/IP addresses + * allocated/etc. + * + * @param cmd + * - the LockAccount command defining the accountId to be locked. + * @return account object + */ + Account lockAccount(DisableAccountCmd cmd); + + /** + * Updates an account name + * + * @param cmd + * - the parameter containing accountId + * @return updated account object + */ + + Account updateAccount(UpdateAccountCmd cmd); + + /** + * Updates an existing resource limit with the specified details. If a limit doesn't exist, will create one. + * + * @param cmd + * the command that wraps the domainId, accountId, type, and max parameters + * @return the updated/created resource limit + */ + ResourceLimit updateResourceLimit(UpdateResourceLimitCmd cmd); + + /** + * Search for resource limits for the given id and/or account and/or type and/or domain. + * + * @param cmd + * the command wrapping the id, type, account, and domain + * @return a list of limits that match the criteria + */ + List searchForLimits(ListResourceLimitsCmd cmd); + + Account getSystemAccount(); + + User getSystemUser(); + + User createUser(CreateUserCmd cmd); + boolean deleteUser(DeleteUserCmd deleteUserCmd); + + boolean isAdmin(short accountType); + + Account finalizeOwner(Account caller, String accountName, Long domainId); + + Pair finalizeAccountDomainForList(Account caller, String accountName, Long domainId); + + Account getActiveAccount(String accountName, Long domainId); + + Account getActiveAccount(Long accountId); + + Account getAccount(Long accountId); + + User getActiveUser(long userId); + + Domain getDomain(long id); + + boolean isRootAdmin(short accountType); + + User getActiveUserByRegistrationToken(String registrationToken); + + void markUserRegistered(long userId); + +} diff --git a/api/src/com/cloud/user/User.java b/api/src/com/cloud/user/User.java index 85c55ee5ba8..5e2ebea5a17 100644 --- a/api/src/com/cloud/user/User.java +++ b/api/src/com/cloud/user/User.java @@ -66,5 +66,9 @@ public interface User extends OwnedBy { public String getTimezone(); public void setTimezone(String timezone); + + String getRegistrationToken(); + + boolean isRegistered(); } \ No newline at end of file diff --git a/api/src/com/cloud/user/UserAccount.java b/api/src/com/cloud/user/UserAccount.java index c40b92d8ef2..636dce221ea 100644 --- a/api/src/com/cloud/user/UserAccount.java +++ b/api/src/com/cloud/user/UserAccount.java @@ -53,5 +53,9 @@ public interface UserAccount { String getAccountState(); - String getTimezone(); + String getTimezone(); + + String getRegistrationToken(); + + boolean isRegistered(); } diff --git a/client/WEB-INF/web.xml b/client/WEB-INF/web.xml index 21dd36e727d..2ad73c538b1 100644 --- a/client/WEB-INF/web.xml +++ b/client/WEB-INF/web.xml @@ -42,6 +42,11 @@ consoleServlet com.cloud.servlet.ConsoleProxyServlet + + + registerCompleteServlet + com.cloud.servlet.RegisterCompleteServlet + apiServlet @@ -52,4 +57,9 @@ consoleServlet /console + + + registerCompleteServlet + /cloudkit/complete + diff --git a/core/src/com/cloud/user/UserAccountVO.java b/core/src/com/cloud/user/UserAccountVO.java index 7442d6d96de..af578d76636 100644 --- a/core/src/com/cloud/user/UserAccountVO.java +++ b/core/src/com/cloud/user/UserAccountVO.java @@ -76,6 +76,12 @@ public class UserAccountVO implements UserAccount { @Column(name="timezone") private String timezone; + + @Column(name="registration_token") + private String registrationToken = null; + + @Column(name="is_registered") + boolean registered; @Column(name="account_name", table="account", insertable=false, updatable=false) private String accountName = null; @@ -243,5 +249,24 @@ public class UserAccountVO implements UserAccount { public void setTimezone(String timezone) { this.timezone = timezone; + } + + @Override + public String getRegistrationToken(){ + return registrationToken; + } + + public void setRegistrationToken(String registrationToken) + { + this.registrationToken = registrationToken; + } + + @Override + public boolean isRegistered() { + return registered; + } + + public void setRegistered(boolean registered) { + this.registered = registered; } } \ No newline at end of file diff --git a/core/src/com/cloud/user/UserVO.java b/core/src/com/cloud/user/UserVO.java index 6709d8d4a07..16f514c29f8 100644 --- a/core/src/com/cloud/user/UserVO.java +++ b/core/src/com/cloud/user/UserVO.java @@ -82,6 +82,12 @@ public class UserVO implements User { @Column(name = "timezone") private String timezone; + + @Column(name="registration_token") + private String registrationToken = null; + + @Column(name="is_registered") + boolean registered; public UserVO() { } @@ -204,6 +210,25 @@ public class UserVO implements User { public void setTimezone(String timezone) { this.timezone = timezone; } + + @Override + public String getRegistrationToken(){ + return registrationToken; + } + + public void setRegistrationToken(String registrationToken) + { + this.registrationToken = registrationToken; + } + + @Override + public boolean isRegistered() { + return registered; + } + + public void setRegistered(boolean registered) { + this.registered = registered; + } @Override public String toString() { diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index 9806e58c868..2247f103342 100755 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -1,898 +1,900 @@ -/** - * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. - * - * This software is licensed under the GNU General Public License v3 or later. - * - * It is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package com.cloud.api; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.io.UnsupportedEncodingException; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.TimeZone; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import org.apache.http.ConnectionClosedException; -import org.apache.http.HttpException; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.HttpServerConnection; -import org.apache.http.HttpStatus; -import org.apache.http.entity.BasicHttpEntity; -import org.apache.http.impl.DefaultHttpResponseFactory; -import org.apache.http.impl.DefaultHttpServerConnection; -import org.apache.http.impl.NoConnectionReuseStrategy; -import org.apache.http.impl.SocketHttpServerConnection; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.CoreConnectionPNames; -import org.apache.http.params.CoreProtocolPNames; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.http.protocol.BasicHttpProcessor; -import org.apache.http.protocol.HttpContext; -import org.apache.http.protocol.HttpRequestHandler; -import org.apache.http.protocol.HttpRequestHandlerRegistry; -import org.apache.http.protocol.HttpService; -import org.apache.http.protocol.ResponseConnControl; -import org.apache.http.protocol.ResponseContent; -import org.apache.http.protocol.ResponseDate; -import org.apache.http.protocol.ResponseServer; -import org.apache.log4j.Logger; - -import com.cloud.api.response.ApiResponseSerializer; -import com.cloud.api.response.ExceptionResponse; -import com.cloud.api.response.ListResponse; -import com.cloud.async.AsyncJob; -import com.cloud.async.AsyncJobManager; -import com.cloud.async.AsyncJobVO; -import com.cloud.cluster.StackMaid; -import com.cloud.configuration.ConfigurationVO; -import com.cloud.configuration.dao.ConfigurationDao; -import com.cloud.domain.Domain; -import com.cloud.domain.DomainVO; -import com.cloud.event.EventUtils; -import com.cloud.exception.CloudAuthenticationException; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.exception.PermissionDeniedException; -import com.cloud.server.ManagementServer; -import com.cloud.user.Account; -import com.cloud.user.AccountService; -import com.cloud.user.User; -import com.cloud.user.UserAccount; -import com.cloud.user.UserContext; -import com.cloud.utils.Pair; -import com.cloud.utils.PropertiesUtil; -import com.cloud.utils.component.ComponentLocator; -import com.cloud.utils.concurrency.NamedThreadFactory; -import com.cloud.utils.db.SearchCriteria; -import com.cloud.utils.db.Transaction; -import com.cloud.utils.encoding.Base64; - -public class ApiServer implements HttpRequestHandler { - private static final Logger s_logger = Logger.getLogger(ApiServer.class.getName()); - private static final Logger s_accessLogger = Logger.getLogger("apiserver." + ApiServer.class.getName()); - - public static final short ADMIN_COMMAND = 1; - public static final short DOMAIN_ADMIN_COMMAND = 4; - public static final short RESOURCE_DOMAIN_ADMIN_COMMAND = 2; - public static final short USER_COMMAND = 8; - private Properties _apiCommands = null; - private ApiDispatcher _dispatcher; - private ManagementServer _ms = null; - private AccountService _accountMgr = null; - private AsyncJobManager _asyncMgr = null; - private Account _systemAccount = null; - private User _systemUser = null; - - private static int _workerCount = 0; - - private static ApiServer s_instance = null; - private static List s_userCommands = null; - private static List s_resellerCommands = null; // AKA domain-admin - private static List s_adminCommands = null; - private static List s_resourceDomainAdminCommands = null; - private static List s_allCommands = null; - - private static ExecutorService _executor = new ThreadPoolExecutor(10, 150, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(), new NamedThreadFactory("ApiServer")); - - static { - s_userCommands = new ArrayList(); - s_resellerCommands = new ArrayList(); - s_adminCommands = new ArrayList(); - s_resourceDomainAdminCommands = new ArrayList(); - s_allCommands = new ArrayList(); - } - - private ApiServer() { - } - - public static void initApiServer(String[] apiConfig) { - if (s_instance == null) { - s_instance = new ApiServer(); - s_instance.init(apiConfig); - } - } - - public static ApiServer getInstance() { - // initApiServer(); - return s_instance; - } - - public Properties get_apiCommands() { - return _apiCommands; - } - - public void init(String[] apiConfig) { - try { - BaseCmd.setComponents(new ApiResponseHelper()); - BaseListCmd.configure(); - _apiCommands = new Properties(); - Properties preProcessedCommands = new Properties(); - if (apiConfig != null) { - for (String configFile : apiConfig) { - File commandsFile = PropertiesUtil.findConfigFile(configFile); - preProcessedCommands.load(new FileInputStream(commandsFile)); - } - for (Object key : preProcessedCommands.keySet()) { - String preProcessedCommand = preProcessedCommands.getProperty((String) key); - String[] commandParts = preProcessedCommand.split(";"); - _apiCommands.put(key, commandParts[0]); - if (commandParts.length > 1) { - try { - short cmdPermissions = Short.parseShort(commandParts[1]); - if ((cmdPermissions & ADMIN_COMMAND) != 0) { - s_adminCommands.add((String) key); - } - if ((cmdPermissions & RESOURCE_DOMAIN_ADMIN_COMMAND) != 0) { - s_resourceDomainAdminCommands.add((String) key); - } - if ((cmdPermissions & DOMAIN_ADMIN_COMMAND) != 0) { - s_resellerCommands.add((String) key); - } - if ((cmdPermissions & USER_COMMAND) != 0) { - s_userCommands.add((String) key); - } - } catch (NumberFormatException nfe) { - s_logger.info("Malformed command.properties permissions value, key = " + key + ", value = " + preProcessedCommand); - } - } - } - - s_allCommands.addAll(s_adminCommands); - s_allCommands.addAll(s_resourceDomainAdminCommands); - s_allCommands.addAll(s_userCommands); - s_allCommands.addAll(s_resellerCommands); - } - } catch (FileNotFoundException fnfex) { - s_logger.error("Unable to find properites file", fnfex); - } catch (IOException ioex) { - s_logger.error("Exception loading properties file", ioex); - } - - _ms = (ManagementServer) ComponentLocator.getComponent(ManagementServer.Name); - ComponentLocator locator = ComponentLocator.getLocator(ManagementServer.Name); - _accountMgr = locator.getManager(AccountService.class); - _asyncMgr = locator.getManager(AsyncJobManager.class); - _systemAccount = _accountMgr.getSystemAccount(); - _systemUser = _accountMgr.getSystemUser(); - _dispatcher = ApiDispatcher.getInstance(); - - int apiPort = 8096; // default port - ConfigurationDao configDao = locator.getDao(ConfigurationDao.class); - SearchCriteria sc = configDao.createSearchCriteria(); - sc.addAnd("name", SearchCriteria.Op.EQ, "integration.api.port"); - List values = configDao.search(sc, null); - if ((values != null) && (values.size() > 0)) { - ConfigurationVO apiPortConfig = values.get(0); - apiPort = Integer.parseInt(apiPortConfig.getValue()); - } - - ListenerThread listenerThread = new ListenerThread(this, apiPort); - listenerThread.start(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { - // get some information for the access log... - StringBuffer sb = new StringBuffer(); - HttpServerConnection connObj = (HttpServerConnection) context.getAttribute("http.connection"); - if (connObj instanceof SocketHttpServerConnection) { - InetAddress remoteAddr = ((SocketHttpServerConnection) connObj).getRemoteAddress(); - sb.append(remoteAddr.toString() + " -- "); - } - sb.append(request.getRequestLine()); - - try { - String uri = request.getRequestLine().getUri(); - int requestParamsStartIndex = uri.indexOf('?'); - if (requestParamsStartIndex >= 0) { - uri = uri.substring(requestParamsStartIndex + 1); - } - - String[] paramArray = uri.split("&"); - if (paramArray.length < 1) { - s_logger.info("no parameters received for request: " + uri + ", aborting..."); - return; - } - - Map parameterMap = new HashMap(); - - String responseType = BaseCmd.RESPONSE_TYPE_XML; - for (String paramEntry : paramArray) { - String[] paramValue = paramEntry.split("="); - if (paramValue.length != 2) { - s_logger.info("malformed parameter: " + paramEntry + ", skipping"); - continue; - } - if ("response".equalsIgnoreCase(paramValue[0])) { - responseType = paramValue[1]; - } else { - // according to the servlet spec, the parameter map should be in the form (name=String, value=String[]), so - // parameter values will be stored in an array - parameterMap.put(/* name */paramValue[0], /* value */new String[] { paramValue[1] }); - } - } - try { - // always trust commands from API port, user context will always be UID_SYSTEM/ACCOUNT_ID_SYSTEM - UserContext.registerContext(_systemUser.getId(), _systemAccount, null, true); - sb.insert(0, "(userId=" + User.UID_SYSTEM + " accountId=" + Account.ACCOUNT_ID_SYSTEM + " sessionId=" + null + ") "); - String responseText = handleRequest(parameterMap, true, responseType, sb); - sb.append(" 200 " + ((responseText == null) ? 0 : responseText.length())); - - writeResponse(response, responseText, HttpStatus.SC_OK, responseType, null); - } catch (ServerApiException se) { - String responseText = getSerializedApiError(se.getErrorCode(), se.getDescription(), parameterMap, responseType); - writeResponse(response, responseText, se.getErrorCode(), responseType, se.getDescription()); - sb.append(" " + se.getErrorCode() + " " + se.getDescription()); - } catch (RuntimeException e) { - // log runtime exception like NullPointerException to help identify the source easier - s_logger.error("Unhandled exception, ", e); - throw e; - } - } finally { - s_accessLogger.info(sb.toString()); - UserContext.unregisterContext(); - } - } - - @SuppressWarnings("rawtypes") - public String handleRequest(Map params, boolean decode, String responseType, StringBuffer auditTrailSb) throws ServerApiException { - String response = null; - String[] command = null; - try { - command = (String[]) params.get("command"); - if (command == null) { - s_logger.error("invalid request, no command sent"); - if (s_logger.isTraceEnabled()) { - s_logger.trace("dumping request parameters"); - for (Object key : params.keySet()) { - String keyStr = (String) key; - String[] value = (String[]) params.get(key); - s_logger.trace(" key: " + keyStr + ", value: " + ((value == null) ? "'null'" : value[0])); - } - } - throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, "Invalid request, no command sent"); - } else { - Map paramMap = new HashMap(); - Set keys = params.keySet(); - Iterator keysIter = keys.iterator(); - while (keysIter.hasNext()) { - String key = (String) keysIter.next(); - if ("command".equalsIgnoreCase(key)) { - continue; - } - String[] value = (String[]) params.get(key); - - String decodedValue = null; - if (decode) { - try { - decodedValue = URLDecoder.decode(value[0], "UTF-8"); - } catch (UnsupportedEncodingException usex) { - s_logger.warn(key + " could not be decoded, value = " + value[0]); - throw new ServerApiException(BaseCmd.PARAM_ERROR, key + " could not be decoded, received value " + value[0]); - } catch (IllegalArgumentException iae) { - s_logger.warn(key + " could not be decoded, value = " + value[0]); - throw new ServerApiException(BaseCmd.PARAM_ERROR, key + " could not be decoded, received value " + value[0] + " which contains illegal characters eg.%"); - } - } else { - decodedValue = value[0]; - } - paramMap.put(key, decodedValue); - } - String cmdClassName = _apiCommands.getProperty(command[0]); - if (cmdClassName != null) { - Class cmdClass = Class.forName(cmdClassName); - BaseCmd cmdObj = (BaseCmd) cmdClass.newInstance(); - - cmdObj.setResponseType(responseType); - // This is where the command is either serialized, or directly dispatched - response = queueCommand(cmdObj, paramMap); - buildAuditTrail(auditTrailSb, command[0], response); - } else { - if (!command[0].equalsIgnoreCase("login") && !command[0].equalsIgnoreCase("logout")) { - String errorString = "Unknown API command: " + ((command == null) ? "null" : command[0]); - s_logger.warn(errorString); - auditTrailSb.append(" " + errorString); - throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, errorString); - } - } - } - } catch (Exception ex) { - if (ex instanceof InvalidParameterValueException) { - throw new ServerApiException(BaseCmd.PARAM_ERROR, ex.getMessage()); - } else if (ex instanceof PermissionDeniedException) { - throw new ServerApiException(BaseCmd.ACCOUNT_ERROR, ex.getMessage()); - } else if (ex instanceof ServerApiException) { - throw (ServerApiException) ex; - } else { - s_logger.error("unhandled exception executing api command: " + ((command == null) ? "null" : command[0]), ex); - throw new ServerApiException(BaseCmd.INTERNAL_ERROR, "Internal server error, unable to execute request."); - } - } - return response; - } - - private String queueCommand(BaseCmd cmdObj, Map params) { - UserContext ctx = UserContext.current(); - Long userId = ctx.getCallerUserId(); - Account account = ctx.getCaller(); - if (cmdObj instanceof BaseAsyncCmd) { - Long objectId = null; - if (cmdObj instanceof BaseAsyncCreateCmd) { - BaseAsyncCreateCmd createCmd = (BaseAsyncCreateCmd) cmdObj; - _dispatcher.dispatchCreateCmd(createCmd, params); - objectId = createCmd.getEntityId(); - params.put("id", objectId.toString()); - } else { - ApiDispatcher.setupParameters(cmdObj, params); - } - - BaseAsyncCmd asyncCmd = (BaseAsyncCmd) cmdObj; - - if (userId != null) { - params.put("ctxUserId", userId.toString()); - } - if (account != null) { - params.put("ctxAccountId", String.valueOf(account.getId())); - } - - long startEventId = ctx.getStartEventId(); - asyncCmd.setStartEventId(startEventId); - - // save the scheduled event - Long eventId = EventUtils.saveScheduledEvent((userId == null) ? User.UID_SYSTEM : userId, asyncCmd.getEntityOwnerId(), asyncCmd.getEventType(), asyncCmd.getEventDescription(), - startEventId); - if (startEventId == 0) { - // There was no create event before, set current event id as start eventId - startEventId = eventId; - } - - params.put("ctxStartEventId", String.valueOf(startEventId)); - - ctx.setAccountId(asyncCmd.getEntityOwnerId()); - - AsyncJobVO job = new AsyncJobVO(); - job.setInstanceId((objectId == null) ? asyncCmd.getInstanceId() : objectId); - job.setInstanceType(asyncCmd.getInstanceType()); - job.setUserId(userId); - job.setAccountId(asyncCmd.getEntityOwnerId()); - - job.setCmd(cmdObj.getClass().getName()); - job.setCmdInfo(ApiGsonHelper.getBuilder().create().toJson(params)); - - long jobId = _asyncMgr.submitAsyncJob(job); - - if (jobId == 0L) { - String errorMsg = "Unable to schedule async job for command " + job.getCmd(); - s_logger.warn(errorMsg); - throw new ServerApiException(BaseCmd.INTERNAL_ERROR, errorMsg); - } - - if (objectId != null) { - return ((BaseAsyncCreateCmd) asyncCmd).getResponse(jobId, objectId); - } - return ApiResponseSerializer.toSerializedString(asyncCmd.getResponse(jobId), asyncCmd.getResponseType()); - } else { - _dispatcher.dispatch(cmdObj, params); - - // if the command is of the listXXXCommand, we will need to also return the - // the job id and status if possible - if (cmdObj instanceof BaseListCmd) { - // validate page size - validatePageSize((BaseListCmd) cmdObj); - buildAsyncListResponse((BaseListCmd) cmdObj, account); - } - return ApiResponseSerializer.toSerializedString((ResponseObject) cmdObj.getResponseObject(), cmdObj.getResponseType()); - } - } - - private void validatePageSize(BaseListCmd command) { - List responses = ((ListResponse) command.getResponseObject()).getResponses(); - int defaultPageLimit = BaseCmd._configService.getDefaultPageSize().intValue(); - if (responses != null && responses.size() > defaultPageLimit && command.getPage() == null && command.getPageSize() == null) { - throw new ServerApiException(BaseCmd.PAGE_LIMIT_EXCEED, "Number of returned objects per page exceed default page limit " + defaultPageLimit - + "; please specify \"page\"/\"pagesize\" parameters"); - } - } - - private void buildAsyncListResponse(BaseListCmd command, Account account) { - List responses = ((ListResponse) command.getResponseObject()).getResponses(); - if (responses != null && responses.size() > 0) { - List jobs = null; - - // list all jobs for ROOT admin - if (account.getType() == Account.ACCOUNT_TYPE_ADMIN) { - jobs = _asyncMgr.findInstancePendingAsyncJobs(command.getInstanceType(), null); - } else { - jobs = _asyncMgr.findInstancePendingAsyncJobs(command.getInstanceType(), account.getId()); - } - - if (jobs.size() == 0) { - return; - } - - // Using maps might possibly be more efficient if the set is large enough but for now, we'll just do a - // comparison of two lists. Either way, there shouldn't be too many async jobs active for the account. - for (AsyncJob job : jobs) { - if (job.getInstanceId() == null) { - continue; - } - for (ResponseObject response : responses) { - if (response.getObjectId() != null && job.getInstanceId().longValue() == response.getObjectId().longValue()) { - response.setJobId(job.getId()); - response.setJobStatus(job.getStatus()); - } - } - } - } - } - - private void buildAuditTrail(StringBuffer auditTrailSb, String command, String result) { - if (result == null) { - return; - } - auditTrailSb.append(" " + HttpServletResponse.SC_OK + " "); - auditTrailSb.append(result); - /* - * if (command.equals("queryAsyncJobResult")){ //For this command we need to also log job status and job resultcode for - * (Pair pair : resultValues){ String key = pair.first(); if (key.equals("jobstatus")){ - * auditTrailSb.append(" "); auditTrailSb.append(key); auditTrailSb.append("="); auditTrailSb.append(pair.second()); - * }else if (key.equals("jobresultcode")){ auditTrailSb.append(" "); auditTrailSb.append(key); auditTrailSb.append("="); - * auditTrailSb.append(pair.second()); } } }else { for (Pair pair : resultValues){ if - * (pair.first().equals("jobid")){ // Its an async job so report the jobid auditTrailSb.append(" "); - * auditTrailSb.append(pair.first()); auditTrailSb.append("="); auditTrailSb.append(pair.second()); } } } - */ - } - - private static boolean isCommandAvailable(String commandName) { - boolean isCommandAvailable = false; - isCommandAvailable = s_allCommands.contains(commandName); - return isCommandAvailable; - } - - public boolean verifyRequest(Map requestParameters, Long userId) throws ServerApiException { - try { - String apiKey = null; - String secretKey = null; - String signature = null; - String unsignedRequest = null; - - String[] command = (String[]) requestParameters.get("command"); - if (command == null) { - s_logger.info("missing command, ignoring request..."); - return false; - } - - String commandName = command[0]; - - // if userId not null, that mean that user is logged in - if (userId != null) { - Long accountId = ApiDBUtils.findUserById(userId).getAccountId(); - Account userAccount = _ms.findAccountById(accountId); - short accountType = userAccount.getType(); - - if (!isCommandAvailable(accountType, commandName)) { - s_logger.warn("The given command:" + commandName + " does not exist"); - throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, "The given command:" + commandName + " does not exist"); - } - return true; - } else { - // check against every available command to see if the command exists or not - if (!isCommandAvailable(commandName) && !commandName.equals("login") && !commandName.equals("logout")) { - s_logger.warn("The given command:" + commandName + " does not exist"); - throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, "The given command:" + commandName + " does not exist"); - } - } - - // - 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() != Account.State.enabled || !account.getState().equals(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; - } - - UserContext.updateContext(user.getId(), account, null); - - if (!isCommandAvailable(account.getType(), commandName)) { - s_logger.warn("The given command:" + commandName + " does not exist"); - throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, "The given command:" + commandName + " does not exist"); - } - - // 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) { - if (ex instanceof ServerApiException && ((ServerApiException) ex).getErrorCode() == BaseCmd.UNSUPPORTED_ACTION_ERROR) { - throw (ServerApiException) ex; - } - s_logger.error("unable to verifty request signature", ex); - } - return false; - } - - public void loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, Map requestParameters) throws CloudAuthenticationException { - // We will always use domainId first. If that does not exist, we will use domain name. If THAT doesn't exist - // we will default to ROOT - if (domainId == null) { - if (domainPath == null || domainPath.trim().length() == 0) { - domainId = DomainVO.ROOT_DOMAIN; - } else { - Domain domainObj = _ms.findDomainByPath(domainPath); - if (domainObj != null) { - domainId = domainObj.getId(); - } else { // if an unknown path is passed in, fail the login call - throw new CloudAuthenticationException("Unable to find the domain from the path " + domainPath); - } - } - } - - UserAccount userAcct = _ms.authenticateUser(username, password, domainId, requestParameters); - if (userAcct != null) { - String timezone = userAcct.getTimezone(); - float offsetInHrs = 0f; - if (timezone != null) { - TimeZone t = TimeZone.getTimeZone(timezone); - s_logger.info("Current user logged in under " + timezone + " timezone"); - - java.util.Date date = new java.util.Date(); - long longDate = date.getTime(); - float offsetInMs = (t.getOffset(longDate)); - offsetInHrs = offsetInMs / (1000 * 60 * 60); - s_logger.info("Timezone offset from UTC is: " + offsetInHrs); - } - - Account account = _ms.findAccountById(userAcct.getAccountId()); - - // set the userId and account object for everyone - session.setAttribute("userid", userAcct.getId()); - session.setAttribute("username", userAcct.getUsername()); - session.setAttribute("firstname", userAcct.getFirstname()); - session.setAttribute("lastname", userAcct.getLastname()); - session.setAttribute("accountobj", account); - session.setAttribute("account", account.getAccountName()); - session.setAttribute("domainid", account.getDomainId()); - session.setAttribute("type", Short.valueOf(account.getType()).toString()); - - if (timezone != null) { - session.setAttribute("timezone", timezone); - session.setAttribute("timezoneoffset", Float.valueOf(offsetInHrs).toString()); - } - - // (bug 5483) generate a session key that the user must submit on every request to prevent CSRF, add that - // to the login response so that session-based authenticators know to send the key back - SecureRandom sesssionKeyRandom = new SecureRandom(); - byte sessionKeyBytes[] = new byte[20]; - sesssionKeyRandom.nextBytes(sessionKeyBytes); - String sessionKey = Base64.encodeBytes(sessionKeyBytes); - session.setAttribute("sessionkey", sessionKey); - - return; - } - throw new CloudAuthenticationException("Unable to find user " + username + " in domain " + domainId); - } - - public void logoutUser(long userId) { - _ms.logoutUser(Long.valueOf(userId)); - return; - } - - public boolean verifyUser(Long userId) { - User user = _ms.findUserById(userId); - Account account = null; - if (user != null) { - account = _ms.findAccountById(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; - } - - public static boolean isCommandAvailable(short accountType, String commandName) { - boolean isCommandAvailable = false; - switch (accountType) { - case Account.ACCOUNT_TYPE_ADMIN: - isCommandAvailable = s_adminCommands.contains(commandName); - break; - case Account.ACCOUNT_TYPE_DOMAIN_ADMIN: - isCommandAvailable = s_resellerCommands.contains(commandName); - break; - case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN: - isCommandAvailable = s_resourceDomainAdminCommands.contains(commandName); - break; - case Account.ACCOUNT_TYPE_NORMAL: - isCommandAvailable = s_userCommands.contains(commandName); - break; - } - return isCommandAvailable; - } - - // FIXME: rather than isError, we might was to pass in the status code to give more flexibility - private void writeResponse(HttpResponse resp, final String responseText, final int statusCode, String responseType, String reasonPhrase) { - try { - resp.setStatusCode(statusCode); - resp.setReasonPhrase(reasonPhrase); - - BasicHttpEntity body = new BasicHttpEntity(); - if (BaseCmd.RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { - // JSON response - body.setContentType("text/javascript"); - if (responseText == null) { - body.setContent(new ByteArrayInputStream("{ \"error\" : { \"description\" : \"Internal Server Error\" } }".getBytes("UTF-8"))); - } - } else { - body.setContentType("text/xml"); - if (responseText == null) { - body.setContent(new ByteArrayInputStream("Internal Server Error".getBytes("UTF-8"))); - } - } - - if (responseText != null) { - body.setContent(new ByteArrayInputStream(responseText.getBytes("UTF-8"))); - } - resp.setEntity(body); - } catch (Exception ex) { - s_logger.error("error!", ex); - } - } - - // FIXME: the following two threads are copied from - // http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/httpcore/src/examples/org/apache/http/examples/ElementalHttpServer.java - // we have to cite a license if we are using this code directly, so we need to add the appropriate citation or modify the - // code to be very specific to our needs - static class ListenerThread extends Thread { - private HttpService _httpService = null; - private ServerSocket _serverSocket = null; - private HttpParams _params = null; - - public ListenerThread(ApiServer requestHandler, int port) { - try { - _serverSocket = new ServerSocket(port); - } catch (IOException ioex) { - s_logger.error("error initializing api server", ioex); - return; - } - - _params = new BasicHttpParams(); - _params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 30000).setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024) - .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false).setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true) - .setParameter(CoreProtocolPNames.ORIGIN_SERVER, "HttpComponents/1.1"); - - // Set up the HTTP protocol processor - BasicHttpProcessor httpproc = new BasicHttpProcessor(); - httpproc.addInterceptor(new ResponseDate()); - httpproc.addInterceptor(new ResponseServer()); - httpproc.addInterceptor(new ResponseContent()); - httpproc.addInterceptor(new ResponseConnControl()); - - // Set up request handlers - HttpRequestHandlerRegistry reqistry = new HttpRequestHandlerRegistry(); - reqistry.register("*", requestHandler); - - // Set up the HTTP service - _httpService = new HttpService(httpproc, new NoConnectionReuseStrategy(), new DefaultHttpResponseFactory()); - _httpService.setParams(_params); - _httpService.setHandlerResolver(reqistry); - } - - @Override - public void run() { - s_logger.info("ApiServer listening on port " + _serverSocket.getLocalPort()); - while (!Thread.interrupted()) { - try { - // Set up HTTP connection - Socket socket = _serverSocket.accept(); - DefaultHttpServerConnection conn = new DefaultHttpServerConnection(); - conn.bind(socket, _params); - - // Execute a new worker task to handle the request - _executor.execute(new WorkerTask(_httpService, conn, _workerCount++)); - } catch (InterruptedIOException ex) { - break; - } catch (IOException e) { - s_logger.error("I/O error initializing connection thread", e); - break; - } - } - } - } - - static class WorkerTask implements Runnable { - private final HttpService _httpService; - private final HttpServerConnection _conn; - - public WorkerTask(final HttpService httpService, final HttpServerConnection conn, final int count) { - _httpService = httpService; - _conn = conn; - } - - @Override - public void run() { - HttpContext context = new BasicHttpContext(null); - try { - while (!Thread.interrupted() && _conn.isOpen()) { - try { - _httpService.handleRequest(_conn, context); - _conn.close(); - } finally { - StackMaid.current().exitCleanup(); - } - } - } catch (ConnectionClosedException ex) { - if (s_logger.isTraceEnabled()) { - s_logger.trace("ApiServer: Client closed connection"); - } - } catch (IOException ex) { - if (s_logger.isTraceEnabled()) { - s_logger.trace("ApiServer: IOException - " + ex); - } - } catch (HttpException ex) { - s_logger.warn("ApiServer: Unrecoverable HTTP protocol violation" + ex); - } finally { - try { - _conn.shutdown(); - } catch (IOException ignore) { - } - } - } - } - - public String getSerializedApiError(int errorCode, String errorText, Map apiCommandParams, String responseType) { - String responseName = null; - String cmdClassName = null; - - String responseText = null; - - try { - if (errorCode == BaseCmd.UNSUPPORTED_ACTION_ERROR || apiCommandParams == null || apiCommandParams.isEmpty()) { - responseName = "errorresponse"; - } else { - String cmdName = ((String[]) apiCommandParams.get("command"))[0]; - cmdClassName = _apiCommands.getProperty(cmdName); - if (cmdClassName != null) { - Class claz = Class.forName(cmdClassName); - responseName = ((BaseCmd) claz.newInstance()).getCommandName(); - } else { - responseName = "errorresponse"; - } - } - - ExceptionResponse apiResponse = new ExceptionResponse(); - apiResponse.setErrorCode(errorCode); - apiResponse.setErrorText(errorText); - apiResponse.setResponseName(responseName); - responseText = ApiResponseSerializer.toSerializedString(apiResponse, responseType); - - } catch (Exception e) { - s_logger.error("Exception responding to http request", e); - } - return responseText; - } -} +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.api; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.http.ConnectionClosedException; +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpServerConnection; +import org.apache.http.HttpStatus; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.DefaultHttpResponseFactory; +import org.apache.http.impl.DefaultHttpServerConnection; +import org.apache.http.impl.NoConnectionReuseStrategy; +import org.apache.http.impl.SocketHttpServerConnection; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.CoreConnectionPNames; +import org.apache.http.params.CoreProtocolPNames; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.BasicHttpProcessor; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpRequestHandler; +import org.apache.http.protocol.HttpRequestHandlerRegistry; +import org.apache.http.protocol.HttpService; +import org.apache.http.protocol.ResponseConnControl; +import org.apache.http.protocol.ResponseContent; +import org.apache.http.protocol.ResponseDate; +import org.apache.http.protocol.ResponseServer; +import org.apache.log4j.Logger; + +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.api.response.ExceptionResponse; +import com.cloud.api.response.ListResponse; +import com.cloud.async.AsyncJob; +import com.cloud.async.AsyncJobManager; +import com.cloud.async.AsyncJobVO; +import com.cloud.cluster.StackMaid; +import com.cloud.configuration.ConfigurationVO; +import com.cloud.configuration.dao.ConfigurationDao; +import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.event.EventUtils; +import com.cloud.exception.CloudAuthenticationException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.server.ManagementServer; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.UserContext; +import com.cloud.utils.Pair; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.encoding.Base64; + +public class ApiServer implements HttpRequestHandler { + private static final Logger s_logger = Logger.getLogger(ApiServer.class.getName()); + private static final Logger s_accessLogger = Logger.getLogger("apiserver." + ApiServer.class.getName()); + + public static final short ADMIN_COMMAND = 1; + public static final short DOMAIN_ADMIN_COMMAND = 4; + public static final short RESOURCE_DOMAIN_ADMIN_COMMAND = 2; + public static final short USER_COMMAND = 8; + private Properties _apiCommands = null; + private ApiDispatcher _dispatcher; + private ManagementServer _ms = null; + private AccountService _accountMgr = null; + private AsyncJobManager _asyncMgr = null; + private Account _systemAccount = null; + private User _systemUser = null; + + private static int _workerCount = 0; + + private static ApiServer s_instance = null; + private static List s_userCommands = null; + private static List s_resellerCommands = null; // AKA domain-admin + private static List s_adminCommands = null; + private static List s_resourceDomainAdminCommands = null; + private static List s_allCommands = null; + + private static ExecutorService _executor = new ThreadPoolExecutor(10, 150, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(), new NamedThreadFactory("ApiServer")); + + static { + s_userCommands = new ArrayList(); + s_resellerCommands = new ArrayList(); + s_adminCommands = new ArrayList(); + s_resourceDomainAdminCommands = new ArrayList(); + s_allCommands = new ArrayList(); + } + + private ApiServer() { + } + + public static void initApiServer(String[] apiConfig) { + if (s_instance == null) { + s_instance = new ApiServer(); + s_instance.init(apiConfig); + } + } + + public static ApiServer getInstance() { + // initApiServer(); + return s_instance; + } + + public Properties get_apiCommands() { + return _apiCommands; + } + + public void init(String[] apiConfig) { + try { + BaseCmd.setComponents(new ApiResponseHelper()); + BaseListCmd.configure(); + _apiCommands = new Properties(); + Properties preProcessedCommands = new Properties(); + if (apiConfig != null) { + for (String configFile : apiConfig) { + File commandsFile = PropertiesUtil.findConfigFile(configFile); + preProcessedCommands.load(new FileInputStream(commandsFile)); + } + for (Object key : preProcessedCommands.keySet()) { + String preProcessedCommand = preProcessedCommands.getProperty((String) key); + String[] commandParts = preProcessedCommand.split(";"); + _apiCommands.put(key, commandParts[0]); + if (commandParts.length > 1) { + try { + short cmdPermissions = Short.parseShort(commandParts[1]); + if ((cmdPermissions & ADMIN_COMMAND) != 0) { + s_adminCommands.add((String) key); + } + if ((cmdPermissions & RESOURCE_DOMAIN_ADMIN_COMMAND) != 0) { + s_resourceDomainAdminCommands.add((String) key); + } + if ((cmdPermissions & DOMAIN_ADMIN_COMMAND) != 0) { + s_resellerCommands.add((String) key); + } + if ((cmdPermissions & USER_COMMAND) != 0) { + s_userCommands.add((String) key); + } + } catch (NumberFormatException nfe) { + s_logger.info("Malformed command.properties permissions value, key = " + key + ", value = " + preProcessedCommand); + } + } + } + + s_allCommands.addAll(s_adminCommands); + s_allCommands.addAll(s_resourceDomainAdminCommands); + s_allCommands.addAll(s_userCommands); + s_allCommands.addAll(s_resellerCommands); + } + } catch (FileNotFoundException fnfex) { + s_logger.error("Unable to find properites file", fnfex); + } catch (IOException ioex) { + s_logger.error("Exception loading properties file", ioex); + } + + _ms = (ManagementServer) ComponentLocator.getComponent(ManagementServer.Name); + ComponentLocator locator = ComponentLocator.getLocator(ManagementServer.Name); + _accountMgr = locator.getManager(AccountService.class); + _asyncMgr = locator.getManager(AsyncJobManager.class); + _systemAccount = _accountMgr.getSystemAccount(); + _systemUser = _accountMgr.getSystemUser(); + _dispatcher = ApiDispatcher.getInstance(); + + int apiPort = 8096; // default port + ConfigurationDao configDao = locator.getDao(ConfigurationDao.class); + SearchCriteria sc = configDao.createSearchCriteria(); + sc.addAnd("name", SearchCriteria.Op.EQ, "integration.api.port"); + List values = configDao.search(sc, null); + if ((values != null) && (values.size() > 0)) { + ConfigurationVO apiPortConfig = values.get(0); + apiPort = Integer.parseInt(apiPortConfig.getValue()); + } + + ListenerThread listenerThread = new ListenerThread(this, apiPort); + listenerThread.start(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { + // get some information for the access log... + StringBuffer sb = new StringBuffer(); + HttpServerConnection connObj = (HttpServerConnection) context.getAttribute("http.connection"); + if (connObj instanceof SocketHttpServerConnection) { + InetAddress remoteAddr = ((SocketHttpServerConnection) connObj).getRemoteAddress(); + sb.append(remoteAddr.toString() + " -- "); + } + sb.append(request.getRequestLine()); + + try { + String uri = request.getRequestLine().getUri(); + int requestParamsStartIndex = uri.indexOf('?'); + if (requestParamsStartIndex >= 0) { + uri = uri.substring(requestParamsStartIndex + 1); + } + + String[] paramArray = uri.split("&"); + if (paramArray.length < 1) { + s_logger.info("no parameters received for request: " + uri + ", aborting..."); + return; + } + + Map parameterMap = new HashMap(); + + String responseType = BaseCmd.RESPONSE_TYPE_XML; + for (String paramEntry : paramArray) { + String[] paramValue = paramEntry.split("="); + if (paramValue.length != 2) { + s_logger.info("malformed parameter: " + paramEntry + ", skipping"); + continue; + } + if ("response".equalsIgnoreCase(paramValue[0])) { + responseType = paramValue[1]; + } else { + // according to the servlet spec, the parameter map should be in the form (name=String, value=String[]), so + // parameter values will be stored in an array + parameterMap.put(/* name */paramValue[0], /* value */new String[] { paramValue[1] }); + } + } + try { + // always trust commands from API port, user context will always be UID_SYSTEM/ACCOUNT_ID_SYSTEM + UserContext.registerContext(_systemUser.getId(), _systemAccount, null, true); + sb.insert(0, "(userId=" + User.UID_SYSTEM + " accountId=" + Account.ACCOUNT_ID_SYSTEM + " sessionId=" + null + ") "); + String responseText = handleRequest(parameterMap, true, responseType, sb); + sb.append(" 200 " + ((responseText == null) ? 0 : responseText.length())); + + writeResponse(response, responseText, HttpStatus.SC_OK, responseType, null); + } catch (ServerApiException se) { + String responseText = getSerializedApiError(se.getErrorCode(), se.getDescription(), parameterMap, responseType); + writeResponse(response, responseText, se.getErrorCode(), responseType, se.getDescription()); + sb.append(" " + se.getErrorCode() + " " + se.getDescription()); + } catch (RuntimeException e) { + // log runtime exception like NullPointerException to help identify the source easier + s_logger.error("Unhandled exception, ", e); + throw e; + } + } finally { + s_accessLogger.info(sb.toString()); + UserContext.unregisterContext(); + } + } + + @SuppressWarnings("rawtypes") + public String handleRequest(Map params, boolean decode, String responseType, StringBuffer auditTrailSb) throws ServerApiException { + String response = null; + String[] command = null; + try { + command = (String[]) params.get("command"); + if (command == null) { + s_logger.error("invalid request, no command sent"); + if (s_logger.isTraceEnabled()) { + s_logger.trace("dumping request parameters"); + for (Object key : params.keySet()) { + String keyStr = (String) key; + String[] value = (String[]) params.get(key); + s_logger.trace(" key: " + keyStr + ", value: " + ((value == null) ? "'null'" : value[0])); + } + } + throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, "Invalid request, no command sent"); + } else { + Map paramMap = new HashMap(); + Set keys = params.keySet(); + Iterator keysIter = keys.iterator(); + while (keysIter.hasNext()) { + String key = (String) keysIter.next(); + if ("command".equalsIgnoreCase(key)) { + continue; + } + String[] value = (String[]) params.get(key); + + String decodedValue = null; + if (decode) { + try { + decodedValue = URLDecoder.decode(value[0], "UTF-8"); + } catch (UnsupportedEncodingException usex) { + s_logger.warn(key + " could not be decoded, value = " + value[0]); + throw new ServerApiException(BaseCmd.PARAM_ERROR, key + " could not be decoded, received value " + value[0]); + } catch (IllegalArgumentException iae) { + s_logger.warn(key + " could not be decoded, value = " + value[0]); + throw new ServerApiException(BaseCmd.PARAM_ERROR, key + " could not be decoded, received value " + value[0] + " which contains illegal characters eg.%"); + } + } else { + decodedValue = value[0]; + } + paramMap.put(key, decodedValue); + } + String cmdClassName = _apiCommands.getProperty(command[0]); + if (cmdClassName != null) { + Class cmdClass = Class.forName(cmdClassName); + BaseCmd cmdObj = (BaseCmd) cmdClass.newInstance(); + + cmdObj.setResponseType(responseType); + // This is where the command is either serialized, or directly dispatched + response = queueCommand(cmdObj, paramMap); + buildAuditTrail(auditTrailSb, command[0], response); + } else { + if (!command[0].equalsIgnoreCase("login") && !command[0].equalsIgnoreCase("logout")) { + String errorString = "Unknown API command: " + ((command == null) ? "null" : command[0]); + s_logger.warn(errorString); + auditTrailSb.append(" " + errorString); + throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, errorString); + } + } + } + } catch (Exception ex) { + if (ex instanceof InvalidParameterValueException) { + throw new ServerApiException(BaseCmd.PARAM_ERROR, ex.getMessage()); + } else if (ex instanceof PermissionDeniedException) { + throw new ServerApiException(BaseCmd.ACCOUNT_ERROR, ex.getMessage()); + } else if (ex instanceof ServerApiException) { + throw (ServerApiException) ex; + } else { + s_logger.error("unhandled exception executing api command: " + ((command == null) ? "null" : command[0]), ex); + throw new ServerApiException(BaseCmd.INTERNAL_ERROR, "Internal server error, unable to execute request."); + } + } + return response; + } + + private String queueCommand(BaseCmd cmdObj, Map params) { + UserContext ctx = UserContext.current(); + Long userId = ctx.getCallerUserId(); + Account account = ctx.getCaller(); + if (cmdObj instanceof BaseAsyncCmd) { + Long objectId = null; + if (cmdObj instanceof BaseAsyncCreateCmd) { + BaseAsyncCreateCmd createCmd = (BaseAsyncCreateCmd) cmdObj; + _dispatcher.dispatchCreateCmd(createCmd, params); + objectId = createCmd.getEntityId(); + params.put("id", objectId.toString()); + } else { + ApiDispatcher.setupParameters(cmdObj, params); + } + + BaseAsyncCmd asyncCmd = (BaseAsyncCmd) cmdObj; + + if (userId != null) { + params.put("ctxUserId", userId.toString()); + } + if (account != null) { + params.put("ctxAccountId", String.valueOf(account.getId())); + } + + long startEventId = ctx.getStartEventId(); + asyncCmd.setStartEventId(startEventId); + + // save the scheduled event + Long eventId = EventUtils.saveScheduledEvent((userId == null) ? User.UID_SYSTEM : userId, asyncCmd.getEntityOwnerId(), asyncCmd.getEventType(), asyncCmd.getEventDescription(), + startEventId); + if (startEventId == 0) { + // There was no create event before, set current event id as start eventId + startEventId = eventId; + } + + params.put("ctxStartEventId", String.valueOf(startEventId)); + + ctx.setAccountId(asyncCmd.getEntityOwnerId()); + + AsyncJobVO job = new AsyncJobVO(); + job.setInstanceId((objectId == null) ? asyncCmd.getInstanceId() : objectId); + job.setInstanceType(asyncCmd.getInstanceType()); + job.setUserId(userId); + job.setAccountId(asyncCmd.getEntityOwnerId()); + + job.setCmd(cmdObj.getClass().getName()); + job.setCmdInfo(ApiGsonHelper.getBuilder().create().toJson(params)); + + long jobId = _asyncMgr.submitAsyncJob(job); + + if (jobId == 0L) { + String errorMsg = "Unable to schedule async job for command " + job.getCmd(); + s_logger.warn(errorMsg); + throw new ServerApiException(BaseCmd.INTERNAL_ERROR, errorMsg); + } + + if (objectId != null) { + return ((BaseAsyncCreateCmd) asyncCmd).getResponse(jobId, objectId); + } + return ApiResponseSerializer.toSerializedString(asyncCmd.getResponse(jobId), asyncCmd.getResponseType()); + } else { + _dispatcher.dispatch(cmdObj, params); + + // if the command is of the listXXXCommand, we will need to also return the + // the job id and status if possible + if (cmdObj instanceof BaseListCmd) { + // validate page size + validatePageSize((BaseListCmd) cmdObj); + buildAsyncListResponse((BaseListCmd) cmdObj, account); + } + return ApiResponseSerializer.toSerializedString((ResponseObject) cmdObj.getResponseObject(), cmdObj.getResponseType()); + } + } + + private void validatePageSize(BaseListCmd command) { + List responses = ((ListResponse) command.getResponseObject()).getResponses(); + int defaultPageLimit = BaseCmd._configService.getDefaultPageSize().intValue(); + if (responses != null && responses.size() > defaultPageLimit && command.getPage() == null && command.getPageSize() == null) { + throw new ServerApiException(BaseCmd.PAGE_LIMIT_EXCEED, "Number of returned objects per page exceed default page limit " + defaultPageLimit + + "; please specify \"page\"/\"pagesize\" parameters"); + } + } + + private void buildAsyncListResponse(BaseListCmd command, Account account) { + List responses = ((ListResponse) command.getResponseObject()).getResponses(); + if (responses != null && responses.size() > 0) { + List jobs = null; + + // list all jobs for ROOT admin + if (account.getType() == Account.ACCOUNT_TYPE_ADMIN) { + jobs = _asyncMgr.findInstancePendingAsyncJobs(command.getInstanceType(), null); + } else { + jobs = _asyncMgr.findInstancePendingAsyncJobs(command.getInstanceType(), account.getId()); + } + + if (jobs.size() == 0) { + return; + } + + // Using maps might possibly be more efficient if the set is large enough but for now, we'll just do a + // comparison of two lists. Either way, there shouldn't be too many async jobs active for the account. + for (AsyncJob job : jobs) { + if (job.getInstanceId() == null) { + continue; + } + for (ResponseObject response : responses) { + if (response.getObjectId() != null && job.getInstanceId().longValue() == response.getObjectId().longValue()) { + response.setJobId(job.getId()); + response.setJobStatus(job.getStatus()); + } + } + } + } + } + + private void buildAuditTrail(StringBuffer auditTrailSb, String command, String result) { + if (result == null) { + return; + } + auditTrailSb.append(" " + HttpServletResponse.SC_OK + " "); + auditTrailSb.append(result); + /* + * if (command.equals("queryAsyncJobResult")){ //For this command we need to also log job status and job resultcode for + * (Pair pair : resultValues){ String key = pair.first(); if (key.equals("jobstatus")){ + * auditTrailSb.append(" "); auditTrailSb.append(key); auditTrailSb.append("="); auditTrailSb.append(pair.second()); + * }else if (key.equals("jobresultcode")){ auditTrailSb.append(" "); auditTrailSb.append(key); auditTrailSb.append("="); + * auditTrailSb.append(pair.second()); } } }else { for (Pair pair : resultValues){ if + * (pair.first().equals("jobid")){ // Its an async job so report the jobid auditTrailSb.append(" "); + * auditTrailSb.append(pair.first()); auditTrailSb.append("="); auditTrailSb.append(pair.second()); } } } + */ + } + + private static boolean isCommandAvailable(String commandName) { + boolean isCommandAvailable = false; + isCommandAvailable = s_allCommands.contains(commandName); + return isCommandAvailable; + } + + public boolean verifyRequest(Map requestParameters, Long userId) throws ServerApiException { + try { + String apiKey = null; + String secretKey = null; + String signature = null; + String unsignedRequest = null; + + String[] command = (String[]) requestParameters.get("command"); + if (command == null) { + s_logger.info("missing command, ignoring request..."); + return false; + } + + String commandName = command[0]; + + // if userId not null, that mean that user is logged in + if (userId != null) { + Long accountId = ApiDBUtils.findUserById(userId).getAccountId(); + Account userAccount = _ms.findAccountById(accountId); + short accountType = userAccount.getType(); + + if (!isCommandAvailable(accountType, commandName)) { + s_logger.warn("The given command:" + commandName + " does not exist"); + throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, "The given command:" + commandName + " does not exist"); + } + return true; + } else { + // check against every available command to see if the command exists or not + if (!isCommandAvailable(commandName) && !commandName.equals("login") && !commandName.equals("logout")) { + s_logger.warn("The given command:" + commandName + " does not exist"); + throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, "The given command:" + commandName + " does not exist"); + } + } + + // - 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() != Account.State.enabled || !account.getState().equals(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; + } + + UserContext.updateContext(user.getId(), account, null); + + if (!isCommandAvailable(account.getType(), commandName)) { + s_logger.warn("The given command:" + commandName + " does not exist"); + throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, "The given command:" + commandName + " does not exist"); + } + + // 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) { + if (ex instanceof ServerApiException && ((ServerApiException) ex).getErrorCode() == BaseCmd.UNSUPPORTED_ACTION_ERROR) { + throw (ServerApiException) ex; + } + s_logger.error("unable to verifty request signature", ex); + } + return false; + } + + public void loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, Map requestParameters) throws CloudAuthenticationException { + // We will always use domainId first. If that does not exist, we will use domain name. If THAT doesn't exist + // we will default to ROOT + if (domainId == null) { + if (domainPath == null || domainPath.trim().length() == 0) { + domainId = DomainVO.ROOT_DOMAIN; + } else { + Domain domainObj = _ms.findDomainByPath(domainPath); + if (domainObj != null) { + domainId = domainObj.getId(); + } else { // if an unknown path is passed in, fail the login call + throw new CloudAuthenticationException("Unable to find the domain from the path " + domainPath); + } + } + } + + UserAccount userAcct = _ms.authenticateUser(username, password, domainId, requestParameters); + if (userAcct != null) { + String timezone = userAcct.getTimezone(); + float offsetInHrs = 0f; + if (timezone != null) { + TimeZone t = TimeZone.getTimeZone(timezone); + s_logger.info("Current user logged in under " + timezone + " timezone"); + + java.util.Date date = new java.util.Date(); + long longDate = date.getTime(); + float offsetInMs = (t.getOffset(longDate)); + offsetInHrs = offsetInMs / (1000 * 60 * 60); + s_logger.info("Timezone offset from UTC is: " + offsetInHrs); + } + + Account account = _ms.findAccountById(userAcct.getAccountId()); + + // set the userId and account object for everyone + session.setAttribute("userid", userAcct.getId()); + session.setAttribute("username", userAcct.getUsername()); + session.setAttribute("firstname", userAcct.getFirstname()); + session.setAttribute("lastname", userAcct.getLastname()); + session.setAttribute("accountobj", account); + session.setAttribute("account", account.getAccountName()); + session.setAttribute("domainid", account.getDomainId()); + session.setAttribute("type", Short.valueOf(account.getType()).toString()); + session.setAttribute("registrationtoken", userAcct.getRegistrationToken()); + session.setAttribute("registered", new Boolean(userAcct.isRegistered()).toString()); + + if (timezone != null) { + session.setAttribute("timezone", timezone); + session.setAttribute("timezoneoffset", Float.valueOf(offsetInHrs).toString()); + } + + // (bug 5483) generate a session key that the user must submit on every request to prevent CSRF, add that + // to the login response so that session-based authenticators know to send the key back + SecureRandom sesssionKeyRandom = new SecureRandom(); + byte sessionKeyBytes[] = new byte[20]; + sesssionKeyRandom.nextBytes(sessionKeyBytes); + String sessionKey = Base64.encodeBytes(sessionKeyBytes); + session.setAttribute("sessionkey", sessionKey); + + return; + } + throw new CloudAuthenticationException("Unable to find user " + username + " in domain " + domainId); + } + + public void logoutUser(long userId) { + _ms.logoutUser(Long.valueOf(userId)); + return; + } + + public boolean verifyUser(Long userId) { + User user = _ms.findUserById(userId); + Account account = null; + if (user != null) { + account = _ms.findAccountById(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; + } + + public static boolean isCommandAvailable(short accountType, String commandName) { + boolean isCommandAvailable = false; + switch (accountType) { + case Account.ACCOUNT_TYPE_ADMIN: + isCommandAvailable = s_adminCommands.contains(commandName); + break; + case Account.ACCOUNT_TYPE_DOMAIN_ADMIN: + isCommandAvailable = s_resellerCommands.contains(commandName); + break; + case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN: + isCommandAvailable = s_resourceDomainAdminCommands.contains(commandName); + break; + case Account.ACCOUNT_TYPE_NORMAL: + isCommandAvailable = s_userCommands.contains(commandName); + break; + } + return isCommandAvailable; + } + + // FIXME: rather than isError, we might was to pass in the status code to give more flexibility + private void writeResponse(HttpResponse resp, final String responseText, final int statusCode, String responseType, String reasonPhrase) { + try { + resp.setStatusCode(statusCode); + resp.setReasonPhrase(reasonPhrase); + + BasicHttpEntity body = new BasicHttpEntity(); + if (BaseCmd.RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { + // JSON response + body.setContentType("text/javascript"); + if (responseText == null) { + body.setContent(new ByteArrayInputStream("{ \"error\" : { \"description\" : \"Internal Server Error\" } }".getBytes("UTF-8"))); + } + } else { + body.setContentType("text/xml"); + if (responseText == null) { + body.setContent(new ByteArrayInputStream("Internal Server Error".getBytes("UTF-8"))); + } + } + + if (responseText != null) { + body.setContent(new ByteArrayInputStream(responseText.getBytes("UTF-8"))); + } + resp.setEntity(body); + } catch (Exception ex) { + s_logger.error("error!", ex); + } + } + + // FIXME: the following two threads are copied from + // http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/httpcore/src/examples/org/apache/http/examples/ElementalHttpServer.java + // we have to cite a license if we are using this code directly, so we need to add the appropriate citation or modify the + // code to be very specific to our needs + static class ListenerThread extends Thread { + private HttpService _httpService = null; + private ServerSocket _serverSocket = null; + private HttpParams _params = null; + + public ListenerThread(ApiServer requestHandler, int port) { + try { + _serverSocket = new ServerSocket(port); + } catch (IOException ioex) { + s_logger.error("error initializing api server", ioex); + return; + } + + _params = new BasicHttpParams(); + _params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 30000).setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024) + .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false).setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true) + .setParameter(CoreProtocolPNames.ORIGIN_SERVER, "HttpComponents/1.1"); + + // Set up the HTTP protocol processor + BasicHttpProcessor httpproc = new BasicHttpProcessor(); + httpproc.addInterceptor(new ResponseDate()); + httpproc.addInterceptor(new ResponseServer()); + httpproc.addInterceptor(new ResponseContent()); + httpproc.addInterceptor(new ResponseConnControl()); + + // Set up request handlers + HttpRequestHandlerRegistry reqistry = new HttpRequestHandlerRegistry(); + reqistry.register("*", requestHandler); + + // Set up the HTTP service + _httpService = new HttpService(httpproc, new NoConnectionReuseStrategy(), new DefaultHttpResponseFactory()); + _httpService.setParams(_params); + _httpService.setHandlerResolver(reqistry); + } + + @Override + public void run() { + s_logger.info("ApiServer listening on port " + _serverSocket.getLocalPort()); + while (!Thread.interrupted()) { + try { + // Set up HTTP connection + Socket socket = _serverSocket.accept(); + DefaultHttpServerConnection conn = new DefaultHttpServerConnection(); + conn.bind(socket, _params); + + // Execute a new worker task to handle the request + _executor.execute(new WorkerTask(_httpService, conn, _workerCount++)); + } catch (InterruptedIOException ex) { + break; + } catch (IOException e) { + s_logger.error("I/O error initializing connection thread", e); + break; + } + } + } + } + + static class WorkerTask implements Runnable { + private final HttpService _httpService; + private final HttpServerConnection _conn; + + public WorkerTask(final HttpService httpService, final HttpServerConnection conn, final int count) { + _httpService = httpService; + _conn = conn; + } + + @Override + public void run() { + HttpContext context = new BasicHttpContext(null); + try { + while (!Thread.interrupted() && _conn.isOpen()) { + try { + _httpService.handleRequest(_conn, context); + _conn.close(); + } finally { + StackMaid.current().exitCleanup(); + } + } + } catch (ConnectionClosedException ex) { + if (s_logger.isTraceEnabled()) { + s_logger.trace("ApiServer: Client closed connection"); + } + } catch (IOException ex) { + if (s_logger.isTraceEnabled()) { + s_logger.trace("ApiServer: IOException - " + ex); + } + } catch (HttpException ex) { + s_logger.warn("ApiServer: Unrecoverable HTTP protocol violation" + ex); + } finally { + try { + _conn.shutdown(); + } catch (IOException ignore) { + } + } + } + } + + public String getSerializedApiError(int errorCode, String errorText, Map apiCommandParams, String responseType) { + String responseName = null; + String cmdClassName = null; + + String responseText = null; + + try { + if (errorCode == BaseCmd.UNSUPPORTED_ACTION_ERROR || apiCommandParams == null || apiCommandParams.isEmpty()) { + responseName = "errorresponse"; + } else { + String cmdName = ((String[]) apiCommandParams.get("command"))[0]; + cmdClassName = _apiCommands.getProperty(cmdName); + if (cmdClassName != null) { + Class claz = Class.forName(cmdClassName); + responseName = ((BaseCmd) claz.newInstance()).getCommandName(); + } else { + responseName = "errorresponse"; + } + } + + ExceptionResponse apiResponse = new ExceptionResponse(); + apiResponse.setErrorCode(errorCode); + apiResponse.setErrorText(errorText); + apiResponse.setResponseName(responseName); + responseText = ApiResponseSerializer.toSerializedString(apiResponse, responseType); + + } catch (Exception e) { + s_logger.error("Exception responding to http request", e); + } + return responseText; + } +} diff --git a/server/src/com/cloud/servlet/RegisterCompleteServlet.java b/server/src/com/cloud/servlet/RegisterCompleteServlet.java new file mode 100644 index 00000000000..807951448c9 --- /dev/null +++ b/server/src/com/cloud/servlet/RegisterCompleteServlet.java @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.servlet; + +import java.util.List; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.log4j.Logger; + +import com.cloud.configuration.Configuration; +import com.cloud.configuration.dao.ConfigurationDao; +import com.cloud.server.ManagementServer; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.user.User; +import com.cloud.user.UserVO; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.SerialVersionUID; +import com.cloud.utils.component.ComponentLocator; + +public class RegisterCompleteServlet extends HttpServlet implements ServletContextListener { + public static final Logger s_logger = Logger.getLogger(RegisterCompleteServlet.class.getName()); + + static final long serialVersionUID = SerialVersionUID.CloudStartupServlet; + + protected static AccountService _accountSvc = null; + protected static ConfigurationDao _configDao = null; + protected static UserDao _userDao = null; + + @Override + public void init() throws ServletException { + ComponentLocator locator = ComponentLocator.getLocator(ManagementServer.Name); + _accountSvc = locator.getManager(AccountService.class); + _configDao = locator.getDao(ConfigurationDao.class); + _userDao = locator.getDao(UserDao.class); + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + try { + init(); + } catch (ServletException e) { + s_logger.error("Exception starting management server ", e); + throw new RuntimeException(e); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + doGet(req, resp); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + String registrationToken = req.getParameter("token"); + if (registrationToken == null || registrationToken.trim().length() == 0) { + // Return an error code + } + + User resourceAdminUser = _accountSvc.getActiveUserByRegistrationToken(registrationToken); + if (resourceAdminUser == null) { + // Return an error code + } + + if(!resourceAdminUser.isRegistered()){ + _accountSvc.markUserRegistered(resourceAdminUser.getId()); + } + + Account resourceAdminAccount = _accountSvc.getActiveAccount(resourceAdminUser.getAccountId()); + Account rsUserAccount = _accountSvc.getActiveAccount(resourceAdminAccount.getAccountName()+"-user", resourceAdminAccount.getDomainId()); + + List users = _userDao.listByAccount(rsUserAccount.getId()); + User rsUser = users.get(0); + + Configuration config = _configDao.findByName("endpointe.url"); + + StringBuffer sb = new StringBuffer(); + sb.append("{ \"registration_info\" : { \"endpoint_url\" : \""+config.getValue()+"\", "); + sb.append("\"domain_id\" : \""+resourceAdminAccount.getDomainId()+"\", "); + sb.append("\"admin_account\" : \""+resourceAdminUser.getUsername()+"\", "); + sb.append("\"admin_account_api_key\" : \""+resourceAdminUser.getApiKey()+"\", "); + sb.append("\"admin_account_secret_key\" : \""+resourceAdminUser.getSecretKey()+"\", "); + sb.append("\"user_account\" : \""+rsUser.getUsername()+"\", "); + sb.append("\"user_account_api_key\" : \""+rsUser.getApiKey()+"\", "); + sb.append("\"user_account_secret_key\" : \""+rsUser.getSecretKey()+"\" "); + sb.append("} }"); + + try { + resp.setContentType("text/javascript; charset=UTF-8"); + resp.setStatus(HttpServletResponse.SC_OK); + resp.getWriter().print(sb.toString()); + } catch (Exception ex) { + s_logger.error("unknown exception writing register complete response", ex); + } + } +} diff --git a/server/src/com/cloud/user/AccountManagerImpl.java b/server/src/com/cloud/user/AccountManagerImpl.java index a0154d08648..eefb5896019 100755 --- a/server/src/com/cloud/user/AccountManagerImpl.java +++ b/server/src/com/cloud/user/AccountManagerImpl.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -1144,6 +1145,13 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag user.setAccountId(accountId.longValue()); user.setEmail(email); user.setTimezone(timezone); + + if(userType == Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN){ + //set registration token + byte[] bytes = (domainId + accountName + username + System.currentTimeMillis()).getBytes(); + String registrationToken = UUID.nameUUIDFromBytes(bytes).toString(); + user.setRegistrationToken(registrationToken); + } if (s_logger.isDebugEnabled()) { s_logger.debug("Creating user: " + username + ", account: " + accountName + " (id:" + accountId + "), domain: " + domainId + " timezone:" + timezone); } @@ -1767,4 +1775,16 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag return new Pair(accountName, domainId); } + + @Override + public User getActiveUserByRegistrationToken(String registrationToken) { + return _userDao.findUserByRegistrationToken(registrationToken); + } + + @Override + public void markUserRegistered(long userId) { + UserVO userForUpdate = _userDao.createForUpdate(); + userForUpdate.setRegistered(true); + _userDao.update(Long.valueOf(userId), userForUpdate); + } } diff --git a/server/src/com/cloud/user/dao/UserDao.java b/server/src/com/cloud/user/dao/UserDao.java index 436a830b353..4ee3a12a85b 100644 --- a/server/src/com/cloud/user/dao/UserDao.java +++ b/server/src/com/cloud/user/dao/UserDao.java @@ -55,5 +55,13 @@ public interface UserDao extends GenericDao{ * @param secretKey * @return */ - UserVO findUserBySecretKey(String secretKey); + UserVO findUserBySecretKey(String secretKey); + + /** + * Finds a user based on the registration token provided. + * @param registrationToken + * @return + */ + UserVO findUserByRegistrationToken(String registrationToken); + } diff --git a/server/src/com/cloud/user/dao/UserDaoImpl.java b/server/src/com/cloud/user/dao/UserDaoImpl.java index 04b16128c0d..9fba2b19d9a 100644 --- a/server/src/com/cloud/user/dao/UserDaoImpl.java +++ b/server/src/com/cloud/user/dao/UserDaoImpl.java @@ -41,7 +41,8 @@ public class UserDaoImpl extends GenericDaoBase implements UserDao protected SearchBuilder UsernameLikeSearch; protected SearchBuilder UserIdSearch; protected SearchBuilder AccountIdSearch; - protected SearchBuilder SecretKeySearch; + protected SearchBuilder SecretKeySearch; + protected SearchBuilder RegistrationTokenSearch; protected UserDaoImpl () { UsernameSearch = createSearchBuilder(); @@ -67,7 +68,11 @@ public class UserDaoImpl extends GenericDaoBase implements UserDao SecretKeySearch = createSearchBuilder(); SecretKeySearch.and("secretKey", SecretKeySearch.entity().getSecretKey(), SearchCriteria.Op.EQ); - SecretKeySearch.done(); + SecretKeySearch.done(); + + RegistrationTokenSearch = createSearchBuilder(); + RegistrationTokenSearch.and("registrationToken", RegistrationTokenSearch.entity().getRegistrationToken(), SearchCriteria.Op.EQ); + RegistrationTokenSearch.done(); } @Override @@ -134,5 +139,12 @@ public class UserDaoImpl extends GenericDaoBase implements UserDao { throw new CloudRuntimeException("unable to update user -- a user with that name exists"); } - } + } + + @Override + public UserVO findUserByRegistrationToken(String registrationToken) { + SearchCriteria sc = RegistrationTokenSearch.create(); + sc.setParameters("registrationToken", registrationToken); + return findOneBy(sc); + } } diff --git a/setup/db/create-schema.sql b/setup/db/create-schema.sql index 42a3ea9190a..f7d52db51b2 100755 --- a/setup/db/create-schema.sql +++ b/setup/db/create-schema.sql @@ -698,6 +698,8 @@ CREATE TABLE `cloud`.`user` ( `created` datetime NOT NULL COMMENT 'date created', `removed` datetime COMMENT 'date removed', `timezone` varchar(30) default NULL, + `registration_token` varchar(255) default NULL, + `is_registered` tinyint NOT NULL DEFAULT 0 COMMENT '1: yes, 0: no', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; diff --git a/ui/cloudkit/scripts/cloudkit.js b/ui/cloudkit/scripts/cloudkit.js new file mode 100644 index 00000000000..7d1967a477d --- /dev/null +++ b/ui/cloudkit/scripts/cloudkit.js @@ -0,0 +1,52 @@ + /** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +var g_loginResponse = null; +$.urlParam = function(name){ var results = new RegExp('[\\?&]' + name + '=([^&#]*)').exec(window.location.href); if (!results) { return 0; } return results[1] || 0;} + +function logout() { + window.location='/client/cloudkit/login.jsp'; + g_loginResponse = null; + return true; +} + +$(document).ready(function() { + + var url = $.urlParam("loginUrl"); + if (url != undefined && url != null && url.length > 0) { + url = unescape("/client/api?"+url); + $.ajax({ + url: url, + dataType: "json", + async: false, + success: function(json) { + g_loginResponse = json.loginresponse; + $("#registration_complete_link").attr("href","https://my.rightscale.com/cloud_registrations/cloudkit/new?callback_url="+encodeURIComponent("http://localhost:8080/client/cloudkit/complete?token="+g_loginResponse.registrationtoken)); + }, + error: function() { + logout(); + }, + beforeSend: function(XMLHttpRequest) { + return true; + } + }); + } else { + logout(); + } +}); + +