=head1 NAME AnyEvent::Porttracker - Porttracker/PortIQ API client interface. =head1 SYNOPSIS use AnyEvent::Porttracker; =head1 DESCRIPTION Porttracker (L) is a product that (among other things) scans switches and routers in a network and gives a coherent view of which end devices are connected to which switch ports on which switches and routers. It also offers a JSON-based client API, for which this module is an implementation. In addition to Porttracker, the PortIQ product is also supported, as it uses the same protocol. If you do not have access to either a Porttracker or PortIQ box then this module will be of little value to you. This module is an L user, you need to make sure that you use and run a supported event loop. To quickly understand how this module works you should read how to construct a new connection object and then read about the event/callback system. =head1 THE AnyEvent::Porttracker CLASS The AnyEvent::Porttracker class represents a single connection. =over 4 =cut package AnyEvent::Porttracker; use common::sense; use Scalar::Util (); use AnyEvent (); use AnyEvent::Handle (); use MIME::Base64 (); use Digest::HMAC_MD6 (); use JSON (); our $VERSION = '0.0'; sub call { my ($self, $type, @args) = @_; $self->{$type} ? $self->{$type}($self, @args) : ($type = (UNIVERSAL::can $self, $type)) ? $type->($self, @args) : () } =item new AnyEvent::Porttracker [key => value...] Creates a new porttracker API connection object and tries to connect to the specified host (see below). After the connection has been established, the TLS handshake (if requested) will take place, followed by a login attempt using either the C, C or C methods, in this order of preference (typically, C is used, which shields against some man-in-the-middle attacks and avoids transferring the password). It is permissible to send requests immediately after creating the object - they will be queued until after successful login. Possible key-value pairs are: =over 4 =item host => $hostname [MANDATORY] The hostname or IP address of the Porttracker box. =item port => $service The service (port) to use (default: C). =item user => $string, pass => $string These are the username and password to use when authentication is required (which it is in almost all cases, so these keys are normally mandatory). =item tls => ... #TODO# =item on_XYZ => $coderef You can specify event callbacks either by subclassing and overriding the respective methods or by specifying coderefs as key-value pairs when constructing the object. =back =cut sub new { my $class = shift; my $self = bless { id => "a", queue => [], # ininitially queue everything @_, }, $class; { Scalar::Util::weaken (my $self = $self); $self->{hdl} = new AnyEvent::Handle connect => [$self->{host}, $self->{port} || "porttracker=55"], on_error => sub { $self->error (); }, on_connect => sub { if ($self->{tls}) { $self->_req (start_tls => sub { $_[1] or return $self->error ("TLS rejected by server"); $self->_login; }); } }, on_read => sub { while ($_[0]{rbuf} =~ s/^([^\x0a]*)\x0a//) { my $msg = JSON::decode_json $1; my $id = shift @$msg; if (defined $id) { my $cb = delete $self->{cb}{$id} or return $self->error ("received unexpected reply msg with id $id"); $cb->($self, @$msg); } else { $msg->[0] = "on_$msg->[0]_notify"; call $self, @$msg; } } }, ; } $self } sub DESTROY { my ($self) = @_; $self->{hdl}->destroy if $self->{hdl}; } sub error { my ($self, $msg) = @_; call on_error => $msg; () } sub _req { my $self = shift; my $cb = pop; my $id = ++$self->{id}; unshift @_, $id; $self->{cb}{$id} = $cb; my $msg = JSON::encode_json \@_; $self->{hdl}->push_write ($msg); } sub req { $_[0]{queue} ? push @{ $_[0]{queue} }, [@_] : &_req } sub on_start_tls_notify { my ($self) = @_; $self->{hdl}->starttls ("connect"); $self->{tls} ||= 1; $self->_login; } sub on_hello_notify { my ($self, $version, $auths, $nonce) = @_; $version == 1 or return $self->error ("protocol mismatch, got $version, expected/supported 1"); $nonce = MIME::Base64::decode_base64 $nonce; $self->{hello} = [$auths, $nonce]; $self->_login unless $self->{tls}; # delay login when trying to handshake tls } sub _login_success { my ($self, $method) = @_; _req @$_ for @{ delete $self->{queue} }; call $self, on_login => $method; } sub _login { my ($self) = @_; my ($auths, $nonce) = @{ delete $self->{hello} or return }; if (grep $_ eq "none", @$auths) { $self->_login_success ("none"); } elsif (grep $_ eq "login_cram_md6", @$auths) { my $cc = join "", map chr 256 * rand, 0..63; my $key = Digest::HMAC_MD6::hmac_md6 $self->{pass}, $self->{user}, 64, 256; my $cr = Digest::HMAC_MD6::hmac_md6_base64 $key, "$cc$nonce", 64, 256; my $sr = Digest::HMAC_MD6::hmac_md6_base64 $key, "$nonce$cc", 64, 256; $cc = MIME::Base64::encode_base64 $cc; $self->_req (login_cram_md6 => $self->{user}, $cr, $cc, sub { my ($self, $ok, $msg) = @_; $ok or return call $self, on_login_failure => $msg; $msg eq $sr or return call $self, on_login_failure => "sr and cr mismatch, possible man in the middle attack"; $self->_login_success ("login_cram_md6"); }); } elsif (grep $_ eq "login", @$auths) { $self->_req (login => $self->{user}, $self->{pass}, sub { my ($self, $ok, $msg) = @_; $ok or return call $self, on_login_failure => $msg; $self->_login_success ("login"); }); } else { call $self, on_login_failure => "no supported auth method (@$auths)"; } # we no longer need these, make it a bit harder to get them delete $self->{user}; delete $self->{pass}; } sub on_info_notify { my ($self, $msg) = @_; warn $msg; } sub on_error_notify { my ($self, $msg) = @_; $self->error ($msg); } sub on_error { my ($self, $msg) = @_; warn $msg; %$self = (); } sub on_login_failure { my ($self, $msg) = @_; $msg =~ s/\n$//; $self->error ("login failed: $msg"); } =back =head2 EVENTS AnyEvent::Porttracker conenctions are fully event-driven, and naturally there are a number of events that can occur. All these events have a name starting with C (example: C). Programs can catch these events in two ways: either by providing constructor arguments with the event name as key and a coderef as value: my $api = new AnyEvent::Porttracker host => ..., user => ..., pass => ..., on_error => sub { my ($api, $msg) = @_; warn $msg; exit 1; }, ; Or by subclassing C and overriding methods of the same name: package MyClass; use base AnyEvent::Porttracker; sub on_error { my ($api, $msg) = @_; warn $msg; exit 1; } Event callbacks are not expected to return anything and are always passed the API object as first argument. Some might have default implementations (for example, C), others are ignored unless overriden. Description of individual events follow: =over 4 =item on_error $api, $msg Is called for every (fatal) error, including C notifies. The default prints the message and destroys the object, so it is highly advisable to override this event. =item on_login $api, $method Called after a successful login, after which commands can be send. It is permissible to send commands before a successful login: those will be queued and sent just before this event is invoked. C<$method> is the auth method that was used. =item on_login_failure $api, $msg Called when all login attempts have failed - the default raises a fatal error with the error message from the server. =item on_hello_notify $api, $version, $authtypes, $nonce This protocol notification is used internally by AnyEvent::Porttracker - you can override it, but the module will most likely not work. =item on_info_notify $api, $msg Called for informational messages from the server - the default implementation calls C but otherwise ignores this notification. =item on_error_notify $api, $msg Called for fatal errors from the server - the default implementation calls C and destroys the API object. =item on_start_tls_notify $api Called when the server wants to start TLS negotiation. This is used internally and - while it is possible to override it - should not be overriden. =item on_XYZ_notify $api, ... In general, any protocol notification will result in an event of the form C. =back =head1 SEE ALSO L, L, L. =head1 AUTHOR Marc Lehmann =cut 1