cloudstack/server/src/com/cloud/api/ApiServer.java

916 lines
42 KiB
Java
Executable File

/**
* 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 <http://www.gnu.org/licenses/>.
*
*/
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.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.maid.StackMaid;
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<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 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>();
}
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<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);
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, 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);
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.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 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<String, String> 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);
if (account != null) {
job.setAccountId(ctx.getCaller().getId());
} else {
// Just have SYSTEM own the job for now. Users won't be able to see this job,
// but in an admin case (like domain admin) they won't be able to see it anyway
// so no loss of service.
job.setAccountId(1L);
}
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) {
validatePageSize((BaseListCmd)cmdObj);
buildAsyncListResponse((BaseListCmd)cmdObj, account);
}
return ApiResponseSerializer.toSerializedString((ResponseObject)cmdObj.getResponseObject(), cmdObj.getResponseType());
}
}
private void validatePageSize(BaseListCmd command) {
List<ResponseObject> 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<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 + " ");
auditTrailSb.append(result);
/*
if (command.equals("queryAsyncJobResult")){ //For this command we need to also log job status and job resultcode
for (Pair<String,Object> 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<String,Object> 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<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 = _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<String> parameterNames = new ArrayList<String>();
for (Object paramNameObj : requestParameters.keySet()) {
parameterNames.add((String)paramNameObj); // put the name in a list that we'll sort later
}
Collections.sort(parameterNames);
for (String paramName : parameterNames) {
// parameters come as name/value pairs in the form String/String[]
String paramValue = ((String[])requestParameters.get(paramName))[0];
if ("signature".equalsIgnoreCase(paramName)) {
signature = paramValue;
} else {
if ("apikey".equalsIgnoreCase(paramName)) {
apiKey = paramValue;
}
if (unsignedRequest == null) {
unsignedRequest = paramName + "=" + URLEncoder.encode(paramValue, "UTF-8").replaceAll("\\+", "%20");
} else {
unsignedRequest = unsignedRequest + "&" + paramName + "=" + URLEncoder.encode(paramValue, "UTF-8").replaceAll("\\+", "%20");
}
}
}
// if api/secret key are passed to the parameters
if ((signature == null) || (apiKey == null)) {
if (s_logger.isDebugEnabled()) {
s_logger.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<User, Account> 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<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 = _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("<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) {
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;
}
}