mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
916 lines
42 KiB
Java
Executable File
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;
|
|
}
|
|
}
|