• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.72
/exist-core/src/main/java/org/exist/management/client/JMXServlet.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 *
24
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
25
 *       The original license header is included below.
26
 *
27
 * =====================================================================
28
 *
29
 * eXist-db Open Source Native XML Database
30
 * Copyright (C) 2001 The eXist-db Authors
31
 *
32
 * info@exist-db.org
33
 * http://www.exist-db.org
34
 *
35
 * This library is free software; you can redistribute it and/or
36
 * modify it under the terms of the GNU Lesser General Public
37
 * License as published by the Free Software Foundation; either
38
 * version 2.1 of the License, or (at your option) any later version.
39
 *
40
 * This library is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43
 * Lesser General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Lesser General Public
46
 * License along with this library; if not, write to the Free Software
47
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
48
 */
49
package org.exist.management.client;
50

51
import java.io.IOException;
52
import java.io.InputStream;
53
import java.io.OutputStream;
54
import java.io.OutputStreamWriter;
55
import java.io.Writer;
56
import java.net.InetAddress;
57
import java.net.UnknownHostException;
58
import java.nio.charset.StandardCharsets;
59
import java.nio.file.Files;
60
import java.nio.file.Path;
61
import java.nio.file.Paths;
62
import java.util.HashSet;
63
import java.util.Properties;
64
import java.util.Set;
65
import javax.management.*;
66
import jakarta.servlet.ServletConfig;
67
import jakarta.servlet.ServletException;
68
import jakarta.servlet.http.HttpServlet;
69
import jakarta.servlet.http.HttpServletRequest;
70
import jakarta.servlet.http.HttpServletResponse;
71
import javax.xml.transform.OutputKeys;
72
import javax.xml.transform.TransformerException;
73

74
import org.apache.logging.log4j.LogManager;
75
import org.apache.logging.log4j.Logger;
76
import org.exist.storage.BrokerPool;
77
import org.exist.util.UUIDGenerator;
78
import org.exist.util.serializer.DOMSerializer;
79
import org.w3c.dom.Element;
80

81
import static org.exist.util.StringUtil.notNullOrEmpty;
82
import static org.exist.util.StringUtil.notNullOrEmptyOrWs;
83

84
/**
85
 * A servlet to monitor the database. It returns status information for the database based on the JMX interface. For
86
 * simplicity, the JMX beans provided by eXist are organized into categories. One calls the servlet with one or more
87
 * categories in parameter "c", e.g.:
88
 *
89
 * /exist/jmx?c=instances&c=memory
90
 *
91
 * If no parameter is specified, all categories will be returned. Valid categories are "memory", "instances", "disk",
92
 * "system", "caches", "locking", "processes", "sanity", "all".
93
 *
94
 * The servlet can also be used to test if the database is responsive by using parameter "operation=ping" and a timeout
95
 * (t=timeout-in-milliseconds). For example, the following call
96
 *
97
 * /exist/jmx?operation=ping&t=1000
98
 *
99
 * will wait for a response within 1000ms. If the ping returns within the specified timeout, the servlet returns the
100
 * attributes of the SanityReport JMX bean, which will include an element <jmx:Status>PING_OK</jmx:Status>.
101
 * If the ping takes longer than the timeout, you'll instead find an element <jmx:error> in the returned XML. In
102
 * this case, additional information on running queries, memory consumption and database locks will be provided.
103
 *
104
 * @author wolf
105
 */
106
public class JMXServlet extends HttpServlet {
1✔
107

108
    protected final static Logger LOG = LogManager.getLogger(JMXServlet.class);
1✔
109

110
    private static final String TOKEN_KEY = "token";
111
    private static final String TOKEN_FILE = "jmxservlet.token";
112
    private static final String WEBINF_DATA_DIR = "WEB-INF/data";
113

114
    private final static Properties defaultProperties = new Properties();
1✔
115

116
    static {
117
        defaultProperties.setProperty(OutputKeys.INDENT, "yes");
1✔
118
        defaultProperties.setProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
1✔
119
    }
1✔
120

121
    private JMXtoXML client;
122
    private final Set<String> localhostAddresses = new HashSet<>();
1✔
123

124
    private Path dataDir;
125
    private Path tokenFile;
126

127

128
    @Override
129
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
130

131
        // Verify if request is from localhost or if user has specific servlet/container managed role.
132
        if (isFromLocalHost(request)) {
1!
133
            // Localhost is always authorized to access
134
            LOG.debug("Local access granted");
1✔
135

136
        } else if (hasSecretToken(request, getToken())) {
1!
137
            // Correct token is provided
138
            LOG.debug("Correct token provided by {}", request.getRemoteHost());
×
139

140
        } else {
×
141
            // Check if user is already authorized, e.g. via MONEX allow user too
142
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access allowed for localhost, or when correct token has been provided.");
×
143
            return;
×
144
        }
145

146
        // Perform actual writing of data
147
        writeXmlData(request, response);
1✔
148
    }
