File Coverage

blib/lib/Net/LDAP/Server/Test.pm
Criterion Covered Total %
statement 323 469 68.8
branch 101 196 51.5
condition 10 23 43.4
subroutine 42 50 84.0
pod 3 3 100.0
total 479 741 64.6


line stmt bran cond sub pod time code
1             package Net::LDAP::Server::Test;
2              
3 15     15   1377120 use warnings;
  15         25  
  15         543  
4 15     15   52 use strict;
  15         21  
  15         236  
5 15     15   41 use Carp;
  15         19  
  15         810  
6 15     15   2101 use IO::Socket;
  15         81145  
  15         79  
7 15     15   8426 use IO::Select;
  15         5601  
  15         492  
8 15     15   6376 use Data::Dump ();
  15         80256  
  15         325  
9 15     15   6840 use Net::LDAP::SID;
  15         6961  
  15         721  
10              
11             our $VERSION = '0.21';
12              
13             =head1 NAME
14              
15             Net::LDAP::Server::Test - test Net::LDAP code
16              
17             =head1 SYNOPSIS
18              
19             use Test::More tests => 10;
20             use Net::LDAP::Server::Test;
21            
22             ok( my $server = Net::LDAP::Server::Test->new(8080),
23             "test LDAP server spawned");
24            
25             # connect to port 8080 with your Net::LDAP code.
26             ok(my $ldap = Net::LDAP->new( 'localhost', port => 8080 ),
27             "new LDAP connection" );
28            
29             # ... test stuff with $ldap ...
30            
31             # server will exit when you call final LDAP unbind().
32             ok($ldap->unbind(), "LDAP server unbound");
33              
34             =head1 DESCRIPTION
35              
36             Now you can test your Net::LDAP code without having a real
37             LDAP server available.
38              
39             =head1 METHODS
40              
41             Only one user-level method is implemented: new().
42              
43             =cut
44              
45             {
46              
47             package # fool Pause
48             MyLDAPServer;
49              
50 15     15   68 use strict;
  15         19  
  15         232  
51 15     15   43 use warnings;
  15         12  
  15         332  
52 15     15   142 use Carp;
  15         14  
  15         745  
53 15         1439 use Net::LDAP::Constant qw(
54             LDAP_SUCCESS
55             LDAP_NO_SUCH_OBJECT
56             LDAP_CONTROL_PAGED
57             LDAP_OPERATIONS_ERROR
58             LDAP_UNWILLING_TO_PERFORM
59             LDAP_ALREADY_EXISTS
60             LDAP_TYPE_OR_VALUE_EXISTS
61             LDAP_NO_SUCH_ATTRIBUTE
62 15     15   2445 );
  15         18726  
63 15     15   35984 use Net::LDAP::Util qw(ldap_explode_dn canonical_dn);
  15         662  
  15         871  
64 15     15   5875 use Net::LDAP::Entry;
  15         377193  
  15         372  
65 15     15   6989 use Net::LDAP::Filter;
  15         27653  
  15         397  
66 15     15   6219 use Net::LDAP::FilterMatch;
  15         59927  
  15         105  
67 15     15   52058 use Net::LDAP::Control;
  15         11575  
  15         496  
68 15     15   86 use Net::LDAP::ASN qw(LDAPRequest LDAPResponse);
  15         21  
  15         97  
69 15     15   1021 use Convert::ASN1 qw(asn_read);
  15         19  
  15         649  
70              
71 15     15   64 use base 'Net::LDAP::Server';
  15         20  
  15         7650  
72 15     15   114755 use fields qw( _flags );
  15         25  
  15         55  
73              
74 15         874 use constant RESULT_OK => {
75             'matchedDN' => '',
76             'errorMessage' => '',
77             'resultCode' => LDAP_SUCCESS
78 15     15   733 };
  15         22  
79              
80 15         707 use constant RESULT_NO_SUCH_OBJECT => {
81             'matchedDN' => '',
82             'errorMessage' => '',
83             'resultCode' => LDAP_NO_SUCH_OBJECT,
84 15     15   69 };
  15         17  
85              
86 15         690 use constant RESULT_ALREADY_EXISTS => {
87             'matchedDN' => '',
88             'errorMessage' => '',
89             'resultCode' => LDAP_ALREADY_EXISTS,
90 15     15   54 };
  15         19  
91              
92 15         695 use constant RESULT_TYPE_OR_VALUE_EXISTS => {
93             'matchedDN' => '',
94             'errorMessage' => '',
95             'resultCode' => LDAP_TYPE_OR_VALUE_EXISTS,
96 15     15   62 };
  15         28  
97              
98 15         40194 use constant RESULT_NO_SUCH_ATTRIBUTE => {
99             'matchedDN' => '',
100             'errorMessage' => '',
101             'resultCode' => LDAP_NO_SUCH_ATTRIBUTE,
102 15     15   47 };
  15         20  
103              
104             our %Data; # package data lasts as long as $$ does.
105             our $Cookies = 0;
106             our %Searches;
107             my @Scopes = qw(base one sub);
108              
109             # constructor
110             sub new {
111 7     7   41 my ( $class, $sock, %args ) = @_;
112 7         262 my $self = $class->SUPER::new($sock);
113             warn sprintf "Accepted connection from: %s\n", $sock->peerhost()
114 7 50       28109 if $ENV{LDAP_DEBUG};
115 7         25 $self->{_flags} = \%args;
116 7         130 return $self;
117             }
118              
119             sub unbind {
120 0     0   0 my $self = shift;
121 0         0 my $reqData = shift;
122 0         0 return RESULT_OK;
123             }
124              
125             # the bind operation
126             sub bind {
127 5     5   12 my $self = shift;
128 5         9 my $reqData = shift;
129 5         15 return RESULT_OK;
130             }
131              
132             # the search operation
133             sub search {
134 17     17   30 my $self = shift;
135              
136 17 100       84 if ( defined $self->{_flags}->{data} ) {
    100          
137 1         5 return $self->_search_user_supplied_data(@_);
138             }
139             elsif ( defined $self->{_flags}->{auto_schema} ) {
140 14         62 return $self->_search_auto_schema_data(@_);
141             }
142             else {
143 2         23 return $self->_search_default_test_data(@_);
144             }
145             }
146              
147             sub _search_user_supplied_data {
148 1     1   2 my ( $self, $reqData ) = @_;
149              
150             # TODO??
151              
152             #warn 'SEARCH USER DATA: ' . Data::Dump::dump \@_;
153 1         1 return RESULT_OK, @{ $self->{_flags}->{data} };
  1         4  
154             }
155              
156             sub _search_auto_schema_data {
157 14     14   21 my ( $self, $reqData, $reqMsg ) = @_;
158              
159             #warn 'SEARCH SCHEMA: ' . Data::Dump::dump \@_;
160              
161 14         17 my @results;
162 14         29 my $base = $reqData->{baseObject};
163              
164             # $reqData->{scope} is a enum but we want a word
165             my $scope
166 14 50       82 = $Scopes[ defined $reqData->{scope} ? $reqData->{scope} : 2 ];
167 14 50       16 my @attrs = @{ $reqData->{attributes} || [] };
  14         54  
168 14         21 my @filters = ();
169              
170 14 50       43 if ( exists $reqData->{filter} ) {
171             push( @filters,
172 14         65 bless( $reqData->{filter}, 'Net::LDAP::Filter' ) );
173              
174             }
175              
176 14 50       67 if ( $ENV{LDAP_DEBUG} ) {
177 0         0 warn "search for '$base' with scope '$scope' in Data: "
178             . Data::Dump::dump \%Data;
179 0         0 warn "filters: " . Data::Dump::dump \@filters;
180             }
181              
182             # support paged results
183 14         20 my ( $page_size, $cookie, $controls, $offset );
184 14 50       54 if ( exists $reqMsg->{controls} ) {
185 0         0 for my $control ( @{ $reqMsg->{controls} } ) {
  0         0  
186              
187 0 0       0 if ( $ENV{LDAP_DEBUG} ) {
188 0         0 warn "control: " . Data::Dump::dump($control) . "\n";
189             }
190              
191 0 0       0 if ( $control->{type} eq LDAP_CONTROL_PAGED ) {
192 0         0 my $asn = Net::LDAP::Control->from_asn($control);
193              
194 0 0       0 if ( $ENV{LDAP_DEBUG} ) {
195 0         0 warn "asn: " . Data::Dump::dump($asn) . "\n";
196             }
197 0         0 $page_size = $asn->size;
198              
199 0 0       0 if ( $ENV{LDAP_DEBUG} ) {
200 0         0 warn "size == $page_size";
201 0         0 warn "cookie == " . $asn->cookie;
202             }
203              
204             # assign a cookie if this is the first page of paged search
205 0 0       0 if ( !$asn->cookie ) {
206 0         0 $asn->cookie( ++$Cookies );
207 0         0 $asn->value; # IMPORTANT!! encode value with cookie
208              
209 0 0       0 if ( $ENV{LDAP_DEBUG} ) {
210 0         0 warn "no cookie assigned. setting to $Cookies";
211             }
212              
213             # keep track of offset
214 0         0 $Searches{ $asn->cookie } = 0;
215             }
216              
217 0         0 $offset = $Searches{ $asn->cookie };
218 0         0 $cookie = $asn->cookie;
219              
220 0         0 push( @$controls, $asn );
221             }
222             }
223             }
224              
225             # loop over all keys looking for match
226             # we sort in order for paged control to work
227 14         108 ENTRY: for my $dn ( sort keys %Data ) {
228              
229 32 100       287 next unless $dn =~ m/$base$/;
230              
231 24 100       66 if ( $scope eq 'base' ) {
    100          
232 12 100       166 next unless $dn eq $base;
233             }
234             elsif ( $scope eq 'one' ) {
235 6         4 my $dn_depth = scalar @{ ldap_explode_dn($dn) };
  6         28  
236 6         788 my $base_depth = scalar @{ ldap_explode_dn($base) };
  6         16  
237              
238             # We're guaranteed to be at or under $base thanks to the m// above
239 6 100       366 next unless $dn_depth == $base_depth + 1;
240             }
241              
242 16         31 my $entry = $Data{$dn};
243              
244 16 50       95 if ( $ENV{LDAP_DEBUG} ) {
245 0         0 warn "trying to match $dn : " . Data::Dump::dump $entry;
246             }
247              
248 16         91 my $match = 0;
249 16         25 for my $filter (@filters) {
250              
251 16 100       127 if ( $filter->match($entry) ) {
252              
253             #warn "$f matches entry $dn";
254 11         1293 $match++;
255             }
256             }
257              
258             #warn "matched $match";
259 16 100       800 if ( $match == scalar(@filters) ) {
260              
261             # clone the entry so that client cannot modify %Data
262 11         41 my $result = $entry->clone;
263              
264             # filter returned attributes to those requested
265 11 50       958 if (@attrs) {
266 0         0 my %wanted = map { $_ => 1 } @attrs;
  0         0  
267             $result->delete($_)
268 0         0 for grep { not $wanted{$_} } $result->attributes;
  0         0  
269             }
270              
271 11         25 push( @results, $result );
272              
273             }
274             }
275              
276             # for paged results we find everything then take a slice.
277             # this is less how a Real Server would do it but does
278             # work for the simple case where we want to make sure our offset
279             # and page size are accurate and we're not returning the same results
280             # in multiple pages.
281             # the $page_size -1 is because we're zero-based.
282              
283 14         29 my $total_found = scalar(@results);
284 14 50       39 if ( $ENV{LDAP_DEBUG} ) {
285 0         0 warn "found $total_found total results for filters:"
286             . Data::Dump::dump( \@filters );
287              
288             #warn Data::Dump::dump( \@results );
289 0 0       0 if ($page_size) {
290 0         0 warn "page_size == $page_size offset == $offset\n";
291             }
292             }
293              
294 14 100 100     157 if ( $scope eq 'base' and $total_found == 0 ) {
295 3         12 return RESULT_NO_SUCH_OBJECT;
296             }
297              
298 11 50 33     86 if ( $page_size && $offset > $#results ) {
    50 33        
    50 33        
299              
300 0 0       0 if ( $ENV{LDAP_DEBUG} ) {
301 0         0 warn "exceeded end of results\n";
302             }
303 0         0 @results = ();
304              
305             # IMPORTANT!! must set pager cookie to false
306             # to indicate no more results
307 0         0 for my $control (@$controls) {
308 0 0       0 if ( $control->isa('Net::LDAP::Control::Paged') ) {
309 0         0 $control->cookie(undef);
310 0         0 $control->value; # IMPORTANT!! re-encode
311             }
312             }
313             }
314             elsif ( $page_size && @results ) {
315              
316 0         0 my $limit = $offset + $page_size - 1;
317 0 0       0 if ( $limit > $#results ) {
318 0         0 $limit = $#results;
319             }
320              
321 0 0       0 if ( $ENV{LDAP_DEBUG} ) {
322 0         0 warn "slice \@results[ $offset .. $limit ]\n";
323             }
324 0         0 @results = @results[ $offset .. $limit ];
325              
326             # update our global marker
327 0         0 $Searches{$cookie} = $limit + 1;
328              
329 0 0       0 if ( $ENV{LDAP_DEBUG} ) {
330 0         0 warn "returning " . scalar(@results) . " total results\n";
331 0         0 warn "next offset start is $Searches{$cookie}\n";
332              
333             #warn Data::Dump::dump( \@results );
334             }
335              
336             }
337              
338             # special case. client is telling server to abort.
339             elsif ( defined $page_size && $page_size == 0 ) {
340              
341 0         0 @results = ();
342              
343             }
344              
345             #warn "search results for " . Data::Dump::dump($reqData) . "\n: "
346             # . Data::Dump::dump \@results;
347              
348 11         47 return ( RESULT_OK, \@results, $controls );
349              
350             }
351              
352             sub _search_default_test_data {
353 2     2   7 my ( $self, $reqData ) = @_;
354              
355             #warn 'SEARCH DEFAULT: ' . Data::Dump::dump \@_;
356              
357 2         9 my $base = $reqData->{'baseObject'};
358              
359             # plain die if dn contains 'dying'
360 2 50       12 die("panic") if $base =~ /dying/;
361              
362             # return a correct LDAPresult, but an invalid entry
363 2 50       13 return RESULT_OK, { test => 1 } if $base =~ /invalid entry/;
364              
365             # return an invalid LDAPresult
366 2 50       4 return { test => 1 } if $base =~ /invalid result/;
367              
368 2         4 my @entries;
369 2 50       9 if ( $reqData->{'scope'} ) {
370              
371             # onelevel or subtree
372 2         11 for ( my $i = 1; $i < 11; $i++ ) {
373 20         44 my $dn = "ou=test $i,$base";
374 20         95 my $entry = Net::LDAP::Entry->new;
375 20         206 $entry->dn($dn);
376 20         157 $entry->add(
377             dn => $dn,
378             sn => 'value1',
379             cn => [qw(value1 value2)]
380             );
381 20         672 push @entries, $entry;
382             }
383              
384 2         12 my $entry1 = Net::LDAP::Entry->new;
385 2         21 $entry1->dn("cn=dying entry,$base");
386 2         13 $entry1->add(
387             cn => 'dying entry',
388             description =>
389             'This entry will result in a dying error when queried'
390             );
391 2         52 push @entries, $entry1;
392              
393 2         5 my $entry2 = Net::LDAP::Entry->new;
394 2         17 $entry2->dn("cn=invalid entry,$base");
395 2         31 $entry2->add(
396             cn => 'invalid entry',
397             description =>
398             'This entry will result in ASN1 error when queried'
399             );
400 2         57 push( @entries, $entry2 );
401              
402 2         8 my $entry3 = Net::LDAP::Entry->new;
403 2         23 $entry3->dn("cn=invalid result,$base");
404 2         11 $entry3->add(
405             cn => 'invalid result',
406             description =>
407             'This entry will result in ASN1 error when queried'
408             );
409 2         54 push @entries, $entry3;
410             }
411             else {
412              
413             # base
414 0         0 my $entry = Net::LDAP::Entry->new;
415 0         0 $entry->dn($base);
416 0         0 $entry->add(
417             dn => $base,
418             sn => 'value1',
419             cn => [qw(value1 value2)]
420             );
421 0         0 push @entries, $entry;
422             }
423 2         13 return RESULT_OK, @entries;
424             }
425              
426             sub add {
427 10     10   14 my ( $self, $reqData, $reqMsg ) = @_;
428              
429 10         17 my $key = $reqData->{objectName};
430 10 50       28 if ( $ENV{LDAP_DEBUG} ) {
431 0         0 warn 'ADD: ' . Data::Dump::dump \@_;
432 0         0 warn "key: $key";
433             }
434              
435 10 100       23 if ( exists $Data{$key} ) {
436 1         8 return RESULT_ALREADY_EXISTS;
437             }
438              
439 9         72 my $entry = Net::LDAP::Entry->new;
440 9         112 $entry->dn($key);
441 9         56 for my $attr ( @{ $reqData->{attributes} } ) {
  9         38  
442 18         215 $entry->add( $attr->{type} => \@{ $attr->{vals} } );
  18         70  
443             }
444              
445 9         128 $Data{$key} = $entry;
446              
447 9 50       24 if ( exists $self->{_flags}->{active_directory} ) {
448 0         0 $self->_add_AD( $reqData, $reqMsg, $key, $entry, \%Data );
449             }
450              
451 9         18 return RESULT_OK;
452             }
453              
454             sub modify {
455 8     8   14 my ( $self, $reqData, $reqMsg ) = @_;
456              
457 8 50       24 if ( $ENV{LDAP_DEBUG} ) {
458 0         0 warn 'MODIFY: ' . Data::Dump::dump \@_;
459             }
460              
461 8         11 my $key = $reqData->{object};
462 8 100       23 if ( !exists $Data{$key} ) {
463 1         2 return RESULT_NO_SUCH_OBJECT;
464             }
465              
466 7         12 my @mods = @{ $reqData->{modification} };
  7         16  
467 7         17 for my $mod (@mods) {
468 7         10 my $attr = $mod->{modification}->{type};
469 7         9 my $vals = $mod->{modification}->{vals};
470 7         9 my $entry = $Data{$key};
471              
472 7         29 my $current_value = $entry->get_value( $attr, asref => 1 );
473              
474 7 100       84 if ( $mod->{operation} == 0 ) {
    100          
    50          
475 3 100       11 if ( defined $current_value ) {
476 2         6 for my $v (@$current_value) {
477 2 100       4 if ( grep { $_ eq $v } @$vals ) {
  2         12  
478 1         3 return RESULT_TYPE_OR_VALUE_EXISTS;
479             }
480             }
481             }
482              
483 2         14 $entry->add( $attr => $vals );
484             }
485             elsif ( $mod->{operation} == 1 ) {
486 3 100       20 if ( !defined $current_value ) {
487 1         3 return RESULT_NO_SUCH_ATTRIBUTE;
488             }
489 2         26 $entry->delete( $attr => $vals );
490             }
491             elsif ( $mod->{operation} == 2 ) {
492 1         9 $entry->replace( $attr => $vals );
493             }
494             else {
495 0         0 croak "unknown modify operation: $mod->{operation}";
496             }
497             }
498              
499 5 50       118 if ( $self->{_flags}->{active_directory} ) {
500 0         0 $self->_modify_AD( $reqData, $reqMsg, \%Data );
501             }
502              
503 5         12 return RESULT_OK;
504              
505             }
506              
507             sub delete {
508 0     0   0 my ( $self, $reqData, $reqMsg ) = @_;
509              
510 0 0       0 if ( $ENV{LDAP_DEBUG} ) {
511 0         0 warn 'DELETE: ' . Data::Dump::dump \@_;
512             }
513              
514 0         0 my $key = $reqData;
515 0 0       0 if ( !exists $Data{$key} ) {
516 0         0 return RESULT_NO_SUCH_OBJECT;
517             }
518 0         0 delete $Data{$key};
519              
520 0         0 return RESULT_OK;
521              
522             }
523              
524             sub modifyDN {
525 3     3   5 my ( $self, $reqData, $reqMsg ) = @_;
526              
527             #warn "modifyDN: " . Data::Dump::dump \@_;
528             #warn "modifyDN: " . Data::Dump::dump($reqData);
529             #warn "existing: " . Data::Dump::dump( \%Data );
530             #warn "existing DNs: " . Data::Dump::dump([keys %Data]);
531              
532 3         6 my $oldkey = $reqData->{entry};
533 3         5 my $newkey = $reqData->{newrdn};
534 3 50       6 if ( defined $reqData->{newSuperior} ) {
535 0         0 $newkey .= ',' . $reqData->{newSuperior};
536             }
537             else {
538             # As we only have the new relative DN, we still
539             # need the base for it. We'll take it from $oldkey
540 3         26 my $exploded_dn = ldap_explode_dn( $oldkey, casefold => 'none' );
541 3         393 shift @$exploded_dn;
542 3         12 $newkey .= ',' . canonical_dn( $exploded_dn, casefold => 'none' );
543             }
544              
545 3 100       94 if ( !exists $Data{$oldkey} ) {
546 1         2 return RESULT_NO_SUCH_OBJECT;
547             }
548 2 100       5 if ( exists $Data{$newkey} ) {
549 1         2 return RESULT_ALREADY_EXISTS;
550             }
551 1         2 my $entry = $Data{$oldkey};
552 1         4 my $newentry = $entry->clone;
553 1         81 $newentry->dn($newkey);
554 1         4 $Data{$newkey} = $newentry;
555              
556             #warn "created new entry: $newkey";
557 1 50       4 if ( $reqData->{deleteoldrdn} ) {
558 1         2 delete $Data{$oldkey};
559              
560             #warn "deleted old entry: $oldkey";
561             }
562              
563 1         4 return RESULT_OK;
564             }
565              
566             sub compare {
567 0     0   0 my ( $self, $reqData, $reqMsg ) = @_;
568              
569             #warn "compare: " . Data::Dump::dump \@_;
570              
571 0         0 return RESULT_OK;
572             }
573              
574             sub abandon {
575 0     0   0 my ( $self, $reqData, $reqMsg ) = @_;
576              
577             #warn "abandon: " . Data::Dump::dump \@_;
578              
579 0         0 return RESULT_OK;
580             }
581              
582             my $token_counter = 100;
583             my $sid_str = 'S-1-2-3-4-5-6-1234';
584              
585 0     0   0 sub _get_server_sid_string { return $sid_str }
586              
587             sub _add_AD {
588 0     0   0 my ( $server, $reqData, $reqMsg, $key, $entry, $data ) = @_;
589              
590 0         0 for my $attr ( @{ $reqData->{attributes} } ) {
  0         0  
591 0 0       0 if ( $attr->{type} eq 'objectClass' ) {
592 0 0       0 if ( grep { $_ eq 'group' } @{ $attr->{vals} } ) {
  0         0  
  0         0  
593              
594             # groups
595 0         0 $token_counter++;
596 0         0 ( my $group_sid_str = _get_server_sid_string() )
597             =~ s/-1234$/-$token_counter/;
598 0 0       0 if ( $ENV{LDAP_DEBUG} ) {
599 0         0 carp "group_sid_str = $group_sid_str";
600             }
601 0         0 $entry->add( 'primaryGroupToken' => $token_counter );
602 0         0 $entry->add( 'objectSID' => "$group_sid_str" );
603 0         0 $entry->add( 'distinguishedName' => $key );
604              
605             }
606             else {
607              
608             # users
609 0         0 my $gid = $entry->get_value('primaryGroupID');
610 0 0       0 $gid = '1234' unless ( defined $gid );
611 0         0 ( my $user_sid_str = _get_server_sid_string() )
612             =~ s/-1234$/-$gid/;
613              
614 0         0 my $user_sid = Net::LDAP::SID->new($user_sid_str);
615 0         0 $entry->add( 'objectSID' => $user_sid->as_binary );
616 0         0 $entry->add( 'distinguishedName' => $key );
617              
618             }
619             }
620              
621             }
622              
623 0         0 _update_groups($data);
624              
625             #dump $reqData;
626             #dump $data;
627              
628             }
629              
630             # AD stores group assignments in 'member' attribute
631             # of each group. 'memberOf' is linked internally to that
632             # attribute. We set 'memberOf' here if mimicing AD.
633             sub _update_groups {
634 0     0   0 my $data = shift;
635              
636             # all groups
637 0         0 for my $key ( keys %$data ) {
638 0         0 my $entry = $data->{$key};
639              
640             #warn "groups: update groups for $key";
641 0 0       0 if ( !$entry->get_value('sAMAccountName') ) {
642              
643             #dump $entry;
644              
645             # group entry.
646             # are the users listed in member
647             # still assigned in their memberOf?
648 0         0 my %users = map { $_ => 1 } $entry->get_value('member');
  0         0  
649 0         0 for my $dn ( keys %users ) {
650              
651             #warn "User $dn is a member in $key";
652 0         0 my $user = $data->{$dn};
653 0         0 my %groups = map { $_ => 1 } $user->get_value('memberOf');
  0         0  
654              
655             # if $user does not list $key (group) as a memberOf,
656             # then add it.
657 0 0 0     0 if ( !exists $groups{$key} && exists $users{$dn} ) {
658 0         0 $groups{$key}++;
659 0         0 $user->replace( memberOf => [ keys %groups ] );
660             }
661             }
662              
663             }
664              
665             }
666              
667             # all users
668              
669 0         0 for my $key ( keys %$data ) {
670 0         0 my $entry = $data->{$key};
671              
672             #warn "users: update groups for $key";
673 0 0       0 if ( $entry->get_value('sAMAccountName') ) {
674              
675             #dump $entry;
676              
677             # user entry
678             # get its groups and add this user to each of them.
679 0         0 my %groups = map { $_ => 1 } $entry->get_value('memberOf');
  0         0  
680 0         0 for my $dn ( keys %groups ) {
681 0         0 my $group = $data->{$dn};
682             my %users
683 0         0 = map { $_ => 1 } ( $group->get_value('member') );
  0         0  
684              
685             # if group no longer lists this user as a member,
686             # remove group from memberOf
687 0 0       0 if ( !exists $users{$key} ) {
688 0         0 delete $groups{$dn};
689 0         0 $entry->replace( memberOf => [ keys %groups ] );
690             }
691             }
692              
693             }
694             }
695              
696             }
697              
698             sub _modify_AD {
699 0     0   0 my ( $server, $reqData, $reqMsg, $data ) = @_;
700              
701             #dump $data;
702 0         0 _update_groups($data);
703              
704             #Data::Dump::dump $data;
705              
706             }
707              
708             # override the default behaviour to support controls
709             sub handle {
710 50     50   79 my $self = shift;
711 50         63 my $socket;
712              
713             #warn "$Net::LDAP::Server::VERSION";
714 50 50       159 if ( $Net::LDAP::Server::VERSION ge '0.43' ) {
715 50         126 $socket = $self->{in};
716             }
717             else {
718 0         0 $socket = $self->{socket};
719             }
720              
721 50         243 asn_read( $socket, my $pdu );
722              
723             #print '-' x 80,"\n";
724             #print "Received:\n";
725             #Convert::ASN1::asn_dump(\*STDOUT,$pdu);
726 50         2229 my $request = $LDAPRequest->decode($pdu);
727 50 100       15868 my $mid = $request->{'messageID'}
728             or return 1;
729              
730             #print "messageID: $mid\n";
731             #use Data::Dumper; print Dumper($request);
732              
733 48         57 my $reqType;
734 48         129 foreach my $type (@Net::LDAP::Server::reqTypes) {
735 305 100       511 if ( defined $request->{$type} ) {
736 48         67 $reqType = $type;
737 48         68 last;
738             }
739             }
740 48 100       278 my $respType = $Net::LDAP::Server::respTypes{$reqType}
741             or
742             return 1; # if no response type is present hangup the connection
743              
744 43         53 my $reqData = $request->{$reqType};
745              
746             # here we can do something with the request of type $reqType
747 43         144 my $method = $Net::LDAP::Server::functions{$reqType};
748 43         55 my ( $result, $controls );
749 43 50       315 if ( $self->can($method) ) {
750 43 100       93 if ( $method eq 'search' ) {
751 17         31 my @entries;
752 17         37 eval {
753 17         68 ( $result, @entries )
754             = $self->search( $reqData, $request );
755 17 100       79 if ( ref( $entries[0] ) eq 'ARRAY' ) {
756 11         16 $controls = pop(@entries);
757 11         13 @entries = @{ shift(@entries) };
  11         25  
758              
759             #warn "got controls";
760             }
761             };
762              
763             # rethrow
764 17 50       138 if ($@) {
765 0         0 croak $@;
766             }
767              
768 17         181 foreach my $entry (@entries) {
769 38         37 my $data;
770              
771             # default is to return a searchResEntry
772 38         71 my $sResType = 'searchResEntry';
773 38 50       96 if ( ref $entry eq 'Net::LDAP::Entry' ) {
    0          
774 38         54 $data = $entry->{'asn'};
775             }
776             elsif ( ref $entry eq 'Net::LDAP::Reference' ) {
777 0         0 $data = $entry->{'asn'};
778 0         0 $sResType = 'searchResRef';
779             }
780             else {
781 0         0 $data = $entry;
782             }
783              
784 38         39 my $response;
785              
786             # is the full message specified?
787 38 50       73 if ( defined $data->{'protocolOp'} ) {
788 0         0 $response = $data;
789 0         0 $response->{'messageID'} = $mid;
790             }
791             else {
792 38         126 $response = {
793             'messageID' => $mid,
794             'protocolOp' => { $sResType => $data },
795             };
796             }
797 38         119 my $pdu = $LDAPResponse->encode($response);
798 38 50       10607 if ($pdu) {
799 38         41 print {$socket} $pdu;
  38         674  
800             }
801             else {
802 0         0 $result = undef;
803 0         0 last;
804             }
805             }
806             }
807             else {
808 26         32 eval { $result = $self->$method( $reqData, $request ) };
  26         91  
809             }
810 43 50       167 $result = Net::LDAP::Server::_operations_error() unless $result;
811             }
812             else {
813 0         0 $result = {
814             'matchedDN' => '',
815             'errorMessage' => sprintf(
816             "%s operation is not supported by %s",
817             $method, ref $self
818             ),
819             'resultCode' => LDAP_UNWILLING_TO_PERFORM
820             };
821             }
822              
823             # and now send the result to the client
824 43         43 print {$socket} _encode_result( $mid, $respType, $result, $controls );
  43         109  
825              
826 43         264 return 0;
827             }
828              
829             sub _encode_result {
830 43     43   75 my ( $mid, $respType, $result, $controls ) = @_;
831              
832 43         160 my $response = {
833             'messageID' => $mid,
834             'protocolOp' => { $respType => $result },
835             };
836 43 50       101 if ( defined $controls ) {
837 0         0 $response->{'controls'} = $controls;
838             }
839              
840             #warn "response: " . Data::Dump::dump($response) . "\n";
841              
842 43         145 my $pdu = $LDAPResponse->encode($response);
843              
844             # if response encoding failed return the error
845 43 50       6882 if ( !$pdu ) {
846 0         0 $response->{'protocolOp'}->{$respType}
847             = Net::LDAP::Server::_operations_error();
848 0         0 delete $response->{'controls'}; # just in case
849 0         0 $pdu = $LDAPResponse->encode($response);
850             }
851              
852 43         987 return $pdu;
853             }
854              
855             } # end MyLDAPServer
856              
857             =head2 new( I, I )
858              
859             Create a new server. Basically this just fork()s a child process
860             listing on I and handling requests using Net::LDAP::Server.
861              
862             I defaults to 10636.
863              
864             I may be an IO::Socket object listening to a local port.
865              
866             I may be:
867              
868             =over
869              
870             =item data
871              
872             I is optional data to return from the Net::LDAP search() function.
873             Typically it would be an array ref of Net::LDAP::Entry objects.
874              
875             =item auto_schema
876              
877             A true value means the add(), modify() and delete() methods will
878             store internal in-memory data based on DN values, so that search()
879             will mimic working on a real LDAP schema.
880              
881             =item active_directory
882              
883             Work in Active Directory mode. This means that entries are automatically
884             assigned a objectSID, and some effort is made to mimic the member/memberOf
885             linking between AD Users and Groups.
886              
887             =back
888              
889             new() will croak() if there was a problem fork()ing a new server.
890              
891             Returns a Net::LDAP::Server::Test object, which is just a
892             blessed reference to the PID of the forked server.
893              
894             =cut
895              
896             my %PORTS; # inside-out tracking of port-per-server
897              
898             # this snippet matches what Net::LDAP does:
899             # check for IPv6 support: prefer IO::Socket::IP 0.20+ over IO::Socket::INET6
900 15         29 use constant CAN_IPV6 => do {
901 15         44 local $SIG{__DIE__};
902              
903 15 50       17 eval { require IO::Socket::INET6; }
  15         7034  
904             ? 'IO::Socket::INET6'
905             : '';
906 15     15   93 };
  15         17  
