ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-Porttracker/Porttracker.pm
Revision: 1.5
Committed: Mon Nov 15 20:43:11 2010 UTC (13 years, 7 months ago) by root
Branch: MAIN
Changes since 1.4: +9 -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 =head1 DESCRIPTION
10
11 Porttracker (L<http://www.porttracker.com/>) is a product that (among
12 other things) scans switches and routers in a network and gives a coherent
13 view of which end devices are connected to which switch ports on which
14 switches and routers. It also offers a JSON-based client API, for which
15 this module is an implementation.
16
17 In addition to Porttracker, the PortIQ product is also supported, as it
18 uses the same protocol.
19
20 If you do not have access to either a Porttracker or PortIQ box then this
21 module will be of little value to you.
22
23 This module is an L<AnyEvent> user, you need to make sure that you use and
24 run a supported event loop.
25
26 To quickly understand how this module works you should read how to
27 construct a new connection object and then read about the event/callback
28 system.
29
30 The actual low-level protocol and, more importantly, the existing
31 requests and responses, are documented in the official Porttracker
32 API documentation (a copy of which is included in this module as
33 L<AnyEvent::Porttracker::protocol>.
34
35 =head1 THE AnyEvent::Porttracker CLASS
36
37 The AnyEvent::Porttracker class represents a single connection.
38
39 =over 4
40
41 =cut
42
43 package AnyEvent::Porttracker;
44
45 use common::sense;
46
47 use Scalar::Util ();
48
49 use AnyEvent ();
50 use AnyEvent::Handle ();
51
52 use MIME::Base64 ();
53 use Digest::HMAC_MD6 ();
54 use JSON ();
55
56 our $VERSION = '0.0';
57
58 sub call {
59 my ($self, $type, @args) = @_;
60
61 $self->{$type}
62 ? $self->{$type}($self, @args)
63 : ($type = (UNIVERSAL::can $self, $type))
64 ? $type->($self, @args)
65 : ()
66 }
67
68 =item $api = new AnyEvent::Porttracker [key => value...]
69
70 Creates a new porttracker API connection object and tries to connect to
71 the specified host (see below). After the connection has been established,
72 the TLS handshake (if requested) will take place, followed by a login
73 attempt using either the C<none>, C<login_cram_md6> or C<login> methods,
74 in this order of preference (typically, C<login_cram_md6> is used, which
75 shields against some man-in-the-middle attacks and avoids transferring the
76 password).
77
78 It is permissible to send requests immediately after creating the object -
79 they will be queued until after successful login.
80
81 Possible key-value pairs are:
82
83 =over 4
84
85 =item host => $hostname [MANDATORY]
86
87 The hostname or IP address of the Porttracker box.
88
89 =item port => $service
90
91 The service (port) to use (default: C<porttracker=55>).
92
93 =item user => $string, pass => $string
94
95 These are the username and password to use when authentication is required
96 (which it is in almost all cases, so these keys are normally mandatory).
97
98 =item tls => ...
99
100 #TODO#
101
102 =item on_XYZ => $coderef
103
104 You can specify event callbacks either by subclassing and overriding the
105 respective methods or by specifying coderefs as key-value pairs when
106 constructing the object.
107
108 =back
109
110 =cut
111
112 sub new {
113 my $class = shift;
114
115 my $self = bless {
116 id => "a",
117 ids => [],
118 queue => [], # ininitially queue everything
119 @_,
120 }, $class;
121
122 {
123 Scalar::Util::weaken (my $self = $self);
124
125 $self->{hdl} = new AnyEvent::Handle
126 connect => [$self->{host}, $self->{port} || "porttracker=55"],
127 on_error => sub {
128 $self->error ();
129 },
130 on_connect => sub {
131 if ($self->{tls}) {
132 $self->_req (start_tls => sub {
133 $_[1]
134 or return $self->error ("TLS rejected by server");
135
136 $self->_login;
137 });
138 }
139 },
140 on_read => sub {
141 while ($_[0]{rbuf} =~ s/^([^\x0a]*)\x0a//) {
142 my $msg = JSON::decode_json $1;
143 my $id = shift @$msg;
144
145 if (defined $id) {
146 my $cb = delete $self->{cb}{$id}
147 or return $self->error ("received unexpected reply msg with id $id");
148
149 push @{ $self->{ids} }, $id;
150
151 $cb->($self, @$msg);
152 } else {
153 $msg->[0] = "on_$msg->[0]_notify";
154 call $self, @$msg;
155 }
156 }
157 },
158 ;
159 }
160
161 $self
162 }
163
164 sub DESTROY {
165 my ($self) = @_;
166
167 $self->{hdl}->destroy
168 if $self->{hdl};
169 }
170
171 sub error {
172 my ($self, $msg) = @_;
173
174 call on_error => $msg;
175
176 ()
177 }
178
179 sub _req {
180 my $self = shift;
181 my $cb = pop;
182
183 my $id = (pop @{ $self->{ids} }) || $self->{id}++;
184
185 unshift @_, $id;
186 $self->{cb}{$id} = $cb;
187
188 my $msg = JSON::encode_json \@_;
189
190 $self->{hdl}->push_write ($msg);
191 }
192
193 =item $api->req ($type => @args, $callback->($api, @args))
194
195 Sends a generic request of type C<$type> to the server. When the server
196 responds, the API object and the response arguments are passed to the
197 callback, which is the last argument to this method.
198
199 The available requests are documented in the Porttracker API
200 documentation (a copy of which is included in this module as
201 L<AnyEvent::Porttracker::protocol>.
202
203 It is permissible to call this (or any other request function) at any
204 time, even before the connection has been established - the API object
205 always waits until after login before it actually sends the requests, and
206 queues them until then.
207
208 Example: ping the porttracker server.
209
210 $api->req ("ping", sub {
211 my ($api, $ok, $timestamp, $pid) = @_;
212 ...
213 });
214
215 Example: determine the product ID.
216
217 $api->req (product_id => sub {
218 my ($api, $ok, $branding, $product_id) = @_;
219 ...
220 });
221
222 Example: set a new license.
223
224 $api->req (set_license => $LICENSE_STRING, sub {
225 my ($api, $ok) = @_;
226
227 $ok or die "failed to set license";
228 });
229
230 =cut
231
232 sub req {
233 $_[0]{queue}
234 ? push @{ $_[0]{queue} }, [@_]
235 : &_req
236 }
237
238 sub on_start_tls_notify {
239 my ($self) = @_;
240
241 $self->{hdl}->starttls ("connect");
242 $self->{tls} ||= 1;
243
244 $self->_login;
245 }
246
247 sub on_hello_notify {
248 my ($self, $version, $auths, $nonce) = @_;
249
250 $version == 1
251 or return $self->error ("protocol mismatch, got $version, expected/supported 1");
252
253 $nonce = MIME::Base64::decode_base64 $nonce;
254
255 $self->{hello} = [$auths, $nonce];
256
257 $self->_login
258 unless $self->{tls}; # delay login when trying to handshake tls
259 }
260
261 sub _login_success {
262 my ($self, $method) = @_;
263
264 _req @$_
265 for @{ delete $self->{queue} };
266
267 call $self, on_login => $method;
268 }
269
270 sub _login {
271 my ($self) = @_;
272
273 my ($auths, $nonce) = @{ delete $self->{hello} or return };
274
275 if (grep $_ eq "none", @$auths) {
276 $self->_login_success ("none");
277
278 } elsif (grep $_ eq "login_cram_md6", @$auths) {
279 my $cc = join "", map chr 256 * rand, 0..63;
280
281 my $key = Digest::HMAC_MD6::hmac_md6 $self->{pass}, $self->{user}, 64, 256;
282 my $cr = Digest::HMAC_MD6::hmac_md6_base64 $key, "$cc$nonce", 64, 256;
283 my $sr = Digest::HMAC_MD6::hmac_md6_base64 $key, "$nonce$cc", 64, 256;
284
285 $cc = MIME::Base64::encode_base64 $cc;
286
287 $self->_req (login_cram_md6 => $self->{user}, $cr, $cc, sub {
288 my ($self, $ok, $msg) = @_;
289
290 $ok
291 or return call $self, on_login_failure => $msg;
292
293 $msg eq $sr
294 or return call $self, on_login_failure => "sr and cr mismatch, possible man in the middle attack";
295
296 $self->_login_success ("login_cram_md6");
297 });
298 } elsif (grep $_ eq "login", @$auths) {
299 $self->_req (login => $self->{user}, $self->{pass}, sub {
300 my ($self, $ok, $msg) = @_;
301
302 $ok
303 or return call $self, on_login_failure => $msg;
304
305 $self->_login_success ("login");
306 });
307 } else {
308 call $self, on_login_failure => "no supported auth method (@$auths)";
309 }
310
311 # we no longer need these, make it a bit harder to get them
312 delete $self->{user};
313 delete $self->{pass};
314 }
315
316 sub on_info_notify {
317 my ($self, $msg) = @_;
318
319 warn $msg;
320 }
321
322 sub on_error_notify {
323 my ($self, $msg) = @_;
324
325 $self->error ($msg);
326 }
327
328 sub on_error {
329 my ($self, $msg) = @_;
330
331 warn $msg;
332
333 %$self = ();
334 }
335
336 sub on_login_failure {
337 my ($self, $msg) = @_;
338
339 $msg =~ s/\n$//;
340 $self->error ("login failed: $msg");
341 }
342
343 =back
344
345 =head2 EVENTS
346
347 AnyEvent::Porttracker conenctions are fully event-driven, and naturally
348 there are a number of events that can occur. All these events have a name
349 starting with C<on_> (example: C<on_login_failure>).
350
351 Programs can catch these events in two ways: either by providing
352 constructor arguments with the event name as key and a coderef as value:
353
354 my $api = new AnyEvent::Porttracker
355 host => ...,
356 user => ..., pass => ...,
357 on_error => sub {
358 my ($api, $msg) = @_;
359 warn $msg;
360 exit 1;
361 },
362 ;
363
364 Or by subclassing C<AnyEvent::Porttracker> and overriding methods of the
365 same name:
366
367 package MyClass;
368
369 use base AnyEvent::Porttracker;
370
371 sub on_error {
372 my ($api, $msg) = @_;
373 warn $msg;
374 exit 1;
375 }
376
377 Event callbacks are not expected to return anything and are always passed
378 the API object as first argument. Some might have default implementations
379 (for example, C<on_error>), others are ignored unless overriden.
380
381 Description of individual events follow:
382
383 =over 4
384
385 =item on_error $api, $msg
386
387 Is called for every (fatal) error, including C<error> notifies. The
388 default prints the message and destroys the object, so it is highly
389 advisable to override this event.
390
391 =item on_login $api, $method
392
393 Called after a successful login, after which commands can be send. It is
394 permissible to send commands before a successful login: those will be
395 queued and sent just before this event is invoked. C<$method> is the auth
396 method that was used.
397
398 =item on_login_failure $api, $msg
399
400 Called when all login attempts have failed - the default raises a fatal
401 error with the error message from the server.
402
403 =item on_hello_notify $api, $version, $authtypes, $nonce
404
405 This protocol notification is used internally by AnyEvent::Porttracker -
406 you can override it, but the module will most likely not work.
407
408 =item on_info_notify $api, $msg
409
410 Called for informational messages from the server - the default
411 implementation calls C<warn> but otherwise ignores this notification.
412
413 =item on_error_notify $api, $msg
414
415 Called for fatal errors from the server - the default implementation calls
416 C<warn> and destroys the API object.
417
418 =item on_start_tls_notify $api
419
420 Called when the server wants to start TLS negotiation. This is used
421 internally and - while it is possible to override it - should not be
422 overriden.
423
424 =item on_XYZ_notify $api, ...
425
426 In general, any protocol notification will result in an event of the form
427 C<on_NOTIFICATION_notify>.
428
429 =back
430
431 =head1 SEE ALSO
432
433 L<AnyEvent>, L<http://www.porttracker.com/>, L<http://www.infoblox.com/en/products/portiq.html>.
434
435 =head1 AUTHOR
436
437 Marc Lehmann <marc@porttracker.net>
438
439 =cut
440
441 1