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 }