1✔
149

150
    private void writeXmlData(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
151
        Element root = null;
1✔
152

153
        final String operation = request.getParameter("operation");
1✔
154
        if ("ping".equals(operation)) {
1!
155
            long timeout = 5000;
×
156
            final String timeoutParam = request.getParameter("t");
×
157
            if (notNullOrEmptyOrWs(timeoutParam)) {
×
158
                try {
159
                    timeout = Long.parseLong(timeoutParam);
×
160
                } catch (final NumberFormatException e) {
×
161
                    throw new ServletException("timeout parameter needs to be a number. Got: " + timeoutParam);
×
162
                }
163
            }
164

165
            final long responseTime = client.ping(BrokerPool.DEFAULT_INSTANCE_NAME, timeout);
×
166
            if (responseTime == JMXtoXML.PING_TIMEOUT) {
×
167
                root = client.generateXMLReport(String.format("no response on ping after %sms", timeout),
×
168
                        new String[]{"sanity", "locking", "processes", "instances", "memory"});
×
169
            } else {
×
170
                root = client.generateXMLReport(null, new String[]{"sanity"});
×
171
            }
172
        } else if (notNullOrEmpty(operation)) {
1!
173
            final String mbean = request.getParameter("mbean");
×
174
            if (mbean == null) {
×
175
                throw new ServletException("to call an operation, you also need to specify parameter 'mbean'");
×
176
            }
177
            String[] args = request.getParameterValues("args");
×
178
            try {
179
                root = client.invoke(mbean, operation, args);
×
180
                if (root == null) {
×
181
                    throw new ServletException("operation " + operation + " not found on " + mbean);
×
182
                }
183
            } catch (InstanceNotFoundException e) {
×
184
                throw new ServletException("mbean " + mbean + " not found: " + e.getMessage(), e);
×
185
            } catch (MalformedObjectNameException | IntrospectionException | ReflectionException | MBeanException e) {
×
186
                throw new ServletException(e.getMessage(), e);
×
187
            }
188
        } else {
189
            String[] categories = request.getParameterValues("c");
1✔
190
            if (categories == null) {
1!
191
                categories = new String[]{"all"};
1✔
192
            }
193
            root = client.generateXMLReport(null, categories);
1✔
194
        }
195

196
        response.setContentType("application/xml");
1✔
197

198
        final Object useAttribute = request.getAttribute("jmx.attribute");
1✔
199
        if (useAttribute != null) {
1!
200
            request.setAttribute(useAttribute.toString(), root);
×
201

202
        } else {
×
203
            final Writer writer = new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8);
1✔
204
            final DOMSerializer streamer = new DOMSerializer(writer, defaultProperties);
1✔
205
            try {
206
                streamer.serialize(root);
1✔
207
            } catch (final TransformerException e) {
1✔
208
                LOG.error(e.getMessageAndLocation());
×
209
                throw new ServletException("Error while serializing result: " + e.getMessage(), e);
×
210
            }
211
            writer.flush();
1✔
212
        }
213
    }
1✔
214

215
    @Override
216
    public void init(ServletConfig config) throws ServletException {
217
        super.init(config);
1✔
218

219
        // Setup JMS client
220
        client = new JMXtoXML();
1✔
221
        client.connect();
1✔
222

223
        // Register all known localhost addresses
224
        registerLocalHostAddresses();
1✔
225

226
        // Get directory for token file
227
        final String jmxDataDir = client.getDataDir();
1✔
228
        if (jmxDataDir == null) {
1!
229
            dataDir = Paths.get(config.getServletContext().getRealPath(WEBINF_DATA_DIR)).normalize();
×
230
        } else {
×
231
            dataDir = Paths.get(jmxDataDir).normalize();
1✔
232
        }
233
        if (!Files.isDirectory(dataDir) || !Files.isWritable(dataDir)) {
1!
234
            LOG.error("Cannot access directory {}", WEBINF_DATA_DIR);
×
235
        }
236

237
        // Setup token and tokenfile
238
        obtainTokenFileReference();
1✔
239

240
        LOG.info("JMXservlet token: {}", getToken());
1✔
241

242
    }
