diff --git a/exist-core/src/main/java/org/exist/management/client/JMXServlet.java b/exist-core/src/main/java/org/exist/management/client/JMXServlet.java index e1ac78263b..aa8f49ce40 100644 --- a/exist-core/src/main/java/org/exist/management/client/JMXServlet.java +++ b/exist-core/src/main/java/org/exist/management/client/JMXServlet.java @@ -56,6 +56,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.HashSet; import java.util.Properties; import java.util.Set; @@ -116,7 +119,7 @@ public class JMXServlet extends HttpServlet { } private JMXtoXML client; - private final Set localhostAddresses = new HashSet<>(); + private final Set serverAddresses = new HashSet<>(); private Path dataDir; private Path tokenFile; @@ -128,11 +131,15 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t // 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"); + if (LOG.isDebugEnabled()) { + LOG.debug("Local access granted"); + } } else if (hasSecretToken(request, getToken())) { // Correct token is provided - LOG.debug("Correct token provided by {}", request.getRemoteHost()); + if (LOG.isDebugEnabled()) { + LOG.debug("Correct token provided by {}", request.getRemoteHost()); + } } else { // Check if user is already authorized, e.g. via MONEX allow user too @@ -218,7 +225,7 @@ public void init(ServletConfig config) throws ServletException { client.connect(); // Register all known localhost addresses - registerLocalHostAddresses(); + registerServerAddresses(); // Get directory for token file final String jmxDataDir = client.getDataDir(); @@ -232,34 +239,47 @@ public void init(ServletConfig config) throws ServletException { } // Setup token and tokenfile - obtainTokenFileReference(); - - LOG.info("JMXservlet token: {}", getToken()); + if (tokenFile == null) { + tokenFile = dataDir.resolve(TOKEN_FILE); + LOG.info("Token file: {}", tokenFile.toAbsolutePath().toAbsolutePath()); + } + // NOTE(AR) make sure to create the token in init when the servlet is loaded at startup so that it is present for Monex + final String token = getToken(); + LOG.info("JMXServlet token: {}", token); } /** - * Register all known IP-addresses for localhost. + * Register all known IP-addresses for server. */ - void registerLocalHostAddresses() { - // The external IP address of the server + void registerServerAddresses() { + // The IPv4 address of the loopback interface of the server - 127.0.0.1 on Windows/Linux/macOS, or 127.0.1.1 on Debian/Ubuntu + try { + serverAddresses.add(InetAddress.getLocalHost().getHostAddress()); + } catch (final UnknownHostException ex) { + LOG.warn("Unable to get loopback IP address for localhost: {}", ex.getMessage()); + } + + // Any additional IPv4 and IPv6 addresses associated with the loopback interface of the server try { - localhostAddresses.add(InetAddress.getLocalHost().getHostAddress()); - } catch (UnknownHostException ex) { - LOG.warn("Unable to get HostAddress for localhost: {}", ex.getMessage()); + for (final InetAddress loopBackAddress : InetAddress.getAllByName("localhost")) { + serverAddresses.add(loopBackAddress.getHostAddress()); + } + } catch (final UnknownHostException ex) { + LOG.warn("Unable to retrieve additional loopback IP addresses for localhost: {}", ex.getMessage()); } - // The configured Localhost addresses + // Any IPv4 and IPv6 addresses associated with other interfaces in the server try { - for (InetAddress address : InetAddress.getAllByName("localhost")) { - localhostAddresses.add(address.getHostAddress()); + for (final InetAddress hostAddress : InetAddress.getAllByName(InetAddress.getLocalHost().getHostName())) { + serverAddresses.add(hostAddress.getHostAddress()); } - } catch (UnknownHostException ex) { - LOG.warn("Unable to retrieve ipaddresses for localhost: {}", ex.getMessage()); + } catch (final UnknownHostException ex) { + LOG.warn("Unable to retrieve additional interface IP addresses for localhost: {}", ex.getMessage()); } - if (localhostAddresses.isEmpty()) { - LOG.error("Unable to determine addresses for localhost, jmx servlet might be disfunctional."); + if (serverAddresses.isEmpty()) { + LOG.error("Unable to determine IP addresses for localhost, JMXServlet might be dysfunctional."); } } @@ -269,8 +289,13 @@ void registerLocalHostAddresses() { * @param request The HTTP request * @return TRUE if request is from LOCALHOST otherwise FALSE */ - boolean isFromLocalHost(HttpServletRequest request) { - return localhostAddresses.contains(request.getRemoteAddr()); + boolean isFromLocalHost(final HttpServletRequest request) { + String remoteAddr = request.getRemoteAddr(); + if (remoteAddr.charAt(0) == '[') { + // Handle IPv6 addresses that are wrapped in [] + remoteAddr = remoteAddr.substring(1, remoteAddr.length() - 1); + } + return serverAddresses.contains(remoteAddr); } /** @@ -291,17 +316,6 @@ boolean hasSecretToken(HttpServletRequest request, String token) { return false; } - /** - * Obtain reference to token file - */ - private void obtainTokenFileReference() { - - if (tokenFile == null) { - tokenFile = dataDir.resolve(TOKEN_FILE); - LOG.info("Token file: {}", tokenFile.toAbsolutePath().toAbsolutePath()); - } - } - /** * Get token from file, create if not existent. Data is read for each call so the file can be updated run-time. * @@ -327,6 +341,13 @@ private String getToken() { // Create and write when needed if (!Files.exists(tokenFile) || token == null) { + final Set permissions = PosixFilePermissions.fromString("rw-r-----"); + try { + tokenFile = Files.createFile(tokenFile, PosixFilePermissions.asFileAttribute(permissions)); + } catch (final Throwable t) { + LOG.warn("Unable to restrict permissions on: " + tokenFile); + } + // Create random token token = UUIDGenerator.getUUIDversion4(); diff --git a/exist-core/src/main/java/org/exist/xquery/PerformanceStatsImpl.java b/exist-core/src/main/java/org/exist/xquery/PerformanceStatsImpl.java index d2c9470a91..6205e79915 100644 --- a/exist-core/src/main/java/org/exist/xquery/PerformanceStatsImpl.java +++ b/exist-core/src/main/java/org/exist/xquery/PerformanceStatsImpl.java @@ -385,48 +385,47 @@ private FunctionStats[] sort() { @Override public void serialize(final MemTreeBuilder builder) { - builder.startElement(new QName(XML_ELEMENT_CALLS, XML_NAMESPACE, XML_PREFIX), null); - if (isEnabled()) { - final AttributesImpl attrs = new AttributesImpl(); - for (final QueryStats stats : queries.values()) { - attrs.clear(); + final AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "tracing-enabled", "tracing-enabled", "CDATA", Boolean.toString(isEnabled())); + builder.startElement(new QName(XML_ELEMENT_CALLS, XML_NAMESPACE, XML_PREFIX), attrs); + for (final QueryStats stats : queries.values()) { + attrs.clear(); + attrs.addAttribute("", "source", "source", "CDATA", stats.source); + attrs.addAttribute("", "elapsed", "elapsed", "CDATA", Double.toString(stats.executionTime / 1000.0)); + attrs.addAttribute("", "calls", "calls", "CDATA", Integer.toString(stats.callCount)); + builder.startElement(new QName("query", XML_NAMESPACE, XML_PREFIX), attrs); + builder.endElement(); + } + for (final FunctionStats stats : functions.values()) { + attrs.clear(); + attrs.addAttribute("", "name", "name", "CDATA", stats.qname.getStringValue()); + attrs.addAttribute("", "elapsed", "elapsed", "CDATA", Double.toString(stats.executionTime / 1000.0)); + attrs.addAttribute("", "calls", "calls", "CDATA", Integer.toString(stats.callCount)); + if (stats.source != null) { attrs.addAttribute("", "source", "source", "CDATA", stats.source); - attrs.addAttribute("", "elapsed", "elapsed", "CDATA", Double.toString(stats.executionTime / 1000.0)); - attrs.addAttribute("", "calls", "calls", "CDATA", Integer.toString(stats.callCount)); - builder.startElement(new QName("query", XML_NAMESPACE, XML_PREFIX), attrs); - builder.endElement(); } - for (final FunctionStats stats : functions.values()) { - attrs.clear(); - attrs.addAttribute("", "name", "name", "CDATA", stats.qname.getStringValue()); - attrs.addAttribute("", "elapsed", "elapsed", "CDATA", Double.toString(stats.executionTime / 1000.0)); - attrs.addAttribute("", "calls", "calls", "CDATA", Integer.toString(stats.callCount)); - if (stats.source != null) { - attrs.addAttribute("", "source", "source", "CDATA", stats.source); - } - builder.startElement(new QName("function", XML_NAMESPACE, XML_PREFIX), attrs); - builder.endElement(); - } - for (final IndexStats stats : indexStats.values()) { - attrs.clear(); - attrs.addAttribute("", "type", "type", "CDATA", stats.indexType); - attrs.addAttribute("", "source", "source", "CDATA", stats.source + " [" + stats.line + ":" + - stats.column + "]"); - attrs.addAttribute("", "elapsed", "elapsed", "CDATA", Double.toString(stats.executionTime / 1000.0)); - attrs.addAttribute("", "calls", "calls", "CDATA", Integer.toString(stats.usageCount)); - attrs.addAttribute("", "optimization-level", "optimization", "CDATA", stats.indexOptimizationLevel.name()); - builder.startElement(new QName("index", XML_NAMESPACE, XML_PREFIX), attrs); - builder.endElement(); - } - for (final OptimizationStats stats : optimizations) { - attrs.clear(); - attrs.addAttribute("", "type", "type", "CDATA", stats.type.toString()); - if (stats.source != null) { - attrs.addAttribute("", "source", "source", "CDATA", stats.source + " [" + stats.line + ":" + stats.column + "]"); - } - builder.startElement(new QName("optimization", XML_NAMESPACE, XML_PREFIX), attrs); - builder.endElement(); + builder.startElement(new QName("function", XML_NAMESPACE, XML_PREFIX), attrs); + builder.endElement(); + } + for (final IndexStats stats : indexStats.values()) { + attrs.clear(); + attrs.addAttribute("", "type", "type", "CDATA", stats.indexType); + attrs.addAttribute("", "source", "source", "CDATA", stats.source + " [" + stats.line + ":" + + stats.column + "]"); + attrs.addAttribute("", "elapsed", "elapsed", "CDATA", Double.toString(stats.executionTime / 1000.0)); + attrs.addAttribute("", "calls", "calls", "CDATA", Integer.toString(stats.usageCount)); + attrs.addAttribute("", "optimization-level", "optimization", "CDATA", stats.indexOptimizationLevel.name()); + builder.startElement(new QName("index", XML_NAMESPACE, XML_PREFIX), attrs); + builder.endElement(); + } + for (final OptimizationStats stats : optimizations) { + attrs.clear(); + attrs.addAttribute("", "type", "type", "CDATA", stats.type.toString()); + if (stats.source != null) { + attrs.addAttribute("", "source", "source", "CDATA", stats.source + " [" + stats.line + ":" + stats.column + "]"); } + builder.startElement(new QName("optimization", XML_NAMESPACE, XML_PREFIX), attrs); + builder.endElement(); } builder.endElement(); } diff --git a/exist-jetty-config/src/main/resources/webapp/WEB-INF/web.xml b/exist-jetty-config/src/main/resources/webapp/WEB-INF/web.xml index 1171737948..83d7303cba 100644 --- a/exist-jetty-config/src/main/resources/webapp/WEB-INF/web.xml +++ b/exist-jetty-config/src/main/resources/webapp/WEB-INF/web.xml @@ -176,6 +176,11 @@ JMXServlet org.exist.management.client.JMXServlet + + 3