001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.network;
018
019import java.net.URI;
020import java.util.Hashtable;
021import java.util.Map;
022import java.util.concurrent.ConcurrentHashMap;
023
024import javax.naming.CommunicationException;
025import javax.naming.Context;
026import javax.naming.NamingEnumeration;
027import javax.naming.directory.Attributes;
028import javax.naming.directory.DirContext;
029import javax.naming.directory.InitialDirContext;
030import javax.naming.directory.SearchControls;
031import javax.naming.directory.SearchResult;
032import javax.naming.event.EventDirContext;
033import javax.naming.event.NamespaceChangeListener;
034import javax.naming.event.NamingEvent;
035import javax.naming.event.NamingExceptionEvent;
036import javax.naming.event.ObjectChangeListener;
037
038import org.apache.activemq.util.URISupport;
039import org.apache.activemq.util.URISupport.CompositeData;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * class to create dynamic network connectors listed in an directory server
045 * using the LDAP v3 protocol as defined in RFC 2251, the entries listed in the
046 * directory server must implement the ipHost and ipService objectClasses as
047 * defined in RFC 2307.
048 *
049 * @see <a href="http://www.faqs.org/rfcs/rfc2251.html">RFC 2251</a>
050 * @see <a href="http://www.faqs.org/rfcs/rfc2307.html">RFC 2307</a>
051 *
052 * @org.apache.xbean.XBean element="ldapNetworkConnector"
053 */
054public class LdapNetworkConnector extends NetworkConnector implements NamespaceChangeListener, ObjectChangeListener {
055    private static final Logger LOG = LoggerFactory.getLogger(LdapNetworkConnector.class);
056
057    // force returned entries to implement the ipHost and ipService object classes (RFC 2307)
058    private static final String REQUIRED_OBJECT_CLASS_FILTER =
059            "(&(objectClass=ipHost)(objectClass=ipService))";
060
061    // connection
062    private URI[] availableURIs = null;
063    private int availableURIsIndex = 0;
064    private String base = null;
065    private boolean failover = false;
066    private long curReconnectDelay = 1000; /* 1 sec */
067    private long maxReconnectDelay = 30000; /* 30 sec */
068
069    // authentication
070    private String user = null;
071    private String password = null;
072    private boolean anonymousAuthentication = false;
073
074    // search
075    private SearchControls searchControls = new SearchControls(/* ONELEVEL_SCOPE */);
076    private String searchFilter = REQUIRED_OBJECT_CLASS_FILTER;
077    private boolean searchEventListener = false;
078
079    // connector management
080    private Map<URI, NetworkConnector> connectorMap = new ConcurrentHashMap<URI, NetworkConnector>();
081    private Map<URI, Integer> referenceMap = new ConcurrentHashMap<URI, Integer>();
082    private Map<String, URI> uuidMap = new ConcurrentHashMap<String, URI>();
083
084    // local context
085    private DirContext context = null;
086    // currently in use URI
087    private URI ldapURI = null;
088
089    /**
090     * returns the next URI from the configured list
091     *
092     * @return random URI from the configured list
093     */
094    public URI getUri() {
095        return availableURIs[++availableURIsIndex % availableURIs.length];
096    }
097
098    /**
099     * sets the LDAP server URI
100     *
101     * @param uri
102     *            LDAP server URI
103     */
104    public void setUri(URI uri) throws Exception {
105        CompositeData data = URISupport.parseComposite(uri);
106        if (data.getScheme().equals("failover")) {
107            availableURIs = data.getComponents();
108            failover = true;
109        } else {
110            availableURIs = new URI[] { uri };
111        }
112    }
113
114    /**
115     * sets the base LDAP dn used for lookup operations
116     *
117     * @param base
118     *            LDAP base dn
119     */
120    public void setBase(String base) {
121        this.base = base;
122    }
123
124    /**
125     * sets the LDAP user for access credentials
126     *
127     * @param user
128     *            LDAP dn of user
129     */
130    public void setUser(String user) {
131        this.user = user;
132    }
133
134    /**
135     * sets the LDAP password for access credentials
136     *
137     * @param password
138     *            user password
139     */
140    @Override
141    public void setPassword(String password) {
142        this.password = password;
143    }
144
145    /**
146     * sets LDAP anonymous authentication access credentials
147     *
148     * @param anonymousAuthentication
149     *            set to true to use anonymous authentication
150     */
151    public void setAnonymousAuthentication(boolean anonymousAuthentication) {
152        this.anonymousAuthentication = anonymousAuthentication;
153    }
154
155    /**
156     * sets the LDAP search scope
157     *
158     * @param searchScope
159     *            LDAP JNDI search scope
160     */
161    public void setSearchScope(String searchScope) throws Exception {
162        int scope;
163        if (searchScope.equals("OBJECT_SCOPE")) {
164            scope = SearchControls.OBJECT_SCOPE;
165        } else if (searchScope.equals("ONELEVEL_SCOPE")) {
166            scope = SearchControls.ONELEVEL_SCOPE;
167        } else if (searchScope.equals("SUBTREE_SCOPE")) {
168            scope = SearchControls.SUBTREE_SCOPE;
169        } else {
170            throw new Exception("ERR: unknown LDAP search scope specified: " + searchScope);
171        }
172        searchControls.setSearchScope(scope);
173    }
174
175    /**
176     * sets the LDAP search filter as defined in RFC 2254
177     *
178     * @param searchFilter
179     *            LDAP search filter
180     * @see <a href="http://www.faqs.org/rfcs/rfc2254.html">RFC 2254</a>
181     */
182    public void setSearchFilter(String searchFilter) {
183        this.searchFilter = "(&" + REQUIRED_OBJECT_CLASS_FILTER + "(" + searchFilter + "))";
184    }
185
186    /**
187     * enables/disable a persistent search to the LDAP server as defined in
188     * draft-ietf-ldapext-psearch-03.txt (2.16.840.1.113730.3.4.3)
189     *
190     * @param searchEventListener
191     *            enable = true, disable = false (default)
192     * @see <a
193     *      href="http://www.ietf.org/proceedings/01mar/I-D/draft-ietf-ldapext-psearch-03.txt">draft-ietf-ldapext-psearch-03.txt</a>
194     */
195    public void setSearchEventListener(boolean searchEventListener) {
196        this.searchEventListener = searchEventListener;
197    }
198
199    /**
200     * start the connector
201     */
202    @Override
203    public void start() throws Exception {
204        LOG.info("connecting...");
205        Hashtable<String, String> env = new Hashtable<String, String>();
206        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
207        this.ldapURI = getUri();
208        LOG.debug("    URI [{}]", this.ldapURI);
209        env.put(Context.PROVIDER_URL, this.ldapURI.toString());
210        if (anonymousAuthentication) {
211            LOG.debug("    login credentials [anonymous]");
212            env.put(Context.SECURITY_AUTHENTICATION, "none");
213        } else {
214            LOG.debug("    login credentials [{}:******]", user);
215            if (user != null && !"".equals(user)) {
216                env.put(Context.SECURITY_PRINCIPAL, user);
217            } else {
218                throw new Exception("Empty username is not allowed");
219            }
220            if (password != null && !"".equals(password)) {
221                env.put(Context.SECURITY_CREDENTIALS, password);
222            } else {
223                throw new Exception("Empty password is not allowed");
224            }
225        }
226        boolean isConnected = false;
227        while (!isConnected) {
228            try {
229                context = new InitialDirContext(env);
230                isConnected = true;
231            } catch (CommunicationException err) {
232                if (failover) {
233                    this.ldapURI = getUri();
234                    LOG.error("connection error [{}], failover connection to [{}]", env.get(Context.PROVIDER_URL), this.ldapURI.toString());
235                    env.put(Context.PROVIDER_URL, this.ldapURI.toString());
236                    Thread.sleep(curReconnectDelay);
237                    curReconnectDelay = Math.min(curReconnectDelay * 2, maxReconnectDelay);
238                } else {
239                    throw err;
240                }
241            }
242        }
243
244        // add connectors from search results
245        LOG.info("searching for network connectors...");
246        LOG.debug("    base   [{}]", base);
247        LOG.debug("    filter [{}]", searchFilter);
248        LOG.debug("    scope  [{}]", searchControls.getSearchScope());
249        NamingEnumeration<SearchResult> results = context.search(base, searchFilter, searchControls);
250        while (results.hasMore()) {
251            addConnector(results.next());
252        }
253
254        // register persistent search event listener
255        if (searchEventListener) {
256            LOG.info("registering persistent search listener...");
257            EventDirContext eventContext = (EventDirContext) context.lookup("");
258            eventContext.addNamingListener(base, searchFilter, searchControls, this);
259        } else { // otherwise close context (i.e. connection as it is no longer needed)
260            context.close();
261        }
262    }
263
264    /**
265     * stop the connector
266     */
267    @Override
268    public void stop() throws Exception {
269        LOG.info("stopping context...");
270        for (NetworkConnector connector : connectorMap.values()) {
271            connector.stop();
272        }
273        connectorMap.clear();
274        referenceMap.clear();
275        uuidMap.clear();
276        context.close();
277    }
278
279    @Override
280    public String toString() {
281        return this.getClass().getName() + getName() + "[" + ldapURI.toString() + "]";
282    }
283
284    /**
285     * add connector of the given URI
286     *
287     * @param result
288     *            search result of connector to add
289     */
290    protected synchronized void addConnector(SearchResult result) throws Exception {
291        String uuid = toUUID(result);
292        if (uuidMap.containsKey(uuid)) {
293            LOG.warn("connector already registered for UUID [{}]", uuid);
294            return;
295        }
296
297        URI connectorURI = toURI(result);
298        if (connectorMap.containsKey(connectorURI)) {
299            int referenceCount = referenceMap.get(connectorURI) + 1;
300            LOG.warn("connector reference added for URI [{}], UUID [{}], total reference(s) [{}]",connectorURI, uuid, referenceCount);
301            referenceMap.put(connectorURI, referenceCount);
302            uuidMap.put(uuid, connectorURI);
303            return;
304        }
305
306        // FIXME: disable JMX listing of LDAP managed connectors, we will
307        // want to map/manage these differently in the future
308        // boolean useJMX = getBrokerService().isUseJmx();
309        // getBrokerService().setUseJmx(false);
310        NetworkConnector connector = getBrokerService().addNetworkConnector(connectorURI);
311        // getBrokerService().setUseJmx(useJMX);
312
313        // Propagate standard connector properties that may have been set via XML
314        connector.setDynamicOnly(isDynamicOnly());
315        connector.setDecreaseNetworkConsumerPriority(isDecreaseNetworkConsumerPriority());
316        connector.setNetworkTTL(getNetworkTTL());
317        connector.setConsumerTTL(getConsumerTTL());
318        connector.setMessageTTL(getMessageTTL());
319        connector.setConduitSubscriptions(isConduitSubscriptions());
320        connector.setExcludedDestinations(getExcludedDestinations());
321        connector.setDynamicallyIncludedDestinations(getDynamicallyIncludedDestinations());
322        connector.setDuplex(isDuplex());
323
324        // XXX: set in the BrokerService.startAllConnectors method and is
325        // required to prevent remote broker exceptions upon connection
326        connector.setLocalUri(getBrokerService().getVmConnectorURI());
327        connector.setBrokerName(getBrokerService().getBrokerName());
328        connector.setDurableDestinations(getBrokerService().getBroker().getDurableDestinations());
329
330        // start network connector
331        connectorMap.put(connectorURI, connector);
332        referenceMap.put(connectorURI, 1);
333        uuidMap.put(uuid, connectorURI);
334        connector.start();
335        LOG.info("connector added with URI [{}]", connectorURI);
336    }
337
338    /**
339     * remove connector of the given URI
340     *
341     * @param result
342     *            search result of connector to remove
343     */
344    protected synchronized void removeConnector(SearchResult result) throws Exception {
345        String uuid = toUUID(result);
346        if (!uuidMap.containsKey(uuid)) {
347            LOG.warn("connector not registered for UUID [{}]", uuid);
348            return;
349        }
350
351        URI connectorURI = uuidMap.get(uuid);
352        if (!connectorMap.containsKey(connectorURI)) {
353            LOG.warn("connector not registered for URI [{}]", connectorURI);
354            return;
355        }
356
357        int referenceCount = referenceMap.get(connectorURI) - 1;
358        referenceMap.put(connectorURI, referenceCount);
359        uuidMap.remove(uuid);
360        LOG.debug("connector referenced removed for URI [{}], UUID[{}], remaining reference(s) [{}]", connectorURI, uuid, referenceCount);
361
362        if (referenceCount > 0) {
363            return;
364        }
365
366        NetworkConnector connector = connectorMap.remove(connectorURI);
367        connector.stop();
368        LOG.info("connector removed with URI [{}]", connectorURI);
369    }
370
371    /**
372     * convert search result into URI
373     *
374     * @param result
375     *            search result to convert to URI
376     */
377    protected URI toURI(SearchResult result) throws Exception {
378        Attributes attributes = result.getAttributes();
379        String address = (String) attributes.get("iphostnumber").get();
380        String port = (String) attributes.get("ipserviceport").get();
381        String protocol = (String) attributes.get("ipserviceprotocol").get();
382        URI connectorURI = new URI("static:(" + protocol + "://" + address + ":" + port + ")");
383        LOG.debug("retrieved URI from SearchResult [{}]", connectorURI);
384        return connectorURI;
385    }
386
387    /**
388     * convert search result into URI
389     *
390     * @param result
391     *            search result to convert to URI
392     */
393    protected String toUUID(SearchResult result) {
394        String uuid = result.getNameInNamespace();
395        LOG.debug("retrieved UUID from SearchResult [{}]", uuid);
396        return uuid;
397    }
398
399    /**
400     * invoked when an entry has been added during a persistent search
401     */
402    @Override
403    public void objectAdded(NamingEvent event) {
404        LOG.debug("entry added");
405        try {
406            addConnector((SearchResult) event.getNewBinding());
407        } catch (Exception err) {
408            LOG.error("ERR: caught unexpected exception", err);
409        }
410    }
411
412    /**
413     * invoked when an entry has been removed during a persistent search
414     */
415    @Override
416    public void objectRemoved(NamingEvent event) {
417        LOG.debug("entry removed");
418        try {
419            removeConnector((SearchResult) event.getOldBinding());
420        } catch (Exception err) {
421            LOG.error("ERR: caught unexpected exception", err);
422        }
423    }
424
425    /**
426     * invoked when an entry has been renamed during a persistent search
427     */
428    @Override
429    public void objectRenamed(NamingEvent event) {
430        LOG.debug("entry renamed");
431        // XXX: getNameInNamespace method does not seem to work properly,
432        // but getName seems to provide the result we want
433        String uuidOld = event.getOldBinding().getName();
434        String uuidNew = event.getNewBinding().getName();
435        URI connectorURI = uuidMap.remove(uuidOld);
436        uuidMap.put(uuidNew, connectorURI);
437        LOG.debug("connector reference renamed for URI [{}], Old UUID [{}], New UUID [{}]", connectorURI, uuidOld, uuidNew);
438    }
439
440    /**
441     * invoked when an entry has been changed during a persistent search
442     */
443    @Override
444    public void objectChanged(NamingEvent event) {
445        LOG.debug("entry changed");
446        try {
447            SearchResult result = (SearchResult) event.getNewBinding();
448            removeConnector(result);
449            addConnector(result);
450        } catch (Exception err) {
451            LOG.error("ERR: caught unexpected exception", err);
452        }
453    }
454
455    /**
456     * invoked when an exception has occurred during a persistent search
457     */
458    @Override
459    public void namingExceptionThrown(NamingExceptionEvent event) {
460        LOG.error("ERR: caught unexpected exception", event.getException());
461    }
462}