# Copyright (c) 1998-2004 Graham Barr . All rights reserved. # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. package LWP::Protocol::ldap; use Carp (); use HTTP::Status qw(HTTP_OK HTTP_BAD_REQUEST HTTP_INTERNAL_SERVER_ERROR HTTP_NOT_IMPLEMENTED); use HTTP::Negotiate (); use HTTP::Response (); use LWP::MediaTypes (); require LWP::Protocol; our @ISA = qw(LWP::Protocol); our $VERSION = '1.25'; use strict; eval { require Net::LDAP; }; my $init_failed = $@ ? $@ : undef; sub request { my($self, $request, $proxy, $arg, $size, $timeout) = @_; $size = 4096 unless $size; LWP::Debug::trace('()') if defined &LWP::Debug::trace; # check proxy if (defined $proxy) { return $self->_error(HTTP_BAD_REQUEST, 'You can not proxy through the ldap'); } my $url = $request->url; my $scheme = $url->scheme; my $userinfo = $url->can('userinfo') ? $url->userinfo : ''; my $dn = $url->dn; my @attrs = $url->attributes; my $scope = $url->scope || 'base'; my $filter = $url->filter; # check scheme if ($scheme !~ /^ldap[si]?$/) { return $self->_error(HTTP_INTERNAL_SERVER_ERROR, "LWP::Protocol::ldap::request called for '$scheme'"); } # check method my $method = $request->method; unless ($method =~ /^(?:GET|HEAD)$/) { return $self->_error(HTTP_NOT_IMPLEMENTED, "Library does not allow method $method for '$scheme:' URLs"); } if ($init_failed) { return $self->_error(HTTP_INTERNAL_SERVER_ERROR, $init_failed); } my ($user, $password) = defined($userinfo) ? split(/:/, $userinfo, 2) : (); my %extn = $url->extensions; my $tls = exists($extn{'x-tls'}) ? 1 : 0; my $format = lc($extn{'x-format'} || 'html'); my $mime_type = 'text/'.$format; # analyse HTTP headers if (my $accept = $request->header('Accept')) { if ($accept =~ m!\b((?:text|application|xml)/(?:x-)?dsml)\b!) { $mime_type = $1; $format = 'dsml'; }; if ($accept =~ m!\b(text/(?:x-)?ldif)\b!) { $mime_type = $1; $format = 'ldif'; }; if ($accept =~ m!\b((?:text|application)/json)\b!) { $mime_type = $1; $format = 'json'; }; } if (!$user) { if (my $authorization = $request->header('Authorization')) { # we only accept Basic authorization for now if ($authorization =~ /^Basic\s+([A-Z0-9+\/=]+)$/i) { require MIME::Base64; ($user, $password) = split(/:/, MIME::Base64::decode($1), 2); } } } # connect to LDAP server my $ldap = new Net::LDAP($url->as_string); if (!$ldap) { return $self->_error(HTTP_BAD_REQUEST, 'Connection to LDAP server failed', "$@"); } # optional: startTLS if ($tls && $scheme ne 'ldaps') { my $mesg = $ldap->start_tls(); return $self->_ldap_error($mesg) if ($mesg->code); } # optional: simple bind if ($user) { my $mesg = $ldap->bind($user, password => $password); return $self->_ldap_error($mesg) if ($mesg->code); } # do the search my %opts = ( scope => $scope ); $opts{base} = $dn if $dn; $opts{filter} = $filter if $filter; $opts{attrs} = \@attrs if @attrs; my $mesg = $ldap->search(%opts); return $self->_ldap_error($mesg) if ($mesg->code); # Create an initial response object my $response = HTTP::Response->new(HTTP_OK, 'Document follows'); $response->request($request); # return data in the format requested my $content = ''; if ($format eq 'ldif') { require Net::LDAP::LDIF; open(my $fh, '>', \$content); my $ldif = Net::LDAP::LDIF->new($fh, 'w', version => 1); while (my $entry = $mesg->shift_entry) { $ldif->write_entry($entry); } $ldif->done; close($fh); } elsif ($format eq 'dsml') { require Net::LDAP::DSML; open(my $fh, '>', \$content); my $dsml = Net::LDAP::DSML->new(output => $fh, pretty_print => 1); $dsml->start_dsml(); while (my $entry = $mesg->shift_entry) { $dsml->write_entry($entry); } $dsml->end_dsml(); close($fh); } elsif ($format eq 'json') { require JSON; my $entry; my %objects; for (my $index = 0 ; $entry = $mesg->entry($index); $index++) { my $dn = $entry->dn; $objects{$dn} = {}; foreach my $attr (sort($entry->attributes)) { $objects{$dn}{$attr} = $entry->get_value($attr, asref => 1); } } $content = JSON::to_json(\%objects, {pretty => 1, utf8 => 1}); } else { my $entry; my $index; $content = "Directory Search Results\n"; for ($index = 0 ; $entry = $mesg->entry($index); $index++) { $content .= $index ? qq{
 \n} : ''; $content .= qq{\n"; foreach my $attr ($entry->attributes) { my $vals = $entry->get_value($attr, asref => 1); $content .= q{\n"; my $j = 0; foreach my $val (@$vals) { $val = qq!$val! if $val =~ /^https?:/; $val = qq!$val! if $val =~ /^[-\w]+\@[-.\w]+$/; $content .= "" if $j++; $content .= "\n"; } } } $content .= '
} . $entry->dn . "
1); $content .= '>' . $attr . " 
" . $val . "
' if $index; $content .= '
'; $content .= $index ? sprintf('%s Match%s found', $index, $index>1 ? 'es' : '') : 'No Matches found'; $content .= "\n"; } $response->header('Content-Type' => $mime_type.'; charset=utf-8'); $response->header('Content-Length', length($content)); $response = $self->collect_once($arg, $response, $content) if ($method ne 'HEAD'); $ldap->unbind; $response; } sub _ldap_error { my($self, $mesg) = @_; $self->_error(HTTP_BAD_REQUEST, 'LDAP return code '.$mesg->code, $mesg->error); } sub _error { my($self, $code, $message, $content) = @_; my $res = HTTP::Response->new($code, $message); if ($content) { $res->content_type('text/plain'); $res->content($content); } $res; } 1; __END__ =head1 NAME LWP::Protocol::ldap - Provide LDAP support for LWP::UserAgent =head1 SYNOPSIS use LWP::UserAgent; $ua = LWP::UserAgent->new(); $res = $ua->get('ldap://ldap.example.com/' . 'o=University%20of%20Michigan,c=US??sub?(cn=Babs%20Jensen)', Accept => 'text/json'): =head1 DESCRIPTION The LWP::Protocol::ldap module provides support for using I schemed URLs following RFC 4516 with LWP. This module is a plug-in to the LWP protocol handling, so you don't use it directly. In addition to being used with LDAP URIs, LWP::Protocol::ldap also acts as the base class for its sibling modules LWP::Protocol::ldaps and LWP::Protocol::ldapi. =head2 Features =head3 HTTP methods supported LWP::Protocol::ldap implements the HTTP I and I methods. They are mapped to the LDAP L operation, =head3 Response format Depending on the HTTP I header provided by the user agent, LWP::Protocol::ldap can answer the requests in one of the following formats: =over 4 =item DSML When the HTTP I header contains the C MIME type, the response is sent as DSMLv1. =item JSON When the HTTP I header contains the C MIME type, the response is sent as JSON. For this to work the I Perl module needs to be installed. =item LDIF When the HTTP I header contains the C MIME type, the response is sent in LDIFv1 format. =item HTML In case no HTTP I header has been sent or none of the above MIME types can be detected, and the I extension has not been provided either, the response is sent using HTML markup in a 2-column table format (roughly modeled on LDIF). =back As an alternative to sending an HTTP I header, LWP::Protocol::ldap also accepts the C extension Example: ldap://ldap.example.com/o=University%20of%20Michigan,c=US??sub?(cn=Babs%20Jensen)?x-format=dsml =head3 TLS support For I and I URIs, the module implements the C extension that switches the LDAP connection to TLS using a call of the L method. Example: ldap://ldap.example.com/o=University%20of%20Michigan,c=US??sub?(cn=Babs%20Jensen)?x-tls=1 Note: In the above example, ideally giving C should be sufficient, but unfortunately the parser in URI::ldap has a little flaw. =head3 Authorization Usually the connection is done anonymously, but if the HTTP I header is provided with credentials for HTTP Basic authorization, the credentials given in that header will be used to do a simple bind to the LDAP server. =head1 SEE ALSO L, L =head1 COPYRIGHT Copyright (c) 1998-2004 Graham Barr, 2012 Peter Marschall. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.