/******************************************************************************* * This file is part of OpenNMS(R). * * Copyright (C) 2011 The OpenNMS Group, Inc. * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc. * * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc. * * OpenNMS(R) is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * OpenNMS(R) is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenNMS(R). If not, see: * http://www.gnu.org/licenses/ * * For more information contact: * OpenNMS(R) Licensing <license@opennms.org> * http://www.opennms.org/ * http://www.opennms.com/ *******************************************************************************/ package org.opennms.protocols.snmp; import java.io.ByteArrayOutputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.io.PrintStream; import java.lang.reflect.Method; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.util.Date; import java.util.LinkedList; import org.opennms.protocols.snmp.asn1.ASN1; import org.opennms.protocols.snmp.asn1.AsnDecodingException; import org.opennms.protocols.snmp.asn1.AsnEncoder; /** * Abstracts the communication related details from the SnmpSession and * SnmpTrapSession. * * @author <a href="mailto:weave@oculan.com">Brian Weaver </a> * @author <a href="mailto:sowmya@opennms.org">Sowmya Nataraj </a> * @author <a href="http://www.opennms.org">OpenNMS </a> * * @see SnmpSession * @see SnmpTrapSession * @see java.net.DatagramSocket */ public class SnmpPortal extends Object { /** * The packet handler that is used to process received SNMP packets and * invalid datagrams. The handler must also process any exceptions that * occurs in the receiving thread. * */ private SnmpPacketHandler m_handler; /** * The datagram socket used to send and receive SNMP messages. * */ private DatagramSocket m_comm; /** * the receiver thread that runs the inner class Receiver. */ private Thread m_recvThread; /** * ASN.1 encoder used to decode the SNMP messages. If the decoded fails to * decode the specific messages the is should throw and appropiate ASN.1 * exception * */ private AsnEncoder m_encoder; /** * When set the portal object's close method has been invoked. This is * needed since the internal receiver thread will block on the communication * channel. To "wake" the thread the close() method on the comm channel is * performed. This will cause an exception to be genereated in the receiver * thread. If the value of m_isClosing is true then the exception is * ignored. */ private volatile boolean m_isClosing; /** * Set to true if it is necessary to set the socket timeout value via the * Socket.setSoTimeout() method in order to keep from blocking indefinitely * on a socket I/O call. This value is configurable at runtime via the * system property "org.opennms.joeSNMP.vmhacks.socketSoTimeoutRequired". If * this property is set to 'no', the bSocketSoTimeoutRequired variable will * be set to false and the SNMP trap socket timeout will not be set. If this * property is set to 'yes' or the property does not exist, the * bSocketSoTimeoutRequired variable will be set to true. and the socket * timeout will be set. Default value is true. */ private boolean bSocketSoTimeoutRequired = true; /** * Identifies the system property that may be used to specify whether or not * a timeout value is set on the SNMP trap socket. Valid values are 'yes' * and 'no'. */ private static final String PROP_SOCKET_TIMEOUT_REQUIRED = "org.opennms.joeSNMP.vmhacks.socketSoTimeoutRequired"; /** * Identifies the system property that may be used to specify the number of * milliseconds to use for the socket timeout. */ private static final String PROP_SOCKET_TIMEOUT_PERIOD = "org.opennms.joeSNMP.vmhacks.socketSoTimeoutPeriod"; /** * Private constructor used to disallow the default constructor. * * @exception java.lang.UnsupportedOperationException * Always thrown! */ @SuppressWarnings("unused") private SnmpPortal() throws java.lang.UnsupportedOperationException { throw new java.lang.UnsupportedOperationException("Illegal constructor call"); } /** * The SnmpPortal constructor. The constructor is used to build a portal on * the specified port, and forward messages to the defined handler. All * messages are decoded using the encoder specified during construction. * * @param handler * The SNMP packet handler. * @param encoder * The ASN.1 codec object. * @param port * The port to send and receive datagram from. * * @exception java.net.SocketException * Thrown if an error occurs setting up the communication * channel. * @exception java.lang.IllegalArgumentException * Thrown if any of the parameters are null or invalid. * */ SnmpPortal(final SnmpPacketHandler handler, final AsnEncoder encoder, final int port) throws SocketException { if (handler == null || encoder == null) throw new IllegalArgumentException("Invalid argument"); m_handler = handler; if (port >= 0) { m_comm = new DatagramSocket(port); } else { m_comm = new DatagramSocket(); } initializePortal(encoder); } SnmpPortal(final SnmpPacketHandler handler, final AsnEncoder encoder, final InetAddress address, final int port) throws SocketException { if (handler == null || encoder == null) throw new IllegalArgumentException("Invalid argument"); m_handler = handler; if (address == null) { if (port >= 0) { m_comm = new DatagramSocket(port); } else { m_comm = new DatagramSocket(); } } else { m_comm = new DatagramSocket(port, address); } initializePortal(encoder); } public void initializePortal(final AsnEncoder encoder) throws SocketException { // // Determine whether or not it is necessary to use the // socket.setSoTimeout() // method to set the socket timeout value thereby mimic'ing non-blocking // socket I/O. // On platforms whose system close() is not preemptive it is necessary // to use the socket timeout // to keep from blocking indefinitely on any socket call that performs // I/O. // bSocketSoTimeoutRequired = true; // Default is to use set the socket // timeout String strSocketSoTimeoutRequired = System.getProperty(PROP_SOCKET_TIMEOUT_REQUIRED); String osName = System.getProperty("os.name"); if (strSocketSoTimeoutRequired != null && strSocketSoTimeoutRequired.equals("no")) { bSocketSoTimeoutRequired = false; } if (bSocketSoTimeoutRequired == true) { String strSocketSoTimeoutPeriod = System.getProperty(PROP_SOCKET_TIMEOUT_PERIOD); int timeout = 3000; // Default socket timeout is 3 seconds if (strSocketSoTimeoutPeriod != null) { try { timeout = Integer.parseInt(strSocketSoTimeoutPeriod); } catch (NumberFormatException e) { timeout = 3000; } } m_comm.setSoTimeout(timeout); } else if (osName != null && osName.equalsIgnoreCase("linux")) { // we must force this issue because we do not know // what VM there running in. If there running in // Sun JDK 1.3.1 with J2SE_PREEMPTCLOSE set then // this is unnecessary. If there not running in 1.3.1 // and the're on linux then THEY MUST have a timeout // set of it will hang a thread. // m_comm.setSoTimeout(100); } m_isClosing = false; m_recvThread = new Thread(new Receiver(), "SnmpPortal-" + m_comm.getPort()); m_encoder = encoder; m_recvThread.start(); } /** * Defines the inner class that monitors the datagram socket and receives * all the PDU responses. If an exception is generated then it is saved in * m_why and can be re-generated with a call to raise(). * */ private class Receiver implements Runnable { /** * Called to setup the communications channel buffers. The method * attempts to set the received buffer size to 64k. If it fails then the * default buffer size is recovered. If the default buffer size cannot * be recovered then a zero is returned. * * @return The communications channel receive buffer size. A zero is * returned on error */ private int setup() { int bufSz = 64 * 1024; // // set the receiver buffer // try { m_comm.setReceiveBufferSize(bufSz); } catch (SocketException err) { bufSz = 0; } if (bufSz == 0) { try { bufSz = m_comm.getReceiveBufferSize(); } catch (SocketException err) { bufSz = 0; } } return bufSz; } /** * The run method is an infinite loop method that receives all datagrams * for the session. If an unrecoverable error occurs then the m_handler * is informed of the error * * If a pdu is recovered from the channel then the associated handler is * invoked to process the pdu. * * @see SnmpPacketHandler */ public void run() { final int bufSz = setup(); if (bufSz == 0) { return; } final LinkedList<DatagramPacket> fastReceiverQ = new LinkedList<DatagramPacket>(); final LinkedList<byte[]> usedBuffers = new LinkedList<byte[]>(); Thread fastReceiver = new Thread(new Runnable() { public void run() { while (!m_isClosing && Thread.interrupted() == false) { byte[] buf = null; synchronized (usedBuffers) { if (!usedBuffers.isEmpty()) buf = (byte[]) usedBuffers.removeFirst(); } if (buf == null || buf.length != bufSz) buf = new byte[bufSz]; try { DatagramPacket pkt = new DatagramPacket(buf, buf.length); m_comm.receive(pkt); synchronized (fastReceiverQ) { fastReceiverQ.addLast(pkt); fastReceiverQ.notify(); } } catch (InterruptedIOException ioe) { synchronized (usedBuffers) { usedBuffers.addLast(buf); } continue; } catch (Exception e) { if (!m_isClosing) { boolean handled = true; try { Class<?> loggerC = Class.forName("org.opennms.core.utils.ThreadCategory"); Class<?>[] methodParmList = { Class.class }; Method loggerM = loggerC.getMethod("getInstance", methodParmList); Object[] parmList = { this.getClass() }; Object loggerI = loggerM.invoke(null, parmList); methodParmList = new Class[] { Object.class, Throwable.class }; Method infoM = loggerC.getMethod("info", methodParmList); parmList = new Object[] { "An unknown error occured decoding the packet", e }; infoM.invoke(loggerI, parmList); } catch (Throwable t) { handled = false; } if (!handled) { System.out.println(new Date() + " - Exception: " + e.getMessage()); } m_handler.processException(e); } } } } }, Thread.currentThread().getName() + "-FastReceiver"); fastReceiver.start(); // // get a buffer for the datagrams // while (!m_isClosing) { DatagramPacket pkt = null; try { // // reset the packet's length // synchronized (fastReceiverQ) { while (fastReceiverQ.isEmpty() && !m_isClosing) fastReceiverQ.wait(300); if (m_isClosing) continue; pkt = (DatagramPacket) fastReceiverQ.removeFirst(); } handlePkt(pkt); } catch (SnmpPduEncodingException err) { boolean handled = true; try { Class<?> loggerC = Class.forName("org.opennms.core.utils.ThreadCategory"); Class<?>[] methodParmList = { Class.class }; Method loggerM = loggerC.getMethod("getInstance", methodParmList); Object[] parmList = { this.getClass() }; Object loggerI = loggerM.invoke(null, parmList); methodParmList = new Class[] { Object.class, Throwable.class }; Method infoM = loggerC.getMethod("info", methodParmList); parmList = new Object[] { "An error occured decoding the protocol data unit", err }; infoM.invoke(loggerI, parmList); methodParmList = new Class[0]; Method debugEnabledM = loggerC.getMethod("isDebugEnabled", methodParmList); parmList = new Object[0]; Boolean isEnabled = (Boolean) debugEnabledM.invoke(loggerI, parmList); if (isEnabled.booleanValue()) { methodParmList = new Class[] { Object.class }; Method debugM = loggerC.getMethod("debug", methodParmList); OutputStream ostream = new ByteArrayOutputStream(); SnmpUtil.dumpHex(new PrintStream(ostream), pkt.getData(), 0, pkt.getLength()); parmList = new Object[] { ostream }; debugM.invoke(loggerI, parmList); } } catch (Throwable t) { handled = false; } if (!handled) { System.out.println(new Date() + " - SnmpPortal.Receiver.run: SnmpPduEncodingException: " + err.getMessage()); SnmpUtil.dumpHex(System.out, pkt.getData(), 0, pkt.getLength()); } m_handler.processBadDatagram(pkt); } catch (AsnDecodingException err) { boolean handled = true; try { Class<?> loggerC = Class.forName("org.opennms.core.utils.ThreadCategory"); Class<?>[] methodParmList = { Class.class }; Method loggerM = loggerC.getMethod("getInstance", methodParmList); Object[] parmList = { this.getClass() }; Object loggerI = loggerM.invoke(null, parmList); methodParmList = new Class[] { Object.class, Throwable.class }; Method infoM = loggerC.getMethod("info", methodParmList); parmList = new Object[] { "An ASN.1 error occured decoding the packet", err }; infoM.invoke(loggerI, parmList); methodParmList = new Class[0]; Method debugEnabledM = loggerC.getMethod("isDebugEnabled", methodParmList); parmList = new Object[0]; Boolean isEnabled = (Boolean) debugEnabledM.invoke(loggerI, parmList); if (isEnabled.booleanValue()) { methodParmList = new Class[] { Object.class }; Method debugM = loggerC.getMethod("debug", methodParmList); OutputStream ostream = new ByteArrayOutputStream(); SnmpUtil.dumpHex(new PrintStream(ostream), pkt.getData(), 0, pkt.getLength()); parmList = new Object[] { ostream }; debugM.invoke(loggerI, parmList); } } catch (Throwable t) { handled = false; } if (!handled) { System.out.println(new Date() + " - SnmpPortal.Receiver.run: AsnEncodingException: " + err.getMessage()); SnmpUtil.dumpHex(System.out, pkt.getData(), 0, pkt.getLength()); } m_handler.processBadDatagram(pkt); } catch (Exception e) { if (!m_isClosing) { boolean handled = true; try { Class<?> loggerC = Class.forName("org.opennms.core.utils.ThreadCategory"); Class<?>[] methodParmList = { Class.class }; Method loggerM = loggerC.getMethod("getInstance", methodParmList); Object[] parmList = { this.getClass() }; Object loggerI = loggerM.invoke(null, parmList); methodParmList = new Class[] { Object.class, Throwable.class }; Method infoM = loggerC.getMethod("info", methodParmList); parmList = new Object[] { "An unknown error occured decoding the packet", e }; infoM.invoke(loggerI, parmList); } catch (Throwable t) { handled = false; } if (!handled) { System.out.println(new Date() + " - Exception: " + e.getMessage()); } m_handler.processException(e); } } // recycle the packet buffer if possible // if (pkt != null) { synchronized (usedBuffers) { // only keep 20 * 16k, or 520k worth // of buffers around // if (usedBuffers.size() < 20) usedBuffers.addLast(pkt.getData()); } } } } // end run() } /** * Recovers a SnmpPduPacket or SnmpPduTrap from the passed datagram and * calls the appropriate method in the handler. * * If an error occurs recovering the packet then an exception is generated. * The pdu can be one of SnmpPduRequest or SnmpPduBulk. The internal session * AsnEncoder defined in the SnmpParameters is used to recover the pdu. * * @param pkt * The datagram packet to be decoded * * @exception SnmpPduEncodingException * Thrown if a pdu or session level error occurs * @exception AsnDecodingException * Thrown if the AsnEncoder encounters an error * * @see SnmpPduTrap * @see SnmpPduPacket * @see SnmpPduRequest * @see SnmpPduBulk * @see SnmpParameters * @see org.opennms.protocols.snmp.asn1.AsnEncoder * */ void handlePkt(DatagramPacket pkt) throws SnmpPduEncodingException, AsnDecodingException { // // first decode the header // byte[] buf = pkt.getData(); int offset = 0; // // Decode the ASN.1 header from the front // of the SNMP message. // Object[] rVals = m_encoder.parseHeader(buf, offset); // // get the return vals // offset = ((Integer) rVals[0]).intValue(); byte asnType = ((Byte) rVals[1]).byteValue(); int asnLength = ((Integer) rVals[2]).intValue(); // // check the ASN.1 Type // if (asnType != (ASN1.SEQUENCE | ASN1.CONSTRUCTOR)) throw new AsnDecodingException("Invalid SNMP ASN.1 type"); // // Check the length of the datagram packet // if (asnLength > pkt.getLength() - offset) { throw new SnmpPduEncodingException("Insufficent data in packet"); } // // get the SNMP version. // SnmpInt32 int32 = new SnmpInt32(); offset = int32.decodeASN(buf, offset, m_encoder); // // check the version // if (int32.getValue() != SnmpSMI.SNMPV1 && int32.getValue() != SnmpSMI.SNMPV2) { throw new SnmpPduEncodingException("Invalid protocol version"); } // // need to get the community // Postpone the community check until the pdu // has been recovered to determine which community // string needs to be verified against. // SnmpOctetString community = new SnmpOctetString(); offset = community.decodeASN(buf, offset, m_encoder); // // get the pdu header, but DO NOT modify the offset // in effect we are peeking into the remainder of the // packet // rVals = m_encoder.parseHeader(buf, offset); // // The command should be sign extended to a // negative number. Thus add 256 to wrap it // int cmd = ((Byte) rVals[1]).intValue() + 256; // // Now process the Protocol Data Unit // switch (cmd) { case SnmpPduPacket.SET: case SnmpPduPacket.GET: case SnmpPduPacket.GETNEXT: case SnmpPduPacket.RESPONSE: case SnmpPduPacket.INFORM: case SnmpPduPacket.V2TRAP: case SnmpPduPacket.REPORT: { SnmpPduPacket pdu = new SnmpPduRequest(); offset = pdu.decodeASN(buf, offset, m_encoder); m_handler.processSnmpMessage(pkt.getAddress(), // From Who? pkt.getPort(), // What Port? int32, // What Version community, // Community cmd, // Snmp Command (Wrapped!) pdu); // The Protocol Data Unit } break; case SnmpPduPacket.GETBULK: { SnmpPduPacket pdu = new SnmpPduBulk(); offset = pdu.decodeASN(buf, offset, m_encoder); m_handler.processSnmpMessage(pkt.getAddress(), // From Who? pkt.getPort(), // Port int32, // Version community, // Community cmd, // Command (positive wrapped) pdu); // Protocol Data Unit } break; case SnmpPduTrap.TRAP: { SnmpPduTrap trap = new SnmpPduTrap(); offset = trap.decodeASN(buf, offset, m_encoder); m_handler.processSnmpTrap(pkt.getAddress(), pkt.getPort(), community, trap); } break; default: throw new SnmpPduEncodingException("No matching PDU type found"); } } /** * Transmits the passed buffer to the respective peer agent. If a failure * occurs then an IOException is thrown. * * @param peer * The SNMP peer destination * @param buf * The buffer to transmit. * @param length * The valid length of the buffer * * @exception java.lang.IOException * For more details see java.net.DatagramSocket. * * @see java.net.DatagramSocket * */ void send(SnmpPeer peer, byte[] buf, int length) throws java.io.IOException { // // create a new datagram packet // DatagramPacket pkt = new DatagramPacket(buf, length, peer.getPeer(), peer.getPort()); m_comm.send(pkt); } /** * Transmits the passed buffer to the respective peer agent. If a failure * occurs then an IOException is thrown. * * @param peer * The SNMP peer destination * @param buf * The buffer to transmit. * * @exception java.lang.IOException * For more details see java.net.DatagramSocket. * * @see java.net.DatagramSocket * */ void send(SnmpPeer peer, byte[] buf) throws java.io.IOException { send(peer, buf, buf.length); } /** * Sets the default SnmpPacketHandler. * * @param hdl * The new handler * */ void setPacketHandler(SnmpPacketHandler hdl) { if (hdl == null) throw new IllegalArgumentException("The packet handler must not be null"); m_handler = hdl; } /** * Gets the default SnmpPacketHandler for the session. * * @return the SnmpPacketHandler */ SnmpPacketHandler getPacketHandler() { return m_handler; } /** * Sets the default encoder. * * @param encoder * The new encoder * */ void setAsnEncoder(AsnEncoder encoder) { if (encoder == null) throw new IllegalArgumentException("The ASN.1 codec must not be null"); m_encoder = encoder; } /** * Gets the AsnEncoder for the session. * * @return the AsnEncoder */ AsnEncoder getAsnEncoder() { return m_encoder; } /** * Returns true if this portal has had it's <CODE>close</CODE> method * called. * */ boolean isClosed() { return m_isClosing; } /** * Used to close the session. Once called the session should be considered * invalid and unusable. * */ void close() { m_isClosing = true; m_comm.close(); try { // // make sure that the caller thread // is not the one we are trying to // join! // if (m_recvThread.equals(Thread.currentThread()) == false) { m_recvThread.join(); } } catch (InterruptedException err) { Thread.currentThread().interrupt(); // reset the flag } } }