ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-Porttracker/Porttracker.pm
Revision: 1.17
Committed: Mon Mar 11 08:43:53 2013 UTC (11 years, 4 months ago) by root
Branch: MAIN
Changes since 1.16: +3 -0 lines
Log Message:
*** empty log message ***

File Contents

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