File Coverage

blib/lib/Net/LDAP/Server/Test.pm
Criterion Covered Total %
statement 315 461 68.3
branch 100 194 51.5
condition 9 20 45.0
subroutine 40 48 83.3
pod 3 3 100.0
total 467 726 64.3


line stmt bran cond sub pod time code
1             package Net::LDAP::Server::Test;
2              
3 15     15   1315706 use warnings;
  15         25  
  15         422  
4 15     15   50 use strict;
  15         14  
  15         215  
5 15     15   41 use Carp;
  15         20  
  15         681  
6 15     15   1992 use IO::Select;
  15         5553  
  15         420  
7 15     15   2572 use IO::Socket;
  15         79841  
  15         68  
8 15     15   12844 use Data::Dump ();
  15         77741  
  15         293  
9 15     15   6311 use Net::LDAP::SID;
  15         6261  
  15         691  
10              
11             our $VERSION = '0.20';
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   64 use strict;
  15         22  
  15         208  
51 15     15   42 use warnings;
  15         18  
  15         416  
52 15     15   148 use Carp;
  15         19  
  15         758  
53 15         1460 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   2426 );
  15         18812  
63 15     15   35117 use Net::LDAP::Util qw(ldap_explode_dn canonical_dn);
  15         684  
  15         826  
64 15     15   5659 use Net::LDAP::Entry;
  15         415377  
  15         363  
65 15     15   7123 use Net::LDAP::Filter;
  15         29130  
  15         438  
66 15     15   6903 use Net::LDAP::FilterMatch;
  15         63795  
  15         102  
67 15     15   51113 use Net::LDAP::Control;
  15         10479  
  15         494  
68 15     15   84 use Net::LDAP::ASN qw(LDAPRequest LDAPResponse);
  15         23  
  15         93  
69 15     15   909 use Convert::ASN1 qw(asn_read);
  15         15  
  15         582  
70              
71 15     15   58 use base 'Net::LDAP::Server';
  15         17  
  15         6980  
72 15     15   105840 use fields qw( _flags );
  15         22  
  15         49  
73              
74 15         853 use constant RESULT_OK => {
75             'matchedDN' => '',
76             'errorMessage' => '',
77             'resultCode' => LDAP_SUCCESS
78 15     15   683 };
  15         24  
79              
80 15         703 use constant RESULT_NO_SUCH_OBJECT => {
81             'matchedDN' => '',
82             'errorMessage' => '',
83             'resultCode' => LDAP_NO_SUCH_OBJECT,
84 15     15   51 };
  15         21  
85              
86 15         672 use constant RESULT_ALREADY_EXISTS => {
87             'matchedDN' => '',
88             'errorMessage' => '',
89             'resultCode' => LDAP_ALREADY_EXISTS,
90 15     15   60 };
  15         17  
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   53 };
  15         35  
