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}