blob: 4002ff8d99b1946a973e0308693da8f12586955d [file] [log] [blame]
// 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.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiServerService;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.managed.context.ManagedContext;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.User;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.StringUtils;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.net.NetUtils;
@Component("apiServlet")
@SuppressWarnings("serial")
public class ApiServlet extends HttpServlet {
public static final Logger s_logger = Logger.getLogger(ApiServlet.class.getName());
private static final Logger s_accessLogger = Logger.getLogger("apiserver." + ApiServer.class.getName());
private final static List<String> s_clientAddressHeaders = Collections
.unmodifiableList(Arrays.asList("X-Forwarded-For",
"HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR", "Remote_Addr"));
@Inject
ApiServerService apiServer;
@Inject
AccountService accountMgr;
@Inject
EntityManager entityMgr;
@Inject
ManagedContext managedContext;
@Inject
APIAuthenticationManager authManager;
public ApiServlet() {
}
@Override
public void init(final ServletConfig config) throws ServletException {
SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, config.getServletContext());
}
@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) {
processRequest(req, resp);
}
@Override
protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) {
processRequest(req, resp);
}
void utf8Fixup(final HttpServletRequest req, final Map<String, Object[]> params) {
if (req.getQueryString() == null) {
return;
}
final String[] paramsInQueryString = req.getQueryString().split("&");
if (paramsInQueryString != null) {
for (final String param : paramsInQueryString) {
final String[] paramTokens = param.split("=", 2);
if (paramTokens.length == 2) {
String name = decodeUtf8(paramTokens[0]);
String value = decodeUtf8(paramTokens[1]);
params.put(name, new String[] {value});
} else {
s_logger.debug("Invalid parameter in URL found. param: " + param);
}
}
}
}
private String decodeUtf8(final String value) {
try {
return URLDecoder.decode(value, "UTF-8");
} catch (final UnsupportedEncodingException e) {
//should never happen
return null;
}
}
private void processRequest(final HttpServletRequest req, final HttpServletResponse resp) {
managedContext.runWithContext(new Runnable() {
@Override
public void run() {
processRequestInContext(req, resp);
}
});
}
void processRequestInContext(final HttpServletRequest req, final HttpServletResponse resp) {
InetAddress remoteAddress = null;
try {
remoteAddress = getClientAddress(req);
} catch (UnknownHostException e) {
s_logger.warn("UnknownHostException when trying to lookup remote IP-Address. This should never happen. Blocking request.", e);
final String response = apiServer.getSerializedApiError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"UnknownHostException when trying to lookup remote IP-Address", null,
HttpUtils.RESPONSE_TYPE_XML);
HttpUtils.writeHttpResponse(resp, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
HttpUtils.RESPONSE_TYPE_XML, ApiServer.JSONcontentType.value());
return;
}
final StringBuilder auditTrailSb = new StringBuilder(128);
auditTrailSb.append(" ").append(remoteAddress.getHostAddress());
auditTrailSb.append(" -- ").append(req.getMethod()).append(' ');
// get the response format since we'll need it in a couple of places
String responseType = HttpUtils.RESPONSE_TYPE_XML;
final Map<String, Object[]> params = new HashMap<String, Object[]>();
params.putAll(req.getParameterMap());
// For HTTP GET requests, it seems that HttpServletRequest.getParameterMap() actually tries
// to unwrap URL encoded content from ISO-9959-1.
// After failed in using setCharacterEncoding() to control it, end up with following hacking:
// for all GET requests, we will override it with our-own way of UTF-8 based URL decoding.
utf8Fixup(req, params);
// logging the request start and end in management log for easy debugging
String reqStr = "";
String cleanQueryString = StringUtils.cleanString(req.getQueryString());
if (s_logger.isDebugEnabled()) {
reqStr = auditTrailSb.toString() + " " + cleanQueryString;
s_logger.debug("===START=== " + reqStr);
}
try {
if (HttpUtils.RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) {
resp.setContentType(ApiServer.JSONcontentType.value());
} else if (HttpUtils.RESPONSE_TYPE_XML.equalsIgnoreCase(responseType)){
resp.setContentType(HttpUtils.XML_CONTENT_TYPE);
}
HttpSession session = req.getSession(false);
final Object[] responseTypeParam = params.get(ApiConstants.RESPONSE);
if (responseTypeParam != null) {
responseType = (String)responseTypeParam[0];
}
final Object[] commandObj = params.get(ApiConstants.COMMAND);
if (commandObj != null) {
final String command = (String) commandObj[0];
APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command);
if (apiAuthenticator != null) {
auditTrailSb.append("command=");
auditTrailSb.append(command);
int httpResponseCode = HttpServletResponse.SC_OK;
String responseString = null;
if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGIN_API) {
if (session != null) {
try {
session.invalidate();
} catch (final IllegalStateException ise) {
}
}
session = req.getSession(true);
if (ApiServer.EnableSecureSessionCookie.value()) {
resp.setHeader("SET-COOKIE", String.format("JSESSIONID=%s;Secure;HttpOnly;Path=/client", session.getId()));
if (s_logger.isDebugEnabled()) {
s_logger.debug("Session cookie is marked secure!");
}
}
}
try {
responseString = apiAuthenticator.authenticate(command, params, session, remoteAddress, responseType, auditTrailSb, req, resp);
if (session != null && session.getAttribute(ApiConstants.SESSIONKEY) != null) {
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly", ApiConstants.SESSIONKEY, session.getAttribute(ApiConstants.SESSIONKEY)));
}
} catch (ServerApiException e) {
httpResponseCode = e.getErrorCode().getHttpCode();
responseString = e.getMessage();
s_logger.debug("Authentication failure: " + e.getMessage());
}
if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) {
if (session != null) {
final Long userId = (Long) session.getAttribute("userid");
final Account account = (Account) session.getAttribute("accountobj");
Long accountId = null;
if (account != null) {
accountId = account.getId();
}
auditTrailSb.insert(0, "(userId=" + userId + " accountId=" + accountId + " sessionId=" + session.getId() + ")");
if (userId != null) {
apiServer.logoutUser(userId);
}
try {
session.invalidate();
} catch (final IllegalStateException ignored) {
}
}
Cookie sessionKeyCookie = new Cookie(ApiConstants.SESSIONKEY, "");
sessionKeyCookie.setMaxAge(0);
resp.addCookie(sessionKeyCookie);
}
HttpUtils.writeHttpResponse(resp, responseString, httpResponseCode, responseType, ApiServer.JSONcontentType.value());
return;
}
}
auditTrailSb.append(cleanQueryString);
final boolean isNew = ((session == null) ? true : session.isNew());
// Initialize an empty context and we will update it after we have verified the request below,
// we no longer rely on web-session here, verifyRequest will populate user/account information
// if a API key exists
Long userId = null;
if (!isNew) {
userId = (Long)session.getAttribute("userid");
final String account = (String) session.getAttribute("account");
final Object accountObj = session.getAttribute("accountobj");
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) {
try {
session.invalidate();
} catch (final IllegalStateException ise) {
}
auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "unable to verify user credentials");
final String serializedResponse =
apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "unable to verify user credentials", params, responseType);
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.JSONcontentType.value());
return;
}
// Do a sanity check here to make sure the user hasn't already been deleted
if ((userId != null) && (account != null) && (accountObj != null) && apiServer.verifyUser(userId)) {
final String[] command = (String[])params.get(ApiConstants.COMMAND);
if (command == null) {
s_logger.info("missing command, ignoring request...");
auditTrailSb.append(" " + HttpServletResponse.SC_BAD_REQUEST + " " + "no command specified");
final String serializedResponse = apiServer.getSerializedApiError(HttpServletResponse.SC_BAD_REQUEST, "no command specified", params, responseType);
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_BAD_REQUEST, responseType, ApiServer.JSONcontentType.value());
return;
}
final User user = entityMgr.findById(User.class, userId);
CallContext.register(user, (Account)accountObj);
} else {
// Invalidate the session to ensure we won't allow a request across management server
// restarts if the userId was serialized to the stored session
try {
session.invalidate();
} catch (final IllegalStateException ise) {
}
auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "unable to verify user credentials");
final String serializedResponse =
apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "unable to verify user credentials", params, responseType);
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.JSONcontentType.value());
return;
}
} else {
CallContext.register(accountMgr.getSystemUser(), accountMgr.getSystemAccount());
}
if (apiServer.verifyRequest(params, userId, remoteAddress)) {
auditTrailSb.insert(0, "(userId=" + CallContext.current().getCallingUserId() + " accountId=" + CallContext.current().getCallingAccount().getId() +
" sessionId=" + (session != null ? session.getId() : null) + ")");
// Add the HTTP method (GET/POST/PUT/DELETE) as well into the params map.
params.put("httpmethod", new String[]{req.getMethod()});
final String response = apiServer.handleRequest(params, responseType, auditTrailSb);
HttpUtils.writeHttpResponse(resp, response != null ? response : "", HttpServletResponse.SC_OK, responseType, ApiServer.JSONcontentType.value());
} else {
if (session != null) {
try {
session.invalidate();
} catch (final IllegalStateException ise) {
}
}
auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "unable to verify user credentials and/or request signature");
final String serializedResponse =
apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "unable to verify user credentials and/or request signature", params,
responseType);
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.JSONcontentType.value());
}
} catch (final ServerApiException se) {
final String serializedResponseText = apiServer.getSerializedApiError(se, params, responseType);
resp.setHeader("X-Description", se.getDescription());
HttpUtils.writeHttpResponse(resp, serializedResponseText, se.getErrorCode().getHttpCode(), responseType, ApiServer.JSONcontentType.value());
auditTrailSb.append(" " + se.getErrorCode() + " " + se.getDescription());
} catch (final Exception ex) {
s_logger.error("unknown exception writing api response", ex);
auditTrailSb.append(" unknown exception writing api response");
} finally {
s_accessLogger.info(auditTrailSb.toString());
if (s_logger.isDebugEnabled()) {
s_logger.debug("===END=== " + reqStr);
}
// cleanup user context to prevent from being peeked in other request context
CallContext.unregister();
}
}
//This method will try to get login IP of user even if servlet is behind reverseProxy or loadBalancer
static InetAddress getClientAddress(final HttpServletRequest request) throws UnknownHostException {
for(final String header : s_clientAddressHeaders) {
final String ip = getCorrectIPAddress(request.getHeader(header));
if (ip != null) {
return InetAddress.getByName(ip);
}
}
return InetAddress.getByName(request.getRemoteAddr());
}
private static String getCorrectIPAddress(String ip) {
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
return null;
}
if(NetUtils.isValidIp4(ip) || NetUtils.isValidIp6(ip)) {
return ip;
}
//it could be possible to have multiple IPs in HTTP header, this happens if there are multiple proxy in between
//the client and the servlet, so parse the client IP
String[] ips = ip.split(",");
for(String i : ips) {
if(NetUtils.isValidIp4(i.trim()) || NetUtils.isValidIp6(i.trim())) {
return i.trim();
}
}
return null;
}
}