View Javadoc
1   /*-
2    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
3    * SNMP Java Client
4    * ჻჻჻჻჻჻
5    * Copyright 2023 MetricsHub
6    * ჻჻჻჻჻჻
7    * This program is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU Lesser General Public License as
9    * published by the Free Software Foundation, either version 3 of the
10   * License, or (at your option) any later version.
11   *
12   * This program is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Lesser Public License for more details.
16   *
17   * You should have received a copy of the GNU General Lesser Public
18   * License along with this program.  If not, see
19   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
20   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
21   */
22  
23  package org.metricshub.snmp.client;
24  
25  import uk.co.westhawk.snmp.pdu.BlockPdu;
26  import uk.co.westhawk.snmp.stack.AsnObject;
27  import uk.co.westhawk.snmp.stack.AsnObjectId;
28  import uk.co.westhawk.snmp.stack.AsnOctets;
29  import uk.co.westhawk.snmp.stack.PduException;
30  import uk.co.westhawk.snmp.stack.SnmpConstants;
31  import uk.co.westhawk.snmp.stack.SnmpContext;
32  import uk.co.westhawk.snmp.stack.SnmpContextv2c;
33  import uk.co.westhawk.snmp.stack.SnmpContextv3;
34  import uk.co.westhawk.snmp.stack.SnmpContextv3Face;
35  import uk.co.westhawk.snmp.stack.varbind;
36  import java.io.IOException;
37  import java.util.ArrayList;
38  import java.util.Arrays;
39  import java.util.Collections;
40  import java.util.HashSet;
41  import java.util.List;
42  import java.util.Set;
43  
44  public class SnmpClient {
45  
46  	// Default SNMP port number
47  	public static final int SNMP_PORT = 161;
48  
49  	// SNMP v1, v2c, v3
50  	public static final int SNMP_V1 = 1;
51  	public static final int SNMP_V2C = 2;
52  	public static final int SNMP_V3 = 3;
53  	public static final String SNMP_AUTH_MD5 = "MD5";
54  	public static final String SNMP_AUTH_SHA = "SHA";
55  	public static final String SNMP_AUTH_SHA256 = "SHA256";
56  	public static final String SNMP_AUTH_SHA512 = "SHA512";
57  	public static final String SNMP_AUTH_SHA224 = "SHA224";
58  	public static final String SNMP_AUTH_SHA384 = "SHA384";
59  	public static final String SNMP_PRIVACY_DES = "DES";
60  	public static final String SNMP_PRIVACY_AES = "AES";
61  	public static final String SNMP_PRIVACY_AES192 = "AES192";
62  	public static final String SNMP_PRIVACY_AES256 = "AES256";
63  	public static final String SNMP_NONE = "None";
64  
65      public static final Set<String> SNMP_PRIVACY_PROTOCOLS = Collections.unmodifiableSet( new HashSet<>(
66      		Arrays.asList(SNMP_PRIVACY_DES, SNMP_PRIVACY_AES, SNMP_PRIVACY_AES192, SNMP_PRIVACY_AES256)));
67      
68      public static final Set<String> SNMP_AUTH_PROTOCOLS = Collections.unmodifiableSet( new HashSet<>(
69      		Arrays.asList(SNMP_AUTH_MD5, SNMP_AUTH_SHA, SNMP_AUTH_SHA256, SNMP_AUTH_SHA512, SNMP_AUTH_SHA224, SNMP_AUTH_SHA384)));
70      
71  	private SnmpContext contextv1 = null;
72  	private SnmpContextv2c contextv2c = null;
73  	private SnmpContextv3 contextv3 = null;
74  	private BlockPdu pdu;
75  	private String host;
76  	private int port;
77  	private String community;
78  	private int snmpVersion;
79  	private String authUsername;
80  	private String authType;
81  	private String authPassword;
82  	private String privacyType;
83  	private String privacyPassword;
84  	private int[] retryIntervals;
85  	private String contextName;
86  	private byte[] contextEngineID;
87  	public static final String SOCKET_TYPE = "Standard";
88  
89  	/**
90  	 * Creates an SNMPClient instance, which connects to the specified SNMP agent
91  	 * with the specified credentials
92  	 * (depending on the version of SNMP)
93  	 * 
94  	 * @param host            The hostname/IP address of the SNMP agent we're
95  	 *                        querying
96  	 * @param port            The port of the SNMP agent (should be 161)
97  	 * @param version         The version of SNMP to use (1, 2 or 3)
98  	 * @param retryIntervals  Timeout in milliseconds after which the elementary
99  	 *                        operations will be retried
100 	 * @param community       <i>(SNMP v1 and v2 only)</i> The SNMP community
101 	 * @param authType        <i>(SNMP v3 only)</i> The authentication method:
102 	 *                        "MD5", "SHA" or ""
103 	 * @param authUsername    <i>(SNMP v3 only)</i> The username
104 	 * @param authPassword    <i>(SNMP v3 only)</i> The password (in clear)
105 	 * @param privacyType     <i>(SNMP v3 only)</i> The encryption type: "DES",
106 	 *                        "AES" or ""
107 	 * @param privacyPassword <i>(SNMP v3 only)</i> The encryption password
108 	 * @param contextName     <i>(SNMP v3 only)</i> The context name
109 	 * @param contextID       <i>(SNMP v3 only)</i> The context ID (??)
110 	 * @throws IllegalArgumentException when specified authType, privType are
111 	 *                                  invalid
112 	 * @throws IllegalStateException    when the specified properties lead to
113 	 *                                  something that cannot work (i.e. privacy
114 	 *                                  without authentication)
115 	 * @throws IOException              when cannot initialize the SNMP context
116 	 */
117 	public SnmpClient(String host, int port, int version, int[] retryIntervals,
118 			String community,
119 			String authType, String authUsername, String authPassword,
120 			String privacyType, String privacyPassword,
121 			String contextName, byte[] contextID) throws IOException {
122 		// First, validate the inputs
123 		validate(version, authType, privacyType);
124 
125 		// Sets the attributes of the class instance
126 		this.host = host;
127 		this.port = port;
128 		this.snmpVersion = version;
129 		this.retryIntervals = retryIntervals;
130 		this.community = community;
131 		this.authType = authType;
132 		this.authUsername = authUsername;
133 		this.authPassword = authPassword;
134 		this.privacyType = privacyType;
135 		this.privacyPassword = privacyPassword;
136 		this.contextName = contextName;
137 		this.contextEngineID = contextID;
138 
139 		// Properly create the SNMP context, based on these properties
140 		initialize();
141 	}
142 
143 	/**
144 	 * Validate the specified inputs. Throws an IllegalArgumentException if needed.
145 	 * 
146 	 * @param version     Specified version of the SNMP protocol
147 	 * @param authType    Specified authType
148 	 * @param privacyType Specified privacyType
149 	 * @throws IllegalArgumentException when invalid inputs are specified
150 	 */
151 	private void validate(int version, String authType, String privacyType) {
152 
153 		// In case of SNMP v3, check the authType and privType (if not empty)
154 		if (version == SNMP_V3) {
155 			if (authType != null) {
156 				if (!authType.isEmpty()) {
157 					if (! SNMP_AUTH_PROTOCOLS.contains(authType)) {
158 						throw new IllegalArgumentException("Invalid authentication method '" + authType + "'." +
159 								" (Valid options are: '" + SNMP_AUTH_MD5
160 								+ "', '" + SNMP_AUTH_SHA
161 								+ "', '" + SNMP_AUTH_SHA256
162 								+ "', '" + SNMP_AUTH_SHA512
163 								+ "', '" + SNMP_AUTH_SHA224
164 								+ "', '" + SNMP_AUTH_SHA384
165 								+ "', or empty)");
166 					}
167 				}
168 			}
169 
170 			if (privacyType != null) {
171 				if (!privacyType.isEmpty()) {
172 					if (! SNMP_PRIVACY_PROTOCOLS.contains(privacyType)) {
173 						throw new IllegalArgumentException(
174 								"Invalid privacy method '" + privacyType + "'." + " (Valid options are:'" + SNMP_PRIVACY_DES
175 								+ "', '" + SNMP_PRIVACY_AES + "', '" + SNMP_PRIVACY_AES192 + "', '" + SNMP_PRIVACY_AES256  + "', or empty)");
176 					}
177 				}
178 			}
179 		}
180 	}
181 
182 	/**
183 	 * Initialize the SNMPClient
184 	 * <p>
185 	 * Creates the context to connect to the SNMP agent. Required before any actual
186 	 * operation is performed.
187 	 * 
188 	 * @throws IOException           when cannot create the SNMP context
189 	 * @throws IllegalStateException when there is an inconsistency in the
190 	 *                               properties that prevent us from moving forward
191 	 */
192 	private void initialize() throws IOException {
193 
194 		// SNMP v2c
195 		if (snmpVersion == SNMP_V2C) {
196 			contextv2c = new SnmpContextv2c(host, port, null, SOCKET_TYPE);
197 			contextv2c.setCommunity(community);
198 		}
199 
200 		// SNMP v3
201 		else if (snmpVersion == SNMP_V3) {
202 			int authProtocolCode = 0;
203 			int privacyProtocolCode = 0;
204 			boolean authenticate = false;
205 			boolean privacy = false;
206 
207 			// Some sanity check with the "context"
208 			if (contextEngineID == null) {
209 				contextEngineID = new byte[0];
210 			}
211 			if (contextName == null) {
212 				contextName = "";
213 			}
214 
215 			// Verify the username
216 			if (authUsername == null) {
217 				authUsername = "";
218 			}
219 
220 			// Verify and translate the authentication type
221 			if (authType == null || authUsername == null || authPassword == null) {
222 				authenticate = false;
223 				authProtocolCode = SnmpContextv3Face.NO_AUTH_PROTOCOL;
224 				authPassword = "";
225 			} else if (authType.isEmpty() || authUsername.isEmpty() || authPassword.isEmpty()) {
226 				authenticate = false;
227 				authProtocolCode = SnmpContextv3Face.NO_AUTH_PROTOCOL;
228 				authPassword = "";
229 			} else if (authType.equals(SNMP_AUTH_MD5)) {
230 				authenticate = true;
231 				authProtocolCode = SnmpContextv3Face.MD5_PROTOCOL;
232 			} else if (authType.equals(SNMP_AUTH_SHA)) {
233 				authenticate = true;
234 				authProtocolCode = SnmpContextv3Face.SHA1_PROTOCOL;
235 			} else if (authType.equals(SNMP_AUTH_SHA256)) {
236 				authenticate = true;
237 				authProtocolCode = SnmpContextv3Face.SHA256_PROTOCOL;
238 			} else if (authType.equals(SNMP_AUTH_SHA512)) {
239 				authenticate = true;
240 				authProtocolCode = SnmpContextv3Face.SHA512_PROTOCOL;
241 			} else if (authType.equals(SNMP_AUTH_SHA224)) {
242 				authenticate = true;
243 				authProtocolCode = SnmpContextv3Face.SHA224_PROTOCOL;
244 
245 			} else if (authType.equals(SNMP_AUTH_SHA384)) {
246 				authenticate = true;
247 				authProtocolCode = SnmpContextv3Face.SHA384_PROTOCOL;
248 			}
249 
250 			// Verify the privacy thing
251 			if (privacyType == null || privacyPassword == null) {
252 				privacy = false;
253 			} else if (privacyType.isEmpty() || privacyPassword.isEmpty()) {
254 				privacy = false;
255 			} else if (privacyType.equals(SNMP_PRIVACY_DES)) {
256 				privacy = true;
257 				privacyProtocolCode = SnmpContextv3Face.DES_ENCRYPT;
258 			} else if (privacyType.equals(SNMP_PRIVACY_AES)) {
259 				privacy = true;
260 				privacyProtocolCode = SnmpContextv3Face.AES_ENCRYPT;
261 			} else if (privacyType.equals(SNMP_PRIVACY_AES192)) {
262 			    privacy = true;
263 			    privacyProtocolCode = SnmpContextv3Face.AES192_ENCRYPT;
264 			} else if (privacyType.equals(SNMP_PRIVACY_AES256)) {
265 			    privacy = true;
266 			    privacyProtocolCode = SnmpContextv3Face.AES256_ENCRYPT;
267 			}
268 			// Privacy with no authentication is impossible
269 			if (privacy && !authenticate) {
270 				throw new IllegalStateException("Authentication is required for privacy to be enforced");
271 			}
272 
273 			// Create the context
274 			contextv3 = new SnmpContextv3(host, port, SOCKET_TYPE);
275 			contextv3.setContextEngineId(contextEngineID);
276 			contextv3.setContextName(contextName);
277 			contextv3.setUserName(authUsername);
278 			contextv3.setUseAuthentication(authenticate);
279 			if (authenticate) {
280 				contextv3.setUserAuthenticationPassword(authPassword);
281 				contextv3.setAuthenticationProtocol(authProtocolCode);
282 				contextv3.setUsePrivacy(privacy);
283 				if (privacy) {
284 					contextv3.setPrivacyProtocol(privacyProtocolCode);
285 					contextv3.setUserPrivacyPassword(privacyPassword);
286 				}
287 			}
288 		}
289 
290 		// SNMP v1 (default)
291 		else {
292 			contextv1 = new SnmpContext(host, port, SOCKET_TYPE);
293 			contextv1.setCommunity(community);
294 		}
295 
296 		// Small thing: set the prefix for hex values (default is "0x" but we're setting
297 		// it to empty)
298 		AsnOctets.setHexPrefix("");
299 
300 		// AsnObject.setDebug(15);
301 
302 	}
303 
304 	/**
305 	 * Releases the resources associated to this instance
306 	 * (or so at least we believe...)
307 	 */
308 	public void freeResources() {
309 		if (contextv1 != null) {
310 			contextv1.destroy();
311 			contextv1 = null;
312 		}
313 		if (contextv2c != null) {
314 			contextv2c.destroy();
315 			contextv2c = null;
316 		}
317 		if (contextv3 != null) {
318 			contextv3.destroy();
319 			contextv3 = null;
320 		}
321 
322 		if (pdu != null) {
323 			pdu = null;
324 		}
325 	}
326 
327 	/**
328 	 * Create the PDU and sets the timeout
329 	 * <p>
330 	 * Note: This method has been created just to avoid duplicate code in the get,
331 	 * getNext and walk functions
332 	 */
333 	private void createPdu() {
334 		// Create the PDU based on the proper context
335 		if (snmpVersion == SNMP_V2C) {
336 			pdu = new BlockPdu(contextv2c);
337 		} else if (snmpVersion == SNMP_V3) {
338 			pdu = new BlockPdu(contextv3);
339 		} else {
340 			pdu = new BlockPdu(contextv1);
341 		}
342 
343 		// Set the timeout
344 		if (retryIntervals != null) {
345 			pdu.setRetryIntervals(retryIntervals);
346 		}
347 	}
348 
349 	/**
350 	 * Perform a GET operation on the specified OID
351 	 * 
352 	 * @param oid OID on which to perform a GET operation
353 	 * @return Value of the specified OID
354 	 * @throws Exception in case of any problem
355 	 */
356 	public String get(String oid) throws Exception {
357 		createPdu();
358 		pdu.setPduType(BlockPdu.GET);
359 		pdu.addOid(oid);
360 		return sendRequest().value;
361 	}
362 
363 	/**
364 	 * Perform a GET operation on the specified OID and return the details of the
365 	 * result (including the type of the value)
366 	 * 
367 	 * @param oid OID on which to perform a GET operation
368 	 * @return A string in the form of the OID, "string" and the value, separated by
369 	 *         tabs (\t)
370 	 * @throws Exception in case of any problem
371 	 */
372 	public String getWithDetails(String oid) throws Exception {
373 		createPdu();
374 		pdu.setPduType(BlockPdu.GET);
375 		pdu.addOid(oid);
376 		SnmpResult result = sendRequest();
377 		return result.oid + "\t" + result.type + "\t" + result.value;
378 	}
379 
380 	/**
381 	 * Perform a GETNEXT operation on the specified OID
382 	 * 
383 	 * @param oid OID on which to perform a GETNEXT operation
384 	 * @return A string in the form of the OID, "string" and the value, separated by
385 	 *         tabs (\t)
386 	 * @throws Exception in case of any problem
387 	 */
388 	public String getNext(String oid) throws Exception {
389 		createPdu();
390 		pdu.setPduType(BlockPdu.GETNEXT);
391 		pdu.addOid(oid);
392 		SnmpResult result = sendRequest();
393 		return result.oid + "\t" + result.type + "\t" + result.value;
394 	}
395 
396 	/**
397 	 * Perform a WALK, i.e. a series of GETNEXT operations until we fall off the
398 	 * tree
399 	 * 
400 	 * @param oid Root OID of the tree
401 	 * @return Result of the WALK operation, as a long String. Each pair of
402 	 *         oid/value is separated with a linefeed (at least, for now!)
403 	 * @throws Exception
404 	 * @throws IllegalArgumentException for bad specified OIDs
405 	 */
406 	public String walk(String oid) throws Exception {
407 
408 		StringBuilder walkResult = new StringBuilder();
409 		String currentOID;
410 		SnmpResult getNextResult;
411 
412 		// Sanity check?
413 		if (oid == null) {
414 			throw new IllegalArgumentException("Invalid SNMP Walk OID: null");
415 		}
416 		if (oid.length() < 3) {
417 			throw new IllegalArgumentException("Invalid SNMP Walk OID: \"" + oid + "\"");
418 		}
419 
420 		// Now, something special:
421 		// In the walk loop below, we will catch any exception and break out of the loop
422 		// if anything happens. At that point, we simply return what we have, i.e. just
423 		// as if everything was okay. Doing so, we fail to report authentication
424 		// problems.
425 		// So, the code using this will think it's just okay, even though the QA team
426 		// intentionally put bad credentials to verify the error message... See
427 		// MATSYA-464.
428 		//
429 		// So, we're going to first run a getNext() for nothing, just so that
430 		// this call will throw the proper exception in case of credentials problems.
431 		getNext(oid);
432 
433 		currentOID = oid;
434 		do {
435 			createPdu();
436 			pdu.setPduType(BlockPdu.GETNEXT);
437 			pdu.addOid(currentOID);
438 			try {
439 				getNextResult = sendRequest();
440 			} catch (Exception e) {
441 				// Something wrong? Get out of the loop and return what we have
442 				break;
443 			}
444 
445 			currentOID = getNextResult.oid;
446 			if (!currentOID.startsWith(oid)) {
447 				// We're off the tree, so get out of the loop
448 				break;
449 			}
450 
451 			// Append the result
452 			walkResult.append(currentOID + "\t" + getNextResult.type + "\t" + getNextResult.value + "\n");
453 
454 		} while (walkResult.length() < 10 * 1048576); // 10 MB is the limit for the result of our WALK operation. Should
455 														// be enough.
456 
457 		// Remove the trailing \n (if any)
458 		int resultLength = walkResult.length();
459 		if (resultLength > 0) {
460 			return walkResult.substring(0, resultLength - 1);
461 		}
462 
463 		// If nothing, return an empty string
464 		return "";
465 	}
466 
467 	/**
468 	 * Read the content of an SNMP table
469 	 * 
470 	 * @param rootOID           Root OID of the SNMP table
471 	 * @param selectColumnArray Array of numbers specifying the column numbers of
472 	 *                          the array to be read. Use "ID" for the row number.
473 	 * @return A semicolon-separated list of values
474 	 * @throws IllegalArgumentException when the specified arguments are wrong
475 	 * @throws Exception                when the underlying SNMP API throws one
476 	 */
477 	public List<List<String>> table(String rootOID, String[] selectColumnArray) throws Exception {
478 
479 		// Sanity check
480 		if (rootOID == null) {
481 			throw new IllegalArgumentException("Invalid SNMP Table OID: null");
482 		}
483 		if (rootOID.length() < 3) {
484 			throw new IllegalArgumentException("Invalid SNMP Table OID: \"" + rootOID + "\"");
485 		}
486 		if (selectColumnArray == null) {
487 			throw new IllegalArgumentException("Invalid SNMP Table column numbers: null");
488 		}
489 		if (selectColumnArray.length < 1) {
490 			throw new IllegalArgumentException("Invalid SNMP Table column numbers: none");
491 		}
492 
493 		// First of all, retrieve the list of IDs in the table
494 		// To do so, we need to see what is the first column number available (it may
495 		// not be 1)
496 		createPdu();
497 		pdu.setPduType(BlockPdu.GETNEXT);
498 		pdu.addOid(rootOID);
499 		String firstValueOid = sendRequest().oid;
500 		if (firstValueOid.isEmpty() || !firstValueOid.startsWith(rootOID)) {
501 			// Empty table
502 			return new ArrayList<>();
503 		}
504 
505 		int tempIndex = firstValueOid.indexOf(".", rootOID.length() + 2);
506 		if (tempIndex < 0) {
507 			// Weird case, there is no "." after the rootOID in the OID of the first value
508 			// we successfully got in the table
509 			return new ArrayList<>();
510 		}
511 		String firstColumnOid = firstValueOid.substring(0, tempIndex);
512 		int firstColumnOidLength = firstColumnOid.length();
513 
514 		// Now, find the list of row IDs in this column. We're going to do something
515 		// like a walk, except we don't care about the values. Just the OIDs.
516 		ArrayList<String> IDArray = new ArrayList<String>(0);
517 		String currentOID = firstColumnOid;
518 		SnmpResult getNextResult;
519 		do {
520 			// Get next until we get out of the tree
521 			createPdu();
522 			pdu.setPduType(BlockPdu.GETNEXT);
523 			pdu.addOid(currentOID);
524 			getNextResult = sendRequest();
525 
526 			currentOID = getNextResult.oid;
527 
528 			// Outside? Exit!
529 			if (!currentOID.startsWith(firstColumnOid)) {
530 				break;
531 			}
532 
533 			// Add the right part of the OID in the list of IDs (the part to the right of
534 			// the column OID)
535 			IDArray.add(currentOID.substring(firstColumnOidLength + 1));
536 
537 		} while (IDArray.size() < 10000); // Not more than 10000 lines, please...
538 
539 		// And finally, build the result table
540 		List<List<String>> tableResult = new ArrayList<>();
541 		for (String ID : IDArray) {
542 			// For each row...
543 			List<String> row = new ArrayList<>();
544 			for (String column : selectColumnArray) {
545 				// For each column...
546 
547 				// If the column has to provide the ID of the row
548 				if (column.equals("ID")) {
549 					row.add(ID);
550 				} else {
551 					// Keep going, even in case of a failure
552 					try {
553 						row.add(get(rootOID + "." + column + "." + ID));
554 					} catch (Exception e) {
555 						row.add("");
556 					}
557 				}
558 			}
559 			tableResult.add(row);
560 		}
561 
562 		// Return the result
563 		return tableResult;
564 	}
565 
566 	/**
567 	 * Sends the SNMP request and perform some minor interpretation of the result
568 	 * 
569 	 * @return Result of the query in the form of a couple {oid;value} (SnmpResult)
570 	 * @throws PduException when an error happens at the SNMP layer
571 	 * @throws IOException  when an error occurs at the network layer
572 	 * @throws Exception    when we cannot get the value of the specified OID,
573 	 *                      because it does not exist
574 	 *                      <p>
575 	 *                      <li>In case of no such OID, an exception is thrown.
576 	 *                      <li>In case of empty value, the result will have the oid
577 	 *                      and an empty value.
578 	 */
579 	private SnmpResult sendRequest() throws PduException, IOException, Exception {
580 
581 		// Declarations
582 		SnmpResult result = new SnmpResult();
583 
584 		// Send the SNMP request
585 		varbind var = pdu.getResponseVariableBinding();
586 
587 		// Retrieve the OID and value of the response (a varbind)
588 		AsnObjectId oid = var.getOid();
589 		AsnObject value = var.getValue();
590 
591 		// No such OID? Throw an exception (this needs to be caught gracefully by other
592 		// functions)
593 		byte valueType = value.getRespType();
594 		if (valueType == SnmpConstants.SNMP_VAR_NOSUCHOBJECT ||
595 				valueType == SnmpConstants.SNMP_VAR_NOSUCHINSTANCE ||
596 				valueType == SnmpConstants.SNMP_VAR_ENDOFMIBVIEW) {
597 			throw new Exception(value.getRespTypeString());
598 		}
599 
600 		// Empty?
601 		else if (valueType == SnmpConstants.ASN_NULL) {
602 			result.oid = oid.toString();
603 			result.type = "null";
604 		}
605 
606 		// ASN_OCTET_STRING? (special case, because it may need to be displayed as an
607 		// hexadecimal string)
608 		// Normally, the API should take care of that, but this is not the case for
609 		// 0x00:00:00:00 values, which are displayed as empty values
610 		else if (valueType == SnmpConstants.ASN_OCTET_STR) {
611 
612 			result.oid = oid.toString();
613 			result.type = "ASN_OCTET_STR";
614 
615 			// Map the value to the specific AsnOctets sub-class
616 			AsnOctets octetStringValue = (AsnOctets) value;
617 
618 			// First, convert the value as a string, using toString().
619 			// AsnOctets.toString() will convert the value to a normal string (with ASCII
620 			// chars)
621 			// or to the hexadecimal version of it, like 0x00:30:31:32 when the "normal"
622 			// string is
623 			// not printable
624 			String octetStringValuetoString = octetStringValue.toString();
625 
626 			// Then forcibly convert the value to its hexadecimal representation, but
627 			// replace ':' with blank spaces, to match with what PATROL does
628 			String octetStringValuetoHex = octetStringValue.toHex().replace(':', ' ');
629 
630 			// So, if the toString() and the toHex() value have the same length, it means
631 			// that toString() is actually returning
632 			// the hexadecimal representation (yeah, there is no other way to retrieve that,
633 			// the SNMP API does not tell us
634 			// whether the value is printable or not)
635 			// If the SNMP API judges that the value is not printable, then we will use the
636 			// toHex() value, with ':' replaced with blank spaces.
637 			// But there is another case: if the original value is just a series of 0x00
638 			// (nul) chars, the SNMP API converts that to an
639 			// empty string, while we need to actually display 00 00 00 00...
640 			// That's what we're doing in the code below
641 
642 			// If octetStringValuetoString is empty while the original value's length was
643 			// greater than 0, then we'll need to convert it to hexadecimal
644 			if (octetStringValuetoString.isEmpty() && octetStringValue.getBytes().length > 0) {
645 				result.value = octetStringValuetoHex;
646 			} else if (octetStringValuetoString.length() == octetStringValuetoHex.length()) {
647 				result.value = octetStringValuetoHex;
648 			} else {
649 				result.value = octetStringValuetoString;
650 			}
651 		}
652 
653 		// Sets the result object
654 		else {
655 			result.oid = oid.toString();
656 			result.type = value.getRespTypeString();
657 			result.value = value.toString();
658 		}
659 
660 		return result;
661 
662 	} // end of sendRequest
663 
664 }// end of class - SNMPClient