package org.jolokia.jvmagent;
/*
* 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.
*/
import java.io.*;
import java.net.InetSocketAddress;
import java.net.URI;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.management.MalformedObjectNameException;
import javax.management.RuntimeMBeanException;
import javax.security.auth.Subject;
import com.sun.net.httpserver.*;
import org.jolokia.backend.BackendManager;
import org.jolokia.config.ConfigKey;
import org.jolokia.config.Configuration;
import org.jolokia.discovery.DiscoveryMulticastResponder;
import org.jolokia.http.HttpRequestHandler;
import org.jolokia.restrictor.*;
import org.jolokia.util.*;
import org.json.simple.JSONAware;
/**
* HttpHandler for handling a jolokia request
*
* @author roland
* @since Mar 3, 2010
*/
public class JolokiaHttpHandler implements HttpHandler {
// Backendmanager for doing request
private BackendManager backendManager;
// The HttpRequestHandler
private HttpRequestHandler requestHandler;
// Context of this request
private String context;
// Content type matching
private Pattern contentTypePattern = Pattern.compile(".*;\\s*charset=([^;,]+)\\s*.*");
// Configuration of this handler
private Configuration configuration;
// Formatted for formatting Date response headers
private final SimpleDateFormat rfc1123Format;
// Loghandler to use
private final LogHandler logHandler;
// Respond for discovery mc requests
private DiscoveryMulticastResponder discoveryMulticastResponder;
/**
* Create a new HttpHandler for processing HTTP request
*
* @param pConfig jolokia specific config tuning the processing behaviour
*/
public JolokiaHttpHandler(Configuration pConfig) {
this(pConfig, null);
}
/**
* Create a new HttpHandler for processing HTTP request
*
* @param pConfig jolokia specific config tuning the processing behaviour
* @param pLogHandler log-handler the log handler to use for jolokia
*/
public JolokiaHttpHandler(Configuration pConfig, LogHandler pLogHandler) {
configuration = pConfig;
context = pConfig.get(ConfigKey.AGENT_CONTEXT);
if (!context.endsWith("/")) {
context += "/";
}
rfc1123Format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
rfc1123Format.setTimeZone(TimeZone.getTimeZone("GMT"));
logHandler = pLogHandler != null ? pLogHandler : createLogHandler(pConfig.get(ConfigKey.LOGHANDLER_CLASS), pConfig.get(ConfigKey.DEBUG));
}
/**
* Start the handler
*
* @param pLazy whether initialisation should be done lazy.
*/
public void start(boolean pLazy) {
Restrictor restrictor = createRestrictor(configuration);
backendManager = new BackendManager(configuration, logHandler, restrictor, pLazy);
requestHandler = new HttpRequestHandler(configuration, backendManager, logHandler);
if (listenForDiscoveryMcRequests(configuration)) {
try {
discoveryMulticastResponder = new DiscoveryMulticastResponder(backendManager, restrictor, logHandler);
discoveryMulticastResponder.start();
} catch (IOException e) {
logHandler.error("Cannot start discovery multicast handler: " + e, e);
}
}
}
private boolean listenForDiscoveryMcRequests(Configuration pConfig) {
String enable = pConfig.get(ConfigKey.DISCOVERY_ENABLED);
String url = pConfig.get(ConfigKey.DISCOVERY_AGENT_URL);
return url != null || enable == null || Boolean.valueOf(enable);
}
/**
* Start the handler and remember connection details which are useful for discovery messages
*
* @param pLazy whether initialisation should be done lazy.
* @param pUrl agent URL
* @param pSecured whether the communication is secured or not
*/
public void start(boolean pLazy, String pUrl, boolean pSecured) {
start(pLazy);
backendManager.getAgentDetails().updateAgentParameters(pUrl, pSecured);
}
/**
* Stop the handler
*/
public void stop() {
if (discoveryMulticastResponder != null) {
discoveryMulticastResponder.stop();
discoveryMulticastResponder = null;
}
backendManager.destroy();
backendManager = null;
requestHandler = null;
}
/**
* Handle a request. If the handler is not yet started, an exception is thrown. If running with JAAS
* security enabled it will run as the given subject.
*
* @param pHttpExchange the request/response object
* @throws IOException if something fails during handling
* @throws IllegalStateException if the handler has not yet been started
*/
public void handle(final HttpExchange pHttpExchange) throws IOException {
Subject subject = (Subject) pHttpExchange.getAttribute(ConfigKey.JAAS_SUBJECT_REQUEST_ATTRIBUTE);
if (subject!=null) {
try {
Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
public Void run() throws IOException {
doHandle(pHttpExchange);
return null;
}
});
} catch (PrivilegedActionException e) {
throw new SecurityException("Security exception: " + e.getCause(),e.getCause());
}
} else {
doHandle(pHttpExchange);
}
}
@SuppressWarnings({"PMD.AvoidCatchingThrowable", "PMD.AvoidInstanceofChecksInCatchClause"})
public void doHandle(HttpExchange pExchange) throws IOException {
if (requestHandler == null) {
throw new IllegalStateException("Handler not yet started");
}
JSONAware json = null;
URI uri = pExchange.getRequestURI();
ParsedUri parsedUri = new ParsedUri(uri,context);
try {
// Check access policy
InetSocketAddress address = pExchange.getRemoteAddress();
requestHandler.checkAccess(address.getHostName(), address.getAddress().getHostAddress(),
extractOriginOrReferer(pExchange));
String method = pExchange.getRequestMethod();
// Dispatch for the proper HTTP request method
if ("GET".equalsIgnoreCase(method)) {
setHeaders(pExchange);
json = executeGetRequest(parsedUri);
} else if ("POST".equalsIgnoreCase(method)) {
setHeaders(pExchange);
json = executePostRequest(pExchange, parsedUri);
} else if ("OPTIONS".equalsIgnoreCase(method)) {
performCorsPreflightCheck(pExchange);
} else {
throw new IllegalArgumentException("HTTP Method " + method + " is not supported.");
}
} catch (Throwable exp) {
json = requestHandler.handleThrowable(
exp instanceof RuntimeMBeanException ? ((RuntimeMBeanException) exp).getTargetException() : exp);
} finally {
sendResponse(pExchange,parsedUri,json);
}
}
// ========================================================================
private Restrictor createRestrictor(Configuration pConfig) {
String location = NetworkUtil.replaceExpression(pConfig.get(ConfigKey.POLICY_LOCATION));
try {
Restrictor ret = RestrictorFactory.lookupPolicyRestrictor(location);
if (ret != null) {
logHandler.info("Using access restrictor " + location);
return ret;
} else {
logHandler.info("No access restrictor found, access to all MBean is allowed");
return new AllowAllRestrictor();
}
} catch (IOException e) {
logHandler.error("Error while accessing access restrictor at " + location +
". Denying all access to MBeans for security reasons. Exception: " + e, e);
return new DenyAllRestrictor();
}
}
// Used for checking origin or referer is an origin policy is enabled
private String extractOriginOrReferer(HttpExchange pExchange) {
Headers headers = pExchange.getRequestHeaders();
String origin = headers.getFirst("Origin");
if (origin == null) {
origin = headers.getFirst("Referer");
}
return origin != null ? origin.replaceAll("[\\n\\r]*","") : null;
}
private JSONAware executeGetRequest(ParsedUri parsedUri) {
return requestHandler.handleGetRequest(parsedUri.getUri().toString(),parsedUri.getPathInfo(), parsedUri.getParameterMap());
}
private JSONAware executePostRequest(HttpExchange pExchange, ParsedUri pUri) throws MalformedObjectNameException, IOException {
String encoding = null;
Headers headers = pExchange.getRequestHeaders();
String cType = headers.getFirst("Content-Type");
if (cType != null) {
Matcher matcher = contentTypePattern.matcher(cType);
if (matcher.matches()) {
encoding = matcher.group(1);
}
}
InputStream is = pExchange.getRequestBody();
return requestHandler.handlePostRequest(pUri.toString(),is, encoding, pUri.getParameterMap());
}
private void performCorsPreflightCheck(HttpExchange pExchange) {
Headers requestHeaders = pExchange.getRequestHeaders();
Map<String,String> respHeaders =
requestHandler.handleCorsPreflightRequest(requestHeaders.getFirst("Origin"),
requestHeaders.getFirst("Access-Control-Request-Headers"));
Headers responseHeaders = pExchange.getResponseHeaders();
for (Map.Entry<String,String> entry : respHeaders.entrySet()) {
responseHeaders.set(entry.getKey(), entry.getValue());
}
}
private void setHeaders(HttpExchange pExchange) {
String origin = requestHandler.extractCorsOrigin(pExchange.getRequestHeaders().getFirst("Origin"));
Headers headers = pExchange.getResponseHeaders();
if (origin != null) {
headers.set("Access-Control-Allow-Origin",origin);
headers.set("Access-Control-Allow-Credentials","true");
}
// Avoid caching at all costs
headers.set("Cache-Control", "no-cache");
headers.set("Pragma","no-cache");
// Check for a date header and set it accordingly to the recommendations of
// RFC-2616. See also {@link AgentServlet#setNoCacheHeaders()}
// Issue: #71
Calendar cal = Calendar.getInstance();
headers.set("Date",rfc1123Format.format(cal.getTime()));
// 1h in the past since it seems, that some servlet set the date header on their
// own so that it cannot be guaranteed that these headers are really equals.
// It happened on Tomcat that "Date:" was finally set *before* "Expires:" in the final
// answers sometimes which seems to be an implementation peculiarity from Tomcat
cal.add(Calendar.HOUR, -1);
headers.set("Expires",rfc1123Format.format(cal.getTime()));
}
private void sendResponse(HttpExchange pExchange, ParsedUri pParsedUri, JSONAware pJson) throws IOException {
OutputStream out = null;
try {
Headers headers = pExchange.getResponseHeaders();
if (pJson != null) {
headers.set("Content-Type", getMimeType(pParsedUri) + "; charset=utf-8");
String json = pJson.toJSONString();
String callback = pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue());
String content = callback == null ? json : callback + "(" + json + ");";
byte[] response = content.getBytes("UTF8");
pExchange.sendResponseHeaders(200,response.length);
out = pExchange.getResponseBody();
out.write(response);
} else {
headers.set("Content-Type", "text/plain");
pExchange.sendResponseHeaders(200,-1);
}
} finally {
if (out != null) {
// Always close in order to finish the request.
// Otherwise the thread blocks.
out.close();
}
}
}
// Get the proper mime type according to configuration
private String getMimeType(ParsedUri pParsedUri) {
if (pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue()) != null) {
return "text/javascript";
} else {
String mimeType = pParsedUri.getParameter(ConfigKey.MIME_TYPE.getKeyValue());
if (mimeType != null) {
return mimeType;
}
mimeType = configuration.get(ConfigKey.MIME_TYPE);
return mimeType != null ? mimeType : ConfigKey.MIME_TYPE.getDefaultValue();
}
}
// Creat a log handler from either the given class or by creating a default log handler printing
// out to stderr
private LogHandler createLogHandler(String pLogHandlerClass, String pDebug) {
if (pLogHandlerClass != null) {
return ClassUtil.newInstance(pLogHandlerClass);
} else {
final boolean debug = Boolean.valueOf(pDebug);
return new LogHandler.StdoutLogHandler(debug);
}
}
}