/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-10 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* $Id$
*/
package org.exist.management.client;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import javax.management.*;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.exist.util.serializer.DOMSerializer;
import org.w3c.dom.Element;
/**
* A servlet to monitor the database. It returns status information for the database based on the JMX interface. For
* simplicity, the JMX beans provided by eXist are organized into categories. One calls the servlet with one or more
* categories in parameter "c", e.g.:
*
* /exist/jmx?c=instances&c=memory
*
* If no parameter is specified, all categories will be returned. Valid categories are "memory", "instances", "disk",
* "system", "caches", "locking", "processes", "sanity", "all".
*
* The servlet can also be used to test if the database is responsive by using parameter "operation=ping" and a timeout
* (t=timeout-in-milliseconds). For example, the following call
*
* /exist/jmx?operation=ping&t=1000
*
* will wait for a response within 1000ms. If the ping returns within the specified timeout, the servlet returns the
* attributes of the SanityReport JMX bean, which will include an element <jmx:Status>PING_OK</jmx:Status>.
* If the ping takes longer than the timeout, you'll instead find an element <jmx:error> in the returned XML. In
* this case, additional information on running queries, memory consumption and database locks will be provided.
*
* @author wolf
*
*/
public class JMXServlet extends HttpServlet {
protected final static Logger LOG = Logger.getLogger(JMXServlet.class);
private static final String TOKEN_KEY = "token";
private static final String TOKEN_FILE = "jmxservlet.token";
private static final String WEBINF_DATA_DIR = "WEB-INF/data";
private final static Properties defaultProperties = new Properties();
static {
defaultProperties.setProperty(OutputKeys.INDENT, "yes");
defaultProperties.setProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
}
private JMXtoXML client;
private final Set<String> localhostAddresses = new HashSet<>();
private File dataDir;
private File tokenFile;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Verify if request is from localhost or if user has specific servlet/container managed role.
if (isFromLocalHost(request)) {
// Localhost is always authorized to access
LOG.debug("Local access granted");
} else if (hasSecretToken(request, getToken())) {
// Correct token is provided
LOG.debug("Correct token provided by " + request.getRemoteHost());
} else {
// Check if user is already authorized, e.g. via MONEX allow user too
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access allowed for localhost, or when correct token has been provided.");
return;
}
// Perform actual writing of data
writeXmlData(request, response);
}
private void writeXmlData(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Element root = null;
final String operation = request.getParameter("operation");
if ("ping".equals(operation)) {
long timeout = 5000;
final String timeoutParam = request.getParameter("t");
if (StringUtils.isNotBlank(timeoutParam)) {
try {
timeout = Long.parseLong(timeoutParam);
} catch (final NumberFormatException e) {
throw new ServletException("timeout parameter needs to be a number. Got: " + timeoutParam);
}
}
final long responseTime = client.ping("exist", timeout);
if (responseTime == JMXtoXML.PING_TIMEOUT) {
root = client.generateXMLReport(String.format("no response on ping after %sms", timeout),
new String[]{"sanity", "locking", "processes", "instances", "memory"});
} else {
root = client.generateXMLReport(null, new String[]{"sanity"});
}
} else if (operation != null && operation.length() > 0) {
final String mbean = request.getParameter("mbean");
if (mbean == null) {
throw new ServletException("to call an operation, you also need to specify parameter 'mbean'");
}
String[] args = request.getParameterValues("args");
try {
root = client.invoke(mbean, operation, args);
if (root == null) {
throw new ServletException("operation " + operation + " not found on " + mbean);
}
} catch (InstanceNotFoundException e) {
throw new ServletException("mbean " + mbean + " not found: " + e.getMessage(), e);
} catch (MalformedObjectNameException e) {
throw new ServletException(e.getMessage(), e);
} catch (MBeanException e) {
throw new ServletException(e.getMessage(), e);
} catch (ReflectionException e) {
throw new ServletException(e.getMessage(), e);
} catch (IntrospectionException e) {
throw new ServletException(e.getMessage(), e);
}
} else {
String[] categories = request.getParameterValues("c");
if (categories == null) {
categories = new String[]{"all"};
}
root = client.generateXMLReport(null, categories);
}
response.setContentType("application/xml");
final Object useAttribute = request.getAttribute("jmx.attribute");
if (useAttribute != null) {
request.setAttribute(useAttribute.toString(), root);
} else {
final Writer writer = new OutputStreamWriter(response.getOutputStream(), "UTF-8");
final DOMSerializer streamer = new DOMSerializer(writer, defaultProperties);
try {
streamer.serialize(root);
} catch (final TransformerException e) {
LOG.error(e.getMessageAndLocation());
throw new ServletException("Error while serializing result: " + e.getMessage(), e);
}
writer.flush();
}
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
// Setup JMS client
client = new JMXtoXML();
client.connect();
// Register all known localhost addresses
registerLocalHostAddresses();
// Get directory for token file
final String jmxDataDir = client.getDataDir();
if (jmxDataDir == null) {
dataDir = new File(config.getServletContext().getRealPath(WEBINF_DATA_DIR));
} else {
dataDir = new File(jmxDataDir);
}
if (!dataDir.isDirectory() || !dataDir.canWrite()) {
LOG.error("Cannot access directory " + WEBINF_DATA_DIR);
}
// Setup token and tokenfile
obtainTokenFileReference();
LOG.info(String.format("JMXservlet token: %s", getToken()));
}
/**
* Register all known IP-addresses for localhost.
*/
void registerLocalHostAddresses() {
// The external IP address of the server
try {
localhostAddresses.add(InetAddress.getLocalHost().getHostAddress());
} catch (UnknownHostException ex) {
LOG.warn(String.format("Unable to get HostAddress for localhost: %s", ex.getMessage()));
}
// The configured Localhost addresses
try {
for (InetAddress address : InetAddress.getAllByName("localhost")) {
localhostAddresses.add(address.getHostAddress());
}
} catch (UnknownHostException ex) {
LOG.warn(String.format("Unable to retrieve ipaddresses for localhost: %s", ex.getMessage()));
}
if (localhostAddresses.isEmpty()) {
LOG.error("Unable to determine addresses for localhost, jmx servlet might be disfunctional.");
}
}
/**
* Determine if HTTP request is originated from localhost.
*
* @param request The HTTP request
* @return TRUE if request is from LOCALHOST otherwise FALSE
*/
boolean isFromLocalHost(HttpServletRequest request) {
return localhostAddresses.contains(request.getRemoteAddr());
}
/**
* Check if URL contains magic Token
*
* @param request The HTTP request
* @return TRUE if request contains correct value for token, else FALSE
*/
boolean hasSecretToken(HttpServletRequest request, String token) {
String[] tokenValue = request.getParameterValues(TOKEN_KEY);
return ArrayUtils.contains(tokenValue, token);
}
/**
* Obtain reference to token file
*/
private void obtainTokenFileReference() {
if (tokenFile == null) {
tokenFile = new File(dataDir, TOKEN_FILE);
LOG.info(String.format("Token file: %s", tokenFile.getAbsolutePath()));
}
}
/**
* Get token from file, create if not existent. Data is read for each call so the file can be updated run-time.
*
* @return Toke for servlet
*/
private String getToken() {
Properties props = new Properties();
String token = null;
// Read if possible
if (tokenFile.exists()) {
try (InputStream is = new FileInputStream(tokenFile)) {
props.load(is);
token = props.getProperty(TOKEN_KEY);
} catch (IOException ex) {
LOG.error(ex.getMessage());
}
}
// Create and write when needed
if (!tokenFile.exists() || token == null) {
// Create random token
token = UUID.randomUUID().toString();
// Set value to properties
props.setProperty(TOKEN_KEY, token);
// Write data to file
try (OutputStream os = new FileOutputStream(tokenFile)) {
props.store(os, "JMXservlet token: http://localhost:8080/exist/status?token=......");
} catch (IOException ex) {
LOG.error(ex.getMessage());
}
LOG.debug(String.format("Token written to file %s", tokenFile.getAbsolutePath()));
}
return token;
}
}