View Javadoc
1   // NAME
2   //      $RCSfile: ListeningContext.java,v $
3   // DESCRIPTION
4   //      [given below in javadoc format]
5   // DELTA
6   //      $Revision: 3.12 $
7   // CREATED
8   //      $Date: 2009/03/05 13:24:00 $
9   // COPYRIGHT
10  //      Westhawk Ltd
11  // TO DO
12  //
13  
14  /*
15   * Copyright (C) 2005 - 2006 by Westhawk Ltd
16   * <a href="www.westhawk.co.uk">www.westhawk.co.uk</a>
17   *
18   * Permission to use, copy, modify, and distribute this software
19   * for any purpose and without fee is hereby granted, provided
20   * that the above copyright notices appear in all copies and that
21   * both the copyright notice and this permission notice appear in
22   * supporting documentation.
23   * This software is provided "as is" without express or implied
24   * warranty.
25   * author <a href="mailto:snmp@westhawk.co.uk">Tim Panton</a>
26   */
27  
28  package uk.co.westhawk.snmp.stack;
29  
30  /*-
31   * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
32   * SNMP Java Client
33   * ჻჻჻჻჻჻
34   * Copyright 2023 MetricsHub, Westhawk
35   * ჻჻჻჻჻჻
36   * This program is free software: you can redistribute it and/or modify
37   * it under the terms of the GNU Lesser General Public License as
38   * published by the Free Software Foundation, either version 3 of the
39   * License, or (at your option) any later version.
40   *
41   * This program is distributed in the hope that it will be useful,
42   * but WITHOUT ANY WARRANTY; without even the implied warranty of
43   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
44   * GNU General Lesser Public License for more details.
45   *
46   * You should have received a copy of the GNU General Lesser Public
47   * License along with this program.  If not, see
48   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
49   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
50   */
51  
52  import java.io.*;
53  import java.util.*;
54  
55  import uk.co.westhawk.snmp.event.*;
56  import uk.co.westhawk.snmp.net.*;
57  import uk.co.westhawk.snmp.util.*;
58  
59  
60  /**
61   * The ListeningContext class will enable this stack to receive packets.
62   * This class replaces the deprecated DefaultTrapContext class.
63   * The context will only start receiving (or listen for) packets when there is
64   * at least one listener registered. 
65   *
66   * <p>
67   * Two kind of listeners can be added; 
68   * the normal and unhandled PDU listeners.
69   * The normal PDU listeners are added via the
70   * <code>addRawPduListener()</code> method, 
71   * the unhandled PDU listeners are added via the 
72   * <code>addUnhandledRawPduListener()</code>.
73   * Both these listeners provide undecoded events.
74   * </p>
75   *
76   * <p>
77   * The SnmpContext classes provide functionality for decoded PDU and
78   * trap events. These classes will register themselves via the
79   * <code>addRawPduListener()</code> to the ListeningContext object and 
80   * only pass the (decoded) event on if it matches their configuration.
81   * </p>
82   *
83   * <p>
84   * On UNIX and Linux operating systems the default port where PDUs and 
85   * traps are sent (i.e. <em>161</em> and <em>162</em>) can only be opened 
86   * as root.
87   * </p>
88   *
89   * <p>
90   * Only one process can listen on a certain port. To prevent more than
91   * one ListeningContext listening on the same port, use the
92   * ListeningContextPool class.
93   * </p>
94   *
95   * @see ListeningContextPool
96   * @see AbstractSnmpContext#addTrapListener
97   * @see AbstractSnmpContext#addRequestPduListener
98   *
99   * @since 4_14
100  * @author <a href="mailto:snmp@westhawk.co.uk">Birgit Arkesteijn</a>
101  * @version $Revision: 3.12 $ $Date: 2009/03/05 13:24:00 $
102  */
103 public class ListeningContext implements ListeningContextFace, Runnable {
104     private static final String version_id = "@(#)$Id: ListeningContext.java,v 3.12 2009/03/05 13:24:00 birgita Exp $ Copyright Westhawk Ltd";
105 
106     private Object soc_lock = new Object();
107     // thanks to Nick Sheen nsheen@tippingpoint.com for pointing out that "" is
108     // interned by the VM
109     private static int counter;
110     private ContextSocketFace soc;
111     private Thread me;
112     private String basename;
113     private volatile boolean stopRequested;
114     // thanks to Nick Sheen nsheen@tippingpoint.com for pointing out that volatile
115     // is needed here
116 
117     protected int maxRecvSize;
118     protected String typeSocket;
119     protected int hostPort;
120     protected String bindAddr;
121 
122     transient private RawPduReceivedSupport pduSupport, unhandledSupport;
123 
124     /**
125      * Constructor, using the Standard socket type.
126      *
127      * @param port The local port where packets are received
128      *
129      * @see SnmpContextBasisFace#STANDARD_SOCKET
130      */
131     public ListeningContext(int port) {
132         this(port, null, SnmpContextBasisFace.STANDARD_SOCKET);
133     }
134 
135     /**
136      * Constructor, using the Standard socket type.
137      *
138      * If bindAddress is null, it will accept connections on
139      * any/all local addresses. If you want to listen to
140      * <ul>
141      * <li>
142      * IPv4 only interfaces, use address "0.0.0.0"
143      * </li>
144      * <li>
145      * IPv6 only interfaces, use address "::"
146      * </li>
147      * </ul>
148      *
149      * @param port        The local port where packets are received
150      * @param bindAddress The local address the server will bind to
151      *
152      * @see SnmpContextBasisFace#STANDARD_SOCKET
153      */
154     public ListeningContext(int port, String bindAddress) {
155         this(port, bindAddress, SnmpContextBasisFace.STANDARD_SOCKET);
156     }
157 
158     /**
159      * Constructor.
160      *
161      * If bindAddress is null, it will accept connections on
162      * any/all local addresses. If you want to listen to
163      * <ul>
164      * <li>
165      * IPv4 only interfaces, use address "0.0.0.0"
166      * </li>
167      * <li>
168      * IPv6 only interfaces, use address "::"
169      * </li>
170      * </ul>
171      *
172      *
173      * The typeSocketA will indicate which type of socket to use. This way
174      * different handlers can be provided.
175      * This parameter should be either STANDARD_SOCKET, TCP_SOCKET or a
176      * fully qualified classname.
177      *
178      * <p>
179      * Note, the TCP_SOCKET does not provide functionality to send a
180      * response back. Listening on such a socket is only useful when
181      * listening for traps.
182      * </p>
183      *
184      * @param port        The local port where packets are received
185      * @param bindAddress The local address the server will bind to
186      * @param typeSocketA The type of socket to use.
187      *
188      * @see SnmpContextBasisFace#STANDARD_SOCKET
189      * @see SnmpContextBasisFace#TCP_SOCKET
190      */
191     public ListeningContext(int port, String bindAddress, String typeSocketA) {
192         hostPort = port;
193         bindAddr = bindAddress;
194         typeSocket = typeSocketA;
195 
196         basename = "" + hostPort + "_" + bindAddr;
197 
198         pduSupport = new RawPduReceivedSupport(this);
199         unhandledSupport = new RawPduReceivedSupport(this);
200         maxRecvSize = SnmpContextBasisFace.MSS;
201     }
202 
203     public int getPort() {
204         return hostPort;
205     }
206 
207     public String getBindAddress() {
208         return bindAddr;
209     }
210 
211     public String getTypeSocket() {
212         return typeSocket;
213     }
214 
215     public int getMaxRecvSize() {
216         return maxRecvSize;
217     }
218 
219     public void setMaxRecvSize(int no) {
220         maxRecvSize = no;
221     }
222 
223     /**
224      * This method will stop the thread listening for packets.
225      * All transmitters, PDUs in flight and traplisteners will be removed
226      * when run() finishes.
227      *
228      * <p>
229      * It closes the socket.
230      * The thread will actually stop/finish when the run() finishes. Since
231      * the socket is closed, the run() will fall through almost instantly.
232      * </p>
233      *
234      * <p>
235      * Note that by calling this method the whole stack will stop listening
236      * for packets on this particular port! The listeners added via the
237      * SnmpContext classes are affected as well.
238      * </p>
239      *
240      * <p>
241      * When you add a new listener, the context will start listening again.
242      * </p>
243      *
244      * <p>
245      * Note: The thread(s) will not die immediately; this will take about
246      * half a minute.
247      * </p>
248      */
249     public void destroy() {
250         synchronized (soc_lock) {
251             stopRequested = true;
252             if (soc != null) {
253                 if (AsnObject.debug > 12) {
254                     System.out.println(getClass().getName() + ".destroy(): Closing socket ");
255                 }
256                 soc.close();
257             }
258         }
259     }
260 
261     /**
262      * We wait for any incoming PDUs and fire a rawpdu received (undecoded) event
263      * if we do.
264      * 
265      * <p>
266      * The undecoded events are fired to all normal listeners (added via
267      * addRawPduListener()), until one of them consumes it.
268      * The SnmpContext classes will consume the event if it matches their
269      * configuration.
270      * </p>
271      *
272      * <p>
273      * If none of them consume the event, the undecoded events are fired to
274      * all unhandled PDU listeners (added via addUnhandledRawPduListener()),
275      * until one of them consumes it.
276      * </p>
277      *
278      * @see RawPduReceivedSupport#fireRawPduReceived
279      * @see #addRawPduListener(RawPduListener)
280      * @see #addUnhandledRawPduListener(RawPduListener)
281      */
282     public void run() {
283         // while It is visible
284         while (!stopRequested) {
285             // block for incoming packets
286             me.yield();
287             try {
288                 if (stopRequested) {
289                     break;
290                 }
291 
292                 StreamPortItem item = soc.receive(maxRecvSize);
293                 ByteArrayInputStream in = item.getStream();
294 
295                 String hostAddress = item.getHostAddress();
296                 int port = item.getHostPort();
297 
298                 // read the bytes of the input stream into bu
299                 int nb = in.available();
300                 byte[] bu = new byte[nb];
301                 in.read(bu);
302                 in.close();
303 
304                 if (AsnObject.debug > 10) {
305                     SnmpUtilities.dumpBytes(getClass().getName()
306                             + ".run(): Received from "
307                             + hostAddress
308                             + ", from port " + port
309                             + ": ", bu);
310                 }
311                 KickProcessIncomingMessage thread = new KickProcessIncomingMessage(hostAddress, port, bu);
312                 thread.start();
313             } catch (IOException exc) {
314                 if (exc instanceof InterruptedIOException) {
315                     if (AsnObject.debug > 15) {
316                         System.out.println(getClass().getName() + ".run(): Idle recv " + exc.getMessage());
317                     }
318                 } else {
319                     if (AsnObject.debug > 0) {
320                         System.out.println(getClass().getName() + ".run(): IOException: " + exc.getMessage());
321                     }
322                 }
323             } catch (Exception exc) {
324                 if (AsnObject.debug > 0) {
325                     System.out.println(getClass().getName() + ".run(): Exception: " + exc.getMessage());
326                     exc.printStackTrace();
327                 }
328             } catch (Error err) {
329                 if (AsnObject.debug > 0) {
330                     System.out.println(getClass().getName() + ".run(): Error: " + err.getMessage());
331                     err.printStackTrace();
332                 }
333             }
334         }
335 
336         me = null;
337         soc = null;
338         pduSupport.empty();
339         unhandledSupport.empty();
340     }
341 
342     public void addRawPduListener(RawPduListener listener)
343             throws IOException {
344         synchronized (soc_lock) {
345             pduSupport.addRawPduListener(listener);
346             startListening();
347         }
348     }
349 
350     public void removeRawPduListener(RawPduListener listener) {
351         synchronized (soc_lock) {
352             pduSupport.removeRawPduListener(listener);
353             destroyIfNoListeners();
354         }
355     }
356 
357     public void addUnhandledRawPduListener(RawPduListener listener)
358             throws IOException {
359         synchronized (soc_lock) {
360             unhandledSupport.addRawPduListener(listener);
361             startListening();
362         }
363     }
364 
365     public void removeUnhandledRawPduListener(RawPduListener listener) {
366         synchronized (soc_lock) {
367             unhandledSupport.removeRawPduListener(listener);
368             destroyIfNoListeners();
369         }
370     }
371 
372     /**
373      * Creates the socket and starts listening for PDUs if we didn't do so
374      * already.
375      * This method is called in addRawPduListener() and
376      * addUnhandledRawPduListener().
377      *
378      * @exception IOException Thrown when the socket cannot be created.
379      * @see #addRawPduListener
380      * @see #addUnhandledRawPduListener
381      */
382     private void startListening()
383             throws IOException {
384         if (soc == null) {
385             // create tempSoc first, so that when 'create' fails, soc
386             // will remain null.
387             ContextSocketFace tempSoc = AbstractSnmpContext.getSocket(typeSocket);
388             if (tempSoc != null) {
389                 tempSoc.create(hostPort, bindAddr);
390                 soc = tempSoc;
391 
392                 if (AsnObject.debug > 12) {
393                     System.out.println(getClass().getName() + ".startListening()"
394                             + ": soc.getLocalSocketAddress() = " + soc.getLocalSocketAddress());
395                     System.out.println(getClass().getName() + ".startListening()"
396                             + ": soc.getRemoteSocketAddress() = " + soc.getRemoteSocketAddress());
397                 }
398             }
399         }
400         if (me == null) {
401             stopRequested = false;
402             me = new Thread(this, basename + "_Listen");
403             me.setPriority(me.NORM_PRIORITY);
404             me.start();
405         }
406     }
407 
408     /**
409      * Returns the hash key. This key is built out of all properties. It
410      * serves as key for the pool of contexts.
411      *
412      * @return The hash key
413      */
414     public String getHashKey() {
415         String str = hostPort
416                 + "_" + bindAddr
417                 + "_" + typeSocket;
418         return str;
419     }
420 
421     /**
422      * Returns a string representation of the object.
423      * 
424      * @return The string
425      */
426     public String toString() {
427         StringBuffer buffer = new StringBuffer("ListeningContext[");
428         buffer.append("port=").append(hostPort);
429         buffer.append(", bindAddress=").append(bindAddr);
430         buffer.append(", socketType=").append(typeSocket);
431         buffer.append(", #rawPduListeners=").append(pduSupport.getListenerCount());
432         buffer.append(", #rawPduUnhandledListeners=").append(unhandledSupport.getListenerCount());
433         buffer.append("]");
434         return buffer.toString();
435     }
436 
437     /**
438      * Processes an incoming packet.
439      *
440      * @see #run
441      */
442     protected void processIncomingMessage(String hostAddress,
443             int port, byte[] bu) throws DecodingException, IOException {
444         AsnDecoderBase rpdu = new AsnDecoderBase();
445         ByteArrayInputStream in = new ByteArrayInputStream(bu);
446         AsnSequence asnTopSeq = rpdu.getAsnSequence(in);
447         int version = rpdu.getSNMPVersion(asnTopSeq);
448 
449         boolean isConsumed = pduSupport.fireRawPduReceived(version, hostAddress, port, bu);
450         if (isConsumed == false) {
451             unhandledSupport.fireRawPduReceived(version, hostAddress, port, bu);
452         }
453     }
454 
455     /**
456      * Only destroy this object when there are no more listeners.
457      *
458      * Thanks to Jeremy Stone (Jeremy.Stone@cyclone-technology.com).
459      * 
460      * @since 6.1
461      */
462     private void destroyIfNoListeners() {
463         if (pduSupport.getListenerCount() == 0
464                 && unhandledSupport.getListenerCount() == 0) {
465             destroy();
466         }
467     }
468 
469     class KickProcessIncomingMessage extends Thread {
470         /**
471          * This class makes sure that dealing with an incoming packet is
472          * done at a separate thread so the ListeningContext can go back
473          * listening immediately.
474          * This will at some point be replaced by a Thread pool of
475          * some kind.
476          */
477         private String hostAddress;
478         private int port;
479         private byte[] bu;
480 
481         KickProcessIncomingMessage(String newHostAddress, int newPort,
482                 byte[] newBu) {
483             hostAddress = newHostAddress;
484             port = newPort;
485             bu = newBu;
486             this.setPriority(Thread.MIN_PRIORITY);
487             this.setName(newHostAddress + "_" + newPort
488                     + "_KickProcessIncomingMessage_" + counter);
489             counter++;
490         }
491 
492         public void run() {
493             try {
494                 processIncomingMessage(hostAddress, port, bu);
495             } catch (IOException exc) {
496                 if (AsnObject.debug > 0) {
497                     System.out.println(getClass().getName() + ".run(): IOException: " + exc.getMessage());
498                 }
499             } catch (DecodingException exc) {
500                 if (AsnObject.debug > 1) {
501                     System.out.println(getClass().getName() + ".run(): DecodingException: " + exc.getMessage());
502                 }
503             }
504         }
505     }
506 
507 }