Handle hostnames with upper-case letters
[webmin.git] / ldap-client / ldap-client-lib.pl
1 # Functions for parsing and updating the LDAP config file
2
3 BEGIN { push(@INC, ".."); };
4 use WebminCore;
5 &init_config();
6
7 @base_types = ("passwd", "shadow", "group", "hosts", "networks", "netmasks",
8                "services", "protocols", "aliases", "netgroup");
9
10 # get_config()
11 # Parses the NSS LDAP config file into a list of names and values
12 sub get_config
13 {
14 local $file = $_[0] || $config{'auth_ldap'};
15 if (!scalar(@get_config_cache)) {
16         local $lnum = 0;
17         @get_config_cache = ( );
18         &open_readfile(CONF, $file);
19         while(<CONF>) {
20                 s/\r|\n//g;
21                 s/#.*$//;
22                 if (/^(#?)(\S+)\s*(.*)/) {
23                         push(@get_config_cache, { 'name' => lc($2),
24                                                   'value' => $3,
25                                                   'enabled' => !$1,
26                                                   'line' => $lnum,
27                                                   'file' => $file });
28                         }
29                 $lnum++;
30                 }
31         close(CONF);
32         }
33 return \@get_config_cache;
34 }
35
36 # find(name, &conf, disabled-mode)
37 sub find
38 {
39 local ($name, $conf, $dis) = @_;
40 local @rv = grep { $_->{'name'} eq $name } @$conf;
41 if ($dis == 0) {
42         # Enabled only
43         @rv = grep { $_->{'enabled'} } @rv;
44         }
45 elsif ($dis == 1) {
46         # Disabled only
47         @rv = grep { !$_->{'enabled'} } @rv;
48         }
49 return wantarray ? @rv : $rv[0];
50 }
51
52 # find_value(name, &conf)
53 sub find_value
54 {
55 local ($name, $conf, $dis) = @_;
56 local @rv = map { $_->{'value'} } &find($name, $conf, $dis);
57 return wantarray ? @rv : $rv[0];
58 }
59
60 sub find_svalue
61 {
62 local $rv = &find_value(@_);
63 return $rv;
64 }
65
66 # save_directive(&conf, name, [value])
67 sub save_directive
68 {
69 local ($conf, $name, $value) = @_;
70 local $old = &find($name, $conf);
71 local $oldcmt = &find($name, $conf, 1);
72 local $lref = &read_file_lines($old ? $old->{'file'} :
73                                $oldcmt ? $oldcmt->{'file'} :
74                                          $config{'auth_ldap'});
75 if (defined($value) && $old) {
76         # Just update value
77         $old->{'value'} = $value;
78         $lref->[$old->{'line'}] = "$name $value";
79         }
80 elsif (defined($value) && $oldcmt) {
81         # Add value after commented version
82         splice(@$lref, $oldcmt->{'line'}+1, 0, "$name $value");
83         &renumber($conf, $oldcmt->{'line'}+1, $oldcmt->{'file'}, 1);
84         push(@$conf, { 'name' => $name,
85                        'value' => $value,
86                        'enabled' => 1,
87                        'line' => $oldcmt->{'line'}+1,
88                        'file' => $oldcmt->{'file'} });
89         }
90 elsif (!defined($value) && $old) {
91         # Delete current value
92         splice(@$lref, $old->{'line'}, 1);
93         &renumber($conf, $old->{'line'}, $old->{'file'}, -1);
94         @$conf = grep { $_ ne $old } @$conf;
95         }
96 elsif ($value) {
97         # Add value at end of file
98         push(@$conf, { 'name' => $name,
99                        'value' => $value,
100                        'enabled' => 1,
101                        'line' => scalar(@$lref),
102                        'file' => $config{'auth_ldap'} });
103         push(@$lref, "$name $value");
104         }
105 }
106
107 sub renumber
108 {
109 local ($conf, $line, $file, $offset) = @_;
110 foreach my $c (@$conf) {
111         if ($c->{'line'} >= $line && $c->{'file'} eq $file) {
112                 $c->{'line'} += $offset;
113                 }
114         }
115 }
116
117 # get_rootbinddn_secret()
118 # Returns the password used when the root user connects to the LDAP server
119 sub get_rootbinddn_secret
120 {
121 local @secrets = split(/\t+/, $config{'secret'});
122 &open_readfile(SECRET, $secrets[0]) || return undef;
123 local $secret = <SECRET>;
124 close(SECRET);
125 $secret =~ s/\r|\n//g;
126 return $secret;
127 }
128
129 # save_rootbinddn_secret(secret)
130 # Saves the password used when the root user connects to the LDAP server
131 sub save_rootbinddn_secret
132 {
133 local @secrets = split(/\t+/, $config{'secret'});
134 if (defined($_[0])) {
135         foreach my $secret (@secrets) {
136                 &open_tempfile(SECRET, ">$secret");
137                 &print_tempfile(SECRET, $_[0],"\n");
138                 &close_tempfile(SECRET);
139                 &set_ownership_permissions(0, 0, 0600, $secret);
140                 }
141         }
142 else {
143         &unlink_file(@secrets);
144         }
145 }
146
147 # ldap_connect(return-error, [&host])
148 # Connect to the LDAP server and return a handle to the Net::LDAP object
149 sub ldap_connect
150 {
151 # Load the LDAP module
152 eval "use Net::LDAP";
153 if ($@) {
154         local $err = &text('ldap_emodule', "<tt>Net::LDAP</tt>",
155                    "../cpan/download.cgi?source=3&".
156                    "cpan=Convert::ASN1%20Net::LDAP&mode=2&".
157                    "return=../$module_name/&".
158                    "returndesc=".&urlize($module_info{'desc'}));
159         if ($_[0]) { return $err; }
160         else { &error($err); }
161         }
162 local $err = &generic_ldap_connect($config{'ldap_hosts'}, $config{'ldap_port'},
163                              $config{'ldap_tls'}, $config{'ldap_user'},
164                              $config{'ldap_pass'});
165 if (ref($err)) { return $err; }         # Worked
166 elsif ($_[0]) { return $err; }          # Caller asked for error return
167 else { &error($err); }                  # Caller asked for error() call
168 }
169
170 # generic_ldap_connect([host], [port], [login], [password])
171 # A generic function for connecting to an LDAP server. Uses the system's
172 # LDAP client config file if any parameters are missing. Returns the LDAP
173 # handle on success or an error message on failure.
174 sub generic_ldap_connect
175 {
176 local ($ldap_hosts, $ldap_port, $ldap_ssl, $ldap_user, $ldap_pass) = @_;
177
178 # Check for perl module and config file
179 eval "use Net::LDAP";
180 if ($@) {
181         return &text('ldap_emodule2', "<tt>Net::LDAP</tt>");
182         }
183 if (!-r $config{'auth_ldap'}) {
184         $ldap_hosts && $ldap_user ||
185                 return &text('ldap_econf', "<tt>$config{'auth_ldap'}</tt>");
186         }
187
188 # Get the host and port
189 local $conf = &get_config();
190 local $uri = &find_svalue("uri", $conf);
191 local ($ldap, $use_ssl, $err);
192 local $ssl = &find_svalue("ssl", $conf);
193 local $cafile = &find_svalue("tls_cacertfile", $conf);
194 local $certfile = &find_svalue("tls_cert", $conf);
195 local $keyfile = &find_svalue("tls_key", $conf);
196 local $ciphers = &find_svalue("tls_ciphers", $conf);
197 if ($ldap_hosts) {
198         # Using hosts from parameter
199         local @hosts = split(/[ \t,]+/, $ldap_hosts);
200         if ($ldap_ssl ne '') {
201                 $use_ssl = $ldap_ssl;
202                 }
203         else {
204                 $use_ssl = $ssl eq 'yes' ? 1 :
205                            $ssl eq 'start_tls' ? 2 : 0;
206                 }
207         local $port = $ldap_port ||
208                       &find_svalue("port", $conf) ||
209                       ($use_ssl == 1 ? 636 : 389);
210         foreach my $host (@hosts) {
211                 eval {
212                         $ldap = Net::LDAP->new($host, port => $port,
213                                 scheme => $use_ssl == 1 ? 'ldaps' : 'ldap',
214                                 inet6 => &should_use_inet6($host));
215                         };
216                 if ($@) {
217                         $err = &text('ldap_econn2',
218                                      "<tt>$host</tt>", "<tt>$port</tt>",
219                                      &html_escape($@));
220                         }
221                 elsif (!$ldap) {
222                         $err = &text('ldap_econn',
223                                      "<tt>$host</tt>", "<tt>$port</tt>");
224                         }
225                 else {
226                         $err = undef;
227                         last;
228                         }
229                 }
230         }
231 elsif ($uri) {
232         # Using uri directive
233         foreach my $u (split(/\s+/, $uri)) {
234                 if ($u =~ /^(ldap|ldaps|ldapi):\/\/([a-z0-9\_\-\.]+)(:(\d+))?/i) {
235                         ($proto, $host, $port) = ($1, $2, $4);
236                         if (!$port && $proto eq "ldap") {
237                                 $port = 389;
238                                 }
239                         elsif (!$port && $proto eq "ldaps") {
240                                 $port = 636;
241                                 }
242                         $ldap = Net::LDAP->new($host, port => $port,
243                                        scheme => $proto,
244                                        inet6 => &should_use_inet6($host));
245                         if (!$ldap) {
246                                 $err = &text('ldap_econn',
247                                              "<tt>$host</tt>","<tt>$port</tt>");
248                                 }
249                         else {
250                                 $err = undef;
251                                 $use_ssl = $proto eq "ldaps" ? 1 :
252                                            $ssl eq 'start_tls' ? 2 : 0;
253                                 last;
254                                 }
255                         }
256                 }
257         if (!$ldap && !$err) {
258                 $err = &text('ldap_eparse', $uri);
259                 }
260         }
261 else {
262         # Using host and port directives
263         $use_ssl = $ssl eq 'yes' ? 1 :
264                    $ssl eq 'start_tls' ? 2 : 0;
265         local @hosts = split(/[ ,]+/, &find_svalue("host", $conf));
266         local $port = &find_svalue("port", $conf) ||
267                       ($use_ssl == 1 ? 636 : 389);
268         @hosts = ( "localhost" ) if (!@hosts);
269
270         foreach $host (@hosts) {
271                 $ldap = Net::LDAP->new($host, port => $port,
272                                scheme => $use_ssl == 1 ? 'ldaps' : 'ldap',
273                                inet6 => &should_use_inet6($host));
274                 if (!$ldap) {
275                         $err = &text('ldap_econn',
276                                      "<tt>$host</tt>", "<tt>$port</tt>");
277                         }
278                 else {
279                         $err = undef;
280                         last;
281                         }
282                 }
283         }
284
285 # Start TLS if configured
286 if ($use_ssl == 2 && !$err) {
287         local $mesg;
288         if ($certfile) {
289                 # Use cert to connect
290                 eval { $mesg = $ldap->start_tls(
291                                         cafile     => $cafile,
292                                         clientcert => $certfile,
293                                         clientkey  => $keyfile,
294                                         ciphers    => $ciphers
295                                         ); };
296
297                 }
298         else {
299                 eval { $mesg = $ldap->start_tls(); };
300                 }
301         if ($@ || !$mesg || $mesg->code) {
302                 $err = &text('ldap_etls', $@ ? $@ : $mesg ? $mesg->error :
303                                           "Unknown error");
304                 }
305         }
306
307 if ($err) {
308         return $err;
309         }
310
311 local ($dn, $password);
312 local $rootbinddn = &find_svalue("rootbinddn", $conf);
313 if ($ldap_user) {
314         # Use login from config
315         $dn = $ldap_user;
316         $password = $ldap_pass;
317         }
318 elsif ($rootbinddn) {
319         # Use the root login if we have one
320         $dn = $rootbinddn;
321         $password = &get_rootbinddn_secret();
322         }
323 else {
324         # Use the normal login
325         $dn = &find_svalue("binddn", $conf);
326         $password = &find_svalue("bindpw", $conf);
327         }
328 local $mesg;
329 if ($password) {
330         $mesg = $ldap->bind(dn => $dn, password => $password);
331         }
332 else {
333         $mesg = $ldap->bind(dn => $dn, anonymous => 1);
334         }
335 if (!$mesg || $mesg->code) {
336         local $err = &text('ldap_elogin', "<tt>$host</tt>",
337                            $dn || $text{'ldap_anon'},
338                            $mesg ? $mesg->error : "Unknown error");
339         if ($_[0]) { return $err; }
340         else { &error($err); }
341         }
342 return $ldap;
343 }
344
345 # should_use_inet6(host)
346 # Returns 1 if some host has a v6 address but not v4
347 sub should_use_inet6
348 {
349 local ($host) = @_;
350 return !&to_ipaddress($host) && &to_ip6address($host);
351 }
352
353 # base_chooser_button(field, node, form)
354 # Returns HTML for a popup LDAP base chooser button
355 sub base_chooser_button
356 {
357 local ($field, $node, $form) = @_;
358 $form ||= 0;
359 local $w = 500;
360 local $h = 500;
361 if ($gconfig{'db_sizeusers'}) {
362         ($w, $h) = split(/x/, $gconfig{'db_sizeusers'});
363         }
364 return "<input type=button onClick='ifield = document.forms[$form].$field; chooser = window.open(\"popup_browser.cgi?node=$node&base=\"+escape(ifield.value), \"chooser\", \"toolbar=no,menubar=no,scrollbars=yes,width=$w,height=$h\"); chooser.ifield = ifield; window.ifield = ifield' value=\"...\">\n";
365 }
366
367 # get_ldap_host()
368 # Returns the hostname probably used for connecting
369 sub get_ldap_host
370 {
371 local @hosts;
372 if ($config{'ldap_hosts'}) {
373         @hosts = split(/\s+/, $config{'ldap_hosts'});
374         }
375 elsif (!-r $config{'auth_ldap'}) {
376         @hosts = ( );
377         }
378 else {
379         local $conf = &get_config();
380         local $uri = &find_svalue("uri", $conf);
381         if ($uri) {
382                 foreach my $u (split(/\s+/, $uri)) {
383                         if ($u =~ /^(ldap|ldaps|ldapi):\/\/([a-z0-9\_\-\.]+)(:(\d+))?/) {
384                                 push(@hosts, $2);
385                                 }
386                         }
387                 }
388         else {
389                 @hosts = split(/[ ,]+/, &find_svalue("host", $conf));
390                 }
391         if (!@hosts) {
392                 @hosts = ( "localhost" );
393                 }
394         }
395 return wantarray ? @hosts : $hosts[0];
396 }
397
398 1;
399