File Coverage

blib/lib/AnyEvent/Porttracker.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             AnyEvent::Porttracker - Porttracker/PortIQ API client interface.
4              
5             =head1 SYNOPSIS
6              
7             use AnyEvent::Porttracker;
8              
9             my $api = new AnyEvent::Porttracker
10             host => "10.0.0.1",
11             user => "admin",
12             pass => "31331",
13             tls => 1,
14             ;
15              
16             # Example 1
17             # a simple request: ping the server synchronously
18              
19             my ($timestamp, $pid) = $api->req_sync ("ping");
20              
21             # Example 2
22             # find all realms, start a discovery on all of them
23             # and wait until all discovery processes have finished
24             # but execute individual discoveries in parallel,
25             # asynchronously
26              
27             my $cv = AE::cv;
28              
29             $cv->begin;
30             # find all realms
31             $api->req (realm_info => ["gid", "name"], sub {
32             my ($api, @realms) = @_;
33              
34             # start discovery on all realms
35             for my $realm (@realms) {
36             my ($gid, $name) = @$realm;
37              
38             $cv->begin;
39             $api->req (realm_discover => $gid, sub {
40             warn "discovery for realm '$name' finished\n";
41             $cv->end;
42             });
43             }
44              
45             $cv->end;
46             });
47              
48             $cv->recv;
49              
50             # Example 3
51             # subscribe to realm_poll_stop events and report each occurance
52              
53             $api->req (subscribe => "realm_poll_stop", sub {});
54             $api->on (realm_poll_stop_event => sub {
55             my ($api, $gid) = @_;
56             warn "this just in: poll for realm <$gid> finished.\n";
57             });
58              
59             AE::cv->recv; # wait forever
60              
61             =head1 DESCRIPTION
62              
63             Porttracker (L) is a product that (among
64             other things) scans switches and routers in a network and gives a coherent
65             view of which end devices are connected to which switch ports on which
66             switches and routers. It also offers a JSON-based client API, for which
67             this module is an implementation.
68              
69             In addition to Porttracker, the PortIQ product is also supported, as it
70             uses the same protocol.
71              
72             If you do not have access to either a Porttracker or PortIQ box then this
73             module will be of little value to you.
74              
75             This module is an L user, you need to make sure that you use and
76             run a supported event loop.
77              
78             To quickly understand how this module works you should read how to
79             construct a new connection object and then read about the event/callback
80             system.
81              
82             The actual low-level protocol and, more importantly, the existing
83             requests and responses, are documented in the official Porttracker
84             API documentation (a copy of which is included in this module as
85             L.
86              
87             =head1 THE AnyEvent::Porttracker CLASS
88              
89             The AnyEvent::Porttracker class represents a single connection.
90              
91             =over 4
92              
93             =cut
94              
95             package AnyEvent::Porttracker;
96              
97 1     1   1326 use common::sense;
  1         7  
  1         4  
98              
99 1     1   41 use Carp ();
  1         2  
  1         12  
100 1     1   6 use Scalar::Util ();
  1         4  
  1         11  
101              
102 1     1   1354 use AnyEvent ();
  1         4970  
  1         18  
103 1     1   1056 use AnyEvent::Handle ();
  1         19466  
  1         22  
104              
105 1     1   830 use MIME::Base64 ();
  1         674  
  1         20  
106 1     1   1382 use Digest::HMAC_MD6 ();
  0            
  0            
107             use JSON ();
108              
109             our $VERSION = '1.01';
110              
111             sub call {
112             my ($self, $type, @args) = @_;
113              
114             $self->{$type}
115             ? $self->{$type}($self, @args)
116             : ($type = (UNIVERSAL::can $self, $type))
117             ? $type->($self, @args)
118             : ()
119             }
120              
121             =item $api = new AnyEvent::Porttracker [key => value...]
122              
123             Creates a new porttracker API connection object and tries to connect to
124             the specified host (see below). After the connection has been established,
125             the TLS handshake (if requested) will take place, followed by a login
126             attempt using either the C, C or C methods,
127             in this order of preference (typically, C is used, which
128             shields against some man-in-the-middle attacks and avoids transferring the
129             password).
130              
131             It is permissible to send requests immediately after creating the object -
132             they will be queued until after successful login.
133              
134             Possible key-value pairs are:
135              
136             =over 4
137              
138             =item host => $hostname [MANDATORY]
139              
140             The hostname or IP address of the Porttracker box.
141              
142             =item port => $service
143              
144             The service (port) to use (default: C).
145              
146             =item user => $string, pass => $string
147              
148             These are the username and password to use when authentication is required
149             (which it is in almost all cases, so these keys are normally mandatory).
150              
151             =item tls => $bool
152              
153             Enables or disables TLS (default: disables). When enabled, then the
154             connection will try to handshake a TLS connection before logging in. If
155             unsuccessful a fatal error will be raised.
156              
157             Since most Porttracker/PortIQ boxes will not have a sensible/verifiable
158             certificate, no attempt at verifying it will be done (which means
159             man-in-the-middle-attacks will be trivial). If you want some form of
160             verification you need to provide your own C object with C<<
161             verify => 1, verify_peername => [1, 1, 1] >> or whatever verification mode
162             you wish to use.
163              
164             =item tls_ctx => $tls_ctx
165              
166             The L object to use. See C, above.
167              
168             =item on_XYZ => $coderef
169              
170             You can specify event callbacks either by sub-classing and overriding the
171             respective methods or by specifying code-refs as key-value pairs when
172             constructing the object. You add or remove event handlers at any time with
173             the C method.
174              
175             =back
176              
177             =cut
178              
179             sub new {
180             my $class = shift;
181              
182             my $self = bless {
183             id => "a",
184             ids => [],
185             queue => [], # initially queue everything
186             @_,
187             }, $class;
188              
189             {
190             Scalar::Util::weaken (my $self = $self);
191              
192             $self->{hdl} = new AnyEvent::Handle
193             connect => [$self->{host}, $self->{port} || "porttracker=55"],
194             on_error => sub {
195             $self->error ($_[2]);
196             },
197             on_connect => sub {
198             if ($self->{tls}) {
199             $self->_req (start_tls => sub {
200             $_[1]
201             or return $self->error ("TLS rejected by server");
202              
203             $self->_login;
204             });
205             }
206             },
207             on_read => sub {
208             while ($_[0]{rbuf} =~ s/^([^\x0a]*)\x0a//) {
209             my $msg = JSON::decode_json $1;
210             my $id = shift @$msg;
211              
212             if (defined $id) {
213             my $cb = delete $self->{cb}{$id}
214             or return $self->error ("received unexpected reply msg with id $id");
215              
216             push @{ $self->{ids} }, $id;
217              
218             $cb->($self, @$msg);
219             } else {
220             $msg->[0] = "on_$msg->[0]_notify";
221             call $self, @$msg;
222             }
223             }
224             },
225             ;
226             }
227              
228             $self
229             }
230              
231             sub DESTROY {
232             my ($self) = @_;
233              
234             $self->{hdl}->destroy
235             if $self->{hdl};
236             }
237              
238             sub error {
239             my ($self, $msg) = @_;
240              
241             call $self, on_error => $msg;
242              
243             ()
244             }
245              
246             sub _req {
247             my $self = shift;
248             my $cb = pop;
249              
250             my $id = (pop @{ $self->{ids} }) || $self->{id}++;
251              
252             unshift @_, $id;
253             $self->{cb}{$id} = $cb;
254              
255             my $msg = JSON::encode_json \@_;
256              
257             $self->{hdl}->push_write ($msg);
258             }
259              
260             =item $api->req ($type => @args, $callback->($api, @reply))
261              
262             Sends a generic request of type C<$type> to the server. When the server
263             responds, the API object and the response arguments (without the success
264             status) are passed to the callback, which is the last argument to this
265             method.
266              
267             If the request fails, then a fatal error will be raised. If you want to
268             handle failures gracefully, you need to use C<< ->req_failok >> instead.
269              
270             The available requests are documented in the Porttracker API
271             documentation (a copy of which is included in this module as
272             L.
273              
274             It is permissible to call this (or any other request function) at any
275             time, even before the connection has been established - the API object
276             always waits until after login before it actually sends the requests, and
277             queues them until then.
278              
279             Example: ping the porttracker server.
280              
281             $api->req ("ping", sub {
282             my ($api, $ok, $timestamp, $pid) = @_;
283             ...
284             });
285              
286             Example: determine the product ID.
287              
288             $api->req (product_id => sub {
289             my ($api, $ok, $branding, $product_id) = @_;
290             ...
291             });
292              
293             Example: set a new license.
294              
295             $api->req (set_license => $LICENSE_STRING, sub {
296             my ($api, $ok) = @_;
297              
298             $ok or die "failed to set license";
299             });
300              
301             =cut
302              
303             sub req {
304             my $cb = pop;
305             push @_, sub {
306             splice @_, 1, 1
307             or $_[0]->error ($_[1]);
308              
309             &$cb
310             };
311              
312             $_[0]{queue}
313             ? push @{ $_[0]{queue} }, [@_]
314             : &_req
315             }
316              
317             =item @res = $api->req_sync ($type => @args)
318              
319             Similar to C<< ->req >>, but waits for the results of the request and on
320             success, returns the values instead (without the success flag, and only
321             the first value in scalar context). On failure, the method will C
322             with the error message.
323              
324             =cut
325              
326             sub req_sync {
327             push @_, my $cv = AE::cv;
328             &req;
329             my ($ok, @res) = $cv->recv;
330              
331             $ok
332             or Carp::croak $res[0];
333              
334             wantarray ? @res : $res[0]
335             }
336              
337             =item $api->req_failok ($type => @args, $callback->($api, $success, @reply))
338              
339             Just like C<< ->req >>, with two differences: first, a failure will not
340             raise an error, second, the initial status reply which indicates success
341             or failure is not removed before calling the callback.
342              
343             =cut
344              
345             sub req_failok {
346             $_[0]{queue}
347             ? push @{ $_[0]{queue} }, [@_]
348             : &_req
349             }
350              
351             =item $api->on (XYZ => $callback)
352              
353             Overwrites any currently registered handler for C or
354             installs a new one. Or, when C<$callback> is undef, unregisters any
355             currently-registered handler.
356              
357             Example: replace/set the handler for C.
358              
359             $api->on (discover_stop_event => sub {
360             my ($api, $gid) = @_;
361             ...
362             });
363              
364             =cut
365              
366             sub on {
367             my $self = shift;
368              
369             while (@_) {
370             my ($event, $cb) = splice @_, 0, 2;
371             $event =~ s/^on_//;
372              
373             $self->{"on_$event"} = $cb;
374             }
375             }
376              
377             sub on_start_tls_notify {
378             my ($self) = @_;
379              
380             $self->{hdl}->starttls (connect => $self->{tls_ctx});
381             $self->{tls} ||= 1;
382              
383             $self->_login;
384             }
385              
386             sub on_hello_notify {
387             my ($self, $version, $auths, $nonce) = @_;
388              
389             $version == 1
390             or return $self->error ("protocol mismatch, got $version, expected/supported 1");
391              
392             $nonce = MIME::Base64::decode_base64 $nonce;
393              
394             $self->{hello} = [$auths, $nonce];
395              
396             $self->_login
397             unless $self->{tls}; # delay login when trying to handshake tls
398             }
399              
400             sub _login_success {
401             my ($self, $method) = @_;
402              
403             _req @$_
404             for @{ delete $self->{queue} };
405              
406             call $self, on_login => $method;
407             }
408              
409             sub _login {
410             my ($self) = @_;
411              
412             my ($auths, $nonce) = @{ delete $self->{hello} or return };
413              
414             if (grep $_ eq "none", @$auths) {
415             $self->_login_success ("none");
416              
417             } elsif (grep $_ eq "login_cram_md6", @$auths) {
418             my $cc = join "", map chr 256 * rand, 0..63;
419              
420             my $key = Digest::HMAC_MD6::hmac_md6 $self->{pass}, $self->{user}, 64, 256;
421             my $cr = Digest::HMAC_MD6::hmac_md6_base64 $key, "$cc$nonce", 64, 256;
422             my $sr = Digest::HMAC_MD6::hmac_md6_base64 $key, "$nonce$cc", 64, 256;
423              
424             $cc = MIME::Base64::encode_base64 $cc;
425              
426             $self->_req (login_cram_md6 => $self->{user}, $cr, $cc, sub {
427             my ($self, $ok, $msg) = @_;
428              
429             $ok
430             or return call $self, on_login_failure => $msg;
431              
432             $msg eq $sr
433             or return call $self, on_login_failure => "sr and cr mismatch, possible man in the middle attack";
434              
435             $self->_login_success ("login_cram_md6");
436             });
437             } elsif (grep $_ eq "login", @$auths) {
438             $self->_req (login => $self->{user}, $self->{pass}, sub {
439             my ($self, $ok, $msg) = @_;
440              
441             $ok
442             or return call $self, on_login_failure => $msg;
443              
444             $self->_login_success ("login");
445             });
446             } else {
447             call $self, on_login_failure => "no supported auth method (@$auths)";
448             }
449              
450             # we no longer need these, make it a bit harder to get them
451             delete $self->{user};
452             delete $self->{pass};
453             }
454              
455             sub on_info_notify {
456             my ($self, $msg) = @_;
457              
458             warn $msg;
459             }
460              
461             sub on_error_notify {
462             my ($self, $msg) = @_;
463              
464             $self->error ($msg);
465             }
466              
467             sub on_error {
468             my ($self, $msg) = @_;
469              
470             warn $msg;
471              
472             %$self = ();
473             }
474              
475             sub on_login_failure {
476             my ($self, $msg) = @_;
477              
478             $msg =~ s/\n$//;
479             $self->error ("login failed: $msg");
480             }
481              
482             sub on_event_notify {
483             my ($self, $event, @args) = @_;
484              
485             call $self, "on_${event}_event", @args;
486             }
487              
488             =back
489              
490             =head1 EVENTS/CALLBACKS
491              
492             AnyEvent::Porttracker connections are fully event-driven, and naturally
493             there are a number of events that can occur. All these events have a name
494             starting with C (example: C).
495              
496             Programs can catch these events in two ways: either by providing
497             constructor arguments with the event name as key and a code-ref as value:
498              
499             my $api = new AnyEvent::Porttracker
500             host => ...,
501             user => ..., pass => ...,
502             on_error => sub {
503             my ($api, $msg) = @_;
504             warn $msg;
505             exit 1;
506             },
507             ;
508              
509             Or by sub-classing C and overriding methods of the
510             same name:
511              
512             package MyClass;
513              
514             use base AnyEvent::Porttracker;
515              
516             sub on_error {
517             my ($api, $msg) = @_;
518             warn $msg;
519             exit 1;
520             }
521              
522             Event callbacks are not expected to return anything and are always passed
523             the API object as first argument. Some might have default implementations
524             (for example, C), others are ignored unless overriden.
525              
526             Description of individual events follow:
527              
528             =over 4
529              
530             =item on_error $api, $msg
531              
532             Is called for every (fatal) error, including C notifies. The
533             default prints the message and destroys the object, so it is highly
534             advisable to override this event.
535              
536             =item on_login $api, $method
537              
538             Called after a successful login, after which commands can be send. It is
539             permissible to send commands before a successful login: those will be
540             queued and sent just before this event is invoked. C<$method> is the auth
541             method that was used.
542              
543             =item on_login_failure $api, $msg
544              
545             Called when all login attempts have failed - the default raises a fatal
546             error with the error message from the server.
547              
548             =item on_hello_notify $api, $version, $authtypes, $nonce
549              
550             This protocol notification is used internally by AnyEvent::Porttracker -
551             you can override it, but the module will most likely not work.
552              
553             =item on_info_notify $api, $msg
554              
555             Called for informational messages from the server - the default
556             implementation calls C but otherwise ignores this notification.
557              
558             =item on_error_notify $api, $msg
559              
560             Called for fatal errors from the server - the default implementation calls
561             C and destroys the API object.
562              
563             =item on_start_tls_notify $api
564              
565             Called when the server wants to start TLS negotiation. This is used
566             internally and - while it is possible to override it - should not be
567             overridden.
568              
569             =item on_event_notify $api, $eventname, @args
570              
571             Called when the server broadcasts an event the API object is subscribed
572             to. The default implementation (which should not be overridden) simply
573             re-issues an "on_eventname_event" event with the @args.
574              
575             =item on_XYZ_notify $api, ...
576              
577             In general, any protocol notification will result in an event of the form
578             C.
579              
580             =item on_XYZ_event $api, ...
581              
582             Called when the server broadcasts the named (XYZ) event.
583              
584             =back
585              
586             =head1 SEE ALSO
587              
588             L, L, L.
589              
590             =head1 AUTHOR
591              
592             Marc Lehmann
593              
594             =cut
595              
596             1