97              
98 15         46286 use constant RESULT_NO_SUCH_ATTRIBUTE => {
99             'matchedDN' => '',
100             'errorMessage' => '',
101             'resultCode' => LDAP_NO_SUCH_ATTRIBUTE,
102 15     15   49 };
  15         13  
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   38 my ( $class, $sock, %args ) = @_;
112 7         231 my $self = $class->SUPER::new($sock);
113             warn sprintf "Accepted connection from: %s\n", $sock->peerhost()
114 7 50       26964 if $ENV{LDAP_DEBUG};
115 7         15 $self->{_flags} = \%args;
116 7         102 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   11 my $self = shift;
128 5         13 my $reqData = shift;
129 5         18 return RESULT_OK;
130             }
131              
132             # the search operation
133             sub search {
134 17     17   26 my $self = shift;
135              
136 17 100       72 if ( defined $self->{_flags}->{data} ) {
    100          
137 1         4 return $self->_search_user_supplied_data(@_);
138             }
139             elsif ( defined $self->{_flags}->{auto_schema} ) {
140 14         59 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         2 return RESULT_OK, @{ $self->{_flags}->{data} };
  1         4  
154             }
155              
156             sub _search_auto_schema_data {
157 14     14   34 my ( $self, $reqData, $reqMsg ) = @_;
158              
159             #warn 'SEARCH SCHEMA: ' . Data::Dump::dump \@_;
160              
161 14         23 my @results;
162 14         27 my $base = $reqData->{baseObject};
163              
164             # $reqData->{scope} is a enum but we want a word
165             my $scope
166 14 50       72 = $Scopes[ defined $reqData->{scope} ? $reqData->{scope} : 2 ];
167 14 50       21 my @attrs = @{ $reqData->{attributes} || [] };
  14         57  
168 14         42 my @filters = ();
169              
170 14 50       38 if ( exists $reqData->{filter} ) {
171             push( @filters,
172 14         61 bless( $reqData->{filter}, 'Net::LDAP::Filter' ) );
173              
174             }
175              
176 14 50       58 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         19 my ( $page_size, $cookie, $controls, $offset );
184 14 50       38 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         112 ENTRY: for my $dn ( sort keys %Data ) {
228              
229 32 100       274 next unless $dn =~ m/$base$/;
230              
231 24 100       64 if ( $scope eq 'base' ) {
    100          
232 12 100       26 next unless $dn eq $base;
233             }
234             elsif ( $scope eq 'one' ) {
235 6         9 my $dn_depth = scalar @{ ldap_explode_dn($dn) };
  6         27  
236 6         707 my $base_depth = scalar @{ ldap_explode_dn($base) };
  6         11  
237              
238             # We're guaranteed to be at or under $base thanks to the m// above
239 6 100       340 next unless $dn_depth == $base_depth + 1;
240             }
241              
242 16         24 my $entry = $Data{$dn};
243              
244 16 50       37 if ( $ENV{LDAP_DEBUG} ) {
245 0         0 warn "trying to match $dn : " . Data::Dump::dump $entry;
246             }
247              
248 16         22 my $match = 0;
249 16         23 for my $filter (@filters) {
250              
251 16 100       117 if ( $filter->match($entry) ) {
252              
253             #warn "$f matches entry $dn";
254 11         1324 $match++;
255             }
256             }
257              
258             #warn "matched $match";
259 16 100       705 if ( $match == scalar(@filters) ) {
260              
261             # clone the entry so that client cannot modify %Data
262 11         147 my $result = $entry->clone;
263              
264             # filter returned attributes to those requested
265 11 50       905 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         91 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         130 my $total_found = scalar(@results);
284 14 50       40 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     91 if ( $scope eq 'base' and $total_found == 0 ) {
295 3         13 return RESULT_NO_SUCH_OBJECT;
296             }
297              
298 11 50 33     88 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   3 my ( $self, $reqData ) = @_;
354              
355             #warn 'SEARCH DEFAULT: ' . Data::Dump::dump \@_;
356              
357 2         5 my $base = $reqData->{'baseObject'};
358              
359             # plain die if dn contains 'dying'
360 2 50       7 die("panic") if $base =~ /dying/;
361              
362             # return a correct LDAPresult, but an invalid entry
363 2 50       10 return RESULT_OK, { test => 1 } if $base =~ /invalid entry/;
364              
365             # return an invalid LDAPresult
366 2 50       6 return { test => 1 } if $base =~ /invalid result/;
367              
368 2         3 my @entries;
369 2 50       6 if ( $reqData->{'scope'} ) {
370              
371             # onelevel or subtree
372 2         15 for ( my $i = 1; $i < 11; $i++ ) {
373 20         29 my $dn = "ou=test $i,$base";
374 20         68 my $entry = Net::LDAP::Entry->new;
375 20         184 $entry->dn($dn);
376 20         113 $entry->add(
377             dn => $dn,
378             sn => 'value1',
379             cn => [qw(value1 value2)]
380             );
381 20         482 push @entries, $entry;
382             }
383              
384 2         6 my $entry1 = Net::LDAP::Entry->new;
385 2         17 $entry1->dn("cn=dying entry,$base");
386 2         14 $entry1->add(
387             cn => 'dying entry',
388             description =>
389             'This entry will result in a dying error when queried'
390             );
391 2         38 push @entries, $entry1;
392              
393 2         5 my $entry2 = Net::LDAP::Entry->new;
394 2         16 $entry2->dn("cn=invalid entry,$base");
395 2         8 $entry2->add(
396             cn => 'invalid entry',
397             description =>
398             'This entry will result in ASN1 error when queried'
399             );
400 2         35 push( @entries, $entry2 );
401              
402 2         8 my $entry3 = Net::LDAP::Entry->new;
403 2         30 $entry3->dn("cn=invalid result,$base");
404 2         14 $entry3->add(
405             cn => 'invalid result',
406             description =>
407             'This entry will result in ASN1 error when queried'
408             );
409 2         41 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         11 return RESULT_OK, @entries;
424             }
425              
426             sub add {
427 10     10   19 my ( $self, $reqData, $reqMsg ) = @_;
428              
429 10         19 my $key = $reqData->{objectName};
430 10 50       32 if ( $ENV{LDAP_DEBUG} ) {
431 0         0 warn 'ADD: ' . Data::Dump::dump \@_;
432 0         0 warn "key: $key";
433             }
434              
435 10 100       29 if ( exists $Data{$key} ) {
436 1         2 return RESULT_ALREADY_EXISTS;
437             }
438              
439 9         81 my $entry = Net::LDAP::Entry->new;
440 9         131 $entry->dn($key);
441 9         56 for my $attr ( @{ $reqData->{attributes} } ) {
  9         41  
442 18         225 $entry->add( $attr->{type} => \@{ $attr->{vals} } );
  18         58  
443             }
444              
445 9         152 $Data{$key} = $entry;
446              
447 9 50       35 if ( exists $self->{_flags}->{active_directory} ) {
448 0         0 $self->_add_AD( $reqData, $reqMsg, $key, $entry, \%Data );
449             }
450              
451 9         22 return RESULT_OK;
452             }
453              
454             sub modify {
455 8     8   14 my ( $self, $reqData, $reqMsg ) = @_;
456              
457 8 50       25 if ( $ENV{LDAP_DEBUG} ) {
458 0         0 warn 'MODIFY: ' . Data::Dump::dump \@_;
459             }
460              
461 8         12 my $key = $reqData->{object};
462 8 100       21 if ( !exists $Data{$key} ) {
463 1         3 return RESULT_NO_SUCH_OBJECT;
464             }
465              
466 7         8 my @mods = @{ $reqData->{modification} };
  7         19  
467 7         24 for my $mod (@mods) {
468 7         11 my $attr = $mod->{modification}->{type};
469 7         11 my $vals = $mod->{modification}->{vals};
470 7         8 my $entry = $Data{$key};
471              
472 7         40 my $current_value = $entry->get_value( $attr, asref => 1 );
473              
474 7 100       91 if ( $mod->{operation} == 0 ) {
    100          
    50          
475 3 100       9 if ( defined $current_value ) {
476 2         5 for my $v (@$current_value) {
477 2 100       6 if ( grep { $_ eq $v } @$vals ) {
  2         11  
478 1         3 return RESULT_TYPE_OR_VALUE_EXISTS;
479             }
480             }
481             }
482              
483 2         7 $entry->add( $attr => $vals );
484             }
485             elsif ( $mod->{operation} == 1 ) {
486 3 100       17 if ( !defined $current_value ) {
487 1         5 return RESULT_NO_SUCH_ATTRIBUTE;
488             }
489 2         18 $entry->delete( $attr => $vals );
490             }
491             elsif ( $mod->{operation} == 2 ) {
492 1         5 $entry->replace( $attr => $vals );
493             }
494             else {
495 0         0 croak "unknown modify operation: $mod->{operation}";
496             }
497             }
498              
499 5 50       121 if ( $self->{_flags}->{active_directory} ) {
500 0         0 $self->_modify_AD( $reqData, $reqMsg, \%Data );
501             }
502              
503 5         10 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   4 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         4 my $newkey = $reqData->{newrdn};
534 3 50       8 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         21 my $exploded_dn = ldap_explode_dn( $oldkey, casefold => 'none' );
541 3         490 shift @$exploded_dn;
542 3         26 $newkey .= ',' . canonical_dn( $exploded_dn, casefold => 'none' );
543             }
544              
545 3 100       116 if ( !exists $Data{$oldkey} ) {
546 1         4 return RESULT_NO_SUCH_OBJECT;
547             }
548 2 100       6 if ( exists $Data{$newkey} ) {
549 1         2 return RESULT_ALREADY_EXISTS;
550             }
551 1         6 my $entry = $Data{$oldkey};
552 1         7 my $newentry = $entry->clone;
553 1         119 $newentry->dn($newkey);
554 1         5 $Data{$newkey} = $newentry;
555              
556             #warn "created new entry: $newkey";
557 1 50       5 if ( $reqData->{deleteoldrdn} ) {
558 1         3 delete $Data{$oldkey};
559              
560             #warn "deleted old entry: $oldkey";
561             }
562              
563 1         6 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   73 my $self = shift;
711 50         42 my $socket;
712              
713             #warn "$Net::LDAP::Server::VERSION";
714 50 50       130 if ( $Net::LDAP::Server::VERSION ge '0.43' ) {
715 50         150 $socket = $self->{in};
716             }
717             else {
718 0         0 $socket = $self->{socket};
719             }
720              
721 50         217 asn_read( $socket, my $pdu );
722              
723             #print '-' x 80,"\n";
724             #print "Received:\n";
725             #Convert::ASN1::asn_dump(\*STDOUT,$pdu);
726 50         1777 my $request = $LDAPRequest->decode($pdu);
727 50 100       15447 my $mid = $request->{'messageID'}
728             or return 1;
729              
730             #print "messageID: $mid\n";
731             #use Data::Dumper; print Dumper($request);
732              
733 48         50 my $reqType;
734 48         130 foreach my $type (@Net::LDAP::Server::reqTypes) {
735 222 100       333 if ( defined $request->{$type} ) {
736 48         55 $reqType = $type;
737 48         56 last;
738             }
739             }
740 48 100       246 my $respType = $Net::LDAP::Server::respTypes{$reqType}
741             or
742             return 1; # if no response type is present hangup the connection
743              
744 43         51 my $reqData = $request->{$reqType};
745              
746             # here we can do something with the request of type $reqType
747 43         112 my $method = $Net::LDAP::Server::functions{$reqType};
748 43         48 my ( $result, $controls );
749 43 50       274 if ( $self->can($method) ) {
750 43 100       107 if ( $method eq 'search' ) {
751 17         33 my @entries;
752 17         25 eval {
753 17         81 ( $result, @entries )
754             = $self->search( $reqData, $request );
755 17 100       65 if ( ref( $entries[0] ) eq 'ARRAY' ) {
756 11         15 $controls = pop(@entries);
757 11         14 @entries = @{ shift(@entries) };
  11         50  
758              
759             #warn "got controls";
760             }
761             };
762              
763             # rethrow
764 17 50       49 if ($@) {
765 0         0 croak $@;
766             }
767              
768 17         34 foreach my $entry (@entries) {
769 38         34 my $data;
770              
771             # default is to return a searchResEntry
772 38         63 my $sResType = 'searchResEntry';
773 38 50       76 if ( ref $entry eq 'Net::LDAP::Entry' ) {
    0          
774 38         43 $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         35 my $response;
785              
786             # is the full message specified?
787 38 50       71 if ( defined $data->{'protocolOp'} ) {
788 0         0 $response = $data;
789 0         0 $response->{'messageID'} = $mid;
790             }
791             else {
792 38         166 $response = {
793             'messageID' => $mid,
794             'protocolOp' => { $sResType => $data },
795             };
796             }
797 38         207 my $pdu = $LDAPResponse->encode($response);
798 38 50       9204 if ($pdu) {
799 38         36 print {$socket} $pdu;
  38         569  
800             }
801             else {
802 0         0 $result = undef;
803 0         0 last;
804             }
805             }
806             }
807             else {
808 26         43 eval { $result = $self->$method( $reqData, $request ) };
  26         93  
809             }
810 43 50       171 $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         41 print {$socket} _encode_result( $mid, $respType, $result, $controls );
  43         98  
825              
826 43         273 return 0;
827             }
828              
829             sub _encode_result {
830 43     43   59 my ( $mid, $respType, $result, $controls ) = @_;
831              
832 43         154 my $response = {
833             'messageID' => $mid,
834             'protocolOp' => { $respType => $result },
835             };
836 43 50       95 if ( defined $controls ) {
837 0         0 $response->{'controls'} = $controls;
838             }
839              
840             #warn "response: " . Data::Dump::dump($response) . "\n";
841              
842 43         144 my $pdu = $LDAPResponse->encode($response);
843              
844             # if response encoding failed return the error
845 43 50       6888 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         1016 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::INET 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             sub new {
899 16     16 1 4577 my $class = shift;
900 16   50     55 my $port = shift || 10636;
901 16         50 my %arg = @_;
902              
903 16 50 66     70 if ( $arg{data} and $arg{auto_schema} ) {
904 0         0 croak
905             "cannot handle both 'data' and 'auto_schema' features. Pick one.";
906             }
907              
908 16         293 pipe( my $r_fh, my $w_fh );
909              
910 16         13533 my $pid = fork();
911              
912 16 50       752 if ( !defined $pid ) {
    100          
913 0         0 croak "can't fork a LDAP test server: $!";
914             }
915             elsif ( $pid == 0 ) {
916              
917             warn "Creating new LDAP server on port "
918             . ( ref $port ? $port->sockport : $port )
919             . " ... \n"
920 7 0       283 if $ENV{LDAP_DEBUG};
    50          
921              
922             # the child (server)
923 7 50       639 my $sock = ref $port ? $port : IO::Socket::INET->new(
    50          
924             Listen => 5,
925             Proto => 'tcp',
926             Reuse => 1,
927             LocalPort => $port
928             ) or die "Unable to listen on port $port: $!";
929              
930             # tickle the pipe to show we've opened ok
931 7         5769 syswrite $w_fh, "Ready\n";
932 7         172 undef $w_fh;
933              
934 7         166 my $sel = IO::Select->new($sock);
935 7         638 my %Handlers;
936 7         50 while ( my @ready = $sel->can_read ) {
937 57         563903 foreach my $fh (@ready) {
938 57 100       188 if ( $fh == $sock ) {
939              
940             # let's create a new socket
941 7         84 my $psock = $sock->accept;
942 7         767 $sel->add($psock);
943 7         380 $Handlers{*$psock} = MyLDAPServer->new( $psock, %arg );
944              
945             #warn "new socket created";
946             }
947             else {
948 50         359 my $result = $Handlers{*$fh}->handle;
949 50 100       299 if ($result) {
950              
951             # we have finished with the socket
952 7         53 $sel->remove($fh);
953 7         358 $fh->close;
954 7         625 delete $Handlers{*$fh};
955              
956             # if there are no open connections,
957             # exit the child process.
958 7 50       41 if ( !keys %Handlers ) {
959             warn " ... shutting down server\n"
960 7 50       30 if $ENV{LDAP_DEBUG};
961 7         783 exit(0);
962             }
963             }
964             }
965             }
966             }
967              
968             # if we get here, we had some kinda problem.
969 0         0 croak "reached the end of while() loop prematurely";
970              
971             }
972             else {
973              
974             # this is the child
975 9 50       286 warn "child pid=$pid" if $ENV{LDAP_DEBUG};
976              
977 9 50       8584 return unless <$r_fh> =~ /Ready/; # newline varies
978 9         89 close($r_fh);
979 9         175 my $self = bless( \$pid, $class );
980 9         216 $PORTS{"$self"} = $port;
981 9         569 return $self;
982             }
983              
984             }
985              
986             =head2 stop
987              
988             Calls waitpid() on the server's associated child process.
989             You may find it helpful to call this method explicitly,
990             especially if you are creating multiple
991             servers in the same test. Otherwise, this method is typically not
992             needed and may even cause your tests to hang indefinitely if
993             they die prematurely. YMMV.
994              
995             To prevent waitpid() from blocking and hanging your test server,
996             it is wrapped in an alarm() call, which will wait 2 seconds
997             and then call kill() on the reluctant pid. You have been warned.
998              
999             =cut
1000              
1001             sub stop {
1002 3     3 1 122887 my $server = shift;
1003 3         12 my $pid = $$server;
1004 3 50       43 warn "\$pid = $pid" if $ENV{LDAP_DEBUG};
1005 3         13 eval {
1006             local $SIG{ALRM}
1007 3     1   95 = sub { die "waitpid($pid, 0) took too long\n" }; # NB: \n required
  1         31  
1008 3         18 alarm 2;
1009 3         2645531 my $ret = waitpid( $pid, 0 );
1010 2 50       26 warn "waitpid returned $ret" if $ENV{LDAP_DEBUG};
1011 2         38 alarm 0;
1012             };
1013 3 100       18 if ($@) {
1014 1         110 warn "$@";
1015 1         29 my $cnt = kill( 9, $pid );
1016 1         42 warn "kill(9,$pid) returned $cnt\n";
1017             }
1018             else {
1019 2 50       6 warn "waitpid($pid, 0) worked" if $ENV{LDAP_DEBUG};
1020             }
1021 3         11 my $tries = 0;
1022 3         16 while ( $server->port_is_open() ) {
1023 0         0 warn "Waiting for port to close...\n";
1024 0         0 sleep(1);
1025 0 0       0 if ( $tries++ > 10 ) {
1026 0         0 warn "Failed to determine that port closed. Giving up.\n";
1027 0         0 last;
1028             }
1029             }
1030 3         1587 return $pid;
1031             }
1032              
1033             =head2 port_is_open
1034              
1035             Returns IO::Socket::INET->new for the current server port.
1036             If the port is already in use, this is a false value.
1037              
1038             =cut
1039              
1040             sub port_is_open {
1041 3     3 1 6 my $self = shift;
1042 3         22 my $port = $PORTS{"$self"};
1043 3         58 return IO::Socket::INET->new(
1044             PeerAddr => '127.0.0.1',
1045             PeerPort => $port,
1046             Proto => 'tcp',
1047             Type => SOCK_STREAM,
1048             );
1049             }
1050              
1051             =head1 AUTHOR
1052              
1053             Peter Karman, C<< >>
1054              
1055             =head1 BUGS
1056              
1057             Please report any bugs or feature requests to
1058             C, or through the web interface at
1059             L.
1060             I will be notified, and then you'll automatically be notified of progress on
1061             your bug as I make changes.
1062              
1063             =head1 SUPPORT
1064              
1065             You can find documentation for this module with the perldoc command.
1066              
1067             perldoc Net::LDAP::Server::Test
1068              
1069             You can also look for information at:
1070              
1071             =over 4
1072              
1073             =item * AnnoCPAN: Annotated CPAN documentation
1074              
1075             L
1076              
1077             =item * CPAN Ratings
1078              
1079             L
1080              
1081             =item * RT: CPAN's request tracker
1082              
1083             L
1084              
1085             =item * Search CPAN
1086              
1087             L
1088              
1089             =back
1090              
1091             =head1 ACKNOWLEDGEMENTS
1092              
1093             The Minnesota Supercomputing Institute C<< http://www.msi.umn.edu/ >>
1094             sponsored the development of this software.
1095              
1096             =head1 COPYRIGHT & LICENSE
1097              
1098             Copyright 2007 by the Regents of the University of Minnesota.
1099              
1100             This program is free software; you can redistribute it and/or modify it
1101             under the same terms as Perl itself.
1102              
1103             =head1 SEE ALSO
1104              
1105             Net::LDAP::Server
1106              
1107             =cut
1108              
1109             1;