1 |
dpavlin |
4 |
package LDAP::Virtual; |
2 |
dpavlin |
1 |
|
3 |
|
|
use strict; |
4 |
|
|
use warnings; |
5 |
|
|
|
6 |
|
|
use Net::LDAP::Constant qw( |
7 |
|
|
LDAP_SUCCESS |
8 |
|
|
LDAP_STRONG_AUTH_NOT_SUPPORTED |
9 |
|
|
LDAP_UNAVAILABLE |
10 |
|
|
LDAP_OPERATIONS_ERROR |
11 |
|
|
); |
12 |
|
|
use Net::LDAP::Server; |
13 |
|
|
use Net::LDAP::Filter; |
14 |
|
|
use base qw(Net::LDAP::Server); |
15 |
|
|
use fields qw(upstream); |
16 |
|
|
|
17 |
dpavlin |
2 |
use Net::LDAP; |
18 |
|
|
|
19 |
dpavlin |
1 |
use URI::Escape; # uri_escape |
20 |
|
|
use IO::Socket::INET; |
21 |
|
|
use IO::Select; |
22 |
|
|
|
23 |
|
|
use Data::Dump qw/dump/; |
24 |
|
|
|
25 |
|
|
=head1 NAME |
26 |
|
|
|
27 |
dpavlin |
4 |
LDAP::Virtual |
28 |
dpavlin |
1 |
|
29 |
|
|
=cut |
30 |
|
|
|
31 |
|
|
=head1 DESCRIPTION |
32 |
|
|
|
33 |
dpavlin |
2 |
Provide LDAP server functionality somewhat similar to C<slapo-rwm> |
34 |
dpavlin |
1 |
|
35 |
|
|
=head1 METHODS |
36 |
|
|
|
37 |
|
|
=head2 run |
38 |
|
|
|
39 |
dpavlin |
4 |
my $pid = LDAP::Virtual->run({ port => 1389, fork => 0 }); |
40 |
dpavlin |
1 |
|
41 |
|
|
=cut |
42 |
|
|
|
43 |
|
|
our $pids; |
44 |
|
|
our $cache; |
45 |
|
|
|
46 |
|
|
sub cache { |
47 |
dpavlin |
5 |
return $cache; |
48 |
dpavlin |
1 |
} |
49 |
|
|
|
50 |
|
|
sub run { |
51 |
|
|
my $self = shift; |
52 |
|
|
|
53 |
|
|
my $args = shift; |
54 |
|
|
# default LDAP port |
55 |
|
|
my $port = $args->{port} ||= 1389; |
56 |
|
|
|
57 |
|
|
if ( $args->{fork} ) { |
58 |
|
|
defined(my $pid = fork()) or die "Can't fork: $!"; |
59 |
|
|
if ( $pid ) { |
60 |
|
|
$pids->{ $port } = $pid; |
61 |
|
|
warn "# pids = ",dump( $pids ); |
62 |
|
|
sleep 1; |
63 |
|
|
return $pid; |
64 |
|
|
} |
65 |
|
|
} |
66 |
|
|
|
67 |
|
|
my $sock = IO::Socket::INET->new( |
68 |
|
|
Listen => 5, |
69 |
|
|
Proto => 'tcp', |
70 |
|
|
Reuse => 1, |
71 |
|
|
LocalPort => $port, |
72 |
|
|
) or die "can't listen on port $port: $!\n"; |
73 |
|
|
|
74 |
|
|
warn "LDAP server listening on port $port\n"; |
75 |
|
|
|
76 |
|
|
my $sel = IO::Select->new($sock) or die "can't select socket: $!\n"; |
77 |
|
|
my %Handlers; |
78 |
|
|
while (my @ready = $sel->can_read) { |
79 |
|
|
foreach my $fh (@ready) { |
80 |
|
|
if ($fh == $sock) { |
81 |
|
|
# let's create a new socket |
82 |
|
|
my $psock = $sock->accept; |
83 |
|
|
$sel->add($psock); |
84 |
dpavlin |
4 |
$Handlers{*$psock} = LDAP::Virtual->new($psock); |
85 |
dpavlin |
1 |
} else { |
86 |
|
|
my $result = $Handlers{*$fh}->handle; |
87 |
|
|
if ($result) { |
88 |
|
|
# we have finished with the socket |
89 |
|
|
$sel->remove($fh); |
90 |
|
|
$fh->close; |
91 |
|
|
delete $Handlers{*$fh}; |
92 |
|
|
} |
93 |
|
|
} |
94 |
|
|
} |
95 |
|
|
} |
96 |
|
|
} |
97 |
|
|
|
98 |
|
|
=head2 stop |
99 |
|
|
|
100 |
dpavlin |
4 |
my $stopped_pids = LDAP::Virtual->stop; |
101 |
dpavlin |
1 |
|
102 |
|
|
=cut |
103 |
|
|
|
104 |
|
|
sub stop { |
105 |
|
|
warn "## stop pids = ",dump( $pids ); |
106 |
|
|
return unless $pids; |
107 |
|
|
my $stopped = 0; |
108 |
|
|
foreach my $port ( keys %$pids ) { |
109 |
|
|
my $pid = delete($pids->{$port}) or die "no pid?"; |
110 |
|
|
warn "# Shutdown LDAP server at port $port pid $pid\n"; |
111 |
|
|
kill(9,$pid) or die "can't kill $pid: $!"; |
112 |
|
|
waitpid($pid,0) or die "waitpid $pid: $!"; |
113 |
|
|
$stopped++; |
114 |
|
|
} |
115 |
|
|
warn "## stopped $stopped processes\n"; |
116 |
|
|
return $stopped; |
117 |
|
|
} |
118 |
|
|
|
119 |
|
|
use constant RESULT_OK => { |
120 |
|
|
'matchedDN' => '', |
121 |
|
|
'errorMessage' => '', |
122 |
|
|
'resultCode' => LDAP_SUCCESS |
123 |
|
|
}; |
124 |
|
|
|
125 |
|
|
# constructor |
126 |
|
|
sub new { |
127 |
|
|
my ($class, $sock) = @_; |
128 |
|
|
my $self = $class->SUPER::new($sock); |
129 |
|
|
printf "Accepted connection from: %s\n", $sock->peerhost(); |
130 |
|
|
return $self; |
131 |
|
|
} |
132 |
|
|
|
133 |
|
|
# the bind operation |
134 |
|
|
sub bind { |
135 |
|
|
my ($self,$req) = @_; |
136 |
|
|
|
137 |
|
|
warn "## bind req = ",dump($req); |
138 |
|
|
|
139 |
|
|
defined($req->{authentication}->{simple}) or return { |
140 |
|
|
matchedDN => '', |
141 |
|
|
errorMessage => '', |
142 |
|
|
resultCode => LDAP_STRONG_AUTH_NOT_SUPPORTED, |
143 |
|
|
}; |
144 |
|
|
|
145 |
dpavlin |
2 |
$self->{upstream} ||= Net::LDAP->new( 'ldaps://ldap.ffzg.hr/' ) or return { |
146 |
dpavlin |
1 |
matchedDN => '', |
147 |
|
|
errorMessage => $@, |
148 |
|
|
resultCode => LDAP_UNAVAILABLE, |
149 |
|
|
}; |
150 |
|
|
|
151 |
dpavlin |
2 |
warn "## upstream = ",dump( $self->{upstream} ); |
152 |
|
|
warn "upstream not Net::LDAP but ",ref($self->{upstream}) unless ref($self->{upstream}) eq 'Net::LDAP'; |
153 |
dpavlin |
1 |
|
154 |
|
|
my $msg; |
155 |
|
|
|
156 |
dpavlin |
2 |
# FIXME we would need to unbind because VLDAP binds us automatically, but that doesn't really work |
157 |
dpavlin |
1 |
#$msg = $self->{upstream}->unbind; |
158 |
|
|
#warn "# unbind msg = ",dump( $msg ); |
159 |
|
|
|
160 |
dpavlin |
3 |
my $bind; |
161 |
|
|
$bind->{dn} = $req->{name} if $req->{name}; |
162 |
|
|
$bind->{password} = $req->{authentication}->{simple} if $req->{authentication}->{simple}; |
163 |
|
|
warn "# bind ",dump( $bind ); |
164 |
|
|
$msg = $self->{upstream}->bind( %$bind ); |
165 |
dpavlin |
1 |
|
166 |
|
|
#warn "# bind msg = ",dump( $msg ); |
167 |
|
|
if ( $msg->code != LDAP_SUCCESS ) { |
168 |
|
|
warn "ERROR: ", $msg->code, ": ", $msg->server_error, "\n"; |
169 |
|
|
return { |
170 |
|
|
matchedDN => '', |
171 |
|
|
errorMessage => $msg->server_error, |
172 |
|
|
resultCode => $msg->code, |
173 |
|
|
}; |
174 |
|
|
} |
175 |
|
|
|
176 |
|
|
return RESULT_OK; |
177 |
|
|
} |
178 |
|
|
|
179 |
|
|
# the search operation |
180 |
|
|
sub search { |
181 |
|
|
my ($self,$req) = @_; |
182 |
|
|
|
183 |
|
|
warn "## search req = ",dump( $req ); |
184 |
|
|
|
185 |
|
|
if ( ! $self->{upstream} ) { |
186 |
|
|
warn "search without bind"; |
187 |
|
|
return { |
188 |
|
|
matchedDN => '', |
189 |
|
|
errorMessage => 'dude, bind first', |
190 |
|
|
resultCode => LDAP_OPERATIONS_ERROR, |
191 |
|
|
}; |
192 |
|
|
} |
193 |
|
|
|
194 |
|
|
my $filter; |
195 |
|
|
if (defined $req->{filter}) { |
196 |
|
|
# $req->{filter} is a ASN1-decoded tree; luckily, this is exactly the |
197 |
|
|
# internal representation Net::LDAP::Filter uses. [FIXME] Eventually |
198 |
|
|
# Net::LDAP::Filter should provide a corresponding constructor. |
199 |
|
|
bless($req->{filter}, 'Net::LDAP::Filter'); |
200 |
|
|
$filter = $req->{filter}->as_string; |
201 |
|
|
# $filter = '(&' . $req->{filter}->as_string |
202 |
|
|
# . '(objectClass=hrEduPerson)(host=aai.irb.hr))'; |
203 |
|
|
} |
204 |
|
|
|
205 |
|
|
warn "search upstream for $filter\n"; |
206 |
|
|
|
207 |
|
|
my $search = $self->{upstream}->search( |
208 |
|
|
base => $req->{baseObject}, |
209 |
|
|
scope => $req->{scope}, |
210 |
|
|
deref => $req->{derefAliases}, |
211 |
|
|
sizelimit => $req->{sizeLimit}, |
212 |
|
|
timelimit => $req->{timeLimit}, |
213 |
|
|
typesonly => $req->{typesOnly}, |
214 |
|
|
filter => $filter, |
215 |
|
|
attrs => $req->{attributes}, |
216 |
|
|
raw => qr/.*/, |
217 |
|
|
); |
218 |
|
|
|
219 |
|
|
# warn "# search = ",dump( $search ); |
220 |
|
|
|
221 |
|
|
if ( $search->code != LDAP_SUCCESS ) { |
222 |
|
|
warn "ERROR: ",$search->code,": ",$search->server_error; |
223 |
|
|
return { |
224 |
|
|
matchedDN => '', |
225 |
|
|
errorMessage => $search->server_error, |
226 |
|
|
resultCode => $search->code, |
227 |
|
|
}; |
228 |
|
|
}; |
229 |
|
|
|
230 |
|
|
my @entries = $search->entries; |
231 |
|
|
warn "## got ", $search->count, " entries for $filter\n"; |
232 |
|
|
foreach my $entry (@entries) { |
233 |
|
|
# $entry->changetype('add'); # Don't record changes. |
234 |
|
|
# foreach my $attr ($entry->attributes) { |
235 |
|
|
# if ($attr =~ /;lang-en$/) { |
236 |
|
|
# $entry->delete($attr); |
237 |
|
|
# } |
238 |
|
|
# } |
239 |
|
|
} |
240 |
|
|
|
241 |
|
|
warn "## entries = ",dump( @entries ); |
242 |
|
|
|
243 |
dpavlin |
5 |
# $self->cache->write_cache( \@entries, uri_escape( $filter )); |
244 |
dpavlin |
1 |
|
245 |
|
|
return RESULT_OK, @entries; |
246 |
|
|
} |
247 |
|
|
|
248 |
|
|
# the rest of the operations will return an "unwilling to perform" |
249 |
|
|
|
250 |
|
|
1; |