907              
908             sub new {
909 16     16 1 4611 my $class = shift;
910 16   50     65 my $port = shift || 10636;
911 16         49 my %arg = @_;
912              
913 16 50 66     68 if ( $arg{data} and $arg{auto_schema} ) {
914 0         0 croak
915             "cannot handle both 'data' and 'auto_schema' features. Pick one.";
916             }
917              
918 16         374 pipe( my $r_fh, my $w_fh );
919              
920 16         14775 my $pid = fork();
921              
922 16 50       795 if ( !defined $pid ) {
    100          
923 0         0 croak "can't fork a LDAP test server: $!";
924             }
925             elsif ( $pid == 0 ) {
926              
927             warn "Creating new LDAP server on port "
928             . ( ref $port ? $port->sockport : $port )
929             . " ... \n"
930 7 0       322 if $ENV{LDAP_DEBUG};
    50          
931              
932             # the child (server)
933 7         214 my $class = _io_socket_class();
934 7 50       565 my $sock = ref $port ? $port : $class->new(
    50          
935             Listen => 5,
936             Proto => 'tcp',
937             Reuse => 1,
938             LocalPort => $port
939             ) or die "Unable to listen on port $port: $! [$@]";
940              
941             # tickle the pipe to show we've opened ok
942 7         9883 syswrite $w_fh, "Ready\n";
943 7         184 undef $w_fh;
944              
945 7         237 my $sel = IO::Select->new($sock);
946 7         808 my %Handlers;
947 7         63 while ( my @ready = $sel->can_read ) {
948 57         581618 foreach my $fh (@ready) {
949 57 100       225 if ( $fh == $sock ) {
950              
951             # let's create a new socket
952 7         58 my $psock = $sock->accept;
953 7         1261 $sel->add($psock);
954 7         497 $Handlers{*$psock} = MyLDAPServer->new( $psock, %arg );
955              
956             #warn "new socket created";
957             }
958             else {
959 50         434 my $result = $Handlers{*$fh}->handle;
960 50 100       319 if ($result) {
961              
962             # we have finished with the socket
963 7         70 $sel->remove($fh);
964 7         418 $fh->close;
965 7         957 delete $Handlers{*$fh};
966              
967             # if there are no open connections,
968             # exit the child process.
969 7 50       49 if ( !keys %Handlers ) {
970             warn " ... shutting down server\n"
971 7 50       38 if $ENV{LDAP_DEBUG};
972 7         1228 exit(0);
973             }
974             }
975             }
976             }
977             }
978              
979             # if we get here, we had some kinda problem.
980 0         0 croak "reached the end of while() loop prematurely";
981              
982             }
983             else {
984              
985             # this is the child
986 9 50       339 warn "child pid=$pid" if $ENV{LDAP_DEBUG};
987              
988 9 50       14362 return unless <$r_fh> =~ /Ready/; # newline varies
989 9         120 close($r_fh);
990 9         198 my $self = bless( \$pid, $class );
991 9         265 $PORTS{"$self"} = $port;
992 9         644 return $self;
993             }
994              
995             }
996              
997             =head2 stop
998              
999             Calls waitpid() on the server's associated child process.
1000             You may find it helpful to call this method explicitly,
1001             especially if you are creating multiple
1002             servers in the same test. Otherwise, this method is typically not
1003             needed and may even cause your tests to hang indefinitely if
1004             they die prematurely. YMMV.
1005              
1006             To prevent waitpid() from blocking and hanging your test server,
1007             it is wrapped in an alarm() call, which will wait 2 seconds
1008             and then call kill() on the reluctant pid. You have been warned.
1009              
1010             =cut
1011              
1012             sub stop {
1013 3     3 1 142722 my $server = shift;
1014 3         16 my $pid = $$server;
1015 3 50       16 warn "\$pid = $pid" if $ENV{LDAP_DEBUG};
1016 3         10 eval {
1017             local $SIG{ALRM}
1018 3     1   98 = sub { die "waitpid($pid, 0) took too long\n" }; # NB: \n required
  1         34  
1019 3         16 alarm 2;
1020 3         3222471 my $ret = waitpid( $pid, 0 );
1021 2 50       34 warn "waitpid returned $ret" if $ENV{LDAP_DEBUG};
1022 2         78 alarm 0;
1023             };
1024 3 100       47 if ($@) {
1025 1         131 warn "$@";
1026 1         32 my $cnt = kill( 9, $pid );
1027 1         577 warn "kill(9,$pid) returned $cnt\n";
1028             }
1029             else {
1030 2 50       12 warn "waitpid($pid, 0) worked" if $ENV{LDAP_DEBUG};
1031             }
1032 3         14 my $tries = 0;
1033 3         26 while ( $server->port_is_open() ) {
1034 0         0 warn "Waiting for port to close...\n";
1035 0         0 sleep(1);
1036 0 0       0 if ( $tries++ > 10 ) {
1037 0         0 warn "Failed to determine that port closed. Giving up.\n";
1038 0         0 last;
1039             }
1040             }
1041 3         2844 return $pid;
1042             }
1043              
1044             =head2 port_is_open
1045              
1046             Returns an IO::Socket (or subclass) for the current server port.
1047             If the port is already in use, this is a false value.
1048              
1049             =cut
1050              
1051             sub port_is_open {
1052 3     3 1 5 my $self = shift;
1053 3   33     48 my $port = shift || $PORTS{"$self"};
1054              
1055 3         16 return _io_socket_class()->new(
1056             PeerAddr => 'localhost',
1057             PeerPort => $port,
1058             Proto => 'tcp',
1059             Type => SOCK_STREAM,
1060             );
1061             }
1062              
1063             sub _io_socket_class {
1064 10     10   247 return CAN_IPV6 ? CAN_IPV6 : 'IO::Socket::INET';
1065             }
1066              
1067             =head1 AUTHOR
1068              
1069             Peter Karman, C<< >>
1070              
1071             =head1 BUGS
1072              
1073             Please report any bugs or feature requests to
1074             C, or through the web interface at
1075             L.
1076             I will be notified, and then you'll automatically be notified of progress on
1077             your bug as I make changes.
1078              
1079             =head1 SUPPORT
1080              
1081             You can find documentation for this module with the perldoc command.
1082              
1083             perldoc Net::LDAP::Server::Test
1084              
1085             You can also look for information at:
1086              
1087             =over 4
1088              
1089             =item * AnnoCPAN: Annotated CPAN documentation
1090              
1091             L
1092              
1093             =item * CPAN Ratings
1094              
1095             L
1096              
1097             =item * RT: CPAN's request tracker
1098              
1099             L
1100              
1101             =item * Search CPAN
1102              
1103             L
1104              
1105             =back
1106              
1107             =head1 ACKNOWLEDGEMENTS
1108              
1109             The Minnesota Supercomputing Institute C<< http://www.msi.umn.edu/ >>
1110             sponsored the development of this software.
1111              
1112             =head1 COPYRIGHT & LICENSE
1113              
1114             Copyright 2007 by the Regents of the University of Minnesota.
1115              
1116             This program is free software; you can redistribute it and/or modify it
1117             under the same terms as Perl itself.
1118              
1119             =head1 SEE ALSO
1120              
1121             Net::LDAP::Server
1122              
1123             =cut
1124              
1125             1;