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 }