org.jolokia.http.AgentServlet Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jolokia-core Show documentation
Show all versions of jolokia-core Show documentation
jar file containing servlet and helper classes
package org.jolokia.http;
import java.io.*;
import java.net.*;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.*;
import javax.management.RuntimeMBeanException;
import javax.security.auth.Subject;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jolokia.backend.BackendManager;
import org.jolokia.config.*;
import org.jolokia.discovery.AgentDetails;
import org.jolokia.discovery.DiscoveryMulticastResponder;
import org.jolokia.restrictor.*;
import org.jolokia.util.*;
import org.json.simple.JSONAware;
/*
* 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.
*/
/**
* Agent servlet which connects to a local JMX MBeanServer for
* JMX operations.
*
*
* It uses a REST based approach which translates a GET Url into a
* request. See the reference documentation
* for a detailed description of this servlet's features.
*
*
* @author [email protected]
* @since Apr 18, 2009
*/
public class AgentServlet extends HttpServlet {
private static final long serialVersionUID = 42L;
// POST- and GET- HttpRequestHandler
private ServletRequestHandler httpGetHandler, httpPostHandler;
// Backend dispatcher
private BackendManager backendManager;
// Used for logging
private LogHandler logHandler;
// Request handler for parsing request parameters and building up a response
private HttpRequestHandler requestHandler;
// Restrictor to use as given in the constructor
private Restrictor restrictor;
// Mime type used for returning the answer
private String configMimeType;
// Listen for discovery request (if switched on)
private DiscoveryMulticastResponder discoveryMulticastResponder;
// whether to allow reverse DNS lookup for checking the remote host
private boolean allowDnsReverseLookup;
/**
* No argument constructor, used e.g. by an servlet
* descriptor when creating the servlet out of web.xml
*/
public AgentServlet() {
this(null);
}
/**
* Constructor taking a restrictor to use
*
* @param pRestrictor restrictor to use or null
if the restrictor
* should be created in the default way ({@link RestrictorFactory#createRestrictor(Configuration,LogHandler)})
*/
public AgentServlet(Restrictor pRestrictor) {
restrictor = pRestrictor;
}
/**
* Get the installed log handler
*
* @return loghandler used for logging.
*/
protected LogHandler getLogHandler() {
return logHandler;
}
/**
* Initialize the backend systems, the log handler and the restrictor. A subclass can tune
* this step by overriding {@link RestrictorFactory#createRestrictor(Configuration,LogHandler)} and {@link #createLogHandler(ServletConfig, boolean)}
*
* @param pServletConfig servlet configuration
*/
@Override
public void init(ServletConfig pServletConfig) throws ServletException {
super.init(pServletConfig);
Configuration config = initConfig(pServletConfig);
// Create a log handler early in the lifecycle, but not too early
String logHandlerClass = config.get(ConfigKey.LOGHANDLER_CLASS);
logHandler = logHandlerClass != null ?
(LogHandler) ClassUtil.newInstance(logHandlerClass) :
createLogHandler(pServletConfig,Boolean.valueOf(config.get(ConfigKey.DEBUG)));
// Different HTTP request handlers
httpGetHandler = newGetHttpRequestHandler();
httpPostHandler = newPostHttpRequestHandler();
if (restrictor == null) {
restrictor = RestrictorFactory.createRestrictor(config,logHandler);
} else {
logHandler.info("Using custom access restriction provided by " + restrictor);
}
configMimeType = config.get(ConfigKey.MIME_TYPE);
backendManager = new BackendManager(config,logHandler, restrictor);
requestHandler = new HttpRequestHandler(config,backendManager,logHandler);
allowDnsReverseLookup = config.getAsBoolean(ConfigKey.ALLOW_DNS_REVERSE_LOOKUP);
initDiscoveryMulticast(config);
}
private void initDiscoveryMulticast(Configuration pConfig) {
String url = findAgentUrl(pConfig);
if (url != null || listenForDiscoveryMcRequests(pConfig)) {
backendManager.getAgentDetails().setUrl(url);
try {
discoveryMulticastResponder = new DiscoveryMulticastResponder(backendManager,restrictor,logHandler);
discoveryMulticastResponder.start();
} catch (IOException e) {
logHandler.error("Cannot start discovery multicast handler: " + e,e);
}
}
}
// Try to find an URL for system props or config
private String findAgentUrl(Configuration pConfig) {
// System property has precedence
String url = System.getProperty("jolokia." + ConfigKey.DISCOVERY_AGENT_URL.getKeyValue());
if (url == null) {
url = System.getenv("JOLOKIA_DISCOVERY_AGENT_URL");
if (url == null) {
url = pConfig.get(ConfigKey.DISCOVERY_AGENT_URL);
}
}
return NetworkUtil.replaceExpression(url);
}
// For war agent needs to be switched on
private boolean listenForDiscoveryMcRequests(Configuration pConfig) {
// Check for system props, system env and agent config
boolean sysProp = System.getProperty("jolokia." + ConfigKey.DISCOVERY_ENABLED.getKeyValue()) != null;
boolean env = System.getenv("JOLOKIA_DISCOVERY") != null;
boolean config = pConfig.getAsBoolean(ConfigKey.DISCOVERY_ENABLED);
return sysProp || env || config;
}
/**
* Create a log handler using this servlet's logging facility for logging. This method can be overridden
* to provide a custom log handler. This method is called before {@link RestrictorFactory#createRestrictor(Configuration,LogHandler)} so the log handler
* can already be used when building up the restrictor.
*
* @return a default log handler
* @param pServletConfig servlet config from where to get information to build up the log handler
* @param pDebug whether to print out debug information.
*/
protected LogHandler createLogHandler(ServletConfig pServletConfig, final boolean pDebug) {
return new LogHandler() {
/** {@inheritDoc} */
public void debug(String message) {
if (pDebug) {
log(message);
}
}
/** {@inheritDoc} */
public void info(String message) {
log(message);
}
/** {@inheritDoc} */
public void error(String message, Throwable t) {
log(message,t);
}
};
}
/** {@inheritDoc} */
@Override
public void destroy() {
backendManager.destroy();
if (discoveryMulticastResponder != null) {
discoveryMulticastResponder.stop();
discoveryMulticastResponder = null;
}
super.destroy();
}
/** {@inheritDoc} */
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
handle(httpGetHandler, req, resp);
}
/** {@inheritDoc} */
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
handle(httpPostHandler, req, resp);
}
/**
* OPTION requests are treated as CORS preflight requests
*
* @param req the original request
* @param resp the response the answer are written to
* */
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Map responseHeaders =
requestHandler.handleCorsPreflightRequest(
req.getHeader("Origin"),
req.getHeader("Access-Control-Request-Headers"));
for (Map.Entry entry : responseHeaders.entrySet()) {
resp.setHeader(entry.getKey(),entry.getValue());
}
}
@SuppressWarnings({ "PMD.AvoidCatchingThrowable", "PMD.AvoidInstanceofChecksInCatchClause" })
private void handle(ServletRequestHandler pReqHandler,HttpServletRequest pReq, HttpServletResponse pResp) throws IOException {
JSONAware json = null;
try {
// Check access policy
requestHandler.checkAccess(allowDnsReverseLookup ? pReq.getRemoteHost() : null,
pReq.getRemoteAddr(),
getOriginOrReferer(pReq));
// Remember the agent URL upon the first request. Needed for discovery
updateAgentDetailsIfNeeded(pReq);
// Dispatch for the proper HTTP request method
json = handleSecurely(pReqHandler, pReq, pResp);
} catch (Throwable exp) {
json = requestHandler.handleThrowable(
exp instanceof RuntimeMBeanException ? ((RuntimeMBeanException) exp).getTargetException() : exp);
} finally {
setCorsHeader(pReq, pResp);
String callback = pReq.getParameter(ConfigKey.CALLBACK.getKeyValue());
String answer = json != null ?
json.toJSONString() :
requestHandler.handleThrowable(new Exception("Internal error while handling an exception")).toJSONString();
if (callback != null) {
// Send a JSONP response
sendResponse(pResp, "text/javascript", callback + "(" + answer + ");");
} else {
sendResponse(pResp, getMimeType(pReq),answer);
}
}
}
private JSONAware handleSecurely(final ServletRequestHandler pReqHandler, final HttpServletRequest pReq, final HttpServletResponse pResp) throws IOException, PrivilegedActionException {
Subject subject = (Subject) pReq.getAttribute(ConfigKey.JAAS_SUBJECT_REQUEST_ATTRIBUTE);
if (subject != null) {
return Subject.doAs(subject, new PrivilegedExceptionAction() {
public JSONAware run() throws IOException {
return pReqHandler.handleRequest(pReq, pResp);
}
});
} else {
return pReqHandler.handleRequest(pReq, pResp);
}
}
private String getOriginOrReferer(HttpServletRequest pReq) {
String origin = pReq.getHeader("Origin");
if (origin == null) {
origin = pReq.getHeader("Referer");
}
return origin != null ? origin.replaceAll("[\\n\\r]*","") : null;
}
// Update the agent URL in the agent details if not already done
private void updateAgentDetailsIfNeeded(HttpServletRequest pReq) {
// Lookup the Agent URL if needed
AgentDetails details = backendManager.getAgentDetails();
if (details.isInitRequired()) {
synchronized (details) {
if (details.isInitRequired()) {
if (details.isUrlMissing()) {
String url = getBaseUrl(NetworkUtil.sanitizeLocalUrl(pReq.getRequestURL().toString()),
extractServletPath(pReq));
details.setUrl(url);
}
if (details.isSecuredMissing()) {
details.setSecured(pReq.getAuthType() != null);
}
details.seal();
}
}
}
}
private String extractServletPath(HttpServletRequest pReq) {
return pReq.getRequestURI().substring(0,pReq.getContextPath().length());
}
// Strip off everything unneeded
private String getBaseUrl(String pUrl, String pServletPath) {
String sUrl;
try {
URL url = new URL(pUrl);
String host = getIpIfPossible(url.getHost());
sUrl = new URL(url.getProtocol(),host,url.getPort(),pServletPath).toExternalForm();
} catch (MalformedURLException exp) {
sUrl = plainReplacement(pUrl, pServletPath);
}
return sUrl;
}
// Check for an IP, since this seems to be safer to return then a plain name
private String getIpIfPossible(String pHost) {
try {
InetAddress address = InetAddress.getByName(pHost);
return address.getHostAddress();
} catch (UnknownHostException e) {
return pHost;
}
}
// Fallback used if URL creation didnt work
private String plainReplacement(String pUrl, String pServletPath) {
int idx = pUrl.lastIndexOf(pServletPath);
String url;
if (idx != -1) {
url = pUrl.substring(0,idx) + pServletPath;
} else {
url = pUrl;
}
return url;
}
// Set an appropriate CORS header if requested and if allowed
private void setCorsHeader(HttpServletRequest pReq, HttpServletResponse pResp) {
String origin = requestHandler.extractCorsOrigin(pReq.getHeader("Origin"));
if (origin != null) {
pResp.setHeader("Access-Control-Allow-Origin", origin);
pResp.setHeader("Access-Control-Allow-Credentials","true");
}
}
// Extract mime type for response (if not JSONP)
private String getMimeType(HttpServletRequest pReq) {
String requestMimeType = pReq.getParameter(ConfigKey.MIME_TYPE.getKeyValue());
if (requestMimeType != null) {
return requestMimeType;
}
return configMimeType;
}
private interface ServletRequestHandler {
/**
* Handle a request and return the answer as a JSON structure
* @param pReq request arrived
* @param pResp response to return
* @return the JSON representation for the answer
* @throws IOException if handling of an input or output stream failed
*/
JSONAware handleRequest(HttpServletRequest pReq, HttpServletResponse pResp)
throws IOException;
}
// factory method for POST request handler
private ServletRequestHandler newPostHttpRequestHandler() {
return new ServletRequestHandler() {
/** {@inheritDoc} */
public JSONAware handleRequest(HttpServletRequest pReq, HttpServletResponse pResp)
throws IOException {
String encoding = pReq.getCharacterEncoding();
InputStream is = pReq.getInputStream();
return requestHandler.handlePostRequest(pReq.getRequestURI(),is, encoding, getParameterMap(pReq));
}
};
}
// factory method for GET request handler
private ServletRequestHandler newGetHttpRequestHandler() {
return new ServletRequestHandler() {
/** {@inheritDoc} */
public JSONAware handleRequest(HttpServletRequest pReq, HttpServletResponse pResp) {
return requestHandler.handleGetRequest(pReq.getRequestURI(),pReq.getPathInfo(), getParameterMap(pReq));
}
};
}
// =======================================================================
// Get parameter map either directly from an Servlet 2.4 compliant implementation
// or by looking it up explictely (thanks to codewax for the patch)
private Map getParameterMap(HttpServletRequest pReq){
try {
// Servlet 2.4 API
return pReq.getParameterMap();
} catch (UnsupportedOperationException exp) {
// Thrown by 'pseudo' 2.4 Servlet API implementations which fake a 2.4 API
// As a service for the parameter map is build up explicitely
Map ret = new HashMap();
Enumeration params = pReq.getParameterNames();
while (params.hasMoreElements()) {
String param = (String) params.nextElement();
ret.put(param, pReq.getParameterValues(param));
}
return ret;
}
}
// Examines servlet config and servlet context for configuration parameters.
// Configuration from the servlet context overrides servlet parameters defined in web.xml
Configuration initConfig(ServletConfig pConfig) {
Configuration config = new Configuration(
ConfigKey.AGENT_ID, NetworkUtil.getAgentId(hashCode(),"servlet"));
// From ServletContext ....
config.updateGlobalConfiguration(new ServletConfigFacade(pConfig));
// ... and ServletConfig
config.updateGlobalConfiguration(new ServletContextFacade(getServletContext()));
// Set type last and overwrite anything written
config.updateGlobalConfiguration(Collections.singletonMap(ConfigKey.AGENT_TYPE.getKeyValue(),"servlet"));
return config;
}
private void sendResponse(HttpServletResponse pResp, String pContentType, String pJsonTxt) throws IOException {
setContentType(pResp, pContentType);
pResp.setStatus(200);
setNoCacheHeaders(pResp);
PrintWriter writer = pResp.getWriter();
writer.write(pJsonTxt);
}
private void setNoCacheHeaders(HttpServletResponse pResp) {
pResp.setHeader("Cache-Control", "no-cache");
pResp.setHeader("Pragma","no-cache");
// Check for a date header and set it accordingly to the recommendations of
// RFC-2616 (http://tools.ietf.org/html/rfc2616#section-14.21)
//
// "To mark a response as "already expired," an origin server sends an
// Expires date that is equal to the Date header value. (See the rules
// for expiration calculations in section 13.2.4.)"
//
// See also #71
long now = System.currentTimeMillis();
pResp.setDateHeader("Date",now);
// 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 some times which seems to be an implementation peculiarity from Tomcat
pResp.setDateHeader("Expires",now - 3600000);
}
private void setContentType(HttpServletResponse pResp, String pContentType) {
boolean encodingDone = false;
try {
pResp.setCharacterEncoding("utf-8");
pResp.setContentType(pContentType);
encodingDone = true;
}
catch (NoSuchMethodError error) { /* Servlet 2.3 */ }
catch (UnsupportedOperationException error) { /* Equinox HTTP Service */ }
if (!encodingDone) {
// For a Servlet 2.3 container or an Equinox HTTP Service, set the charset by hand
pResp.setContentType(pContentType + "; charset=utf-8");
}
}
// =======================================================================================
// Helper classes for extracting configuration from servlet classes
// Implementation for the ServletConfig
private static final class ServletConfigFacade implements ConfigExtractor {
private final ServletConfig config;
private ServletConfigFacade(ServletConfig pConfig) {
config = pConfig;
}
/** {@inheritDoc} */
public Enumeration getNames() {
return config.getInitParameterNames();
}
/** {@inheritDoc} */
public String getParameter(String pName) {
return config.getInitParameter(pName);
}
}
// Implementation for ServletContextFacade
private static final class ServletContextFacade implements ConfigExtractor {
private final ServletContext servletContext;
private ServletContextFacade(ServletContext pServletContext) {
servletContext = pServletContext;
}
/** {@inheritDoc} */
public Enumeration getNames() {
return servletContext.getInitParameterNames();
}
/** {@inheritDoc} */
public String getParameter(String pName) {
return servletContext.getInitParameter(pName);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy