File Coverage

blib/lib/POE/Component/Client/LDAP.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             package POE::Component::Client::LDAP;
2              
3             =head1 NAME
4              
5             POE::Component::Client::LDAP - subclass of Net::LDAP which uses POE to speak via sockets in async mode.
6              
7             =head1 SYNOPSIS
8              
9             use POE;
10             use POE::Component::Client::LDAP;
11            
12             POE::Session->create(
13             inline_states => {
14             _start => sub {
15             my ($heap, $session) = @_[HEAP, SESSION];
16             $heap->{ldap} = POE::Component::Client::LDAP->new(
17             'localhost',
18             callback => $session->postback( 'connect' ),
19             );
20             },
21             connect => sub {
22             my ($heap, $session, $callback_args) = @_[HEAP, SESSION, ARG1];
23             if ( $callback_args->[0] ) {
24             $heap->{ldap}->bind(
25             callback => $session->postback( 'bind' ),
26             );
27             }
28             else {
29             delete $heap->{ldap};
30             print "Connection Failed\n";
31             }
32             },
33             bind => sub {
34             my ($heap, $session) = @_[HEAP, SESSION];
35             $heap->{ldap}->search(
36             base => "ou=People,dc=domain,dc=net",
37             filter => "(objectClass=person)",
38             callback => $session->postback( 'search' ),
39             );
40             },
41             search => sub {
42             my ($heap, $ldap_return) = @_[HEAP, ARG1];
43             my $ldap_search = shift @$ldap_return;
44            
45             foreach (@$ldap_return) {
46             print $_->dump;
47             }
48            
49             delete $heap->{ldap} if $ldap_search->done;
50             },
51             },
52             );
53            
54             POE::Kernel->run();
55              
56             =head1 DESCRIPTION
57              
58             POE::Component::Client::LDAP->new() starts up a new POE::Session and POE::Wheel to manage socket communications for an underlying Net::LDAP object, allowing it to be used in async mode properly within a POE program.
59              
60             =cut
61              
62 1     1   27631 use base 'Net::LDAP';
  1         2  
  1         1189  
63              
64 1     1   248489 use 5.006;
  1         4  
  1         38  
65              
66 1     1   5 use strict;
  1         8  
  1         40  
67 1     1   6 use warnings;
  1         2  
  1         36  
68              
69 1     1   6 use Net::LDAP::ASN qw(LDAPResponse);
  1         1  
  1         7  
70 1     1   66 use Net::LDAP::Constant qw(LDAP_SERVER_DOWN);
  1         2  
  1         70  
71 1     1   616 use POE qw(Filter::Stream Filter::ASN1 Wheel::SocketFactory Wheel::Null Wheel::ReadWrite Driver::SysRW);
  0            
  0            
72             use Carp;
73              
74             BEGIN {
75             eval "sub DEBUGGING () { " . (exists( $ENV{LDAP_DEBUG} ) ? $ENV{LDAP_DEBUG} : 0) . " }"
76             unless defined &DEBUGGING;
77             }
78              
79             sub DEBUG {
80             map { $_ = '' unless defined( $_ ) } my @stuff = @_;
81             warn "[$$] @stuff\n";
82             }
83              
84             our $VERSION = '0.04';
85              
86             my $poe_states = {
87             _start => sub {
88             my ($kernel, $session, $heap, $ldap_object, $host, $arg) = @_[KERNEL, SESSION, HEAP, ARG0..ARG2];
89              
90             DEBUG( "LDAP management session startup." ) if DEBUGGING;
91              
92             $heap->{ldap_object} = $ldap_object;
93            
94             $ldap_object->{_heap} = $heap;
95             $ldap_object->{_shutdown_callback} = $session->callback( 'shutdown' );
96             $ldap_object->{_connect_callback} = $session->callback( 'connect' );
97             },
98             connect => sub {
99             my ($heap, $callback_args) = @_[HEAP, ARG1];
100              
101             my $host = $heap->{host};
102             my $port = $heap->{port};
103              
104             DEBUG( "Attempting conenction to host: $host" ) if DEBUGGING;
105             $heap->{ldap_object}->{_send_callback} = sub {
106             confess( "LDAP send attempted before connection set up" );
107             };
108              
109             $heap->{wheel} = POE::Wheel::SocketFactory->new(
110             RemoteAddress => $host,
111             RemotePort => $port,
112             # No way to do LocalAddr, Proto, MultiHomed, or Timeout yet
113             SuccessEvent => 'sf_success',
114             FailureEvent => 'sf_failure',
115             );
116             },
117             _stop => sub {
118             DEBUG( "LDAP management session shutdown" ) if DEBUGGING;
119             },
120             sf_success => sub {
121             my ($heap, $session, $sock, $addr, $port) = @_[HEAP, SESSION, ARG0..ARG2];
122             $heap->{wheel} = POE::Wheel::ReadWrite->new(
123             Handle => $sock,
124             Driver => POE::Driver::SysRW->new(),
125             InputFilter => POE::Filter::ASN1->new(),
126             OutputFilter => POE::Filter::Stream->new(),
127             InputEvent => 'readwrite_input',
128             FlushedEvent => 'readwrite_flushed',
129             ErrorEvent => 'readwrite_error',
130             );
131             $heap->{ldap_object}->{_send_callback} = $session->callback( 'send_message' );
132             $heap->{connection_callback}->( 1, $heap->{host}, $addr, $port); # Decide what the hell to pass here
133             },
134             sf_failure => sub {
135             my ($kernel, $heap, $operation, $errnum, $errstr) = @_[KERNEL, HEAP, ARG0..ARG2];
136            
137             DEBUG( "LDAP sf_failure: ", @_ ) if DEBUGGING;
138            
139             $heap->{connection_callback}->( 0, $heap->{host}, $operation, $errnum, $errstr ); # Decide what the hell to pass here
140             $heap->{ldap_object}->{_send_callback} = sub {
141             confess( "Send attempted after connection failure" );
142             };
143             },
144             shutdown => sub {
145             my $heap = $_[HEAP];
146              
147             my $ldap_object = $heap->{ldap_object};
148             delete $ldap_object->{_shutdown_callback};
149             delete $ldap_object->{_send_callback};
150              
151             delete $heap->{ldap_object};
152             delete $heap->{connection_callback};
153             $heap->{wheel} = POE::Wheel::Null->new();
154             },
155             readwrite_input => sub {
156             my ($heap, $input) = @_[HEAP, ARG0];
157             my $result = $LDAPResponse->decode($input);
158              
159             my $ldap = $heap->{ldap_object}->inner;
160              
161             my $mid = $result->{messageID};
162             my $mesg = $ldap->{net_ldap_mesg}->{$mid};
163              
164             if ($mesg) {
165             $mesg->decode( $result );
166             }
167             else {
168             if (my $ext = $result->{protocolOp}{extendedResp}) {
169             if (exists( $ext->{responseName} ) and defined( $ext->{responseName} )) {
170             my $responseName = $ext->{responseName};
171             if ($responseName eq '1.3.6.1.4.1.1466.20036') {
172             DEBUG( "Notice of Disconnection" ) if DEBUGGING;
173             $heap->{connection_callback}->( -1, LDAP_SERVER_DOWN, "Notice of Disconnection" );
174             $heap->{wheel} = POE::Wheel::Null->new();
175            
176             if (my $msgs = $ldap->{net_ldap_mesg}) {
177             foreach my $mesg (values %$msgs) {
178             $mesg->set_error( LDAP_SERVER_DOWN, "Notice of Disconnection" );
179             }
180             }
181            
182             $ldap->{net_ldap_mesg} = {};
183             } else {
184             DEBUG( "Unexpected PDU: '$responseName', ignored" ) if DEBUGGING;
185             }
186             }
187             else {
188             DEBUG( "Unnamed PDU, ignored\n" );
189             }
190             }
191             else {
192             DEBUG( "Input without message or extended response, ignored\n" ) if DEBUGGING;
193             # TODO: handle this, maybe
194             }
195             }
196             },
197             readwrite_flushed => sub {
198             DEBUG( "ReadWrite Flushed: ", @_ ) if DEBUGGING;
199             },
200             readwrite_error => sub {
201             DEBUG( "ReadWrite Error: ", @_ ) if DEBUGGING;
202             },
203             send_message => sub {
204             my ($heap, $response_args) = @_[HEAP, ARG1];
205             $heap->{wheel}->put( $response_args->[0] );
206             },
207             };
208              
209             =head1 INTERFACE DIFFERENCES
210              
211             With regards to Net::LDAP, all interfaces are to be used as documented, with the following exceptions.
212              
213             =over 2
214              
215             =item POE::Component::Client::LDAP->new( hostname, OPTIONS )
216             =item POE::Component::Client::LDAP->new( OPTIONS )
217             =item POE::Component::Client::LDAP->new()
218              
219             A call to new() is non-blocking, always returning an object.
220              
221             If a hostname is supplied, new() also acts as though you have called connect(). Please read the docs for connect() to see how the arguments work.
222              
223             =cut
224              
225             sub new {
226             my $class = shift;
227             my $self = bless {}, (ref $class || $class);
228              
229             my $host = shift if @_ % 2;
230             my $arg = &Net::LDAP::_options;
231              
232             if (ref( $host ) eq 'ARRAY') {
233             die( "POE::Component::Client::LDAP doesn't support a list of hostnames.\n" );
234             }
235            
236             POE::Session->create(
237             inline_states => $poe_states,
238             args => [ $self ],
239             );
240              
241             $self->{_send_callback} = sub {
242             confess( "LDAP send attempted while no connection open" );
243             };
244              
245             $self->{net_ldap_resp} = {};
246             $self->{net_ldap_async} = 1;
247            
248             $self->{net_ldap_version} = (exists( $arg->{version} ) ? $arg->{version} : $Net::LDAP::LDAP_VERSION);
249            
250             $self->debug( exists( $arg->{debug} ) ? $arg->{debug} : 0 );
251              
252             my $heap = $self->{_heap};
253              
254             $heap->{connection_callback} = $arg->{callback}
255             if (exists( $arg->{callback} ));
256              
257             $heap->{port} = exists( $arg->{port} ) ? $arg->{port} : 389;
258              
259             if (defined( $host )) {
260             $heap->{host} = $host;
261             $self->{_connect_callback}->(); # Try to connect
262             }
263              
264             return $self->outer();
265             }
266              
267             =item $object->connect( hostname, OPTIONS )
268             =item $object->connect( OPTIONS )
269             =item $object->connect()
270              
271             The 'callback' argument has been added and should always be supplied to notify your code when a connection is established.
272              
273             Only LDAP connections are supported at this time, LDAPS and LDAPI will be in a future release.
274              
275             Connection errors are not handled at this time, again in a future release.
276              
277             The 'async' option is always turned on, and whatever value you pass in will be ignored.
278              
279             =cut
280              
281             sub connect {
282             my $self = shift;
283              
284             my $host = shift if @_ % 2;
285            
286             my $arg = &Net::LDAP::_options;
287              
288             $self->{net_ldap_resp} = {};
289             $self->{net_ldap_version} = $arg->{version}
290             if (exists( $arg->{version} ));
291              
292             my $heap = $self->{_heap};
293              
294             $heap->{connection_callback} = $arg->{callback}
295             if (exists( $arg->{callback} ));
296              
297             $heap->{port} = exists( $arg->{port} ) ? $arg->{port} : 389;
298              
299             $heap->{host} = $arg->{host}
300             if (defined( $host ));
301              
302             $self->{_connect_callback}->(); # Try to connect
303             }
304              
305             =item $object->async()
306              
307             Async mode is always turned on and so this call will always return true, if you pass it a value to set it a fatal exception will be raised, even if value is true.
308              
309             =cut
310              
311             sub async {
312             my $self = shift;
313             if (@_) {
314             die( "Setting async() under POE::Component::Client::LDAP is not something you want to do.\n" );
315             }
316             else {
317             return $self->inner->{net_ldap_async};
318             }
319             }
320              
321             =item $object->sync()
322              
323             Async mode is required, this call will cause a fatal exception.
324              
325             =cut
326              
327             sub sync {
328             die( "Setting sync() under POE::Component::Client::LDAP is not something you want to do.\n" );
329             }
330              
331             =item $object->sock()
332              
333             This call will throw a fatal exception.
334              
335             Because POE is being used to handle socket communications I have chosen to not expose the raw socket at this time.
336              
337             =back
338              
339             =cut
340              
341             sub socket {
342             die( "socket() as a method call is not supported under PoCo::Client::LDAP\n" );
343             }
344              
345             sub disconnect {
346             my $self = shift;
347             $self->inner->_drop_conn()
348             }
349              
350             sub _drop_conn {
351             # Called as inner
352             my $self = shift;
353             DEBUG( "_drop_conn" ) if DEBUGGING;
354             $self->{_shutdown_callback}->();
355             }
356              
357             sub _sendmesg {
358             my $self = shift;
359             my $mesg = shift;
360              
361             $self->{_send_callback}->( $mesg->pdu );
362              
363             my $mid = $mesg->mesg_id;
364              
365             $self->inner->{net_ldap_mesg}->{$mid} = $mesg;
366              
367             DEBUG( "Message $mid queued for sending" ) if DEBUGGING;
368             }
369              
370             sub _recvresp {
371             die( "POE::Component::Client::LDAP internal issue, _recvresp called.\n" );
372             }
373              
374             sub DESTROY {
375             my $self = shift;
376             $self->inner->_drop_conn()
377             unless --$self->inner->{net_ldap_refcnt};
378            
379             DEBUG( "Net::LDAP Refcount: " . $self->inner->{net_ldap_refcnt} ) if DEBUGGING;
380             }
381              
382             =head1 CALLBACK SEMANTICS
383              
384             The callback semantics documented here are for reference, the callbacks are handled by Net::LDAP and I've only documented them for reference here. The exception to this is the callback for new() which does not exist in Net::LDAP, and thus I have defined myself.
385              
386             =over 2
387              
388             =item new
389             =item connect
390              
391             No arguments are passed to indicate that an existing connection has been closed.
392              
393             The first argument is a boolean indicator of whether a connection has succeeded or failed. The second argument contains the host spec used to attempt the connection.
394              
395             In the case of a success the third and fourth arguments contain the address and port connected to respectively.
396              
397             In the case of a failure the third argument contains the name of the operation that failed, and the fourth and fifth arguments hold numeric and string values of $! respectively.
398              
399             =item search
400              
401             The first argument is always the Net::LDAP::Search object presiding over this search run. The 'done' method on this object may be consulted to know when all the possible replies have been received.
402              
403             The second and following arguments are Net::LDAP::Entry objects returned from the search.
404              
405             =item others
406              
407             Forthcoming
408              
409             =back
410              
411             =head1 BUGS
412              
413             Failures of many kinds are not very well handled at this time, also canceling running connection requests is not implemented.
414              
415             =head1 AUTHOR
416              
417             Jonathan Steinert
418             hachi@cpan.org
419              
420             =head1 LICENSE
421              
422             Copyright 2004 Jonathan Steinert (hachi@cpan.org)
423              
424             This program is free software; you can redistribute it
425             and/or modify it under the same terms as Perl itself.
426              
427             =cut
428