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 org.apache.directory.server.core.entry.ClonedServerEntry;
024    import org.apache.directory.server.i18n.I18n;
025    import org.apache.directory.server.ldap.LdapSession;
026    import org.apache.directory.shared.ldap.codec.controls.ManageDsaITControl;
027    import org.apache.directory.shared.ldap.codec.util.LdapURLEncodingException;
028    import org.apache.directory.shared.ldap.constants.SchemaConstants;
029    import org.apache.directory.shared.ldap.entry.EntryAttribute;
030    import org.apache.directory.shared.ldap.entry.Value;
031    import org.apache.directory.shared.ldap.exception.LdapException;
032    import org.apache.directory.shared.ldap.exception.LdapInvalidDnException;
033    import org.apache.directory.shared.ldap.exception.LdapOperationException;
034    import org.apache.directory.shared.ldap.message.ReferralImpl;
035    import org.apache.directory.shared.ldap.message.ResultCodeEnum;
036    import org.apache.directory.shared.ldap.message.internal.InternalLdapResult;
037    import org.apache.directory.shared.ldap.message.internal.InternalReferral;
038    import org.apache.directory.shared.ldap.message.internal.InternalResultResponseRequest;
039    import org.apache.directory.shared.ldap.message.internal.InternalSearchRequest;
040    import org.apache.directory.shared.ldap.name.DN;
041    import org.apache.directory.shared.ldap.util.ExceptionUtils;
042    import org.apache.directory.shared.ldap.util.LdapURL;
043    import org.slf4j.Logger;
044    import org.slf4j.LoggerFactory;
045    
046    
047    /**
048     * A based class for handlers which deal with SingleReplyRequests.  This class 
049     * provides various capabilities out of the box for these kinds of requests so
050     * common handling code is not duplicated.  Namely, exception handling and 
051     * referral handling code common to most SingleReplyRequests (minus 
052     * ExtendedRequests) are handled thanks to this class.
053     *
054     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
055     * @version $Rev$, $Date$
056     */
057    public abstract class ReferralAwareRequestHandler<T extends InternalResultResponseRequest> extends LdapRequestHandler<T>
058    {
059        private static final Logger LOG = LoggerFactory.getLogger( ReferralAwareRequestHandler.class );
060        
061        /** Speedup for logs */
062        private static final boolean IS_DEBUG = LOG.isDebugEnabled();
063    
064        
065        /* (non-Javadoc)
066         * @see org.apache.directory.server.ldap.handlers.LdapRequestHandler#handle(org.apache.directory.server.ldap.LdapSession, org.apache.directory.shared.ldap.message.Request)
067         */
068        @Override
069        public final void handle( LdapSession session, T req ) throws Exception
070        {
071            LOG.debug( "Handling single reply request: {}", req );
072            
073            // First, if we have the ManageDSAIt control, go directly
074            // to the handling without pre-processing the request
075            if ( req.getControls().containsKey( ManageDsaITControl.CONTROL_OID ) )
076            {
077                // If the ManageDsaIT control is present, we will
078                // consider that the user wants to get entry which
079                // are referrals as plain entry. We have to return
080                // SearchResponseEntry elements instead of 
081                // SearchResponseReference elements.
082                LOG.debug( "ManageDsaITControl detected." );
083                handleIgnoringReferrals( session, req );
084            }
085            else
086            {
087                // No ManageDsaIT control. If the found entries is a referral,
088                // we will return SearchResponseReference elements.
089                LOG.debug( "ManageDsaITControl NOT detected." );
090        
091                switch ( req.getType() )
092                {
093                    case SEARCH_REQUEST:
094                        handleWithReferrals( session, ( ( InternalSearchRequest ) req ).getBase(), req );
095                        break;
096    
097                    case EXTENDED_REQUEST:
098                        throw new IllegalStateException( I18n.err( I18n.ERR_684 ) );
099                        
100                    default:
101                        throw new IllegalStateException( I18n.err( I18n.ERR_685, req ) );
102                }
103                
104            }
105    
106        }
107    
108        
109        public static final boolean isEntryReferral( ClonedServerEntry entry ) throws Exception
110        {
111            return entry.getOriginalEntry().contains( SchemaConstants.OBJECT_CLASS_AT, SchemaConstants.REFERRAL_OC );
112        }
113        
114        
115        /**
116         * Searches up the ancestry of a DN searching for the farthest referral 
117         * ancestor.  This is required to properly handle referrals.  Note that 
118         * this function is quite costly since it attempts to lookup all the 
119         * ancestors up the hierarchy just to see if they represent referrals. 
120         * Techniques can be employed later to improve this performance hit by
121         * having an intelligent referral cache.
122         *
123         * @return the farthest referral ancestor or null
124         * @throws Exception if there are problems during this search
125         */
126        public static final ClonedServerEntry getFarthestReferralAncestor( LdapSession session, DN target ) 
127            throws Exception
128        {
129            ClonedServerEntry entry;
130            ClonedServerEntry farthestReferralAncestor = null;
131            DN dn = ( DN ) target.clone();
132            
133            try
134            {
135                dn.remove( dn.size() - 1 );
136            }
137            catch ( LdapInvalidDnException e2 )
138            {
139                // never thrown
140            }
141            
142            while ( ! dn.isEmpty() )
143            {
144                LOG.debug( "Walking ancestors of {} to find referrals.", dn );
145                
146                try
147                {
148                    entry = session.getCoreSession().lookup( dn );
149    
150                    if ( isEntryReferral( entry ) )
151                    {
152                        farthestReferralAncestor = entry;
153                    }
154    
155                    dn.remove( dn.size() - 1 );
156                }
157                catch ( LdapException e )
158                {
159                    LOG.debug( "Entry for {} not found.", dn );
160    
161                    // update the DN as we strip last component 
162                    try
163                    {
164                        dn.remove( dn.size() - 1 );
165                    }
166                    catch ( LdapInvalidDnException e1 )
167                    {
168                        // never happens
169                    }
170                }
171            }
172            
173            return farthestReferralAncestor;
174        }
175        
176        
177        /**
178         * Handles processing with referrals without ManageDsaIT control and with 
179         * an ancestor that is a referral.  The original entry was not found and 
180         * the walk of the ancestry returned a referral.
181         * 
182         * @param referralAncestor the farthest referral ancestor of the missing 
183         * entry  
184         */
185        public InternalReferral getReferralOnAncestor( LdapSession session, DN reqTargetDn, T req, 
186            ClonedServerEntry referralAncestor ) throws Exception
187        {
188            LOG.debug( "Inside getReferralOnAncestor()" );
189            
190            EntryAttribute refAttr =referralAncestor.getOriginalEntry()
191                .get( SchemaConstants.REF_AT );
192            InternalReferral referral = new ReferralImpl();
193    
194            for ( Value<?> value : refAttr )
195            {
196                String ref = value.getString();
197    
198                LOG.debug( "Calculating LdapURL for referrence value {}", ref );
199    
200                // need to add non-ldap URLs as-is
201                if ( ! ref.startsWith( "ldap" ) )
202                {
203                    referral.addLdapUrl( ref );
204                    continue;
205                }
206                
207                // parse the ref value and normalize the DN  
208                LdapURL ldapUrl = new LdapURL();
209                try
210                {
211                    ldapUrl.parse( ref.toCharArray() );
212                }
213                catch ( LdapURLEncodingException e )
214                {
215                    LOG.error( I18n.err( I18n.ERR_165, ref, referralAncestor ) );
216                }
217                
218                DN urlDn = new DN( ldapUrl.getDn().getName() );
219                urlDn.normalize( session.getCoreSession().getDirectoryService().getSchemaManager()
220                    .getNormalizerMapping() ); 
221                
222                if ( urlDn.getNormName().equals( referralAncestor.getDn().getNormName() ) )
223                {
224                    // according to the protocol there is no need for the dn since it is the same as this request
225                    StringBuilder buf = new StringBuilder();
226                    buf.append( ldapUrl.getScheme() );
227                    buf.append( ldapUrl.getHost() );
228    
229                    if ( ldapUrl.getPort() > 0 )
230                    {
231                        buf.append( ":" );
232                        buf.append( ldapUrl.getPort() );
233                    }
234    
235                    referral.addLdapUrl( buf.toString() );
236                    continue;
237                }
238                
239                /*
240                 * If we get here then the DN of the referral was not the same as the 
241                 * DN of the ref LDAP URL.  We must calculate the remaining (difference)
242                 * name past the farthest referral DN which the target name extends.
243                 */
244                int diff = reqTargetDn.size() - referralAncestor.getDn().size();
245                DN extra = new DN();
246    
247                // TODO - fix this by access unormalized RDN values
248                // seems we have to do this because get returns normalized rdns
249                DN reqUnnormalizedDn = new DN( reqTargetDn.getName() );
250                for ( int jj = 0; jj < diff; jj++ )
251                {
252                    extra.add( reqUnnormalizedDn.get( referralAncestor.getDn().size() + jj ) );
253                }
254    
255                urlDn.addAll( extra );
256    
257                StringBuilder buf = new StringBuilder();
258                buf.append( ldapUrl.getScheme() );
259                buf.append( ldapUrl.getHost() );
260    
261                if ( ldapUrl.getPort() > 0 )
262                {
263                    buf.append( ":" );
264                    buf.append( ldapUrl.getPort() );
265                }
266    
267                buf.append( "/" );
268                buf.append( LdapURL.urlEncode( urlDn.getName(), false ) );
269                referral.addLdapUrl( buf.toString() );
270            }
271            
272            return referral;
273        }
274        
275        
276        /**
277         * Handles processing with referrals without ManageDsaIT control and with 
278         * an ancestor that is a referral.  The original entry was not found and 
279         * the walk of the ancestry returned a referral.
280         * 
281         * @param referralAncestor the farthest referral ancestor of the missing 
282         * entry  
283         */
284        public InternalReferral getReferralOnAncestorForSearch( LdapSession session, InternalSearchRequest req, 
285            ClonedServerEntry referralAncestor ) throws Exception
286        {
287            LOG.debug( "Inside getReferralOnAncestor()" );
288         
289            EntryAttribute refAttr = referralAncestor.getOriginalEntry()
290                .get( SchemaConstants.REF_AT );
291            InternalReferral referral = new ReferralImpl();
292    
293            for ( Value<?> value : refAttr )
294            {
295                String ref = value.getString();
296    
297                LOG.debug( "Calculating LdapURL for referrence value {}", ref );
298    
299                // need to add non-ldap URLs as-is
300                if ( ! ref.startsWith( "ldap" ) )
301                {
302                    referral.addLdapUrl( ref );
303                    continue;
304                }
305                
306                // Parse the ref value   
307                LdapURL ldapUrl = new LdapURL();
308                try
309                {
310                    ldapUrl.parse( ref.toCharArray() );
311                }
312                catch ( LdapURLEncodingException e )
313                {
314                    LOG.error( I18n.err( I18n.ERR_165, ref, referralAncestor ) );
315                }
316                
317                // Normalize the DN to check for same dn
318                DN urlDn = new DN( ldapUrl.getDn().getName() );
319                urlDn.normalize( session.getCoreSession().getDirectoryService().getSchemaManager()
320                    .getNormalizerMapping() ); 
321                
322                if ( urlDn.getNormName().equals( req.getBase().getNormName() ) )
323                {
324                    ldapUrl.setForceScopeRendering( true );
325                    ldapUrl.setAttributes( req.getAttributes() );
326                    ldapUrl.setScope( req.getScope().getScope() );
327                    referral.addLdapUrl( ldapUrl.toString() );
328                    continue;
329                }
330                
331                /*
332                 * If we get here then the DN of the referral was not the same as the 
333                 * DN of the ref LDAP URL.  We must calculate the remaining (difference)
334                 * name past the farthest referral DN which the target name extends.
335                 */
336                int diff = req.getBase().size() - referralAncestor.getDn().size();
337                DN extra = new DN();
338    
339                // TODO - fix this by access unormalized RDN values
340                // seems we have to do this because get returns normalized rdns
341                DN reqUnnormalizedDn = new DN( req.getBase().getName() );
342                for ( int jj = 0; jj < diff; jj++ )
343                {
344                    extra.add( reqUnnormalizedDn.get( referralAncestor.getDn().size() + jj ) );
345                }
346    
347                ldapUrl.getDn().addAll( extra );
348                ldapUrl.setForceScopeRendering( true );
349                ldapUrl.setAttributes( req.getAttributes() );
350                ldapUrl.setScope( req.getScope().getScope() );
351                referral.addLdapUrl( ldapUrl.toString() );
352            }
353            
354            return referral;
355        }
356        
357        
358        /**
359         * Handles processing with referrals without ManageDsaIT control.
360         */
361        public void handleException( LdapSession session, InternalResultResponseRequest req, Exception e )
362        {
363            InternalLdapResult result = req.getResultResponse().getLdapResult();
364    
365            /*
366             * Set the result code or guess the best option.
367             */
368            ResultCodeEnum code;
369            
370            if ( e instanceof LdapOperationException )
371            {
372                code = ( ( LdapOperationException ) e ).getResultCode();
373            }
374            else
375            {
376                code = ResultCodeEnum.getBestEstimate( e, req.getType() );
377            }
378            
379            result.setResultCode( code );
380    
381            /*
382             * Setup the error message to put into the request and put entire
383             * exception into the message if we are in debug mode.  Note we 
384             * embed the result code name into the message.
385             */
386            String msg = code.toString() + ": failed for " + req + ": " + e.getLocalizedMessage();
387            LOG.debug( msg, e );
388            
389            if ( IS_DEBUG )
390            {
391                msg += ":\n" + ExceptionUtils.getStackTrace( e );
392            }
393            
394            result.setErrorMessage( msg );
395    
396            if ( e instanceof LdapOperationException )
397            {
398                LdapOperationException ne = ( LdapOperationException ) e;
399    
400                // Add the matchedDN if necessary
401                boolean setMatchedDn = 
402                    code == ResultCodeEnum.NO_SUCH_OBJECT             || 
403                    code == ResultCodeEnum.ALIAS_PROBLEM              ||
404                    code == ResultCodeEnum.INVALID_DN_SYNTAX          || 
405                    code == ResultCodeEnum.ALIAS_DEREFERENCING_PROBLEM;
406                
407                if ( ( ne.getResolvedDn() != null ) && setMatchedDn )
408                {
409                    result.setMatchedDn( ( DN ) ne.getResolvedDn() );
410                }
411            }
412    
413            session.getIoSession().write( req.getResultResponse() );
414        }
415    
416        
417        /**
418         * Handles processing without referral handling in effect: either with the
419         * ManageDsaIT control or when the entry or all of it's ancestors are non-
420         * referral entries.
421         * 
422         * Implementors
423         * 
424         * @param session the LDAP session under which processing occurs
425         * @param reqTargetDn the target entry DN associated with the request
426         * @param entry the target entry if it exists and has been looked up, may 
427         * be null even if the entry exists, offered in case the entry is looked
428         * up to avoid repeat lookups.  Implementations should check if the entry
429         * is null and attempt a lookup instead of presuming the entry does not 
430         * exist.
431         * @param req the request to be handled
432         */
433        public abstract void handleIgnoringReferrals( LdapSession session, T req );
434    
435    
436        /**
437         * Handles processing with referrals without ManageDsaIT control.
438         */
439        public abstract void handleWithReferrals( LdapSession session, DN reqTargetDn, T req ) throws LdapException;
440    }