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 implements ISnmpClient {
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 	@Override
309 	public void freeResources() {
310 		if (contextv1 != null) {
311 			contextv1.destroy();
312 			contextv1 = null;
313 		}
314 		if (contextv2c != null) {
315 			contextv2c.destroy();
316 			contextv2c = null;
317 		}
318 		if (contextv3 != null) {
319 			contextv3.destroy();
320 			contextv3 = null;
321 		}
322 
323 		if (pdu != null) {
324 			pdu = null;
325 		}
326 	}
327 
328 	/**
329 	 * Create the PDU and sets the timeout
330 	 * <p>
331 	 * Note: This method has been created just to avoid duplicate code in the get,
332 	 * getNext and walk functions
333 	 */
334 	private void createPdu() {
335 		// Create the PDU based on the proper context
336 		if (snmpVersion == SNMP_V2C) {
337 			pdu = new BlockPdu(contextv2c);
338 		} else if (snmpVersion == SNMP_V3) {
339 			pdu = new BlockPdu(contextv3);
340 		} else {
341 			pdu = new BlockPdu(contextv1);
342 		}
343 
344 		// Set the timeout
345 		if (retryIntervals != null) {
346 			pdu.setRetryIntervals(retryIntervals);
347 		}
348 	}
349 
350 	/**
351 	 * Perform a GET operation on the specified OID
352 	 * 
353 	 * @param oid OID on which to perform a GET operation
354 	 * @return Value of the specified OID
355 	 * @throws Exception in case of any problem
356 	 */
357 	public String get(String oid) throws Exception {
358 		createPdu();
359 		pdu.setPduType(BlockPdu.GET);
360 		pdu.addOid(oid);
361 		return sendRequest().value;
362 	}
363 
364 	/**
365 	 * Perform a GET operation on the specified OID and return the details of the
366 	 * result (including the type of the value)
367 	 * 
368 	 * @param oid OID on which to perform a GET operation
369 	 * @return A string in the form of the OID, "string" and the value, separated by
370 	 *         tabs (\t)
371 	 * @throws Exception in case of any problem
372 	 */
373 	public String getWithDetails(String oid) throws Exception {
374 		createPdu();
375 		pdu.setPduType(BlockPdu.GET);
376 		pdu.addOid(oid);
377 		SnmpResult result = sendRequest();
378 		return result.oid + "\t" + result.type + "\t" + result.value;
379 	}
380 
381 	/**
382 	 * Perform a GETNEXT operation on the specified OID
383 	 * 
384 	 * @param oid OID on which to perform a GETNEXT operation
385 	 * @return A string in the form of the OID, "string" and the value, separated by
386 	 *         tabs (\t)
387 	 * @throws Exception in case of any problem
388 	 */
389 	public String getNext(String oid) throws Exception {
390 		createPdu();
391 		pdu.setPduType(BlockPdu.GETNEXT);
392 		pdu.addOid(oid);
393 		SnmpResult result = sendRequest();
394 		return result.oid + "\t" + result.type + "\t" + result.value;
395 	}
396 
397 	/**
398 	 * Perform a WALK, i.e. a series of GETNEXT operations until we fall off the
399 	 * tree
400 	 * 
401 	 * @param oid Root OID of the tree
402 	 * @return Result of the WALK operation, as a long String. Each pair of
403 	 *         oid/value is separated with a linefeed (at least, for now!)
404 	 * @throws Exception
405 	 * @throws IllegalArgumentException for bad specified OIDs
406 	 */
407 	public String walk(String oid) throws Exception {
408 
409 		StringBuilder walkResult = new StringBuilder();
410 		String currentOID;
411 		SnmpResult getNextResult;
412 
413 		// Sanity check?
414 		if (oid == null) {
415 			throw new IllegalArgumentException("Invalid SNMP Walk OID: null");
416 		}
417 		if (oid.length() < 3) {
418 			throw new IllegalArgumentException("Invalid SNMP Walk OID: \"" + oid + "\"");
419 		}
420 
421 		// Now, something special:
422 		// In the walk loop below, we will catch any exception and break out of the loop
423 		// if anything happens. At that point, we simply return what we have, i.e. just
424 		// as if everything was okay. Doing so, we fail to report authentication
425 		// problems.
426 		// So, the code using this will think it's just okay, even though the QA team
427 		// intentionally put bad credentials to verify the error message... See
428 		// MATSYA-464.
429 		//
430 		// So, we're going to first run a getNext() for nothing, just so that
431 		// this call will throw the proper exception in case of credentials problems.
432 		getNext(oid);
433 
434 		currentOID = oid;
435 		do {
436 			createPdu();
437 			pdu.setPduType(BlockPdu.GETNEXT);
438 			pdu.addOid(currentOID);
439 			try {
440 				getNextResult = sendRequest();
441 			} catch (Exception e) {
442 				// Something wrong? Get out of the loop and return what we have
443 				break;
444 			}
445 
446 			currentOID = getNextResult.oid;
447 			if (!currentOID.startsWith(oid)) {
448 				// We're off the tree, so get out of the loop
449 				break;
450 			}
451 
452 			// Append the result
453 			walkResult.append(currentOID + "\t" + getNextResult.type + "\t" + getNextResult.value + "\n");
454 
455 		} while (walkResult.length() < 10 * 1048576); // 10 MB is the limit for the result of our WALK operation. Should
456 														// be enough.
457 
458 		// Remove the trailing \n (if any)
459 		int resultLength = walkResult.length();
460 		if (resultLength > 0) {
461 			return walkResult.substring(0, resultLength - 1);
462 		}
463 
464 		// If nothing, return an empty string
465 		return "";
466 	}
467 
468 	/**
469 	 * Read the content of an SNMP table
470 	 * 
471 	 * @param rootOID           Root OID of the SNMP table
472 	 * @param selectColumnArray Array of numbers specifying the column numbers of
473 	 *                          the array to be read. Use "ID" for the row number.
474 	 * @return A semicolon-separated list of values
475 	 * @throws IllegalArgumentException when the specified arguments are wrong
476 	 * @throws Exception                when the underlying SNMP API throws one
477 	 */
478 	public List<List<String>> table(String rootOID, String[] selectColumnArray) throws Exception {
479 
480 		// Sanity check
481 		if (rootOID == null) {
482 			throw new IllegalArgumentException("Invalid SNMP Table OID: null");
483 		}
484 		if (rootOID.length() < 3) {
485 			throw new IllegalArgumentException("Invalid SNMP Table OID: \"" + rootOID + "\"");
486 		}
487 		if (selectColumnArray == null) {
488 			throw new IllegalArgumentException("Invalid SNMP Table column numbers: null");
489 		}
490 		if (selectColumnArray.length < 1) {
491 			throw new IllegalArgumentException("Invalid SNMP Table column numbers: none");
492 		}
493 
494 		// First of all, retrieve the list of IDs in the table
495 		// To do so, we need to see what is the first column number available (it may
496 		// not be 1)
497 		createPdu();
498 		pdu.setPduType(BlockPdu.GETNEXT);
499 		pdu.addOid(rootOID);
500 		String firstValueOid = sendRequest().oid;
501 		if (firstValueOid.isEmpty() || !firstValueOid.startsWith(rootOID)) {
502 			// Empty table
503 			return new ArrayList<>();
504 		}
505 
506 		int tempIndex = firstValueOid.indexOf(".", rootOID.length() + 2);
507 		if (tempIndex < 0) {
508 			// Weird case, there is no "." after the rootOID in the OID of the first value
509 			// we successfully got in the table
510 			return new ArrayList<>();
511 		}
512 		String firstColumnOid = firstValueOid.substring(0, tempIndex);
513 		int firstColumnOidLength = firstColumnOid.length();
514 
515 		// Now, find the list of row IDs in this column. We're going to do something
516 		// like a walk, except we don't care about the values. Just the OIDs.
517 		ArrayList<String> IDArray = new ArrayList<String>(0);
518 		String currentOID = firstColumnOid;
519 		SnmpResult getNextResult;
520 		do {
521 			// Get next until we get out of the tree
522 			createPdu();
523 			pdu.setPduType(BlockPdu.GETNEXT);
524 			pdu.addOid(currentOID);
525 			getNextResult = sendRequest();
526 
527 			currentOID = getNextResult.oid;
528 
529 			// Outside? Exit!
530 			if (!currentOID.startsWith(firstColumnOid)) {
531 				break;
532 			}
533 
534 			// Add the right part of the OID in the list of IDs (the part to the right of
535 			// the column OID)
536 			IDArray.add(currentOID.substring(firstColumnOidLength + 1));
537 
538 		} while (IDArray.size() < 10000); // Not more than 10000 lines, please...
539 
540 		// And finally, build the result table
541 		List<List<String>> tableResult = new ArrayList<>();
542 		for (String ID : IDArray) {
543 			// For each row...
544 			List<String> row = new ArrayList<>();
545 			for (String column : selectColumnArray) {
546 				// For each column...
547 
548 				// If the column has to provide the ID of the row
549 				if (column.equals("ID")) {
550 					row.add(ID);
551 				} else {
552 					// Keep going, even in case of a failure
553 					try {
554 						row.add(get(rootOID + "." + column + "." + ID));
555 					} catch (Exception e) {
556 						row.add("");
557 					}
558 				}
559 			}
560 			tableResult.add(row);
561 		}
562 
563 		// Return the result
564 		return tableResult;
565 	}
566 
567 	/**
568 	 * Sends the SNMP request and perform some minor interpretation of the result
569 	 * 
570 	 * @return Result of the query in the form of a couple {oid;value} (SnmpResult)
571 	 * @throws PduException when an error happens at the SNMP layer
572 	 * @throws IOException  when an error occurs at the network layer
573 	 * @throws Exception    when we cannot get the value of the specified OID,
574 	 *                      because it does not exist
575 	 *                      <p>
576 	 *                      <li>In case of no such OID, an exception is thrown.
577 	 *                      <li>In case of empty value, the result will have the oid
578 	 *                      and an empty value.
579 	 */
580 	private SnmpResult sendRequest() throws PduException, IOException, Exception {
581 
582 		// Declarations
583 		SnmpResult result = new SnmpResult();
584 
585 		// Send the SNMP request
586 		varbind var = pdu.getResponseVariableBinding();
587 
588 		// Retrieve the OID and value of the response (a varbind)
589 		AsnObjectId oid = var.getOid();
590 		AsnObject value = var.getValue();
591 
592 		// No such OID? Throw an exception (this needs to be caught gracefully by other
593 		// functions)
594 		byte valueType = value.getRespType();
595 		if (valueType == SnmpConstants.SNMP_VAR_NOSUCHOBJECT ||
596 				valueType == SnmpConstants.SNMP_VAR_NOSUCHINSTANCE ||
597 				valueType == SnmpConstants.SNMP_VAR_ENDOFMIBVIEW) {
598 			throw new Exception(value.getRespTypeString());
599 		}
600 
601 		// Empty?
602 		else if (valueType == SnmpConstants.ASN_NULL) {
603 			result.oid = oid.toString();
604 			result.type = "null";
605 		}
606 
607 		// ASN_OCTET_STRING? (special case, because it may need to be displayed as an
608 		// hexadecimal string)
609 		// Normally, the API should take care of that, but this is not the case for
610 		// 0x00:00:00:00 values, which are displayed as empty values
611 		else if (valueType == SnmpConstants.ASN_OCTET_STR) {
612 
613 			result.oid = oid.toString();
614 			result.type = "ASN_OCTET_STR";
615 
616 			// Map the value to the specific AsnOctets sub-class
617 			AsnOctets octetStringValue = (AsnOctets) value;
618 
619 			// First, convert the value as a string, using toString().
620 			// AsnOctets.toString() will convert the value to a normal string (with ASCII
621 			// chars)
622 			// or to the hexadecimal version of it, like 0x00:30:31:32 when the "normal"
623 			// string is
624 			// not printable
625 			String octetStringValuetoString = octetStringValue.toString();
626 
627 			// Then forcibly convert the value to its hexadecimal representation, but
628 			// replace ':' with blank spaces, to match with what PATROL does
629 			String octetStringValuetoHex = octetStringValue.toHex().replace(':', ' ');
630 
631 			// So, if the toString() and the toHex() value have the same length, it means
632 			// that toString() is actually returning
633 			// the hexadecimal representation (yeah, there is no other way to retrieve that,
634 			// the SNMP API does not tell us
635 			// whether the value is printable or not)
636 			// If the SNMP API judges that the value is not printable, then we will use the
637 			// toHex() value, with ':' replaced with blank spaces.
638 			// But there is another case: if the original value is just a series of 0x00
639 			// (nul) chars, the SNMP API converts that to an
640 			// empty string, while we need to actually display 00 00 00 00...
641 			// That's what we're doing in the code below
642 
643 			// If octetStringValuetoString is empty while the original value's length was
644 			// greater than 0, then we'll need to convert it to hexadecimal
645 			if (octetStringValuetoString.isEmpty() && octetStringValue.getBytes().length > 0) {
646 				result.value = octetStringValuetoHex;
647 			} else if (octetStringValuetoString.length() == octetStringValuetoHex.length()) {
648 				result.value = octetStringValuetoHex;
649 			} else {
650 				result.value = octetStringValuetoString;
651 			}
652 		}
653 
654 		// Sets the result object
655 		else {
656 			result.oid = oid.toString();
657 			result.type = value.getRespTypeString();
658 			result.value = value.toString();
659 		}
660 
661 		return result;
662 
663 	} // end of sendRequest
664 
665 }// end of class - SNMPClient