Optionally use the server side pager control for search results.
authorMahlon E. Smith <mahlon@martini.nu>
Wed, 07 Jun 2017 14:38:08 -0700
changeset 92 a1aa55019077
parent 91 80ec00959fbd
child 93 4c7843b9a047
Optionally use the server side pager control for search results.
shelldap
--- a/shelldap	Sat Mar 05 00:00:00 2016 -0800
+++ b/shelldap	Wed Jun 07 14:38:08 2017 -0700
@@ -138,6 +138,19 @@
 
 =over 4
 
+=item B<paginate>
+
+Integer.  If enabled, shelldap will attempt to use server side
+pagination to build listings.  Note: if you're using this to avoid
+sizelimit errors, you'll likely need server configuration to raise the
+limits for paginated results.
+
+	--paginate 100
+
+=back
+
+=over 4
+
 =item B<promptpass>
 
 Force password prompting.  Useful to temporarily override cached
@@ -448,15 +461,17 @@
 	LDAP_OTHER
 	LDAP_TIMEOUT
 	LDAP_NO_MEMORY
-	LDAP_CONNECT_ERROR /;
+	LDAP_CONNECT_ERROR
+	LDAP_CONTROL_PAGED /;
 use Net::LDAP::Util qw/ canonical_dn ldap_explode_dn /;
 use Net::LDAP::LDIF;
+use Net::LDAP::Extension::SetPassword;
+use Net::LDAP::Control::Paged;
 use Data::Dumper;
 use File::Temp;
 use Algorithm::Diff;
 use Carp 'confess';
 use base 'Term::Shell';
-require Net::LDAP::Extension::SetPassword;
 
 my $conf = $main::conf;
 
@@ -508,6 +523,16 @@
 		print "Cipher in use: ", $self->ldap()->cipher(), "\n";
 	}
 
+	# check for the pagination extension on the server early, and bail
+	# if necessary.
+	if ( $conf->{'paginate'} && $conf->{'paginate'} =~ /^\d+$/ && $conf->{'paginate'} > 0 ) {
+		my $has_pagination = ( grep $_ eq LDAP_CONTROL_PAGED, $self->{'root_dse'}->get_value('supportedControl') );
+		die "Server pagination is enabled, but the server doesn't seem to support it.\n" unless $has_pagination;
+	}
+	else {
+		$conf->{'paginate'} = undef;
+	}
+
 	# try an initial search and bail early if it doesn't work. (bad baseDN?)
 	my $s = $self->search();
 	die "LDAP baseDN error: ", $s->{'message'}, "\n" if $s->{'code'};
@@ -873,7 +898,8 @@
 }
 
 
-### Perform an LDAP search.
+### Perform an LDAP search, optionally with the server side pager
+### control.
 ###
 ### Returns a hashref containing the return code and
 ### an arrayref of Net::LDAP::Entry objects.
@@ -882,11 +908,18 @@
 {
 	my $self = shift;
 	my $opts = shift || {};
+	my $controls = [];
 
 	$opts->{'base'}   ||= $self->base(),
 	$opts->{'filter'} ||= '(objectClass=*)';
 	$opts->{'scope'}  ||= 'base';
 
+	my $pager;
+	if ( $conf->{'paginate'} ) {
+		$pager = Net::LDAP::Control::Paged->new( size => $conf->{'paginate'} );
+		push( @$controls, $pager );
+	}
+
 	my $search = sub { 
 		return $self->ldap->search(
 			base	  => $opts->{'base'},
@@ -894,19 +927,41 @@
 			scope	  => $opts->{'scope'},
 			timelimit => $conf->{'timeout'},
 			typesonly => ! $opts->{'vals'},
-			attrs	  => $opts->{'attrs'} || ['*']
+			attrs	  => $opts->{'attrs'} || ['*'],
+			control   => $controls
 		);
 	};
 
-	my $s = $self->with_retry( $search );
+	my $s;
+	my $entries = [];
+   	my $token  = '-';
+
+	if ( $conf->{'paginate'} ) {
+		while( $token ) {
+			$s = $self->with_retry( $search );
+			push( @$entries, $s->entries() );
+
+			my $page_response = $s->control( LDAP_CONTROL_PAGED ) or last;
+			$token = $page_response->cookie;
+			$pager->cookie( $token );
+		}
+	}
+	else {
+		$s = $self->with_retry( $search );
+		$entries = [ $s->entries() ];
+	}
+
 	my $rv = {
 		code	=> $s->code(),
-		message => $s->error(),
-		entries => []
+		message => $s->error()
 	};
 
-	$rv->{'entries'} =
-	  $opts->{'scope'} eq 'base' ? [ $s->shift_entry() ] : [ $s->entries() ];
+	if ( $opts->{'scope'} eq 'base' ) {
+		$rv->{'entries'} = [ $s->shift_entry() ]
+	}
+	else {
+		$rv->{'entries'} = $entries;
+	}
 
 	return $rv;
 }
@@ -2322,6 +2377,7 @@
 	'binddn|D=s',
 	'basedn|b=s',
 	'cacheage=i',
+	'paginate=i',
 	'promptpass|W',
 	'timeout=i',
 	'sasl|Y=s',
@@ -2350,7 +2406,6 @@
 	while ( my ($k, $v) = each %{$conf} ) { $conf->{ $k } = $v }
 }
 
-
 # defaults
 $conf->{'configfile'} ||= "$ENV{'HOME'}/.shelldap.rc";
 $conf->{'cacheage'} ||= 300;