| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| 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.InputStream; |
| 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.text.DateFormat; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Date; |
| 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.commons.codec.binary.Base64; |
| 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.Config; |
| 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.AccountManager; |
| import com.cloud.user.DomainManager; |
| import com.cloud.user.User; |
| import com.cloud.user.UserAccount; |
| import com.cloud.user.UserContext; |
| import com.cloud.user.UserVO; |
| import com.cloud.utils.IdentityProxy; |
| import com.cloud.utils.Pair; |
| import com.cloud.utils.PropertiesUtil; |
| import com.cloud.utils.StringUtils; |
| import com.cloud.utils.component.ComponentLocator; |
| import com.cloud.utils.component.PluggableService; |
| import com.cloud.utils.concurrency.NamedThreadFactory; |
| import com.cloud.utils.db.SearchCriteria; |
| import com.cloud.utils.db.Transaction; |
| import com.cloud.utils.exception.CSExceptionErrorCode; |
| import com.cloud.uuididentity.dao.IdentityDao; |
| |
| |
| 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; |
| public static boolean encodeApiResponse = false; |
| public static String jsonContentType = "text/javascript"; |
| private Properties _apiCommands = null; |
| private ApiDispatcher _dispatcher; |
| private AccountManager _accountMgr = null; |
| private DomainManager _domainMgr = 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<String> s_userCommands = null; |
| private static List<String> s_resellerCommands = null; // AKA domain-admin |
| private static List<String> s_adminCommands = null; |
| private static List<String> s_resourceDomainAdminCommands = null; |
| private static List<String> s_allCommands = null; |
| private static List<String> s_pluggableServiceCommands = null; |
| private static final DateFormat _dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); |
| |
| private static ExecutorService _executor = new ThreadPoolExecutor(10, 150, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new NamedThreadFactory("ApiServer")); |
| |
| static { |
| s_userCommands = new ArrayList<String>(); |
| s_resellerCommands = new ArrayList<String>(); |
| s_adminCommands = new ArrayList<String>(); |
| s_resourceDomainAdminCommands = new ArrayList<String>(); |
| s_allCommands = new ArrayList<String>(); |
| s_pluggableServiceCommands = new ArrayList<String>(); |
| } |
| |
| 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 static boolean isPluggableServiceCommand(String cmdClassName) { |
| if (s_pluggableServiceCommands != null) { |
| if (s_pluggableServiceCommands.contains(cmdClassName)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private String[] getPluggableServicesApiConfigs() { |
| List<String> pluggableServicesApiConfigs = new ArrayList<String>(); |
| |
| ComponentLocator locator = ComponentLocator.getLocator(ManagementServer.Name); |
| List<PluggableService> services = locator.getAllPluggableServices(); |
| for (PluggableService service : services) { |
| pluggableServicesApiConfigs.add(service.getPropertiesFile()); |
| } |
| return pluggableServicesApiConfigs.toArray(new String[0]); |
| } |
| |
| private void processConfigFiles(String[] apiConfig, boolean pluggableServicesConfig) { |
| try { |
| if (_apiCommands == null) { |
| _apiCommands = new Properties(); |
| } |
| Properties preProcessedCommands = new Properties(); |
| if (apiConfig != null) { |
| for (String configFile : apiConfig) { |
| File commandsFile = PropertiesUtil.findConfigFile(configFile); |
| if (commandsFile != null) { |
| try { |
| preProcessedCommands.load(new FileInputStream(commandsFile)); |
| } catch (FileNotFoundException fnfex) { |
| // in case of a file within a jar in classpath, try to open stream using url |
| InputStream stream = PropertiesUtil.openStreamFromURL(configFile); |
| if (stream != null) { |
| preProcessedCommands.load(stream); |
| } else { |
| s_logger.error("Unable to find properites file", fnfex); |
| } |
| } |
| } |
| } |
| for (Object key : preProcessedCommands.keySet()) { |
| String preProcessedCommand = preProcessedCommands.getProperty((String) key); |
| String[] commandParts = preProcessedCommand.split(";"); |
| _apiCommands.put(key, commandParts[0]); |
| |
| if (pluggableServicesConfig) { |
| s_pluggableServiceCommands.add(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); |
| } |
| } |
| |
| public void init(String[] apiConfig) { |
| BaseCmd.setComponents(new ApiResponseHelper()); |
| BaseListCmd.configure(); |
| processConfigFiles(apiConfig, false); |
| |
| // get commands for all pluggable services |
| String[] pluggableServicesApiConfigs = getPluggableServicesApiConfigs(); |
| processConfigFiles(pluggableServicesApiConfigs, true); |
| |
| ComponentLocator locator = ComponentLocator.getLocator(ManagementServer.Name); |
| _accountMgr = locator.getManager(AccountManager.class); |
| _asyncMgr = locator.getManager(AsyncJobManager.class); |
| _systemAccount = _accountMgr.getSystemAccount(); |
| _systemUser = _accountMgr.getSystemUser(); |
| _dispatcher = ApiDispatcher.getInstance(); |
| _domainMgr = locator.getManager(DomainManager.class); |
| |
| Integer apiPort = null; // api port, null by default |
| ConfigurationDao configDao = locator.getDao(ConfigurationDao.class); |
| SearchCriteria<ConfigurationVO> sc = configDao.createSearchCriteria(); |
| sc.addAnd("name", SearchCriteria.Op.EQ, "integration.api.port"); |
| List<ConfigurationVO> values = configDao.search(sc, null); |
| if ((values != null) && (values.size() > 0)) { |
| ConfigurationVO apiPortConfig = values.get(0); |
| if (apiPortConfig.getValue() != null) { |
| apiPort = Integer.parseInt(apiPortConfig.getValue()); |
| } |
| } |
| |
| encodeApiResponse = Boolean.valueOf(configDao.getValue(Config.EncodeApiResponse.key())); |
| |
| String jsonType = configDao.getValue(Config.JavaScriptDefaultContentType.key()); |
| if (jsonType != null) { |
| jsonContentType = jsonType; |
| } |
| |
| if (apiPort != null) { |
| 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(StringUtils.cleanString(request.getRequestLine().toString())); |
| |
| 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, String[]>(); |
| |
| 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, se); |
| 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<String, String> paramMap = new HashMap<String, String>(); |
| 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.setFullUrlParams(paramMap); |
| 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) { |
| InvalidParameterValueException ref = (InvalidParameterValueException)ex; |
| ServerApiException e = new ServerApiException(BaseCmd.PARAM_ERROR, ex.getMessage()); |
| // copy over the IdentityProxy information as well and throw the serverapiexception. |
| ArrayList<IdentityProxy> idList = ref.getIdProxyList(); |
| if (idList != null) { |
| // Iterate through entire arraylist and copy over each proxy id. |
| for (int i = 0 ; i < idList.size(); i++) { |
| IdentityProxy obj = idList.get(i); |
| e.addProxyObject(obj.getTableName(), obj.getValue(), obj.getidFieldName()); |
| } |
| } |
| // Also copy over the cserror code and the function/layer in which it was thrown. |
| e.setCSErrorCode(ref.getCSErrorCode()); |
| throw e; |
| } else if (ex instanceof PermissionDeniedException) { |
| PermissionDeniedException ref = (PermissionDeniedException)ex; |
| ServerApiException e = new ServerApiException(BaseCmd.ACCOUNT_ERROR, ex.getMessage()); |
| // copy over the IdentityProxy information as well and throw the serverapiexception. |
| ArrayList<IdentityProxy> idList = ref.getIdProxyList(); |
| if (idList != null) { |
| // Iterate through entire arraylist and copy over each proxy id. |
| for (int i = 0 ; i < idList.size(); i++) { |
| IdentityProxy obj = idList.get(i); |
| e.addProxyObject(obj.getTableName(), obj.getValue(), obj.getidFieldName()); |
| } |
| } |
| e.setCSErrorCode(ref.getCSErrorCode()); |
| throw e; |
| } else if (ex instanceof ServerApiException) { |
| throw (ServerApiException) ex; |
| } else { |
| s_logger.error("unhandled exception executing api command: " + ((command == null) ? "null" : command[0]), ex); |
| ServerApiException e = new ServerApiException(BaseCmd.INTERNAL_ERROR, "Internal server error, unable to execute request."); |
| e.setCSErrorCode(CSExceptionErrorCode.getCSErrCode("ServerApiException")); |
| throw e; |
| } |
| } |
| return response; |
| } |
| |
| private String queueCommand(BaseCmd cmdObj, Map<String, String> params) { |
| UserContext ctx = UserContext.current(); |
| Long callerUserId = ctx.getCallerUserId(); |
| Account caller = ctx.getCaller(); |
| if (cmdObj instanceof BaseAsyncCmd) { |
| Long objectId = null; |
| String objectEntityTable = null; |
| if (cmdObj instanceof BaseAsyncCreateCmd) { |
| BaseAsyncCreateCmd createCmd = (BaseAsyncCreateCmd) cmdObj; |
| _dispatcher.dispatchCreateCmd(createCmd, params); |
| objectId = createCmd.getEntityId(); |
| objectEntityTable = createCmd.getEntityTable(); |
| params.put("id", objectId.toString()); |
| } else { |
| ApiDispatcher.setupParameters(cmdObj, params); |
| ApiDispatcher.plugService(cmdObj); |
| } |
| |
| BaseAsyncCmd asyncCmd = (BaseAsyncCmd) cmdObj; |
| |
| if (callerUserId != null) { |
| params.put("ctxUserId", callerUserId.toString()); |
| } |
| if (caller != null) { |
| params.put("ctxAccountId", String.valueOf(caller.getId())); |
| } |
| |
| long startEventId = ctx.getStartEventId(); |
| asyncCmd.setStartEventId(startEventId); |
| |
| // save the scheduled event |
| Long eventId = EventUtils.saveScheduledEvent((callerUserId == null) ? User.UID_SYSTEM : callerUserId, |
| 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()); |
| |
| Long instanceId = (objectId == null) ? asyncCmd.getInstanceId() : objectId; |
| AsyncJobVO job = new AsyncJobVO(callerUserId, caller.getId(), cmdObj.getClass().getName(), |
| ApiGsonHelper.getBuilder().create().toJson(params), instanceId, asyncCmd.getInstanceType()); |
| |
| 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) { |
| SerializationContext.current().setUuidTranslation(true); |
| return ((BaseAsyncCreateCmd) asyncCmd).getResponse(jobId, objectId, objectEntityTable); |
| } |
| |
| SerializationContext.current().setUuidTranslation(true); |
| 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) { |
| buildAsyncListResponse((BaseListCmd) cmdObj, caller); |
| } |
| |
| SerializationContext.current().setUuidTranslation(true); |
| return ApiResponseSerializer.toSerializedString((ResponseObject) cmdObj.getResponseObject(), cmdObj.getResponseType()); |
| } |
| } |
| |
| private void buildAsyncListResponse(BaseListCmd command, Account account) { |
| List<ResponseObject> responses = ((ListResponse) command.getResponseObject()).getResponses(); |
| if (responses != null && responses.size() > 0) { |
| List<? extends AsyncJob> 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 + " "); |
| if (command.equals("createSSHKeyPair")){ |
| auditTrailSb.append("This result was not logged because it contains sensitive data."); |
| } else { |
| auditTrailSb.append(StringUtils.cleanString(result)); |
| } |
| } |
| |
| private static boolean isCommandAvailable(String commandName) { |
| boolean isCommandAvailable = false; |
| isCommandAvailable = s_allCommands.contains(commandName); |
| return isCommandAvailable; |
| } |
| |
| public boolean verifyRequest(Map<String, Object[]> 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 = _accountMgr.getAccount(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 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 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<String> parameterNames = new ArrayList<String>(); |
| |
| for (Object paramNameObj : requestParameters.keySet()) { |
| parameterNames.add((String) paramNameObj); // put the name in a list that we'll sort later |
| } |
| |
| Collections.sort(parameterNames); |
| |
| String signatureVersion = null; |
| String expires = null; |
| |
| 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; |
| } |
| else if ("signatureversion".equalsIgnoreCase(paramName)) { |
| signatureVersion = paramValue; |
| } else if ("expires".equalsIgnoreCase(paramName)) { |
| expires = 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 |
| } |
| |
| Date expiresTS = null; |
| if ("3".equals(signatureVersion)) { |
| // New signature authentication. Check for expire parameter and its validity |
| if (expires == null) { |
| s_logger.info("missing Expires parameter -- ignoring request...sig: " + signature + ", apiKey: " + apiKey); |
| return false; |
| } |
| synchronized (_dateFormat) { |
| try { |
| expiresTS = _dateFormat.parse(expires); |
| } catch (ParseException pe) { |
| s_logger.info("Incorrect date format for Expires parameter", pe); |
| return false; |
| } |
| } |
| Date now = new Date(System.currentTimeMillis()); |
| if (expiresTS.before(now)) { |
| s_logger.info("Request expired -- ignoring ...sig: " + signature + ", apiKey: " + apiKey); |
| return false; |
| } |
| } |
| |
| Transaction txn = Transaction.open(Transaction.CLOUD_DB); |
| txn.close(); |
| User user = null; |
| // verify there is a user with this api key |
| Pair<User, Account> userAcctPair = _accountMgr.findUserByApiKey(apiKey); |
| if (userAcctPair == null) { |
| s_logger.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.encodeBase64String(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 Long fetchDomainId(String domainUUID){ |
| ComponentLocator locator = ComponentLocator.getLocator(ManagementServer.Name); |
| IdentityDao identityDao = locator.getDao(IdentityDao.class); |
| try{ |
| Long domainId = identityDao.getIdentityId("domain", domainUUID); |
| return domainId; |
| }catch(InvalidParameterValueException ex){ |
| return null; |
| } |
| } |
| |
| public void loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, String loginIpAddress ,Map<String, Object[]> 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 = _domainMgr.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 = _accountMgr.authenticateUser(username, password, domainId, loginIpAddress, 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 = _accountMgr.getAccount(userAcct.getAccountId()); |
| |
| // set the userId and account object for everyone |
| session.setAttribute("userid", userAcct.getId()); |
| UserVO user = (UserVO) _accountMgr.getActiveUser(userAcct.getId()); |
| if(user.getUuid() != null){ |
| session.setAttribute("user_UUID", user.getUuid()); |
| } |
| |
| 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()); |
| DomainVO domain = (DomainVO) _domainMgr.getDomain(account.getDomainId()); |
| if(domain.getUuid() != null){ |
| session.setAttribute("domain_UUID", domain.getUuid()); |
| } |
| |
| 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.encodeBase64String(sessionKeyBytes); |
| session.setAttribute("sessionkey", sessionKey); |
| |
| return; |
| } |
| throw new CloudAuthenticationException("Failed to authenticate user " + username + " in domain " + domainId + "; please provide valid credentials"); |
| } |
| |
| public void logoutUser(long userId) { |
| _accountMgr.logoutUser(Long.valueOf(userId)); |
| return; |
| } |
| |
| public boolean verifyUser(Long userId) { |
| User user = _accountMgr.getUserIncludingRemoved(userId); |
| Account account = null; |
| if (user != null) { |
| account = _accountMgr.getAccount(user.getAccountId()); |
| } |
| |
| if ((user == null) || (user.getRemoved() != null) || !user.getState().equals(Account.State.enabled) || (account == null) || !account.getState().equals(Account.State.enabled)) { |
| s_logger.warn("Deleted/Disabled/Locked user with id=" + userId + " attempting to access public API"); |
| return false; |
| } |
| return true; |
| } |
| |
| 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(jsonContentType); |
| 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("<error>Internal Server Error</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<String, Object[]> apiCommandParams, String responseType, Exception ex) { |
| String responseName = null; |
| String cmdClassName = null; |
| |
| String responseText = null; |
| |
| try { |
| if (errorCode == BaseCmd.UNSUPPORTED_ACTION_ERROR || apiCommandParams == null || apiCommandParams.isEmpty()) { |
| responseName = "errorresponse"; |
| } else { |
| Object cmdObj = apiCommandParams.get("command"); |
| // cmd name can be null when "command" parameter is missing in the request |
| if (cmdObj != null) { |
| String cmdName = ((String[]) cmdObj)[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); |
| // Also copy over the IdentityProxy object List into this new apiResponse, from |
| // the exception caught. When invoked from handle(), the exception here can |
| // be either ServerApiException, PermissionDeniedException or InvalidParameterValue |
| // Exception. When invoked from ApiServlet's processRequest(), this can be |
| // a standard exception like NumberFormatException. We'll leave the standard ones alone. |
| if (ex != null) { |
| if (ex instanceof ServerApiException || ex instanceof PermissionDeniedException |
| || ex instanceof InvalidParameterValueException) { |
| // Cast the exception appropriately and retrieve the IdentityProxy |
| if (ex instanceof ServerApiException) { |
| ServerApiException ref = (ServerApiException) ex; |
| ArrayList<IdentityProxy> idList = ref.getIdProxyList(); |
| if (idList != null) { |
| for (int i=0; i < idList.size(); i++) { |
| IdentityProxy id = idList.get(i); |
| apiResponse.addProxyObject(id.getTableName(), id.getValue(), id.getidFieldName()); |
| } |
| } |
| // Also copy over the cserror code and the function/layer in which it was thrown. |
| apiResponse.setCSErrorCode(ref.getCSErrorCode()); |
| } else if (ex instanceof PermissionDeniedException) { |
| PermissionDeniedException ref = (PermissionDeniedException) ex; |
| ArrayList<IdentityProxy> idList = ref.getIdProxyList(); |
| if (idList != null) { |
| for (int i=0; i < idList.size(); i++) { |
| IdentityProxy id = idList.get(i); |
| apiResponse.addProxyObject(id.getTableName(), id.getValue(), id.getidFieldName()); |
| } |
| } |
| // Also copy over the cserror code and the function/layer in which it was thrown. |
| apiResponse.setCSErrorCode(ref.getCSErrorCode()); |
| } else if (ex instanceof InvalidParameterValueException) { |
| InvalidParameterValueException ref = (InvalidParameterValueException) ex; |
| ArrayList<IdentityProxy> idList = ref.getIdProxyList(); |
| if (idList != null) { |
| for (int i=0; i < idList.size(); i++) { |
| IdentityProxy id = idList.get(i); |
| apiResponse.addProxyObject(id.getTableName(), id.getValue(), id.getidFieldName()); |
| } |
| } |
| // Also copy over the cserror code and the function/layer in which it was thrown. |
| apiResponse.setCSErrorCode(ref.getCSErrorCode()); |
| } |
| } |
| } |
| SerializationContext.current().setUuidTranslation(true); |
| responseText = ApiResponseSerializer.toSerializedString(apiResponse, responseType); |
| |
| } catch (Exception e) { |
| s_logger.error("Exception responding to http request", e); |
| } |
| return responseText; |
| } |
| } |