1✔
243

244
    /**
245
     * Register all known IP-addresses for localhost.
246
     */
247
    void registerLocalHostAddresses() {
248
        // The external IP address of the server
249
        try {
250
            localhostAddresses.add(InetAddress.getLocalHost().getHostAddress());
1✔
251
        } catch (UnknownHostException ex) {
1✔
252
            LOG.warn("Unable to get HostAddress for localhost: {}", ex.getMessage());
×
253
        }
254

255
        // The configured Localhost addresses
256
        try {
257
            for (InetAddress address : InetAddress.getAllByName("localhost")) {
1✔
258
                localhostAddresses.add(address.getHostAddress());
1✔
259
            }
260
        } catch (UnknownHostException ex) {
1✔
261
            LOG.warn("Unable to retrieve ipaddresses for localhost: {}", ex.getMessage());
×
262
        }
263

264
        if (localhostAddresses.isEmpty()) {
1!
265
            LOG.error("Unable to determine addresses for localhost, jmx servlet might be disfunctional.");
×
266
        }
267
    }
1✔
268

269
    /**
270
     * Determine if HTTP request is originated from localhost.
271
     *
272
     * @param request The HTTP request
273
     * @return TRUE if request is from LOCALHOST otherwise FALSE
274
     */
275
    boolean isFromLocalHost(HttpServletRequest request) {
276
        return localhostAddresses.contains(request.getRemoteAddr());
1✔
277
    }
278

279
    /**
280
     * Check if URL contains magic Token
281
     *
282
     * @param request The HTTP request
283
     * @return TRUE if request contains correct value for token, else FALSE
284
     */
285
    boolean hasSecretToken(HttpServletRequest request, String token) {
286
        final String[] tokenValues = request.getParameterValues(TOKEN_KEY);
×
287
        if (tokenValues != null) {
×
288
            for (final String tokenValue : tokenValues) {
×
289
                if (tokenValue.equals(token)) {
×
290
                    return true;
×
291
                }
292
            }
293
        }
294
        return false;
×
295
    }
296

297
    /**
298
     * Obtain reference to token file
299
     */
300
    private void obtainTokenFileReference() {
301

302
        if (tokenFile == null) {
1!
303
            tokenFile = dataDir.resolve(TOKEN_FILE);
1✔
304
            LOG.info("Token file:  {}", tokenFile.toAbsolutePath().toAbsolutePath());
1✔
305
        }
306
    }
1✔
307

308
    /**
309
     * Get token from file, create if not existent. Data is read for each call so the file can be updated run-time.
310
     *
311
     * @return Toke for servlet
312
     */
313
    private String getToken() {
314

315
        Properties props = new Properties();
1✔
316
        String token = null;
1✔
317

318
        // Read if possible
319
        if (Files.exists(tokenFile)) {
1!
320

321
            try (final InputStream is = Files.newInputStream(tokenFile)) {
×
322
                props.load(is);
×
323
                token = props.getProperty(TOKEN_KEY);
×
324
            } catch (IOException ex) {
×
325
                LOG.error(ex.getMessage());
×
326
            }
327

328
        }
329

330
        // Create and write when needed
331
        if (!Files.exists(tokenFile) || token == null) {
1!
332

333
            // Create random token
334
            token = UUIDGenerator.getUUIDversion4();
1✔
335

336
            // Set value to properties
337
            props.setProperty(TOKEN_KEY, token);
1✔
338

339
            // Write data to file
340
            try (final OutputStream os = Files.newOutputStream(tokenFile)) {
1✔
341
                props.store(os, "JMXservlet token: http://localhost:8080/exist/status?token=......");
1✔
342
            } catch (IOException ex) {
×
343
                LOG.error(ex.getMessage());
×
344
            }
345

346
            LOG.debug("Token written to file {}", tokenFile.toAbsolutePath().toString());
1✔
347

348
        }
349

350
        return token;
1✔
351
    }
352

353
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc