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