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