1 /*
2 ** Zabbix
3 ** Copyright (C) 2001-2021 Zabbix SIA
4 **
5 ** This program is free software; you can redistribute it and/or modify
6 ** it under the terms of the GNU General Public License as published by
7 ** the Free Software Foundation; either version 2 of the License, or
8 ** (at your option) any later version.
9 **
10 ** This program is distributed in the hope that it will be useful,
11 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 ** GNU General Public License for more details.
14 **
15 ** You should have received a copy of the GNU General Public License
16 ** along with this program; if not, write to the Free Software
17 ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 **/
19 
20 package com.zabbix.gateway;
21 
22 import java.io.IOException;
23 import java.util.HashMap;
24 import java.util.Map;
25 import java.util.HashSet;
26 import javax.management.AttributeList;
27 
28 import javax.management.InstanceNotFoundException;
29 import javax.management.AttributeNotFoundException;
30 import javax.management.MBeanAttributeInfo;
31 import javax.management.MBeanServerConnection;
32 import javax.management.ObjectName;
33 import javax.management.MalformedObjectNameException;
34 import javax.management.openmbean.CompositeData;
35 import javax.management.openmbean.TabularDataSupport;
36 import javax.management.openmbean.TabularData;
37 import javax.management.remote.JMXConnector;
38 import javax.management.remote.JMXServiceURL;
39 import javax.rmi.ssl.SslRMIClientSocketFactory;
40 
41 import org.json.*;
42 
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45 
46 class JMXItemChecker extends ItemChecker
47 {
48 	private static final Logger logger = LoggerFactory.getLogger(JMXItemChecker.class);
49 
50 	private JMXServiceURL url;
51 	private JMXConnector jmxc;
52 	private MBeanServerConnection mbsc;
53 
54 	private String username;
55 	private String password;
56 	private String jmx_endpoint;
57 
58 	private enum DiscoveryMode {
59 		ATTRIBUTES,
60 		BEANS
61 	}
62 
63 	private static HashMap<String, Boolean> useRMISSLforURLHintCache = new HashMap<String, Boolean>();
64 
JMXItemChecker(JSONObject request)65 	JMXItemChecker(JSONObject request) throws ZabbixException
66 	{
67 		super(request);
68 
69 		try
70 		{
71 			jmx_endpoint = request.getString(JSON_TAG_JMX_ENDPOINT);
72 		}
73 		catch (Exception e)
74 		{
75 			throw new ZabbixException(e);
76 		}
77 
78 		try
79 		{
80 			url = new JMXServiceURL(jmx_endpoint);
81 			jmxc = null;
82 			mbsc = null;
83 
84 			username = request.optString(JSON_TAG_USERNAME, null);
85 			password = request.optString(JSON_TAG_PASSWORD, null);
86 		}
87 		catch (Exception e)
88 		{
89 			throw new ZabbixException("%s: %s", e, jmx_endpoint);
90 		}
91 	}
92 
93 	@Override
getValues()94 	JSONArray getValues() throws ZabbixException
95 	{
96 		JSONArray values = new JSONArray();
97 
98 		try
99 		{
100 			HashMap<String, Object> env = new HashMap<String, Object>();
101 
102 			if (null != username && null != password)
103 			{
104 				env.put(JMXConnector.CREDENTIALS, new String[] {username, password});
105 			}
106 
107 			if (!useRMISSLforURLHintCache.containsKey(url.getURLPath()) ||
108 					!useRMISSLforURLHintCache.get(url.getURLPath()))
109 			{
110 				try
111 				{
112 					jmxc = ZabbixJMXConnectorFactory.connect(url, env);
113 					useRMISSLforURLHintCache.put(url.getURLPath(), false);
114 				}
115 				catch (IOException e)
116 				{
117 					env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory());
118 					jmxc = ZabbixJMXConnectorFactory.connect(url, env);
119 					useRMISSLforURLHintCache.put(url.getURLPath(), true);
120 				}
121 			}
122 			else
123 			{
124 				try
125 				{
126 					env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory());
127 					jmxc = ZabbixJMXConnectorFactory.connect(url, env);
128 					useRMISSLforURLHintCache.put(url.getURLPath(), true);
129 				}
130 				catch (IOException e)
131 				{
132 					env.remove("com.sun.jndi.rmi.factory.socket");
133 					jmxc = ZabbixJMXConnectorFactory.connect(url, env);
134 					useRMISSLforURLHintCache.put(url.getURLPath(), false);
135 				}
136 			}
137 
138 			mbsc = jmxc.getMBeanServerConnection();
139 			logger.debug("using RMI SSL for " + url.getURLPath() + ": " + useRMISSLforURLHintCache.get(url.getURLPath()));
140 
141 			for (String key : keys)
142 				values.put(getJSONValue(key));
143 		}
144 		catch (SecurityException e1)
145 		{
146 			JSONObject value = new JSONObject();
147 
148 			logger.warn("cannot process keys '{}': {}: {}", new Object[] {keys,
149 					ZabbixException.getRootCauseMessage(e1), url});
150 			logger.debug("error caused by", e1);
151 
152 			try
153 			{
154 				value.put(JSON_TAG_ERROR, ZabbixException.getRootCauseMessage(e1));
155 			}
156 			catch (JSONException e2)
157 			{
158 				Object[] logInfo = {JSON_TAG_ERROR, e1.getMessage(),
159 						ZabbixException.getRootCauseMessage(e2)};
160 				logger.warn("cannot add JSON attribute '{}' with message '{}': {}", logInfo);
161 				logger.debug("error caused by", e2);
162 			}
163 
164 			for (int i = 0; i < keys.size(); i++)
165 				values.put(value);
166 		}
167 		catch (Exception e)
168 		{
169 			throw new ZabbixException("%s: %s", ZabbixException.getRootCauseMessage(e), url);
170 		}
171 		finally
172 		{
173 			try { if (null != jmxc) jmxc.close(); } catch (java.io.IOException exception) { }
174 
175 			jmxc = null;
176 			mbsc = null;
177 		}
178 
179 		return values;
180 	}
181 
182 	@Override
getStringValue(String key)183 	protected String getStringValue(String key) throws Exception
184 	{
185 		ZabbixItem item = new ZabbixItem(key);
186 		int argumentCount = item.getArgumentCount();
187 
188 		if (item.getKeyId().equals("jmx"))
189 		{
190 			if (2 != argumentCount && 3 != argumentCount)
191 				throw new ZabbixException("required key format: jmx[<object name>,<attribute name>,<unique short description>]");
192 
193 			ObjectName objectName = new ObjectName(item.getArgument(1));
194 			String attributeName = item.getArgument(2);
195 			String realAttributeName;
196 			String fieldNames = "";
197 
198 			// Attribute name and composite data field names are separated by dots. On the other hand the
199 			// name may contain a dot too. In this case user needs to escape it with a backslash. Also the
200 			// backslash symbols in the name must be escaped. So a real separator is unescaped dot and
201 			// separatorIndex() is used to locate it.
202 
203 			int sep = HelperFunctionChest.separatorIndex(attributeName);
204 
205 			if (-1 != sep)
206 			{
207 				logger.trace("'{}' contains composite data", attributeName);
208 
209 				realAttributeName = attributeName.substring(0, sep);
210 				fieldNames = attributeName.substring(sep + 1);
211 			}
212 			else
213 				realAttributeName = attributeName;
214 
215 			// unescape possible dots or backslashes that were escaped by user
216 			realAttributeName = HelperFunctionChest.unescapeUserInput(realAttributeName);
217 
218 			logger.trace("attributeName:'{}'", realAttributeName);
219 			logger.trace("fieldNames:'{}'", fieldNames);
220 
221 			try
222 			{
223 				Object dataObject = mbsc.getAttribute(objectName, realAttributeName);
224 
225 				if (dataObject instanceof TabularData)
226 				{
227 					logger.trace("'{}' contains tabular data", attributeName);
228 					return getTabularData((TabularData)dataObject).toString();
229 				}
230 
231 				return getPrimitiveAttributeValue(dataObject, fieldNames);
232 			}
233 			catch (AttributeNotFoundException e)
234 			{
235 				throw new ZabbixException("Attribute not found: %s", ZabbixException.getRootCauseMessage(e));
236 			}
237 			catch (InstanceNotFoundException e)
238 			{
239 				throw new ZabbixException("Object or attribute not found: %s", ZabbixException.getRootCauseMessage(e));
240 			}
241 		}
242 		else if (item.getKeyId().equals("jmx.discovery") || item.getKeyId().equals("jmx.get"))
243 		{
244 			if (3 < argumentCount)
245 				throw new ZabbixException("required key format: " + item.getKeyId() +
246 						"[<discovery mode>,<object name>,<unique short description>]");
247 
248 			ObjectName filter;
249 
250 			try
251 			{
252 				filter = (2 <= argumentCount) ? new ObjectName(item.getArgument(2)) : null;
253 			}
254 			catch (MalformedObjectNameException e)
255 			{
256 				throw new ZabbixException("invalid object name format: " + item.getArgument(2));
257 			}
258 
259 			boolean mapped = item.getKeyId().equals("jmx.discovery");
260 			JSONArray counters = new JSONArray();
261 			DiscoveryMode mode = DiscoveryMode.ATTRIBUTES;
262 			if (0 < argumentCount)
263 			{
264 				String modeName = item.getArgument(1);
265 				if (modeName.equals("beans"))
266 					mode = DiscoveryMode.BEANS;
267 				else if (!modeName.equals("attributes"))
268 					throw new ZabbixException("invalid discovery mode: " + modeName);
269 			}
270 
271 			switch(mode)
272 			{
273 				case ATTRIBUTES:
274 					discoverAttributes(counters, filter, mapped);
275 					break;
276 				case BEANS:
277 					discoverBeans(counters, filter, mapped);
278 					break;
279 			}
280 
281 			if (mapped)
282 			{
283 				JSONObject mapping = new JSONObject();
284 				mapping.put(ItemChecker.JSON_TAG_DATA, counters);
285 				return mapping.toString();
286 			}
287 			else
288 			{
289 				return counters.toString();
290 			}
291 		}
292 		else
293 			throw new ZabbixException("key ID '%s' is not supported", item.getKeyId());
294 	}
295 
getPrimitiveAttributeValue(Object dataObject, String fieldNames)296 	private String getPrimitiveAttributeValue(Object dataObject, String fieldNames) throws Exception
297 	{
298 		logger.trace("drilling down with data object '{}' and field names '{}'", dataObject, fieldNames);
299 
300 		if (null == dataObject)
301 			throw new ZabbixException("data object is null");
302 
303 		if (fieldNames.equals(""))
304 		{
305 			try
306 			{
307 				if (isPrimitiveAttributeType(dataObject))
308 					return dataObject.toString();
309 				else
310 					throw new NoSuchMethodException();
311 			}
312 			catch (NoSuchMethodException e)
313 			{
314 				throw new ZabbixException("Data object type cannot be converted to string.");
315 			}
316 		}
317 
318 		if (dataObject instanceof CompositeData)
319 		{
320 			logger.trace("'{}' contains composite data", dataObject);
321 
322 			CompositeData comp = (CompositeData)dataObject;
323 
324 			String dataObjectName;
325 			String newFieldNames = "";
326 
327 			int sep = HelperFunctionChest.separatorIndex(fieldNames);
328 
329 			if (-1 != sep)
330 			{
331 				dataObjectName = fieldNames.substring(0, sep);
332 				newFieldNames = fieldNames.substring(sep + 1);
333 			}
334 			else
335 				dataObjectName = fieldNames;
336 
337 			// unescape possible dots or backslashes that were escaped by user
338 			dataObjectName = HelperFunctionChest.unescapeUserInput(dataObjectName);
339 
340 			return getPrimitiveAttributeValue(comp.get(dataObjectName), newFieldNames);
341 		}
342 		else
343 			throw new ZabbixException("unsupported data object type along the path: %s", dataObject.getClass());
344 	}
345 
getTabularData(TabularData data)346 	private JSONArray getTabularData(TabularData data) throws JSONException
347 	{
348 		JSONArray values = new JSONArray();
349 
350 		for (Object value : data.values())
351 		{
352 			JSONObject tmp = getCompositeDataValues((CompositeData)value);
353 
354 			if (tmp.length() > 0)
355 				values.put(tmp);
356 		}
357 
358 		return values;
359 	}
360 
getCompositeDataValues(CompositeData compData)361 	private JSONObject getCompositeDataValues(CompositeData compData) throws JSONException
362 	{
363 		JSONObject value = new JSONObject();
364 
365 		for (String key : compData.getCompositeType().keySet())
366 		{
367 			Object data = compData.get(key);
368 
369 			if (data == null)
370 			{
371 				value.put(key, JSONObject.NULL);
372 			}
373 			else if (data.getClass().isArray())
374 			{
375 				logger.trace("found attribute of a known, unsupported type: {}", data.getClass());
376 				continue;
377 			}
378 			else if (data instanceof TabularData)
379 			{
380 				value.put(key, getTabularData((TabularData)data));
381 			}
382 			else if (data instanceof CompositeData)
383 			{
384 				value.put(key, getCompositeDataValues((CompositeData)data));
385 			}
386 			else
387 				value.put(key, data);
388 		}
389 
390 		return value;
391 	}
392 
discoverAttributes(JSONArray counters, ObjectName filter, boolean propertiesAsMacros)393 	private void discoverAttributes(JSONArray counters, ObjectName filter, boolean propertiesAsMacros) throws Exception
394 	{
395 		for (ObjectName name : mbsc.queryNames(filter, null))
396 		{
397 			Map<String, Object> values = new HashMap<String, Object>();
398 			MBeanAttributeInfo[] attributeArray = mbsc.getMBeanInfo(name).getAttributes();
399 
400 			if (0 == attributeArray.length)
401 			{
402 				logger.trace("object has no attributes");
403 				return;
404 			}
405 
406 			String[] attributeNames = getAttributeNames(attributeArray);
407 			AttributeList attributes;
408 			String discoveredObjKey = jmx_endpoint + "#" + name;
409 			Long expirationTime = JavaGateway.iterativeObjects.get(discoveredObjKey);
410 			long now = System.currentTimeMillis();
411 
412 			if (null != expirationTime && now <= expirationTime)
413 			{
414 				attributes = getAttributesIterative(name, attributeNames);
415 			}
416 			else
417 			{
418 				try
419 				{
420 					attributes = getAttributesBulk(name, attributeNames);
421 
422 					if (null != expirationTime)
423 						JavaGateway.iterativeObjects.remove(discoveredObjKey);
424 				}
425 				catch (Exception e)
426 				{
427 					attributes = getAttributesIterative(name, attributeNames);
428 
429 					// This object's attributes will be collected iteratively for next 24h. After that it will
430 					// be checked if it is possible to successfully collect all attributes in bulk mode.
431 					JavaGateway.iterativeObjects.put(discoveredObjKey, now + SocketProcessor.MILLISECONDS_IN_HOUR * 24);
432 				}
433 			}
434 
435 			if (attributes.isEmpty())
436 			{
437 				logger.warn("cannot process any attribute for object '{}'", name);
438 				return;
439 			}
440 
441 			for (javax.management.Attribute attribute : attributes.asList())
442 				values.put(attribute.getName(), attribute.getValue());
443 
444 			for (MBeanAttributeInfo attrInfo : attributeArray)
445 			{
446 				logger.trace("discovered attribute '{}'", attrInfo.getName());
447 
448 				Object attribute;
449 
450 				if (null == (attribute = values.get(attrInfo.getName())))
451 				{
452 					logger.trace("cannot retrieve attribute value, skipping");
453 					continue;
454 				}
455 
456 				try
457 				{
458 					String descr = (attrInfo.getName().equals(attrInfo.getDescription()) ? null :
459 							attrInfo.getDescription());
460 
461 					if (attribute instanceof TabularData)
462 					{
463 						logger.trace("looking for attributes of tabular types");
464 
465 						formatPrimitiveTypeResult(counters, name, descr, attrInfo.getName(), attribute,
466 							propertiesAsMacros, getTabularData((TabularData)attribute));
467 					}
468 					else
469 					{
470 						logger.trace("looking for attributes of primitive types");
471 						getAttributeFields(counters, name, descr, attrInfo.getName(), attribute,
472 								propertiesAsMacros);
473 					}
474 				}
475 				catch (Exception e)
476 				{
477 					Object[] logInfo = {name, attrInfo.getName(), ZabbixException.getRootCauseMessage(e)};
478 					logger.warn("attribute processing '{},{}' failed: {}", logInfo);
479 					logger.debug("error caused by", e);
480 				}
481 			}
482 		}
483 	}
484 
getAttributeNames(MBeanAttributeInfo[] attributeArray)485 	private String[] getAttributeNames(MBeanAttributeInfo[] attributeArray)
486 	{
487 		int i = 0;
488 		String[] attributeNames = new String[attributeArray.length];
489 
490 		for (MBeanAttributeInfo attrInfo : attributeArray)
491 		{
492 			if (!attrInfo.isReadable())
493 			{
494 				logger.trace("attribute '{}' not readable, skipping", attrInfo.getName());
495 				continue;
496 			}
497 
498 			attributeNames[i++] = attrInfo.getName();
499 		}
500 
501 		return attributeNames;
502 	}
503 
getAttributesBulk(ObjectName name, String[] attributeNames)504 	private AttributeList getAttributesBulk(ObjectName name, String[] attributeNames) throws Exception
505 	{
506 		return mbsc.getAttributes(name, attributeNames);
507 	}
508 
getAttributesIterative(ObjectName name, String[] attributeNames)509 	private AttributeList getAttributesIterative(ObjectName name, String[] attributeNames)
510 	{
511 		AttributeList attributes = new AttributeList();
512 
513 		for (String attributeName: attributeNames)
514 		{
515 			try
516 			{
517 				Object attrValue = mbsc.getAttribute(name, attributeName);
518 				attributes.add(new javax.management.Attribute(attributeName, attrValue));
519 			}
520 			catch (Exception e)
521 			{
522 				Object[] logInfo = {name, attributeName, ZabbixException.getRootCauseMessage(e)};
523 				logger.warn("attribute processing '{},{}' failed: {}", logInfo);
524 				logger.debug("error caused by", e);
525 			}
526 		}
527 
528 		return attributes;
529 	}
530 
discoverBeans(JSONArray counters, ObjectName filter, boolean propertiesAsMacros)531 	private void discoverBeans(JSONArray counters, ObjectName filter, boolean propertiesAsMacros) throws Exception
532 	{
533 		for (ObjectName name : mbsc.queryNames(filter, null))
534 		{
535 			logger.trace("discovered bean '{}'", name);
536 
537 			try
538 			{
539 				JSONObject counter = new JSONObject();
540 
541 				if (propertiesAsMacros)
542 				{
543 					HashSet<String> properties = new HashSet<String>();
544 
545 					// Default properties are added.
546 					counter.put("{#JMXOBJ}", name);
547 					counter.put("{#JMXDOMAIN}", name.getDomain());
548 					properties.add("OBJ");
549 					properties.add("DOMAIN");
550 
551 					for (Map.Entry<String, String> property : name.getKeyPropertyList().entrySet())
552 					{
553 						String key = property.getKey().toUpperCase();
554 
555 						// Property key should only contain valid characters and should not be already added to attribute list.
556 						if (key.matches("^[A-Z0-9_\\.]+$") && !properties.contains(key))
557 						{
558 							counter.put("{#JMX" + key + "}" , property.getValue());
559 							properties.add(key);
560 						}
561 						else
562 							logger.trace("bean '{}' property '{}' was ignored", name, property.getKey());
563 					}
564 				}
565 				else
566 				{
567 					JSONObject properties = new JSONObject();
568 					counter.put("object", name);
569 					counter.put("domain", name.getDomain());
570 
571 					for (Map.Entry<String, String> property : name.getKeyPropertyList().entrySet())
572 					{
573 						String key = property.getKey();
574 						properties.put(key, property.getValue());
575 					}
576 					counter.put("properties", properties);
577 				}
578 
579 				counters.put(counter);
580 			}
581 			catch (Exception e)
582 			{
583 				logger.warn("bean processing '{}' failed: {}", name, ZabbixException.getRootCauseMessage(e));
584 				logger.debug("error caused by", e);
585 			}
586 		}
587 	}
588 
getAttributeFields(JSONArray counters, ObjectName name, String descr, String attrPath, Object attribute, boolean propertiesAsMacros)589 	private void getAttributeFields(JSONArray counters, ObjectName name, String descr, String attrPath,
590 			Object attribute, boolean propertiesAsMacros) throws NoSuchMethodException, JSONException
591 	{
592 		if (null == attribute || isPrimitiveAttributeType(attribute))
593 		{
594 			logger.trace("found attribute of a primitive type: {}", null == attribute ? "null" :
595 					attribute.getClass());
596 			formatPrimitiveTypeResult(counters, name, descr, attrPath, attribute, propertiesAsMacros, attribute);
597 		}
598 		else if (attribute instanceof CompositeData)
599 		{
600 			logger.trace("found attribute of a composite type: {}", attribute.getClass());
601 
602 			CompositeData comp = (CompositeData)attribute;
603 
604 			for (String key : comp.getCompositeType().keySet())
605 			{
606 				logger.trace("drilling down with attribute path '{}'", attrPath + "." + key);
607 				getAttributeFields(counters, name, comp.getCompositeType().getDescription(key),
608 						attrPath + "." + key, comp.get(key), propertiesAsMacros);
609 			}
610 		}
611 		else if (attribute.getClass().isArray())
612 		{
613 			logger.trace("found attribute of a known, unsupported type: {}", attribute.getClass());
614 		}
615 		else
616 			logger.trace("found attribute of an unknown, unsupported type: {}", attribute.getClass());
617 	}
618 
formatPrimitiveTypeResult(JSONArray counters, ObjectName name, String descr, String attrPath, Object attribute, boolean propertiesAsMacros, Object value)619 	private void formatPrimitiveTypeResult(JSONArray counters, ObjectName name, String descr, String attrPath,
620 			Object attribute, boolean propertiesAsMacros, Object value) throws JSONException
621 	{
622 		JSONObject counter = new JSONObject();
623 
624 		String checkedDescription = null == descr ? name + "," + attrPath : descr;
625 		Object checkedType = null == attribute ? JSONObject.NULL : attribute.getClass().getName();
626 		Object checkedValue = null == value ? JSONObject.NULL : value.toString();
627 
628 		if (propertiesAsMacros)
629 		{
630 			counter.put("{#JMXDESC}", checkedDescription);
631 			counter.put("{#JMXOBJ}", name);
632 			counter.put("{#JMXATTR}", attrPath);
633 			counter.put("{#JMXTYPE}", checkedType);
634 			counter.put("{#JMXVALUE}", checkedValue);
635 		}
636 		else
637 		{
638 			counter.put("name", attrPath);
639 			counter.put("object", name);
640 			counter.put("description", checkedDescription);
641 			counter.put("type", checkedType);
642 			counter.put("value", checkedValue);
643 		}
644 
645 		counters.put(counter);
646 	}
647 
isPrimitiveAttributeType(Object obj)648 	private boolean isPrimitiveAttributeType(Object obj) throws NoSuchMethodException
649 	{
650 		Class<?>[] clazzez = {Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class,
651 			Float.class, Double.class, String.class, java.math.BigDecimal.class, java.math.BigInteger.class,
652 			java.util.Date.class, javax.management.ObjectName.class, java.util.concurrent.atomic.AtomicBoolean.class,
653 			java.util.concurrent.atomic.AtomicInteger.class, java.util.concurrent.atomic.AtomicLong.class};
654 
655 		// check if the type is either primitive or overrides toString()
656 		return HelperFunctionChest.arrayContains(clazzez, obj.getClass()) ||
657 				(!(obj instanceof CompositeData)) && (!(obj instanceof TabularDataSupport)) &&
658 				(obj.getClass().getMethod("toString").getDeclaringClass() != Object.class);
659 	}
660 
cleanUseRMISSLforURLHintCache()661 	public void cleanUseRMISSLforURLHintCache()
662 	{
663 		int s = useRMISSLforURLHintCache.size();
664 		useRMISSLforURLHintCache.clear();
665 		logger.debug("Finished cleanup of RMI SSL hint cache. " + s + " entries removed.");
666 	}
667 }
668