26
26
import java .io .OutputStream ;
27
27
import java .io .OutputStreamWriter ;
28
28
import java .io .Writer ;
29
- import java .net .InetAddress ;
30
- import java .net .UnknownHostException ;
29
+ import java .net .*;
31
30
import java .nio .charset .StandardCharsets ;
32
31
import java .nio .file .Files ;
33
32
import java .nio .file .Path ;
34
33
import java .nio .file .Paths ;
35
- import java .util .HashSet ;
36
- import java .util .Properties ;
37
- import java .util .Set ;
34
+ import java .util .*;
35
+ import java .util .function .Predicate ;
38
36
import javax .management .*;
39
37
import jakarta .servlet .ServletConfig ;
40
38
import jakarta .servlet .ServletException ;
57
55
* A servlet to monitor the database. It returns status information for the database based on the JMX interface. For
58
56
* simplicity, the JMX beans provided by eXist are organized into categories. One calls the servlet with one or more
59
57
* categories in parameter "c", e.g.:
60
- *
58
+ * <p>
61
59
* /exist/jmx?c=instances&c=memory
62
- *
60
+ * <p>
63
61
* If no parameter is specified, all categories will be returned. Valid categories are "memory", "instances", "disk",
64
62
* "system", "caches", "locking", "processes", "sanity", "all".
65
- *
63
+ * <p>
66
64
* The servlet can also be used to test if the database is responsive by using parameter "operation=ping" and a timeout
67
65
* (t=timeout-in-milliseconds). For example, the following call
68
- *
66
+ * <p>
69
67
* /exist/jmx?operation=ping&t=1000
70
- *
68
+ * <p>
71
69
* will wait for a response within 1000ms. If the ping returns within the specified timeout, the servlet returns the
72
70
* attributes of the SanityReport JMX bean, which will include an element <jmx:Status>PING_OK</jmx:Status>.
73
71
* If the ping takes longer than the timeout, you'll instead find an element <jmx:error> in the returned XML. In
74
72
* this case, additional information on running queries, memory consumption and database locks will be provided.
75
- *
73
+ * <p>
76
74
* @author wolf
77
75
*/
78
76
public class JMXServlet extends HttpServlet {
@@ -96,9 +94,8 @@ public class JMXServlet extends HttpServlet {
96
94
private Path dataDir ;
97
95
private Path tokenFile ;
98
96
99
-
100
97
@ Override
101
- protected void doGet (HttpServletRequest request , HttpServletResponse response ) throws ServletException , IOException {
98
+ protected void doGet (final HttpServletRequest request , final HttpServletResponse response ) throws ServletException , IOException {
102
99
103
100
// Verify if request is from localhost or if user has specific servlet/container managed role.
104
101
if (isFromLocalHost (request )) {
@@ -119,8 +116,8 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t
119
116
writeXmlData (request , response );
120
117
}
121
118
122
- private void writeXmlData (HttpServletRequest request , HttpServletResponse response ) throws ServletException , IOException {
123
- Element root = null ;
119
+ private void writeXmlData (final HttpServletRequest request , final HttpServletResponse response ) throws ServletException , IOException {
120
+ final Element root ;
124
121
125
122
final String operation = request .getParameter ("operation" );
126
123
if ("ping" .equals (operation )) {
@@ -146,15 +143,15 @@ private void writeXmlData(HttpServletRequest request, HttpServletResponse respon
146
143
if (mbean == null ) {
147
144
throw new ServletException ("to call an operation, you also need to specify parameter 'mbean'" );
148
145
}
149
- String [] args = request .getParameterValues ("args" );
146
+ final String [] args = request .getParameterValues ("args" );
150
147
try {
151
148
root = client .invoke (mbean , operation , args );
152
149
if (root == null ) {
153
150
throw new ServletException ("operation " + operation + " not found on " + mbean );
154
151
}
155
- } catch (InstanceNotFoundException e ) {
152
+ } catch (final InstanceNotFoundException e ) {
156
153
throw new ServletException ("mbean " + mbean + " not found: " + e .getMessage (), e );
157
- } catch (MalformedObjectNameException | IntrospectionException | ReflectionException | MBeanException e ) {
154
+ } catch (final MalformedObjectNameException | IntrospectionException | ReflectionException | MBeanException e ) {
158
155
throw new ServletException (e .getMessage (), e );
159
156
}
160
157
} else {
@@ -185,7 +182,7 @@ private void writeXmlData(HttpServletRequest request, HttpServletResponse respon
185
182
}
186
183
187
184
@ Override
188
- public void init (ServletConfig config ) throws ServletException {
185
+ public void init (final ServletConfig config ) throws ServletException {
189
186
super .init (config );
190
187
191
188
// Setup JMS client
@@ -217,24 +214,32 @@ public void init(ServletConfig config) throws ServletException {
217
214
* Register all known IP-addresses for localhost.
218
215
*/
219
216
void registerLocalHostAddresses () {
220
- // The external IP address of the server
221
- try {
222
- localhostAddresses .add (InetAddress .getLocalHost ().getHostAddress ());
223
- } catch (UnknownHostException ex ) {
224
- LOG .warn ("Unable to get HostAddress for localhost: {}" , ex .getMessage ());
225
- }
226
217
227
- // The configured Localhost addresses
228
218
try {
229
- for (InetAddress address : InetAddress .getAllByName ("localhost" )) {
230
- localhostAddresses .add (address .getHostAddress ());
231
- }
232
- } catch (UnknownHostException ex ) {
233
- LOG .warn ("Unable to retrieve ipaddresses for localhost: {}" , ex .getMessage ());
219
+ final Predicate <NetworkInterface > isLoopback = networkInterface -> {
220
+ try {
221
+ return networkInterface .isLoopback ();
222
+ } catch (final SocketException e ) {
223
+ LOG .error ("Unable to determine if {} is a loopback interface" , e .getMessage (), e );
224
+ return false ;
225
+ }
226
+ };
227
+
228
+ NetworkInterface .networkInterfaces ()
229
+ .filter (isLoopback )
230
+ .flatMap (NetworkInterface ::inetAddresses )
231
+ .map (InetAddress ::getHostAddress )
232
+ .map (a -> a .replaceFirst ("%.+" , "" ))
233
+ .forEach (localhostAddresses ::add );
234
+
235
+ } catch (final SocketException e ) {
236
+ LOG .error ("Unable to determine localhost loopback addresses: {} " , e .getMessage (), e );
234
237
}
235
238
236
239
if (localhostAddresses .isEmpty ()) {
237
- LOG .error ("Unable to determine addresses for localhost, jmx servlet might be disfunctional." );
240
+ LOG .error ("Unable to determine addresses for localhost, jmx servlet might be dysfunctional." );
241
+ } else {
242
+ LOG .info ("Detected loopback addresses: {}" , localhostAddresses );
238
243
}
239
244
}
240
245
@@ -244,8 +249,10 @@ void registerLocalHostAddresses() {
244
249
* @param request The HTTP request
245
250
* @return TRUE if request is from LOCALHOST otherwise FALSE
246
251
*/
247
- boolean isFromLocalHost (HttpServletRequest request ) {
248
- return localhostAddresses .contains (request .getRemoteAddr ());
252
+ boolean isFromLocalHost (final HttpServletRequest request ) {
253
+ String remoteAddr = request .getRemoteAddr ();
254
+ remoteAddr = remoteAddr .startsWith ("[" ) ? remoteAddr .substring (1 , remoteAddr .length () - 1 ) : remoteAddr ;
255
+ return localhostAddresses .contains (remoteAddr );
249
256
}
250
257
251
258
/**
@@ -254,8 +261,8 @@ boolean isFromLocalHost(HttpServletRequest request) {
254
261
* @param request The HTTP request
255
262
* @return TRUE if request contains correct value for token, else FALSE
256
263
*/
257
- boolean hasSecretToken (HttpServletRequest request , String token ) {
258
- String [] tokenValue = request .getParameterValues (TOKEN_KEY );
264
+ boolean hasSecretToken (final HttpServletRequest request , final String token ) {
265
+ final String [] tokenValue = request .getParameterValues (TOKEN_KEY );
259
266
return ArrayUtils .contains (tokenValue , token );
260
267
}
261
268
@@ -277,7 +284,7 @@ private void obtainTokenFileReference() {
277
284
*/
278
285
private String getToken () {
279
286
280
- Properties props = new Properties ();
287
+ final Properties props = new Properties ();
281
288
String token = null ;
282
289
283
290
// Read if possible
@@ -286,7 +293,7 @@ private String getToken() {
286
293
try (final InputStream is = Files .newInputStream (tokenFile )) {
287
294
props .load (is );
288
295
token = props .getProperty (TOKEN_KEY );
289
- } catch (IOException ex ) {
296
+ } catch (final IOException ex ) {
290
297
LOG .error (ex .getMessage ());
291
298
}
292
299
@@ -304,7 +311,7 @@ private String getToken() {
304
311
// Write data to file
305
312
try (final OutputStream os = Files .newOutputStream (tokenFile )) {
306
313
props .store (os , "JMXservlet token: http://localhost:8080/exist/status?token=......" );
307
- } catch (IOException ex ) {
314
+ } catch (final IOException ex ) {
308
315
LOG .error (ex .getMessage ());
309
316
}
310
317
0 commit comments