001    /*
002     *  Licensed to the Apache Software Foundation (ASF) under one
003     *  or more contributor license agreements.  See the NOTICE file
004     *  distributed with this work for additional information
005     *  regarding copyright ownership.  The ASF licenses this file
006     *  to you under the Apache License, Version 2.0 (the
007     *  "License"); you may not use this file except in compliance
008     *  with the License.  You may obtain a copy of the License at
009     *  
010     *    http://www.apache.org/licenses/LICENSE-2.0
011     *  
012     *  Unless required by applicable law or agreed to in writing,
013     *  software distributed under the License is distributed on an
014     *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     *  KIND, either express or implied.  See the License for the
016     *  specific language governing permissions and limitations
017     *  under the License. 
018     *  
019     */
020    package org.apache.directory.server.ldap.handlers;
021    
022    
023    import static java.lang.Math.min;
024    import static org.apache.directory.server.ldap.LdapServer.NO_SIZE_LIMIT;
025    import static org.apache.directory.server.ldap.LdapServer.NO_TIME_LIMIT;
026    
027    import java.util.concurrent.TimeUnit;
028    
029    import org.apache.directory.server.core.DirectoryService;
030    import org.apache.directory.server.core.ReferralManager;
031    import org.apache.directory.server.core.entry.ClonedServerEntry;
032    import org.apache.directory.server.core.event.EventType;
033    import org.apache.directory.server.core.event.NotificationCriteria;
034    import org.apache.directory.server.core.filtering.EntryFilteringCursor;
035    import org.apache.directory.server.core.partition.PartitionNexus;
036    import org.apache.directory.server.i18n.I18n;
037    import org.apache.directory.server.ldap.LdapSession;
038    import org.apache.directory.server.ldap.handlers.controls.PagedSearchContext;
039    import org.apache.directory.shared.ldap.codec.controls.ManageDsaITControl;
040    import org.apache.directory.shared.ldap.codec.search.controls.pagedSearch.PagedResultsControl;
041    import org.apache.directory.shared.ldap.codec.search.controls.persistentSearch.PersistentSearchControl;
042    import org.apache.directory.shared.ldap.codec.util.LdapURLEncodingException;
043    import org.apache.directory.shared.ldap.constants.SchemaConstants;
044    import org.apache.directory.shared.ldap.entry.StringValue;
045    import org.apache.directory.shared.ldap.entry.EntryAttribute;
046    import org.apache.directory.shared.ldap.entry.Value;
047    import org.apache.directory.shared.ldap.exception.LdapException;
048    import org.apache.directory.shared.ldap.exception.OperationAbandonedException;
049    import org.apache.directory.shared.ldap.filter.EqualityNode;
050    import org.apache.directory.shared.ldap.filter.OrNode;
051    import org.apache.directory.shared.ldap.filter.PresenceNode;
052    import org.apache.directory.shared.ldap.filter.SearchScope;
053    import org.apache.directory.shared.ldap.message.ReferralImpl;
054    import org.apache.directory.shared.ldap.message.ResultCodeEnum;
055    import org.apache.directory.shared.ldap.message.SearchResponseEntryImpl;
056    import org.apache.directory.shared.ldap.message.SearchResponseReferenceImpl;
057    import org.apache.directory.shared.ldap.message.internal.InternalLdapResult;
058    import org.apache.directory.shared.ldap.message.internal.InternalReferral;
059    import org.apache.directory.shared.ldap.message.internal.InternalResponse;
060    import org.apache.directory.shared.ldap.message.internal.InternalSearchRequest;
061    import org.apache.directory.shared.ldap.message.internal.InternalSearchResponseDone;
062    import org.apache.directory.shared.ldap.message.internal.InternalSearchResponseEntry;
063    import org.apache.directory.shared.ldap.message.internal.InternalSearchResponseReference;
064    import org.apache.directory.shared.ldap.name.DN;
065    import org.apache.directory.shared.ldap.schema.AttributeType;
066    import org.apache.directory.shared.ldap.util.LdapURL;
067    import org.apache.directory.shared.ldap.util.StringTools;
068    import org.slf4j.Logger;
069    import org.slf4j.LoggerFactory;
070    
071    
072    /**
073     * A handler for processing search requests.
074     *
075     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
076     * @version $Rev: 664302 $
077     */
078    public class SearchHandler extends ReferralAwareRequestHandler<InternalSearchRequest>
079    {
080        private static final Logger LOG = LoggerFactory.getLogger( SearchHandler.class );
081    
082        /** Speedup for logs */
083        private static final boolean IS_DEBUG = LOG.isDebugEnabled();
084    
085        /** cached to save redundant lookups into registries */ 
086        private AttributeType objectClassAttributeType;
087        
088        
089        /**
090         * Constructs a new filter EqualityNode asserting that a candidate 
091         * objectClass is a referral.
092         *
093         * @param session the {@link LdapSession} to construct the node for
094         * @return the {@link EqualityNode} (objectClass=referral) non-normalized
095         * @throws Exception in the highly unlikely event of schema related failures
096         */
097        private EqualityNode<String> newIsReferralEqualityNode( LdapSession session ) throws Exception
098        {
099            if ( objectClassAttributeType == null )
100            {
101                objectClassAttributeType = session.getCoreSession().getDirectoryService().
102                    getSchemaManager().lookupAttributeTypeRegistry( SchemaConstants.OBJECT_CLASS_AT );
103            }
104            
105            EqualityNode<String> ocIsReferral = new EqualityNode<String>( SchemaConstants.OBJECT_CLASS_AT,
106                new StringValue( objectClassAttributeType, SchemaConstants.REFERRAL_OC ) );
107            
108            return ocIsReferral;
109        }
110        
111        
112        /**
113         * Handles search requests containing the persistent search control but 
114         * delegates to doSimpleSearch() if the changesOnly parameter of the 
115         * control is set to false.
116         *
117         * @param session the LdapSession for which this search is conducted 
118         * @param req the search request containing the persistent search control
119         * @param psearchControl the persistent search control extracted
120         * @throws Exception if failures are encountered while searching
121         */
122        private void handlePersistentSearch( LdapSession session, InternalSearchRequest req, 
123            PersistentSearchControl psearchControl ) throws Exception 
124        {
125            /*
126             * We want the search to complete first before we start listening to 
127             * events when the control does NOT specify changes ONLY mode.
128             */
129            if ( ! psearchControl.isChangesOnly() )
130            {
131                InternalSearchResponseDone done = doSimpleSearch( session, req );
132                
133                // ok if normal search beforehand failed somehow quickly abandon psearch
134                if ( done.getLdapResult().getResultCode() != ResultCodeEnum.SUCCESS )
135                {
136                    session.getIoSession().write( done );
137                    return;
138                }
139            }
140    
141            if ( req.isAbandoned() )
142            {
143                return;
144            }
145            
146            // now we process entries forever as they change
147            PersistentSearchListener handler = new PersistentSearchListener( session, req );
148            
149            // compose notification criteria and add the listener to the event 
150            // service using that notification criteria to determine which events 
151            // are to be delivered to the persistent search issuing client
152            NotificationCriteria criteria = new NotificationCriteria();
153            criteria.setAliasDerefMode( req.getDerefAliases() );
154            criteria.setBase( req.getBase() );
155            criteria.setFilter( req.getFilter() );
156            criteria.setScope( req.getScope() );
157            criteria.setEventMask( EventType.getEventTypes( psearchControl.getChangeTypes() ) );
158            getLdapServer().getDirectoryService().getEventService().addListener( handler, criteria );
159            req.addAbandonListener( new SearchAbandonListener( ldapServer, handler ) );
160            return;
161        }
162        
163        
164        /**
165         * Handles search requests on the RootDSE. 
166         * 
167         * @param session the LdapSession for which this search is conducted 
168         * @param req the search request on the RootDSE
169         * @throws Exception if failures are encountered while searching
170         */
171        private void handleRootDseSearch( LdapSession session, InternalSearchRequest req ) throws Exception
172        {
173            EntryFilteringCursor cursor = null;
174            
175            try
176            {
177                cursor = session.getCoreSession().search( req );
178                
179                // Position the cursor at the beginning
180                cursor.beforeFirst();
181                boolean hasRootDSE = false;
182                
183                while ( cursor.next() )
184                {
185                    if ( hasRootDSE )
186                    {
187                        // This is an error ! We should never find more than one rootDSE !
188                        LOG.error( I18n.err( I18n.ERR_167 ) );
189                    }
190                    else
191                    {
192                        hasRootDSE = true;
193                        ClonedServerEntry entry = cursor.get();
194                        session.getIoSession().write( generateResponse( session, req, entry ) );
195                    }
196                }
197        
198                // write the SearchResultDone message
199                session.getIoSession().write( req.getResultResponse() );
200            }
201            finally
202            {
203                // Close the cursor now.
204                if ( cursor != null )
205                {
206                    try
207                    {
208                        cursor.close();
209                    }
210                    catch ( Exception e )
211                    {
212                        LOG.error( I18n.err( I18n.ERR_168 ), e );
213                    }
214                }
215            }
216        }
217        
218        
219        /**
220         * Based on the server maximum time limits configured for search and the 
221         * requested time limits this method determines if at all to replace the 
222         * default ClosureMonitor of the result set Cursor with one that closes
223         * the Cursor when either server mandated or request mandated time limits 
224         * are reached.
225         *
226         * @param req the {@link InternalSearchRequest} issued
227         * @param session the {@link LdapSession} on which search was requested
228         * @param cursor the {@link EntryFilteringCursor} over the search results
229         */
230        private void setTimeLimitsOnCursor( InternalSearchRequest req, LdapSession session, final EntryFilteringCursor cursor )
231        {
232            // Don't bother setting time limits for administrators
233            if ( session.getCoreSession().isAnAdministrator() && req.getTimeLimit() == NO_TIME_LIMIT )
234            {
235                return;
236            }
237            
238            /*
239             * Non administrator based searches are limited by time if the server 
240             * has been configured with unlimited time and the request specifies 
241             * unlimited search time
242             */
243            if ( ldapServer.getMaxTimeLimit() == NO_TIME_LIMIT && req.getTimeLimit() == NO_TIME_LIMIT )
244            {
245                return;
246            }
247            
248            /*
249             * If the non-administrator user specifies unlimited time but the server 
250             * is configured to limit the search time then we limit by the max time 
251             * allowed by the configuration 
252             */
253            if ( req.getTimeLimit() == 0 )
254            {
255                cursor.setClosureMonitor( new SearchTimeLimitingMonitor( ldapServer.getMaxTimeLimit(), TimeUnit.SECONDS ) );
256                return;
257            }
258            
259            /*
260             * If the non-administrative user specifies a time limit equal to or 
261             * less than the maximum limit configured in the server then we 
262             * constrain search by the amount specified in the request
263             */
264            if ( ldapServer.getMaxTimeLimit() >= req.getTimeLimit() )
265            {
266                cursor.setClosureMonitor( new SearchTimeLimitingMonitor( req.getTimeLimit(), TimeUnit.SECONDS ) );
267                return;
268            }
269    
270            /*
271             * Here the non-administrative user's requested time limit is greater 
272             * than what the server's configured maximum limit allows so we limit
273             * the search to the configured limit
274             */
275            cursor.setClosureMonitor( new SearchTimeLimitingMonitor( ldapServer.getMaxTimeLimit(), TimeUnit.SECONDS ) );
276        }
277        
278        
279        /**
280         * Return the server size limit
281         */
282        private long getServerSizeLimit( LdapSession session, InternalSearchRequest request )
283        {
284            if ( session.getCoreSession().isAnAdministrator() )
285            {
286                if ( request.getSizeLimit() == NO_SIZE_LIMIT )
287                {
288                    return Long.MAX_VALUE;
289                }
290                else
291                {
292                    return request.getSizeLimit();
293                }
294            }
295            else
296            {
297                if ( ldapServer.getMaxSizeLimit() == NO_SIZE_LIMIT )
298                {
299                    return Long.MAX_VALUE;
300                }
301                else
302                {
303                    return ldapServer.getMaxSizeLimit();
304                }
305            }
306        }
307        
308        
309        private void readResults( LdapSession session, InternalSearchRequest req, InternalLdapResult ldapResult,
310        EntryFilteringCursor cursor, long sizeLimit ) throws Exception
311        {
312            long count = 0;
313    
314            while ( (count < sizeLimit ) && cursor.next() )
315            {
316                // Handle closed session
317                if ( session.getIoSession().isClosing() )
318                {
319                    // The client has closed the connection
320                    LOG.debug( "Request terminated for message {}, the client has closed the session", 
321                        req.getMessageId() );
322                    break;
323                }
324    
325                if ( req.isAbandoned() )
326                {
327                    // The cursor has been closed by an abandon request.
328                    LOG.debug( "Request terminated by an AbandonRequest for message {}", 
329                        req.getMessageId() );
330                    break;
331                }
332                
333                ClonedServerEntry entry = cursor.get();
334                session.getIoSession().write( generateResponse( session, req, entry ) );
335                LOG.debug( "Sending {}", entry.getDn() );
336                count++;
337            }
338            
339            // DO NOT WRITE THE RESPONSE - JUST RETURN IT
340            ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
341    
342            if ( ( count >= sizeLimit ) && ( cursor.next() ) )
343            {
344                // We have reached the limit
345                // Move backward on the cursor to restore the previous position, as we moved forward
346                // to check if there is one more entry available
347                cursor.previous();
348                // Special case if the user has requested more elements than the request size limit
349                ldapResult.setResultCode( ResultCodeEnum.SIZE_LIMIT_EXCEEDED );
350            }
351        }
352        
353        
354        private void readPagedResults( LdapSession session, InternalSearchRequest req, InternalLdapResult ldapResult,  
355            EntryFilteringCursor cursor, long sizeLimit, int pagedLimit, boolean isPaged, 
356            PagedSearchContext pagedContext, PagedResultsControl pagedResultsControl ) throws Exception
357        {
358            req.addAbandonListener( new SearchAbandonListener( ldapServer, cursor ) );
359            setTimeLimitsOnCursor( req, session, cursor );
360            LOG.debug( "using <{},{}> for size limit", sizeLimit, pagedLimit );
361            long cookieValue = 0;
362            
363            int count = pagedContext.getCurrentPosition();
364            int pageCount = 0;
365            
366            while ( ( count < sizeLimit ) && ( pageCount < pagedLimit ) && cursor.next() )
367            {
368                if ( session.getIoSession().isClosing() )
369                {
370                    break;
371                }
372                
373                ClonedServerEntry entry = cursor.get();
374                session.getIoSession().write( generateResponse( session, req, entry ) );
375                count++;
376                pageCount++;
377            }
378            
379            // DO NOT WRITE THE RESPONSE - JUST RETURN IT
380            ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
381            
382            boolean hasMoreEntry = cursor.next();
383            
384            if ( hasMoreEntry )
385            {
386                cursor.previous();
387            }
388    
389            if ( !hasMoreEntry )
390            {
391                // That means we don't have anymore entry
392                // If we are here, it means we have returned all the entries
393                // We have to remove the cookie from the session
394                cookieValue = pagedContext.getCookieValue();
395                PagedSearchContext psCookie = session.removePagedSearchContext( cookieValue );
396                
397                // Close the cursor if there is one
398                if ( psCookie != null )
399                {
400                    cursor = psCookie.getCursor();
401                    
402                    if ( cursor != null )
403                    {
404                        cursor.close();
405                    }
406                }
407                
408                pagedResultsControl = new PagedResultsControl();
409                pagedResultsControl.setCritical( true );
410                pagedResultsControl.setSize( 0 );
411                req.getResultResponse().add( pagedResultsControl );
412    
413                return;
414            }
415            else
416            {
417                // We have reached one limit
418                
419                if ( count < sizeLimit )
420                {
421                    // We stop here. We have to add a ResponseControl
422                    // DO NOT WRITE THE RESPONSE - JUST RETURN IT
423                    ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
424                    req.getResultResponse().add( pagedResultsControl );
425                    
426                    // Stores the cursor current position 
427                    pagedContext.incrementCurrentPosition( pageCount );
428                    return;
429                }
430                else
431                {
432                    // Return an exception, close the cursor, and clean the session
433                    ldapResult.setResultCode( ResultCodeEnum.SIZE_LIMIT_EXCEEDED );
434                    
435                    if ( cursor != null )
436                    {
437                        cursor.close();
438                    }
439                    
440                    session.removePagedSearchContext( cookieValue );
441                    
442                    return;
443                }
444            }
445        }
446        
447        
448        /**
449         * Manage the abandoned Paged Search (when paged size = 0). We have to
450         * remove the cookie and its associated cursor from the session.
451         */
452        private InternalSearchResponseDone abandonPagedSearch( LdapSession session, InternalSearchRequest req ) 
453            throws Exception
454        {
455            PagedResultsControl pagedResultsControl = null;
456            PagedResultsControl pagedSearchControl = 
457                ( PagedResultsControl )req.getControls().get( PagedResultsControl.CONTROL_OID );
458            byte [] cookie= pagedSearchControl.getCookie();
459            
460            if ( !StringTools.isEmpty( cookie ) )
461            {
462                // If the cookie is not null, we have to destroy the associated
463                // cursor stored into the session (if any)
464                int cookieValue = pagedSearchControl.getCookieValue();
465                PagedSearchContext psCookie =  session.removePagedSearchContext( cookieValue );
466                pagedResultsControl = new PagedResultsControl();
467                pagedResultsControl.setCookie( psCookie.getCookie() );
468                pagedResultsControl.setSize( 0 );
469                pagedResultsControl.setCritical( true );
470                
471                // Close the cursor
472                EntryFilteringCursor cursor = psCookie.getCursor();
473                
474                if ( cursor != null )
475                {
476                    cursor.close();
477                }
478            }
479            else
480            {
481                pagedResultsControl = new PagedResultsControl();
482                pagedResultsControl.setSize( 0 );
483                pagedResultsControl.setCritical( true );
484            }
485            
486            // and return
487            // DO NOT WRITE THE RESPONSE - JUST RETURN IT
488            InternalLdapResult ldapResult = req.getResultResponse().getLdapResult();
489            ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
490            req.getResultResponse().add( pagedResultsControl );
491            return ( InternalSearchResponseDone ) req.getResultResponse();
492        }
493        
494        
495        /**
496         * Remove a cookie instance from the session, if it exists.
497         */
498        private PagedSearchContext removeContext( LdapSession session, PagedSearchContext cookieInstance )
499        {
500            if ( cookieInstance == null )
501            {
502                return null;
503            }
504            
505            long cookieValue = cookieInstance.getCookieValue();
506            
507            return session.removePagedSearchContext( cookieValue );
508        }
509        
510        
511        /**
512         * Handle a Paged Search request.
513         */
514        private InternalSearchResponseDone doPagedSearch( LdapSession session, InternalSearchRequest req, PagedResultsControl control )
515            throws Exception
516        {
517            PagedResultsControl pagedSearchControl = ( PagedResultsControl )control;
518            PagedResultsControl pagedResultsControl = null;
519    
520            // Get the size limits
521            // Don't bother setting size limits for administrators that don't ask for it
522            long serverLimit = getServerSizeLimit( session, req );
523            
524            long requestLimit = req.getSizeLimit() == 0L ?
525                Long.MAX_VALUE : req.getSizeLimit();
526            long sizeLimit = min( serverLimit, requestLimit );
527    
528            int pagedLimit = pagedSearchControl.getSize();
529            EntryFilteringCursor cursor = null;
530            PagedSearchContext pagedContext = null;
531    
532            // We have the following cases :
533            // 1) The SIZE is 0 and the cookie is the same than the previous one : this
534            // is a abandon request for this paged search.
535            // 2) The cookie is empty : this is a new request. If the requested
536            // size is above the serverLimit and the request limit, this is a normal
537            // search
538            // 3) The cookie is not empty and the request is the same, we return
539            // the next SIZE elements
540            // 4) The cookie is not empty, but the request is not the same : this is 
541            // a new request (we have to discard the cookie and do a new search from
542            // the beginning)
543            // 5) The SIZE is above the size-limit : the request is treated as if it
544            // was a simple search
545            
546            // Case 1
547            if ( pagedLimit == 0L )
548            {
549                // An abandoned paged search
550                return abandonPagedSearch( session, req );
551            }
552            
553            // Now, depending on the cookie, we will deal with case 2, 3, 4 and 5
554            byte [] cookie= pagedSearchControl.getCookie();
555            InternalLdapResult ldapResult = req.getResultResponse().getLdapResult();
556            
557            if ( StringTools.isEmpty( cookie ) )
558            {
559                // This is a new search. We have a special case when the paged size
560                // is above the server size limit : in this case, we default to a 
561                // standard search
562                if ( pagedLimit > sizeLimit )
563                {
564                    // Normal search : create the cursor, and set pagedControl to false
565                    try
566                    {
567                        // No cursor : do a search.
568                        cursor = session.getCoreSession().search( req );
569    
570                        // Position the cursor at the beginning
571                        cursor.beforeFirst();
572                        
573                        // And read the entries
574                        readResults( session, req, ldapResult, cursor, sizeLimit );
575                    }
576                    finally
577                    {
578                        try
579                        {
580                            cursor.close();
581                        }
582                        catch ( Exception e )
583                        {
584                            LOG.error( I18n.err( I18n.ERR_168 ), e );
585                        }
586                    }
587                    
588                    // If we had a cookie in the session, remove it
589                    removeContext( session, pagedContext );
590                    return ( InternalSearchResponseDone ) req.getResultResponse();
591                }
592                else
593                {
594                    // Case 2 : create the context
595                    pagedContext = new PagedSearchContext( req );
596    
597                    session.addPagedSearchContext( pagedContext );
598                    cookie = pagedContext.getCookie();
599                    pagedResultsControl = new PagedResultsControl();
600                    pagedResultsControl.setCookie( cookie );
601                    pagedResultsControl.setSize( 0 );
602                    pagedResultsControl.setCritical( true );
603    
604    
605                    // No cursor : do a search.
606                    cursor = session.getCoreSession().search( req );
607    
608                    // Position the cursor at the beginning
609                    cursor.beforeFirst();
610                    
611                    // And stores the cursor into the session
612                    pagedContext.setCursor( cursor );
613                }
614            }
615            else
616            {
617                // We have a cookie
618                // Either case 3, 4 or 5
619                int cookieValue = pagedSearchControl.getCookieValue();
620                pagedContext = session.getPagedSearchContext( cookieValue );
621                
622                if ( pagedContext == null )
623                {
624                    // We didn't found the cookie into the session : it must be invalid
625                    // send an error.
626                    ldapResult.setErrorMessage( "Invalid cookie for this PagedSearch request." );
627                    ldapResult.setResultCode( ResultCodeEnum.UNWILLING_TO_PERFORM );
628                    
629                    return ( InternalSearchResponseDone ) req.getResultResponse();
630                }
631                
632                if ( pagedContext.hasSameRequest( req, session ) )
633                {
634                    // Case 3 : continue the search
635                    cursor = pagedContext.getCursor();
636                    
637                    // get the cookie
638                    cookie = pagedContext.getCookie();
639                    pagedResultsControl = new PagedResultsControl();
640                    pagedResultsControl.setCookie( cookie );
641                    pagedResultsControl.setSize( 0 );
642                    pagedResultsControl.setCritical( true );
643    
644                }
645                else
646                {
647                    // case 2 : create a new cursor
648                    // We have to close the cursor
649                    cursor = pagedContext.getCursor();
650                    
651                    if ( cursor != null )
652                    {
653                        cursor.close();
654                    }
655                    
656                    // Now create a new context and stores it into the session
657                    pagedContext = new PagedSearchContext( req );
658    
659                    session.addPagedSearchContext( pagedContext );
660                    
661                    cookie = pagedContext.getCookie();
662                    pagedResultsControl = new PagedResultsControl();
663                    pagedResultsControl.setCookie( cookie );
664                    pagedResultsControl.setSize( 0 );
665                    pagedResultsControl.setCritical( true );
666    
667                }
668            }
669            
670            // Now, do the real search
671            /*
672             * Iterate through all search results building and sending back responses
673             * for each search result returned.
674             */
675            try
676            {
677                readPagedResults( session, req, ldapResult, cursor, sizeLimit, pagedLimit, true, pagedContext, pagedResultsControl );
678            }
679            catch ( Exception e )
680            {
681                if ( cursor != null )
682                {
683                    try
684                    {
685                        cursor.close();
686                    }
687                    catch ( Exception ne )
688                    {
689                        LOG.error( I18n.err( I18n.ERR_168 ), ne );
690                    }
691                }
692            }
693            
694            return ( InternalSearchResponseDone ) req.getResultResponse();
695        }
696    
697        
698        /**
699         * Conducts a simple search across the result set returning each entry 
700         * back except for the search response done.  This is calculated but not
701         * returned so the persistent search mechanism can leverage this method
702         * along with standard search.<br>
703         * <br>
704         * @param session the LDAP session object for this request
705         * @param req the search request 
706         * @return the result done 
707         * @throws Exception if there are failures while processing the request
708         */
709        private InternalSearchResponseDone doSimpleSearch( LdapSession session, InternalSearchRequest req ) 
710            throws Exception
711        {
712            InternalLdapResult ldapResult = req.getResultResponse().getLdapResult();
713            
714            // Check if we are using the Paged Search Control
715            Object control = req.getControls().get( PagedResultsControl.CONTROL_OID );
716            
717            if ( control != null )
718            {
719                // Let's deal with the pagedControl
720                return doPagedSearch( session, req, (PagedResultsControl)control );
721            }
722            
723            // A normal search
724            // Check that we have a cursor or not. 
725            // No cursor : do a search.
726            EntryFilteringCursor cursor = session.getCoreSession().search( req );
727    
728            // Position the cursor at the beginning
729            cursor.beforeFirst();
730            
731            /*
732             * Iterate through all search results building and sending back responses
733             * for each search result returned.
734             */
735            try
736            {
737                // Get the size limits
738                // Don't bother setting size limits for administrators that don't ask for it
739                long serverLimit = getServerSizeLimit( session, req );
740                
741                long requestLimit = req.getSizeLimit() == 0L ?
742                    Long.MAX_VALUE : req.getSizeLimit();
743    
744                req.addAbandonListener( new SearchAbandonListener( ldapServer, cursor ) );
745                setTimeLimitsOnCursor( req, session, cursor );
746                LOG.debug( "using <{},{}> for size limit", requestLimit, serverLimit );
747                long sizeLimit = min( requestLimit, serverLimit );
748                
749                readResults( session, req, ldapResult, cursor, sizeLimit );
750            }
751            finally
752            {
753                if ( cursor != null )
754                {
755                    try
756                    {
757                        cursor.close();
758                    }
759                    catch ( Exception e )
760                    {
761                        LOG.error( I18n.err( I18n.ERR_168 ), e );
762                    }
763                }
764            }
765            
766            return ( InternalSearchResponseDone ) req.getResultResponse();
767        }
768        
769    
770        /**
771         * Generates a response for an entry retrieved from the server core based 
772         * on the nature of the request with respect to referral handling.  This 
773         * method will either generate a SearchResponseEntry or a 
774         * SearchResponseReference depending on if the entry is a referral or if 
775         * the ManageDSAITControl has been enabled.
776         *
777         * @param req the search request
778         * @param entry the entry to be handled
779         * @return the response for the entry
780         * @throws Exception if there are problems in generating the response
781         */
782        private InternalResponse generateResponse( LdapSession session, InternalSearchRequest req, ClonedServerEntry entry ) throws Exception
783        {
784            EntryAttribute ref = entry.getOriginalEntry().get( SchemaConstants.REF_AT );
785            boolean hasManageDsaItControl = req.getControls().containsKey( ManageDsaITControl.CONTROL_OID );
786    
787            if ( ( ref != null ) && ! hasManageDsaItControl )
788            {
789                // The entry is a referral.
790                InternalSearchResponseReference respRef;
791                respRef = new SearchResponseReferenceImpl( req.getMessageId() );
792                respRef.setReferral( new ReferralImpl() );
793                
794                for ( Value<?> val : ref )
795                {
796                    String url = val.getString();
797                    
798                    if ( ! url.startsWith( "ldap" ) )
799                    {
800                        respRef.getReferral().addLdapUrl( url );
801                    }
802                    
803                    LdapURL ldapUrl = new LdapURL();
804                    ldapUrl.setForceScopeRendering( true );
805                    try
806                    {
807                        ldapUrl.parse( url.toCharArray() );
808                    }
809                    catch ( LdapURLEncodingException e )
810                    {
811                        LOG.error( I18n.err( I18n.ERR_165, url, entry ) );
812                    }
813    
814                    switch( req.getScope() )
815                    {
816                        case SUBTREE:
817                            ldapUrl.setScope( SearchScope.SUBTREE.getScope() );
818                            break;
819                            
820                        case ONELEVEL: // one level here is object level on remote server
821                            ldapUrl.setScope( SearchScope.OBJECT.getScope() );
822                            break;
823                            
824                        default:
825                            throw new IllegalStateException( I18n.err( I18n.ERR_686 ) );
826                    }
827                    
828                    respRef.getReferral().addLdapUrl( ldapUrl.toString() );
829                }
830                
831                return respRef;
832            }
833            else 
834            {
835                // The entry is not a referral, or the ManageDsaIt control is set
836                InternalSearchResponseEntry respEntry;
837                respEntry = new SearchResponseEntryImpl( req.getMessageId() );
838                respEntry.setEntry( entry );
839                respEntry.setObjectName( entry.getDn() );
840                
841                // Filter the userPassword if the server mandate to do so
842                if ( session.getCoreSession().getDirectoryService().isPasswordHidden() )
843                {
844                    // Remove the userPassord attribute from the entry.
845                    respEntry.getEntry().removeAttributes( SchemaConstants.USER_PASSWORD_AT );
846                }
847                
848                return respEntry;
849            }
850        }
851        
852        
853        /**
854         * Alters the filter expression based on the presence of the 
855         * ManageDsaIT control.  If the control is not present, the search
856         * filter will be altered to become a disjunction with two terms.
857         * The first term is the original filter.  The second term is a
858         * (objectClass=referral) assertion.  When OR'd together these will
859         * make sure we get all referrals so we can process continuations 
860         * properly without having the filter remove them from the result 
861         * set.
862         * 
863         * NOTE: original filter is first since most entries are not referrals 
864         * so it has a higher probability on average of accepting and shorting 
865         * evaluation before having to waste cycles trying to evaluate if the 
866         * entry is a referral.
867         *
868         * @param session the session to use to construct the filter (schema access)
869         * @param req the request to get the original filter from
870         * @throws Exception if there are schema access problems
871         */
872        public void modifyFilter( LdapSession session, InternalSearchRequest req ) throws Exception
873        {
874            if ( req.hasControl( ManageDsaITControl.CONTROL_OID ) )
875            {
876                return;
877            }
878            
879            /*
880             * Do not add the OR'd (objectClass=referral) expression if the user 
881             * searches for the subSchemaSubEntry as the SchemaIntercepter can't 
882             * handle an OR'd filter.
883             */
884            if ( isSubSchemaSubEntrySearch( session, req ) )
885            {
886                return;
887            }
888            
889            /*
890             * Most of the time the search filter is just (objectClass=*) and if 
891             * this is the case then there's no reason at all to OR this with an
892             * (objectClass=referral).  If we detect this case then we leave it 
893             * as is to represent the OR condition:
894             * 
895             *  (| (objectClass=referral)(objectClass=*)) == (objectClass=*)
896             */
897            if ( req.getFilter() instanceof PresenceNode )
898            {
899                PresenceNode presenceNode = ( PresenceNode ) req.getFilter();
900                
901                AttributeType at = session.getCoreSession().getDirectoryService()
902                    .getSchemaManager().lookupAttributeTypeRegistry( presenceNode.getAttribute() );
903                if ( at.getOid().equals( SchemaConstants.OBJECT_CLASS_AT_OID ) )
904                {
905                    return;
906                }
907            }
908    
909            // using varags to add two expressions to an OR node 
910            req.setFilter( new OrNode( req.getFilter(), newIsReferralEqualityNode( session ) ) );
911        }
912        
913        
914        /**
915         * Main message handing method for search requests.  This will be called 
916         * even if the ManageDsaIT control is present because the super class does
917         * not know that the search operation has more to do after finding the 
918         * base.  The call to this means that finding the base can ignore 
919         * referrals.
920         * 
921         * @param session the associated session
922         * @param req the received SearchRequest
923         */
924        public void handleIgnoringReferrals( LdapSession session, InternalSearchRequest req )
925        {
926            if ( IS_DEBUG )
927            {
928                LOG.debug( "Message received:  {}", req.toString() );
929            }
930    
931            // A flag set if we have a persistent search
932            boolean isPersistentSearch = false;
933            
934            // A flag set when we've got an exception while processing a
935            // persistent search
936            boolean persistentSearchException = false;
937            
938            // add the search request to the registry of outstanding requests for this session
939            session.registerOutstandingRequest( req );
940    
941            try
942            {
943                // ===============================================================
944                // Handle search in rootDSE differently.
945                // ===============================================================
946                if ( isRootDSESearch( req ) )
947                {
948                    handleRootDseSearch( session, req );
949                    
950                    return;
951                }
952    
953                // modify the filter to affect continuation support
954                modifyFilter( session, req );
955                
956                // ===============================================================
957                // Handle psearch differently
958                // ===============================================================
959    
960                PersistentSearchControl psearchControl = ( PersistentSearchControl ) 
961                    req.getControls().get( PersistentSearchControl.CONTROL_OID );
962                
963                if ( psearchControl != null )
964                {
965                    // Set the flag to avoid the request being removed
966                    // from the session
967                    isPersistentSearch = true;
968    
969                    handlePersistentSearch( session, req, psearchControl );
970                    
971                    return;
972                }
973    
974                // ===============================================================
975                // Handle regular search requests from here down
976                // ===============================================================
977    
978                InternalSearchResponseDone done = doSimpleSearch( session, req );
979                session.getIoSession().write( done );
980            }
981            catch ( Exception e )
982            {
983                /*
984                 * From RFC 2251 Section 4.11:
985                 *
986                 * In the event that a server receives an Abandon Request on a Search
987                 * operation in the midst of transmitting responses to the Search, that
988                 * server MUST cease transmitting entry responses to the abandoned
989                 * request immediately, and MUST NOT send the SearchResultDone. Of
990                 * course, the server MUST ensure that only properly encoded LDAPMessage
991                 * PDUs are transmitted.
992                 *
993                 * SO DON'T SEND BACK ANYTHING!!!!!
994                 */
995                if ( e instanceof OperationAbandonedException )
996                {
997                    return;
998                }
999    
1000                // If it was a persistent search and if we had an exception,
1001                // we set the flag to remove the request from the session
1002                if ( isPersistentSearch )
1003                {
1004                    persistentSearchException = true;
1005                }
1006                
1007                handleException( session, req, e );
1008            }
1009            finally 
1010            {
1011                
1012                // remove the request from the session, except if
1013                // we didn't got an exception for a Persistent search 
1014                if ( !isPersistentSearch || persistentSearchException )
1015                {
1016                    session.unregisterOutstandingRequest( req );
1017                }
1018            }
1019        }
1020    
1021    
1022        /**
1023         * Handles processing with referrals without ManageDsaIT control.
1024         */
1025        public void handleWithReferrals( LdapSession session, DN reqTargetDn, InternalSearchRequest req ) throws LdapException
1026        {
1027            InternalLdapResult result = req.getResultResponse().getLdapResult();
1028            ClonedServerEntry entry = null;
1029            boolean isReferral = false;
1030            boolean isparentReferral = false;
1031            ReferralManager referralManager = session.getCoreSession().getDirectoryService().getReferralManager();
1032            
1033            reqTargetDn.normalize( session.getCoreSession().getDirectoryService().
1034                getSchemaManager().getNormalizerMapping() );
1035            
1036            // Check if the entry itself is a referral
1037            referralManager.lockRead();
1038            
1039            isReferral = referralManager.isReferral( reqTargetDn );
1040            
1041            if ( !isReferral )
1042            {
1043                // Check if the entry has a parent which is a referral
1044                isparentReferral = referralManager.hasParentReferral( reqTargetDn );
1045            }
1046            
1047            referralManager.unlock();
1048            
1049            if ( !isReferral && !isparentReferral )
1050            {
1051                // This is not a referral and it does not have a parent which 
1052                // is a referral : standard case, just deal with the request
1053                LOG.debug( "Entry {} is NOT a referral.", reqTargetDn );
1054                handleIgnoringReferrals( session, req );
1055                return;
1056            }
1057            else
1058            {
1059                // -------------------------------------------------------------------
1060                // Lookup Entry
1061                // -------------------------------------------------------------------
1062                
1063                // try to lookup the entry but ignore exceptions when it does not   
1064                // exist since entry may not exist but may have an ancestor that is a 
1065                // referral - would rather attempt a lookup that fails then do check 
1066                // for existence than have to do another lookup to get entry info
1067                try
1068                {
1069                    entry = session.getCoreSession().lookup( reqTargetDn );
1070                    LOG.debug( "Entry for {} was found: ", reqTargetDn, entry );
1071                }
1072                catch ( LdapException e )
1073                {
1074                    /* ignore */
1075                    LOG.debug( "Entry for {} not found.", reqTargetDn );
1076                }
1077                catch ( Exception e )
1078                {
1079                    /* serious and needs handling */
1080                    handleException( session, req, e );
1081                    return;
1082                }
1083                
1084                // -------------------------------------------------------------------
1085                // Handle Existing Entry
1086                // -------------------------------------------------------------------
1087                
1088                if ( entry != null )
1089                {
1090                    try
1091                    {
1092                        LOG.debug( "Entry is a referral: {}", entry );
1093                        
1094                        handleReferralEntryForSearch( session, ( InternalSearchRequest ) req, entry );
1095    
1096                        return;
1097                    }
1098                    catch ( Exception e )
1099                    {
1100                        handleException( session, req, e );
1101                    }
1102                }
1103        
1104                // -------------------------------------------------------------------
1105                // Handle Non-existing Entry
1106                // -------------------------------------------------------------------
1107                
1108                // if the entry is null we still have to check for a referral ancestor
1109                // also the referrals need to be adjusted based on the ancestor's ref
1110                // values to yield the correct path to the entry in the target DSAs
1111                
1112                else
1113                {
1114                    // The entry is null : it has a parent referral.
1115                    ClonedServerEntry referralAncestor = null;
1116        
1117                    try
1118                    {
1119                        referralAncestor = getFarthestReferralAncestor( session, reqTargetDn );
1120                    }
1121                    catch ( Exception e )
1122                    {
1123                        handleException( session, req, e );
1124                        return;
1125                    }
1126        
1127                    if ( referralAncestor == null )
1128                    {
1129                        result.setErrorMessage( "Entry not found." );
1130                        result.setResultCode( ResultCodeEnum.NO_SUCH_OBJECT );
1131                        session.getIoSession().write( req.getResultResponse() );
1132                        return;
1133                    }
1134                      
1135                    // if we get here then we have a valid referral ancestor
1136                    try
1137                    {
1138                        InternalReferral referral = getReferralOnAncestorForSearch( session, ( InternalSearchRequest ) req, referralAncestor );
1139                        
1140                        result.setResultCode( ResultCodeEnum.REFERRAL );
1141                        result.setReferral( referral );
1142                        session.getIoSession().write( req.getResultResponse() );
1143                    }
1144                    catch ( Exception e )
1145                    {
1146                        handleException( session, req, e );
1147                    }
1148                }
1149            }
1150        }
1151        
1152        
1153        /**
1154         * Handles processing a referral response on a target entry which is a 
1155         * referral.  It will for any request that returns an LdapResult in it's 
1156         * response.
1157         *
1158         * @param session the session to use for processing
1159         * @param reqTargetDn the dn of the target entry of the request
1160         * @param req the request
1161         * @param entry the entry associated with the request
1162         */
1163        private void handleReferralEntryForSearch( LdapSession session, InternalSearchRequest req, ClonedServerEntry entry )
1164            throws Exception
1165        {
1166            InternalLdapResult result = req.getResultResponse().getLdapResult();
1167            ReferralImpl referral = new ReferralImpl();
1168            result.setReferral( referral );
1169            result.setResultCode( ResultCodeEnum.REFERRAL );
1170            result.setErrorMessage( "Encountered referral attempting to handle request." );
1171            result.setMatchedDn( req.getBase() );
1172    
1173            EntryAttribute refAttr = entry.getOriginalEntry().get( SchemaConstants.REF_AT );
1174            
1175            for ( Value<?> refval : refAttr )
1176            {
1177                String refstr = refval.getString();
1178                
1179                // need to add non-ldap URLs as-is
1180                if ( ! refstr.startsWith( "ldap" ) )
1181                {
1182                    referral.addLdapUrl( refstr );
1183                    continue;
1184                }
1185                
1186                // parse the ref value and normalize the DN  
1187                LdapURL ldapUrl = new LdapURL();
1188                try
1189                {
1190                    ldapUrl.parse( refstr.toCharArray() );
1191                }
1192                catch ( LdapURLEncodingException e )
1193                {
1194                    LOG.error( I18n.err( I18n.ERR_165, refstr, entry ) );
1195                    continue;
1196                }
1197                
1198                ldapUrl.setForceScopeRendering( true );
1199                ldapUrl.setAttributes( req.getAttributes() );
1200                ldapUrl.setScope( req.getScope().getScope() );
1201                referral.addLdapUrl( ldapUrl.toString() );
1202            }
1203    
1204            session.getIoSession().write( req.getResultResponse() );
1205        }
1206        
1207        
1208        /**
1209         * Determines if a search request is on the RootDSE of the server.
1210         * 
1211         * It is a RootDSE search if :
1212         * - the base DN is empty
1213         * - and the scope is BASE OBJECT
1214         * - and the filter is (ObjectClass = *)
1215         * 
1216         * (RFC 4511, 5.1, par. 1 & 2)
1217         *
1218         * @param req the request issued
1219         * @return true if the search is on the RootDSE false otherwise
1220         */
1221        private static boolean isRootDSESearch( InternalSearchRequest req )
1222        {
1223            boolean isBaseIsRoot = req.getBase().isEmpty();
1224            boolean isBaseScope = req.getScope() == SearchScope.OBJECT;
1225            boolean isRootDSEFilter = false;
1226            
1227            if ( req.getFilter() instanceof PresenceNode )
1228            {
1229                String attribute = ( ( PresenceNode ) req.getFilter() ).getAttribute();
1230                isRootDSEFilter = attribute.equalsIgnoreCase( SchemaConstants.OBJECT_CLASS_AT ) ||
1231                                    attribute.equals( SchemaConstants.OBJECT_CLASS_AT_OID );
1232            }
1233            
1234            return isBaseIsRoot && isBaseScope && isRootDSEFilter;
1235        }
1236        
1237        
1238        /**
1239         * <p>
1240         * Determines if a search request is a subSchemaSubEntry search.
1241         * </p>
1242         * <p>
1243         * It is a schema search if:
1244         * - the base DN is the DN of the subSchemaSubEntry of the root DSE
1245         * - and the scope is BASE OBJECT
1246         * - and the filter is (objectClass=subschema)
1247         * (RFC 4512, 4.4,)
1248         * </p>
1249         * <p>
1250         * However in this method we only check the first condition to avoid
1251         * performance issues.
1252         * </p>
1253         * 
1254         * @param session the LDAP session
1255         * @param req the request issued
1256         * 
1257         * @return true if the search is on the subSchemaSubEntry, false otherwise
1258         * 
1259         * @throws Exception the exception
1260         */
1261        private static boolean isSubSchemaSubEntrySearch( LdapSession session, InternalSearchRequest req ) throws Exception
1262        {
1263            DN base = req.getBase();
1264            String baseNormForm = ( base.isNormalized() ? base.getNormName() : base.getNormName() );
1265    
1266            DirectoryService ds = session.getCoreSession().getDirectoryService();
1267            PartitionNexus nexus = ds.getPartitionNexus();
1268            Value<?> subschemaSubentry = nexus.getRootDSE( null ).get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
1269            DN subschemaSubentryDn = new DN( subschemaSubentry.getString() );
1270            subschemaSubentryDn.normalize( ds.getSchemaManager().getNormalizerMapping() );
1271            String subschemaSubentryDnNorm = subschemaSubentryDn.getNormName();
1272            
1273            return subschemaSubentryDnNorm.equals( baseNormForm );
1274        }
1275    }