package org.jolokia.http;
import java.io.*;
import java.net.URLDecoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.management.*;
import org.jolokia.backend.BackendManager;
import org.jolokia.config.*;
import org.jolokia.request.JmxRequest;
import org.jolokia.request.JmxRequestFactory;
import org.jolokia.util.LogHandler;
import org.json.simple.*;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
/*
* Copyright 2009-2013 Roland Huss
*
* Licensed 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.
*/
/**
* Request handler with no dependency on the servlet API so that it can be used in
* several different environments (like for the Sun JDK 6 {@link com.sun.net.httpserver.HttpServer}.
*
* @author roland
* @since Mar 3, 2010
*/
public class HttpRequestHandler {
// handler for contacting the MBean server(s)
private BackendManager backendManager;
// Logging abstraction
private LogHandler logHandler;
// Global configuration
private Configuration config;
/**
* Request handler for parsing HTTP request and dispatching to the appropriate
* request handler (with help of the backend manager)
*
* @param pBackendManager backend manager to user
* @param pLogHandler log handler to where to put out logging
*/
public HttpRequestHandler(Configuration pConfig, BackendManager pBackendManager, LogHandler pLogHandler) {
backendManager = pBackendManager;
logHandler = pLogHandler;
config = pConfig;
}
/**
* Handle a GET request
*
* @param pUri URI leading to this request
* @param pPathInfo path of the request
* @param pParameterMap parameters of the GET request @return the response
*/
public JSONAware handleGetRequest(String pUri, String pPathInfo, Map<String, String[]> pParameterMap) {
String pathInfo = extractPathInfo(pUri, pPathInfo);
JmxRequest jmxReq =
JmxRequestFactory.createGetRequest(pathInfo,getProcessingParameter(pParameterMap));
if (backendManager.isDebug()) {
logHandler.debug("URI: " + pUri);
logHandler.debug("Path-Info: " + pathInfo);
logHandler.debug("Request: " + jmxReq.toString());
}
return executeRequest(jmxReq);
}
private ProcessingParameters getProcessingParameter(Map<String, String[]> pParameterMap) {
Map<String,String> ret = new HashMap<String, String>();
if (pParameterMap != null) {
for (Map.Entry<String,String[]> entry : pParameterMap.entrySet()) {
String values[] = entry.getValue();
if (values != null && values.length > 0) {
ret.put(entry.getKey(), values[0]);
}
}
}
return config.getProcessingParameters(ret);
}
/**
* Handle the input stream as given by a POST request
*
*
* @param pUri URI leading to this request
* @param pInputStream input stream of the post request
* @param pEncoding optional encoding for the stream. If null, the default encoding is used
* @param pParameterMap additional processing parameters
* @return the JSON object containing the json results for one or more {@link JmxRequest} contained
* within the answer.
*
* @throws IOException if reading from the input stream fails
*/
public JSONAware handlePostRequest(String pUri, InputStream pInputStream, String pEncoding, Map<String, String[]> pParameterMap)
throws IOException {
if (backendManager.isDebug()) {
logHandler.debug("URI: " + pUri);
}
Object jsonRequest = extractJsonRequest(pInputStream,pEncoding);
if (jsonRequest instanceof JSONArray) {
List<JmxRequest> jmxRequests = JmxRequestFactory.createPostRequests((List) jsonRequest,getProcessingParameter(pParameterMap));
JSONArray responseList = new JSONArray();
for (JmxRequest jmxReq : jmxRequests) {
if (backendManager.isDebug()) {
logHandler.debug("Request: " + jmxReq.toString());
}
// Call handler and retrieve return value
JSONObject resp = executeRequest(jmxReq);
responseList.add(resp);
}
return responseList;
} else if (jsonRequest instanceof JSONObject) {
JmxRequest jmxReq = JmxRequestFactory.createPostRequest((Map<String, ?>) jsonRequest,getProcessingParameter(pParameterMap));
return executeRequest(jmxReq);
} else {
throw new IllegalArgumentException("Invalid JSON Request " + jsonRequest);
}
}
/**
* Handling an option request which is used for preflight checks before a CORS based browser request is
* sent (for certain circumstances).
*
* See the <a href="http://www.w3.org/TR/cors/">CORS specification</a>
* (section 'preflight checks') for more details.
*
* @param pOrigin the origin to check. If <code>null</code>, no headers are returned
* @param pRequestHeaders extra headers to check against
* @return headers to set
*/
public Map<String, String> handleCorsPreflightRequest(String pOrigin, String pRequestHeaders) {
Map<String,String> ret = new HashMap<String, String>();
if (pOrigin != null && backendManager.isOriginAllowed(pOrigin,false)) {
// CORS is allowed, we set exactly the origin in the header, so there are no problems with authentication
ret.put("Access-Control-Allow-Origin","null".equals(pOrigin) ? "*" : pOrigin);
if (pRequestHeaders != null) {
ret.put("Access-Control-Allow-Headers",pRequestHeaders);
}
// Fix for CORS with authentication (#104)
ret.put("Access-Control-Allow-Credentials","true");
// Allow for one year. Changes in access.xml are reflected directly in the cors request itself
ret.put("Access-Control-Allow-Max-Age","" + 3600 * 24 * 365);
}
return ret;
}
private Object extractJsonRequest(InputStream pInputStream, String pEncoding) throws IOException {
InputStreamReader reader = null;
try {
reader =
pEncoding != null ?
new InputStreamReader(pInputStream, pEncoding) :
new InputStreamReader(pInputStream);
JSONParser parser = new JSONParser();
return parser.parse(reader);
} catch (ParseException exp) {
throw new IllegalArgumentException("Invalid JSON request " + reader,exp);
}
}
/**
* Execute a single {@link JmxRequest}. If a checked exception occurs,
* this gets translated into the appropriate JSON object which will get returned.
* Note, that these exceptions gets *not* translated into an HTTP error, since they are
* supposed <em>Jolokia</em> specific errors above the transport layer.
*
* @param pJmxReq the request to execute
* @return the JSON representation of the answer.
*/
private JSONObject executeRequest(JmxRequest pJmxReq) {
// Call handler and retrieve return value
try {
return backendManager.handleRequest(pJmxReq);
} catch (ReflectionException e) {
return getErrorJSON(404,e, pJmxReq);
} catch (InstanceNotFoundException e) {
return getErrorJSON(404,e, pJmxReq);
} catch (MBeanException e) {
return getErrorJSON(500,e.getTargetException(), pJmxReq);
} catch (AttributeNotFoundException e) {
return getErrorJSON(404,e, pJmxReq);
} catch (UnsupportedOperationException e) {
return getErrorJSON(500,e, pJmxReq);
} catch (IOException e) {
return getErrorJSON(500,e, pJmxReq);
} catch (IllegalArgumentException e) {
return getErrorJSON(400,e, pJmxReq);
} catch (SecurityException e) {
// Wipe out stacktrace
return getErrorJSON(403,new Exception(e.getMessage()), pJmxReq);
} catch (RuntimeMBeanException e) {
// Use wrapped exception
return errorForUnwrappedException(e,pJmxReq);
}
}
/**
* Utility method for handling single runtime exceptions and errors. This method is called
* in addition to and after {@link #executeRequest(JmxRequest)} to catch additional errors.
* They are two different methods because of bulk requests, where each individual request can
* lead to an error. So, each individual request is wrapped with the error handling of
* {@link #executeRequest(JmxRequest)}
* whereas the overall handling is wrapped with this method. It is hence more coarse grained,
* leading typically to an status code of 500.
*
* Summary: This method should be used as last security belt is some exception should escape
* from a single request processing in {@link #executeRequest(JmxRequest)}.
*
* @param pThrowable exception to handle
* @return its JSON representation
*/
public JSONObject handleThrowable(Throwable pThrowable) {
if (pThrowable instanceof IllegalArgumentException) {
return getErrorJSON(400,pThrowable, null);
} else if (pThrowable instanceof SecurityException) {
// Wipe out stacktrace
return getErrorJSON(403,new Exception(pThrowable.getMessage()), null);
} else {
return getErrorJSON(500,pThrowable, null);
}
}
/**
* Get the JSON representation for a an exception
*
*
* @param pErrorCode the HTTP error code to return
* @param pExp the exception or error occured
* @param pJmxReq request from where to get processing options
* @return the json representation
*/
public JSONObject getErrorJSON(int pErrorCode, Throwable pExp, JmxRequest pJmxReq) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("status",pErrorCode);
jsonObject.put("error",getExceptionMessage(pExp));
jsonObject.put("error_type", pExp.getClass().getName());
addErrorInfo(jsonObject, pExp, pJmxReq);
if (backendManager.isDebug()) {
backendManager.error("Error " + pErrorCode,pExp);
}
if (pJmxReq != null) {
jsonObject.put("request",pJmxReq.toJSON());
}
return jsonObject;
}
/**
* Check whether the given host and/or address is allowed to access this agent.
*
* @param pHost host to check
* @param pAddress address to check
* @param pOrigin (optional) origin header to check also.
*/
public void checkAccess(String pHost, String pAddress, String pOrigin) {
if (!backendManager.isRemoteAccessAllowed(pHost, pAddress)) {
throw new SecurityException("No access from client " + pAddress + " allowed");
}
if (pOrigin != null && !backendManager.isOriginAllowed(pOrigin,true)) {
throw new SecurityException("Origin " + pOrigin + " is not allowed to call this agent");
}
}
/**
* Check whether for the given host is a cross-browser request allowed. This check is delegated to the
* backendmanager which is responsible for the security configuration.
* Also, some sanity checks are applied.
*
* @param pOrigin the origin URL to check against
* @return the origin to put in the response header or null if none is to be set
*/
public String extractCorsOrigin(String pOrigin) {
if (pOrigin != null) {
// Prevent HTTP response splitting attacks
String origin = pOrigin.replaceAll("[\\n\\r]*","");
if (backendManager.isOriginAllowed(origin,false)) {
return "null".equals(origin) ? "*" : origin;
} else {
return null;
}
}
return null;
}
private void addErrorInfo(JSONObject pErrorResp, Throwable pExp, JmxRequest pJmxReq) {
if (config.getAsBoolean(ConfigKey.ALLOW_ERROR_DETAILS)) {
String includeStackTrace = pJmxReq != null ?
pJmxReq.getParameter(ConfigKey.INCLUDE_STACKTRACE) : "true";
if (includeStackTrace.equalsIgnoreCase("true") ||
(includeStackTrace.equalsIgnoreCase("runtime") && pExp instanceof RuntimeException)) {
StringWriter writer = new StringWriter();
pExp.printStackTrace(new PrintWriter(writer));
pErrorResp.put("stacktrace", writer.toString());
}
if (pJmxReq != null && pJmxReq.getParameterAsBool(ConfigKey.SERIALIZE_EXCEPTION)) {
pErrorResp.put("error_value", backendManager.convertExceptionToJson(pExp, pJmxReq));
}
}
}
// Extract class and exception message for an error message
private String getExceptionMessage(Throwable pException) {
String message = pException.getLocalizedMessage();
return pException.getClass().getName() + (message != null ? " : " + message : "");
}
// Unwrap an exception to get to the 'real' exception
// and extract the error code accordingly
private JSONObject errorForUnwrappedException(Exception e, JmxRequest pJmxReq) {
Throwable cause = e.getCause();
int code = cause instanceof IllegalArgumentException ? 400 : cause instanceof SecurityException ? 403 : 500;
return getErrorJSON(code,cause, pJmxReq);
}
// Path info might need some special handling in case when the URL
// contains two following slashes. These slashes get collapsed
// when calling getPathInfo() but are still present in the URI.
// This situation can happen, when slashes are escaped and the last char
// of an path part is such an escaped slash
// (e.g. "read/domain:type=name!//attribute")
// In this case, we extract the path info on our own
private static final Pattern PATH_PREFIX_PATTERN = Pattern.compile("^/?[^/]+/");
private String extractPathInfo(String pUri, String pPathInfo) {
if (pUri.contains("!//")) {
// Special treatment for trailing slashes in paths
Matcher matcher = PATH_PREFIX_PATTERN.matcher(pPathInfo);
if (matcher.find()) {
String prefix = matcher.group();
String pathInfoEncoded = pUri.replaceFirst("^.*?" + prefix, prefix);
try {
return URLDecoder.decode(pathInfoEncoded, "UTF-8");
} catch (UnsupportedEncodingException e) {
// Should not happen at all ... so we silently fall through
}
}
}
return pPathInfo;
}
}