private synchronized void doConfigClearspace() throws UnauthorizedException { Log.debug("Starting Clearspace configuration."); List<String> bindInterfaces = getServerInterfaces(); if (bindInterfaces.size() == 0) { // We aren't up and running enough to tell Clearspace what interfaces to bind to. Log.debug("No bind interfaces found to config Clearspace"); throw new IllegalStateException("There are no binding interfaces."); } try { XMPPServerInfo serverInfo = XMPPServer.getInstance().getServerInfo(); String path = IM_URL_PREFIX + "configureComponent/"; // Creates the XML with the data Document groupDoc = DocumentHelper.createDocument(); Element rootE = groupDoc.addElement("configureComponent"); Element domainE = rootE.addElement("domain"); domainE.setText(serverInfo.getXMPPDomain()); for (String bindInterface : bindInterfaces) { Element hostsE = rootE.addElement("hosts"); hostsE.setText(bindInterface); } Element portE = rootE.addElement("port"); portE.setText(String.valueOf(ExternalComponentManager.getServicePort())); Log.debug( "Trying to configure Clearspace with: Domain: " + serverInfo.getXMPPDomain() + ", hosts: " + bindInterfaces.toString() + ", port: " + port); executeRequest(POST, path, rootE.asXML()); // Done, Clearspace was configured correctly, clear the task Log.debug("Clearspace was configured, stopping the task."); TaskEngine.getInstance().cancelScheduledTask(configClearspaceTask); configClearspaceTask = null; } catch (UnauthorizedException ue) { throw ue; } catch (Exception e) { // It is not supported exception, wrap it into an UnsupportedOperationException throw new UnsupportedOperationException("Unexpected error", e); } }
private synchronized void startClearspaceConfig() { // If the task is running, stop it if (configClearspaceTask != null) { configClearspaceTask.cancel(); Log.debug("Stopping previous configuration Clearspace task."); } // Create and schedule a confi task every minute configClearspaceTask = new ConfigClearspaceTask(); // Wait some time to start the task until Openfire has binding address TaskEngine.getInstance() .schedule(configClearspaceTask, JiveConstants.SECOND * 30, JiveConstants.MINUTE); Log.debug("Starting configuration Clearspace task in 10 seconds."); }
/** * Centralized administration of Clearspace connections. The {@link #getInstance()} method should be * used to get an instance. The following properties configure this manager: * * <p> * * <ul> * <li>clearspace.uri * <li>clearspace.sharedSecret * </ul> * * @author Daniel Henninger */ public class ClearspaceManager extends BasicModule implements ExternalComponentManagerListener, ComponentEventListener, PropertyEventListener, CertificateEventListener { /** * This is the username of the user that Openfires uses to connect to Clearspace. It is fixed a * well known by Openfire and Clearspace. */ private static final String OPENFIRE_USERNAME = "******"; private static final String WEBSERVICES_PATH = "rpc/rest/"; protected static final String IM_URL_PREFIX = "imService/"; protected static final String CHAT_URL_PREFIX = "chatService/"; public static final String MUC_SUBDOMAIN = "clearspace-conference"; private static final String MUC_DESCRIPTION = "Clearspace Conference Services"; public static final String CLEARSPACE_COMPONENT = "clearspace"; private static ThreadLocal<XMPPPacketReader> localParser = null; private static XmlPullParserFactory factory = null; /** This map is used to transale exceptions from CS to OF */ private static final Map<String, String> exceptionMap; private static ClearspaceManager instance; static { try { factory = XmlPullParserFactory.newInstance(MXParser.class.getName(), null); factory.setNamespaceAware(true); } catch (XmlPullParserException e) { Log.error("Error creating a parser factory", e); } // Create xmpp parser to keep in each thread localParser = new ThreadLocal<XMPPPacketReader>() { protected XMPPPacketReader initialValue() { XMPPPacketReader parser = new XMPPPacketReader(); factory.setNamespaceAware(true); parser.setXPPFactory(factory); return parser; } }; // Add a new exception map from CS to OF and it will be automatically translated. exceptionMap = new HashMap<String, String>(); exceptionMap.put( "com.jivesoftware.base.UserNotFoundException", "org.jivesoftware.openfire.user.UserNotFoundException"); exceptionMap.put( "com.jivesoftware.base.UserAlreadyExistsException", "org.jivesoftware.openfire.user.UserAlreadyExistsException"); exceptionMap.put( "com.jivesoftware.base.GroupNotFoundException", "org.jivesoftware.openfire.group.GroupNotFoundException"); exceptionMap.put( "com.jivesoftware.base.GroupAlreadyExistsException", "org.jivesoftware.openfire.group.GroupAlreadyExistsException"); exceptionMap.put( "org.acegisecurity.BadCredentialsException", "org.jivesoftware.openfire.auth.UnauthorizedException"); exceptionMap.put( "com.jivesoftware.base.UnauthorizedException", "org.jivesoftware.openfire.auth.UnauthorizedException"); exceptionMap.put( "com.jivesoftware.community.NotFoundException", "org.jivesoftware.util.NotFoundException"); } private ConfigClearspaceTask configClearspaceTask; private Map<String, String> properties; private String uri; private String host; private int port; private String sharedSecret; private Map<String, Long> userIDCache; private Map<Long, String> usernameCache; private Map<String, Long> groupIDCache; // Current nonce storage private Cache<String, Long> nonceCache; // Nonce generator private Random nonceGenerator; /** Records transcripts for group chat rooms in Clearspace. */ private ClearspaceMUCTranscriptManager mucTranscriptManager = new ClearspaceMUCTranscriptManager(TaskEngine.getInstance()); /** Keep the domains of Clearspace components */ private final List<String> clearspaces = new ArrayList<String>(); /** * Provides singleton access to an instance of the ClearspaceManager class. * * @return an ClearspaceManager instance. */ public static ClearspaceManager getInstance() { return instance; } /** * Constructs a new ClearspaceManager instance. Typically, {@link #getInstance()} should be called * instead of this method. ClearspaceManager instances should only be created directly for testing * purposes. * * @param properties the Map that contains properties used by the Clearspace manager, such as * Clearspace host and shared secret. */ public ClearspaceManager(Map<String, String> properties) { super("Clearspace integration module for testing only"); this.properties = properties; init(); } /** * Constructs a new ClearspaceManager instance. Typically, {@link #getInstance()} should be called * instead of this method. ClearspaceManager instances should only be created directly for testing * purposes. */ public ClearspaceManager() { super("Clearspace integration module"); // Create a special Map implementation to wrap XMLProperties. We only implement // the get, put, and remove operations, since those are the only ones used. Using a Map // makes it easier to perform LdapManager testing. this.properties = new Map<String, String>() { public String get(Object key) { return JiveGlobals.getProperty((String) key); } public String put(String key, String value) { JiveGlobals.setProperty(key, value); // Always return null since XMLProperties doesn't support the normal semantics. return null; } public String remove(Object key) { JiveGlobals.deleteProperty((String) key); // Always return null since XMLProperties doesn't support the normal semantics. return null; } public int size() { return 0; } public boolean isEmpty() { return false; } public boolean containsKey(Object key) { return false; } public boolean containsValue(Object value) { return false; } public void putAll(Map<? extends String, ? extends String> t) {} public void clear() {} public Set<String> keySet() { return null; } public Collection<String> values() { return null; } public Set<Entry<String, String>> entrySet() { return null; } }; init(); instance = this; } private void init() { // Register the trust manager to use when using HTTPS Protocol easyhttps = new Protocol("https", (ProtocolSocketFactory) new SSLProtocolSocketFactory(this), 443); Protocol.registerProtocol("https", easyhttps); // Convert XML based provider setup to Database based JiveGlobals.migrateProperty("clearspace.uri"); JiveGlobals.migrateProperty("clearspace.sharedSecret"); // Make sure that all Clearspace components are set up, unless they were overridden // Note that the auth provider is our way of knowing that we are set up with Clearspace, // so don't bother checking to set it. if (isEnabled()) { if (JiveGlobals.getProperty("provider.user.className") == null) { JiveGlobals.setProperty( "provider.user.className", "org.jivesoftware.openfire.clearspace.ClearspaceUserProvider"); } if (JiveGlobals.getProperty("provider.group.className") == null) { JiveGlobals.setProperty( "provider.group.className", "org.jivesoftware.openfire.clearspace.ClearspaceGroupProvider"); } if (JiveGlobals.getProperty("provider.vcard.className") == null) { JiveGlobals.setProperty( "provider.vcard.className", "org.jivesoftware.openfire.clearspace.ClearspaceVCardProvider"); } if (JiveGlobals.getProperty("provider.lockout.className") == null) { JiveGlobals.setProperty( "provider.lockout.className", "org.jivesoftware.openfire.clearspace.ClearspaceLockOutProvider"); } if (JiveGlobals.getProperty("provider.securityAudit.className") == null) { JiveGlobals.setProperty( "provider.securityAudit.className", "org.jivesoftware.openfire.clearspace.ClearspaceSecurityAuditProvider"); } if (JiveGlobals.getProperty("provider.admin.className") == null) { JiveGlobals.setProperty( "provider.admin.className", "org.jivesoftware.openfire.clearspace.ClearspaceAdminProvider"); } } this.uri = properties.get("clearspace.uri"); if (uri != null) { if (!this.uri.endsWith("/")) { this.uri = this.uri + "/"; } // Updates the host/port attributes based on the uri updateHostPort(); } sharedSecret = properties.get("clearspace.sharedSecret"); // Creates the cache maps userIDCache = new DefaultCache<String, Long>("clearspace.userid", 1000, JiveConstants.DAY); groupIDCache = new DefaultCache<String, Long>("clearspace.groupid", 1000, JiveConstants.DAY); usernameCache = new DefaultCache<Long, String>("clearspace.username", 1000, JiveConstants.DAY); if (Log.isDebugEnabled()) { StringBuilder buf = new StringBuilder(); buf.append("Created new ClearspaceManager() instance, fields:\n"); buf.append("\t URI: ").append(uri).append("\n"); buf.append("\t sharedSecret: ").append(sharedSecret).append("\n"); Log.debug("ClearspaceManager: " + buf.toString()); } // Init nonce cache nonceCache = CacheFactory.createCache("Clearspace SSO Nonce"); // Init nonce generator nonceGenerator = new Random(); } /** Updates the host port attributes based on the URI. */ private void updateHostPort() { if (uri != null && !"".equals(uri.trim())) { try { URL url = new URL(uri); host = url.getHost(); port = url.getPort(); } catch (MalformedURLException e) { // this won't happen } } } /** * Check a username/password pair for valid authentication. * * @param username Username to authenticate against. * @param password Password to use for authentication. * @return True or false of the authentication succeeded. */ public Boolean checkAuthentication(String username, String password) { try { // Un-escape username. username = JID.unescapeNode(username); // Encode potentially non-ASCII characters username = URLUTF8Encoder.encode(username); String path = ClearspaceAuthProvider.URL_PREFIX + "authenticate/" + username + "/" + password; executeRequest(GET, path); return true; } catch (Exception e) { // Nothing to do. Log.warn("Failed authenticating user with Clearspace. User = "******"testCredentials"; executeRequest(GET, path); return null; } catch (Exception e) { // It is not ok, return false. Log.warn("Failed testing communicating with Clearspace", e); return e; } } /** * Returns true if Openfire is connected to Clearspace. This method may delay some time since it * has to ping Clearspace to know if Openfire is able to connect to it. * * @return true if Openfire is connected to Clearspace. */ public Boolean isOpenfireConnected() { return testConnection() == null; } /** * Returns true if Clearspce is connected to Openfire. * * @return true if Clearspce is connected to Openfire. */ public Boolean isClearspaceConnected() { XMPPServer server = XMPPServer.getInstance(); if (server == null) { return false; } Collection<ComponentSession> componentSessions = server.getSessionManager().getComponentSessions(); for (ComponentSession cs : componentSessions) { // All Clearspace sessions start with "clearspace" if (cs.getAddress().getDomain().startsWith("clearspace")) { return true; } } return false; } public Map<String, String> getProperties() { return properties; } /** * Returns the Clearspace service URI; e.g. <tt>https://localhost:80/clearspace</tt>. This value * is stored as the Jive Property <tt>clearspace.uri</tt>. * * @return the Clearspace service URI. */ public String getConnectionURI() { return uri; } /** * Sets the URI of the Clearspace service; e.g., <tt>https://localhost:80/clearspace</tt>. This * value is stored as the Jive Property <tt>clearspace.uri</tt>. * * @param uri the Clearspace service URI. */ public void setConnectionURI(String uri) { if (!uri.endsWith("/")) { uri = uri + "/"; } this.uri = uri; properties.put("clearspace.uri", uri); // Updates the host/port attributes updateHostPort(); if (isEnabled()) { startClearspaceConfig(); } } /** * Returns the password, configured in Clearspace, that Openfire will use to authenticate with * Clearspace to perform it's integration. * * @return the password Openfire will use to authenticate with Clearspace. */ public String getSharedSecret() { return sharedSecret; } /** * Sets the shared secret for the Clearspace service we're connecting to. * * @param sharedSecret the password configured in Clearspace to authenticate Openfire. */ public void setSharedSecret(String sharedSecret) { // Set new password for external component ExternalComponentConfiguration configuration = new ExternalComponentConfiguration( "clearspace", true, ExternalComponentConfiguration.Permission.allowed, sharedSecret); try { ExternalComponentManager.allowAccess(configuration); } catch (ModificationNotAllowedException e) { Log.warn("Failed to configure password for Clearspace", e); } // After updating the component information we can update the field, but not before. // If it is done before, OF won't be able to execute the updateSharedsecret webservice // since it would try with the new password. this.sharedSecret = sharedSecret; properties.put("clearspace.sharedSecret", sharedSecret); } /** * Returns true if Clearspace is being used as the backend of Openfire. When integrated with * Clearspace then users and groups will be pulled out from Clearspace. User authentication will * also rely on Clearspace. * * @return true if Clearspace is being used as the backend of Openfire. */ public static boolean isEnabled() { return AuthFactory.getAuthProvider() instanceof ClearspaceAuthProvider; } public void start() throws IllegalStateException { super.start(); if (isEnabled()) { // Before starting up service make sure there is a default secret if (ExternalComponentManager.getDefaultSecret() == null || "".equals(ExternalComponentManager.getDefaultSecret())) { try { ExternalComponentManager.setDefaultSecret(StringUtils.randomString(10)); } catch (ModificationNotAllowedException e) { Log.warn("Failed to set a default secret to external component service", e); } } // Make sure that external component service is enabled if (!ExternalComponentManager.isServiceEnabled()) { try { ExternalComponentManager.setServiceEnabled(true); } catch (ModificationNotAllowedException e) { Log.warn("Failed to start external component service", e); } } // Listen for changes to external component settings ExternalComponentManager.addListener(this); // Listen for registration of new components InternalComponentManager.getInstance().addListener(this); // Listen for changes in certificates CertificateManager.addListener(this); // Listen for property changes PropertyEventDispatcher.addListener(this); // Set up custom clearspace MUC service // Create service if it doesn't exist, load if it does. MultiUserChatServiceImpl muc = (MultiUserChatServiceImpl) XMPPServer.getInstance() .getMultiUserChatManager() .getMultiUserChatService(MUC_SUBDOMAIN); if (muc == null) { try { muc = XMPPServer.getInstance() .getMultiUserChatManager() .createMultiUserChatService(MUC_SUBDOMAIN, MUC_DESCRIPTION, true); } catch (AlreadyExistsException e) { Log.error( "ClearspaceManager: Found no " + MUC_SUBDOMAIN + " service, but got already exists when creation attempted? Service probably not started!"); } } if (muc != null) { // Set up special delegate for Clearspace MUC service muc.setMUCDelegate(new ClearspaceMUCEventDelegate()); // Set up additional features for Clearspace MUC service muc.addExtraFeature("clearspace:service"); // Set up additional identity of conference service to Clearspace MUC service muc.addExtraIdentity("conference", "Clearspace Chat Service", "text"); } // Starts the clearspace configuration task startClearspaceConfig(); // Starts the Clearspace MUC transcript manager mucTranscriptManager.start(); } } public void stop() { super.stop(); // Stops the Clearspace MUC transcript manager mucTranscriptManager.stop(); // Unregister/shut down custom MUC service XMPPServer.getInstance() .getMultiUserChatManager() .unregisterMultiUserChatService(MUC_SUBDOMAIN); } public synchronized boolean configClearspace() { // If the task is running, stop it if (configClearspaceTask != null) { configClearspaceTask.cancel(); Log.debug("Stopping previous configuration Clearspace task."); } boolean configured = false; try { doConfigClearspace(); updateClearspaceClientSettings(); configured = true; } catch (UnauthorizedException e) { Log.info("Unauthorized to configure Clearspace.", e); } catch (UnsupportedOperationException e) { Log.info("Error configuring Clearspace.", e); } if (!configured) { startClearspaceConfig(); } return configured; } /** */ private synchronized void startClearspaceConfig() { // If the task is running, stop it if (configClearspaceTask != null) { configClearspaceTask.cancel(); Log.debug("Stopping previous configuration Clearspace task."); } // Create and schedule a confi task every minute configClearspaceTask = new ConfigClearspaceTask(); // Wait some time to start the task until Openfire has binding address TaskEngine.getInstance() .schedule(configClearspaceTask, JiveConstants.SECOND * 30, JiveConstants.MINUTE); Log.debug("Starting configuration Clearspace task in 10 seconds."); } private synchronized void doConfigClearspace() throws UnauthorizedException { Log.debug("Starting Clearspace configuration."); List<String> bindInterfaces = getServerInterfaces(); if (bindInterfaces.size() == 0) { // We aren't up and running enough to tell Clearspace what interfaces to bind to. Log.debug("No bind interfaces found to config Clearspace"); throw new IllegalStateException("There are no binding interfaces."); } try { XMPPServerInfo serverInfo = XMPPServer.getInstance().getServerInfo(); String path = IM_URL_PREFIX + "configureComponent/"; // Creates the XML with the data Document groupDoc = DocumentHelper.createDocument(); Element rootE = groupDoc.addElement("configureComponent"); Element domainE = rootE.addElement("domain"); domainE.setText(serverInfo.getXMPPDomain()); for (String bindInterface : bindInterfaces) { Element hostsE = rootE.addElement("hosts"); hostsE.setText(bindInterface); } Element portE = rootE.addElement("port"); portE.setText(String.valueOf(ExternalComponentManager.getServicePort())); Log.debug( "Trying to configure Clearspace with: Domain: " + serverInfo.getXMPPDomain() + ", hosts: " + bindInterfaces.toString() + ", port: " + port); executeRequest(POST, path, rootE.asXML()); // Done, Clearspace was configured correctly, clear the task Log.debug("Clearspace was configured, stopping the task."); TaskEngine.getInstance().cancelScheduledTask(configClearspaceTask); configClearspaceTask = null; } catch (UnauthorizedException ue) { throw ue; } catch (Exception e) { // It is not supported exception, wrap it into an UnsupportedOperationException throw new UnsupportedOperationException("Unexpected error", e); } } /** * Returns true if Clerspace was configured at least one time since Openfire startup. * * @return true if Clerspace was configured at least one time since Openfire startup. */ public boolean isClearspaceConfigured() { return configClearspaceTask == null; } private List<String> getServerInterfaces() { List<String> bindInterfaces = new ArrayList<String>(); String interfaceName = JiveGlobals.getXMLProperty("network.interface"); String bindInterface = null; if (interfaceName != null) { if (interfaceName.trim().length() > 0) { bindInterface = interfaceName; } } int adminPort = JiveGlobals.getXMLProperty("adminConsole.port", 9090); int adminSecurePort = JiveGlobals.getXMLProperty("adminConsole.securePort", 9091); if (bindInterface == null) { try { Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces(); for (NetworkInterface netInterface : Collections.list(nets)) { Enumeration<InetAddress> addresses = netInterface.getInetAddresses(); for (InetAddress address : Collections.list(addresses)) { if ("127.0.0.1".equals(address.getHostAddress())) { continue; } if (address.getHostAddress().startsWith("0.")) { continue; } Socket socket = new Socket(); InetSocketAddress remoteAddress = new InetSocketAddress(address, adminPort > 0 ? adminPort : adminSecurePort); try { socket.connect(remoteAddress); bindInterfaces.add(address.getHostAddress()); break; } catch (IOException e) { // Ignore this address. Let's hope there is more addresses to validate } } } } catch (SocketException e) { // We failed to discover a valid IP address where the admin console is running return null; } } else { bindInterfaces.add(bindInterface); } return bindInterfaces; } private void updateClearspaceSharedSecret(String newSecret) { try { String path = IM_URL_PREFIX + "updateSharedSecret/"; // Creates the XML with the data Document groupDoc = DocumentHelper.createDocument(); Element rootE = groupDoc.addElement("updateSharedSecret"); rootE.addElement("newSecret").setText(newSecret); executeRequest(POST, path, groupDoc.asXML()); } catch (UnauthorizedException ue) { Log.error("Error updating the password of Clearspace", ue); } catch (Exception e) { Log.error("Error updating the password of Clearspace", e); } } private void updateClearspaceClientSettings() { String xmppBoshSslPort = "0"; String xmppBoshPort = "0"; String xmppPort = String.valueOf(XMPPServer.getInstance().getConnectionManager().getClientListenerPort()); if (JiveGlobals.getBooleanProperty( HttpBindManager.HTTP_BIND_ENABLED, HttpBindManager.HTTP_BIND_ENABLED_DEFAULT)) { int boshSslPort = HttpBindManager.getInstance().getHttpBindSecurePort(); int boshPort = HttpBindManager.getInstance().getHttpBindUnsecurePort(); try { if (HttpBindManager.getInstance().isHttpsBindActive() && LocalClientSession.getTLSPolicy() != org.jivesoftware.openfire.Connection.TLSPolicy.disabled) { xmppBoshSslPort = String.valueOf(boshSslPort); } } catch (Exception e) { // Exception while working with certificate Log.debug( "Error while checking SSL certificate. Instructing Clearspace not to use SSL port."); } if (HttpBindManager.getInstance().isHttpBindActive() && boshPort > 0) { xmppBoshPort = String.valueOf(boshPort); } } try { String path = CHAT_URL_PREFIX + "updateClientSettings/"; // Creates the XML with the data Document groupDoc = DocumentHelper.createDocument(); Element rootE = groupDoc.addElement("updateClientSettings"); rootE.addElement("boshSslPort").setText(xmppBoshSslPort); rootE.addElement("boshPort").setText(xmppBoshPort); rootE.addElement("tcpPort").setText(xmppPort); executeRequest(POST, path, groupDoc.asXML()); } catch (UnauthorizedException ue) { Log.error("Error updating the client settings of Clearspace", ue); } catch (Exception e) { Log.error("Error updating the client settings of Clearspace", e); } } public void serviceEnabled(boolean enabled) throws ModificationNotAllowedException { // Do not let admins shutdown the external component service if (!enabled) { throw new ModificationNotAllowedException( "Service cannot be disabled when integrated with Clearspace."); } } public void portChanged(int newPort) throws ModificationNotAllowedException { startClearspaceConfig(); } public void defaultSecretChanged(String newSecret) throws ModificationNotAllowedException { // Do nothing } public void permissionPolicyChanged(ExternalComponentManager.PermissionPolicy newPolicy) throws ModificationNotAllowedException { // Do nothing } public void componentAllowed(String subdomain, ExternalComponentConfiguration configuration) throws ModificationNotAllowedException { if (subdomain.startsWith("clearspace")) { updateClearspaceSharedSecret(configuration.getSecret()); } } public void componentBlocked(String subdomain) throws ModificationNotAllowedException { if (subdomain.startsWith("clearspace")) { throw new ModificationNotAllowedException("Communication with Clearspace cannot be blocked."); } } public void componentSecretUpdated(String subdomain, String newSecret) throws ModificationNotAllowedException { if (subdomain.startsWith("clearspace")) { updateClearspaceSharedSecret(newSecret); } } public void componentConfigurationDeleted(String subdomain) throws ModificationNotAllowedException { // Do not let admins delete configuration of Clearspace component if (subdomain.startsWith("clearspace")) { throw new ModificationNotAllowedException("Use 'Profile Settings' to change password."); } } /** * Makes a rest request of either type GET or DELETE at the specified urlSuffix. The urlSuffix * should be of the form /userService/users. If CS throws an exception it handled and transalated * to a Openfire exception if possible. This is done using the check fault method that has an * exception mapping from CS to OF. If no mapping is found then it throws a <code>Exception</code> * with the message of the CS exception. * * @param type Must be GET or DELETE * @param urlSuffix The url suffix of the rest request * @return The response as a xml doc. * @throws ConnectionException Thrown if there are issues perfoming the request. * @throws Exception Thrown if the response from Clearspace contains an exception. */ public Element executeRequest(HttpType type, String urlSuffix) throws ConnectionException, Exception { assert (type == HttpType.GET || type == HttpType.DELETE); return executeRequest(type, urlSuffix, null); } /** * Makes a rest request of any type at the specified urlSuffix. The urlSuffix should be of the * form /userService/users. If CS throws an exception it handled and transalated to a Openfire * exception if possible. This is done using the check fault method that tries to throw the best * maching exception. * * @param type Must be GET or DELETE * @param urlSuffix The url suffix of the rest request * @param xmlParams The xml with the request params, must be null if type is GET or DELETE only * @return The response as a xml doc. * @throws ConnectionException Thrown if there are issues perfoming the request. * @throws Exception Thrown if the response from Clearspace contains an exception. */ public Element executeRequest(HttpType type, String urlSuffix, String xmlParams) throws ConnectionException, Exception { if (Log.isDebugEnabled()) { Log.debug("Outgoing REST call [" + type + "] to " + urlSuffix + ": " + xmlParams); } String wsUrl = getConnectionURI() + WEBSERVICES_PATH + urlSuffix; String secret = getSharedSecret(); HttpClient client = new HttpClient(); HttpMethod method; // Configures the authentication client.getParams().setAuthenticationPreemptive(true); Credentials credentials = new UsernamePasswordCredentials(OPENFIRE_USERNAME, secret); AuthScope scope = new AuthScope(host, port, AuthScope.ANY_REALM); client.getState().setCredentials(scope, credentials); // Creates the method switch (type) { case GET: method = new GetMethod(wsUrl); break; case POST: PostMethod pm = new PostMethod(wsUrl); StringRequestEntity requestEntity = new StringRequestEntity(xmlParams); pm.setRequestEntity(requestEntity); method = pm; break; case PUT: PutMethod pm1 = new PutMethod(wsUrl); StringRequestEntity requestEntity1 = new StringRequestEntity(xmlParams); pm1.setRequestEntity(requestEntity1); method = pm1; break; case DELETE: method = new DeleteMethod(wsUrl); break; default: throw new IllegalArgumentException(); } method.setRequestHeader("Accept", "text/xml"); method.setDoAuthentication(true); try { // Executes the request client.executeMethod(method); // Parses the result String body = method.getResponseBodyAsString(); if (Log.isDebugEnabled()) { Log.debug("Outgoing REST call results: " + body); } // Checks the http status if (method.getStatusCode() != 200) { if (method.getStatusCode() == 401) { throw new ConnectionException( "Invalid password to connect to Clearspace.", ConnectionException.ErrorType.AUTHENTICATION); } else if (method.getStatusCode() == 404) { throw new ConnectionException( "Web service not found in Clearspace.", ConnectionException.ErrorType.PAGE_NOT_FOUND); } else if (method.getStatusCode() == 503) { throw new ConnectionException( "Web service not avaible in Clearspace.", ConnectionException.ErrorType.SERVICE_NOT_AVAIBLE); } else { throw new ConnectionException( "Error connecting to Clearspace, http status code: " + method.getStatusCode(), new HTTPConnectionException(method.getStatusCode()), ConnectionException.ErrorType.OTHER); } } else if (body.contains("Clearspace Upgrade Console")) { // TODO Change CS to send a more standard error message throw new ConnectionException( "Clearspace is in an update state.", ConnectionException.ErrorType.UPDATE_STATE); } Element response = localParser.get().parseDocument(body).getRootElement(); // Check for exceptions checkFault(response); // Since there is no exception, returns the response return response; } catch (DocumentException e) { throw new ConnectionException( "Error parsing the response of Clearspace.", e, ConnectionException.ErrorType.OTHER); } catch (HttpException e) { throw new ConnectionException( "Error performing http request to Clearspace", e, ConnectionException.ErrorType.OTHER); } catch (UnknownHostException e) { throw new ConnectionException( "Unknown Host " + getConnectionURI() + " trying to connect to Clearspace", e, ConnectionException.ErrorType.UNKNOWN_HOST); } catch (IOException e) { throw new ConnectionException( "Error peforming http request to Clearspace.", e, ConnectionException.ErrorType.OTHER); } finally { method.releaseConnection(); } } /** * If CS throws an exception it handled and transalated to a Openfire exception if possible. This * is done using <code>exceptionMap</code> that has a mapping from CS to OF. If no mapping is * found then it tries to instantiete the original exception. If this fails it throws a <code> * Exception</code> with the message of the CS exception. * * @param response the response from CS to check if it is an exception message. * @throws Exception if the response is an exception message. */ private void checkFault(Element response) throws Exception { Node node = response.selectSingleNode("ns1:faultstring"); if (node != null) { String exceptionText = node.getText(); // Text accepted samples: // 'java.lang.Exception: Exception message' // 'java.lang.Exception' // Get the exception class and message if any int index = exceptionText.indexOf(":"); String className; String message; // If there is no message, save the class only if (index == -1) { className = exceptionText; message = null; } else { // Else save both className = exceptionText.substring(0, index); message = exceptionText.substring(index + 2); } // Map the exception to a Openfire one, if possible if (exceptionMap.containsKey(className)) { className = exceptionMap.get(className); } // Tries to create an instance with the message Exception exception; try { Class exceptionClass = Class.forName(className); if (message == null) { exception = (Exception) exceptionClass.newInstance(); } else { Constructor constructor = exceptionClass.getConstructor(String.class); exception = (Exception) constructor.newInstance(message); } } catch (Exception e) { // failed to create an specific exception, creating a standard one. exception = new Exception(exceptionText); } throw exception; } } /** * Returns the Clearspace user id the user by username. * * @param username Username to retrieve ID of. * @return The ID number of the user in Clearspace. * @throws org.jivesoftware.openfire.user.UserNotFoundException If the user was not found. */ protected long getUserID(String username) throws UserNotFoundException { // Gets the part before of @ of the username param if (username.contains("@")) { // User's id are only for local users if (!XMPPServer.getInstance().isLocal(new JID(username))) { throw new UserNotFoundException("Cannot load user of remote server: " + username); } username = username.substring(0, username.lastIndexOf("@")); } // Checks if it is in the cache if (userIDCache.containsKey(username)) { return userIDCache.get(username); } // Un-escape username. String unescapedUsername = JID.unescapeNode(username); // Encode potentially non-ASCII characters unescapedUsername = URLUTF8Encoder.encode(unescapedUsername); // Gets the user's ID from Clearspace try { String path = ClearspaceUserProvider.USER_URL_PREFIX + "users/" + unescapedUsername; Element element = executeRequest(org.jivesoftware.openfire.clearspace.ClearspaceManager.HttpType.GET, path); Long id = Long.valueOf(WSUtils.getElementText(element.selectSingleNode("return"), "ID")); userIDCache.put(username, id); return id; } catch (UserNotFoundException unfe) { // It is a supported exception, throw it again throw unfe; } catch (Exception e) { // It is not a supported exception, wrap it into a UserNotFoundException throw new UserNotFoundException("Unexpected error", e); } } /** * Returns the Clearspace user id the user by JID. * * @param user JID of user to retrieve ID of. * @return The ID number of the user in Clearspace. * @throws org.jivesoftware.openfire.user.UserNotFoundException If the user was not found. */ protected long getUserID(JID user) throws UserNotFoundException { // User's id are only for local users XMPPServer server = XMPPServer.getInstance(); if (!server.isLocal(user)) { throw new UserNotFoundException("Cannot load user of remote server: " + user.toString()); } return getUserID(user.getNode()); } /** * Returns the Clearspace username of the user by id. * * @param id ID to retrieve Username of. * @return The username of the user in Clearspace. * @throws org.jivesoftware.openfire.user.UserNotFoundException If the user was not found. */ protected String getUsernameByID(Long id) throws UserNotFoundException { // Checks if it is in the cache if (usernameCache.containsKey(id)) { return usernameCache.get(id); } // Gets the user's ID from Clearspace try { String path = ClearspaceUserProvider.USER_URL_PREFIX + "usersByID/" + id; Element element = executeRequest(org.jivesoftware.openfire.clearspace.ClearspaceManager.HttpType.GET, path); String username = WSUtils.getElementText( element.selectSingleNode("return"), "username"); // TODO: is this right? // Escape the username so that it can be used as a JID. username = JID.escapeNode(username); usernameCache.put(id, username); return username; } catch (UserNotFoundException unfe) { // It is a supported exception, throw it again throw unfe; } catch (Exception e) { // It is not a supported exception, wrap it into a UserNotFoundException throw new UserNotFoundException("Unexpected error", e); } } /** * Returns the Clearspace group id of the group. * * @param groupname Name of the group to retrieve ID of. * @return The ID number of the group in Clearspace. * @throws org.jivesoftware.openfire.group.GroupNotFoundException If the group was not found. */ protected long getGroupID(String groupname) throws GroupNotFoundException { if (groupIDCache.containsKey(groupname)) { return groupIDCache.get(groupname); } try { // Encode potentially non-ASCII characters groupname = URLUTF8Encoder.encode(groupname); String path = ClearspaceGroupProvider.URL_PREFIX + "groups/" + groupname; Element element = executeRequest(org.jivesoftware.openfire.clearspace.ClearspaceManager.HttpType.GET, path); Long id = Long.valueOf(WSUtils.getElementText(element.selectSingleNode("return"), "ID")); // Saves it into the cache groupIDCache.put(groupname, id); return id; } catch (GroupNotFoundException gnfe) { // It is a supported exception, throw it again throw gnfe; } catch (Exception e) { // It is not a supported exception, wrap it into a GroupNotFoundException throw new GroupNotFoundException("Unexpected error", e); } } /** * Returns true if a given JID belongs to a known Clearspace component domain. * * @param address Address to check. * @return True if the specified address is a Clearspace component. */ public boolean isFromClearspace(JID address) { return clearspaces.contains(address.getDomain()); } /** * Sends an IQ packet to the Clearspace external component and returns the IQ packet returned by * CS or <tt>null</tt> if no answer was received before the specified timeout. * * @param packet IQ packet to send. * @param timeout milliseconds to wait before timing out. * @return IQ packet returned by Clearspace responsing the packet we sent. */ public IQ query(final IQ packet, int timeout) { // Complain if FROM is empty if (packet.getFrom() == null) { throw new IllegalStateException("IQ packets with no FROM cannot be sent to Clearspace"); } // If CS is not connected then return null if (clearspaces.isEmpty()) { return null; } // Set the target address to the IQ packet. Roate list so we distribute load String component; synchronized (clearspaces) { component = clearspaces.get(0); Collections.rotate(clearspaces, 1); } packet.setTo(component); final LinkedBlockingQueue<IQ> answer = new LinkedBlockingQueue<IQ>(8); final IQRouter router = XMPPServer.getInstance().getIQRouter(); router.addIQResultListener( packet.getID(), new IQResultListener() { public void receivedAnswer(IQ packet) { answer.offer(packet); } public void answerTimeout(String packetId) { Log.warn("No answer from Clearspace was received for IQ stanza: " + packet); } }); XMPPServer.getInstance().getIQRouter().route(packet); IQ reply = null; try { reply = answer.poll(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { // Ignore } return reply; } public void componentRegistered(JID componentJID) { // Do nothing } public void componentUnregistered(JID componentJID) { // Remove stored information about this component clearspaces.remove(componentJID.getDomain()); } public void componentInfoReceived(IQ iq) { // Check if it's a Clearspace component boolean isClearspace = false; Element childElement = iq.getChildElement(); for (Iterator it = childElement.elementIterator("identity"); it.hasNext(); ) { Element identity = (Element) it.next(); if ("component".equals(identity.attributeValue("category")) && "clearspace".equals(identity.attributeValue("type"))) { isClearspace = true; } } // If component is Clearspace then keep track of the component if (isClearspace) { clearspaces.add(iq.getFrom().getDomain()); } } /** * Returns a nonce generated by Clearspace to be used in a SSO login. * * @return a unique nonce. */ public String getNonce() { try { String path = IM_URL_PREFIX + "generateNonce"; Element element = executeRequest(GET, path); return WSUtils.getReturn(element); } catch (Exception e) { Log.error("Failed executing #generateNonce with Clearspace", e); } return null; } /** * Generates a new nonce. The <code>isValidNonce</code> method will return true when using nonces * generated by this method. * * @return a unique nonce */ public String generateNonce() { String nonce = String.valueOf(nonceGenerator.nextLong()); nonceCache.put(nonce, System.currentTimeMillis()); return nonce; } /** * Returns true if the nonce was generated usig <code>generateNonce</code> and if this is the * first check for that nonce. * * @param nonce the nonce to be checked * @return true if the nonce if the nonce was generated and this is the first check for that nonce */ public boolean isValidNonce(String nonce) { Long time = nonceCache.remove(nonce); if (time == null) { return false; } return System.currentTimeMillis() - time < JiveConstants.MINUTE; } public void propertySet(String property, Map params) { if (property.equalsIgnoreCase(HttpBindManager.HTTP_BIND_ENABLED) || property.equalsIgnoreCase(HttpBindManager.HTTP_BIND_PORT) || property.equalsIgnoreCase(HttpBindManager.HTTP_BIND_SECURE_PORT) || property.equalsIgnoreCase("xmpp.socket.plain.port")) { updateClearspaceClientSettings(); } } public void propertyDeleted(String property, Map params) { if (property.equalsIgnoreCase(HttpBindManager.HTTP_BIND_ENABLED) || property.equalsIgnoreCase(HttpBindManager.HTTP_BIND_PORT) || property.equalsIgnoreCase(HttpBindManager.HTTP_BIND_SECURE_PORT) || property.equalsIgnoreCase("xmpp.socket.plain.port")) { updateClearspaceClientSettings(); } } public void xmlPropertySet(String property, Map params) {} public void xmlPropertyDeleted(String property, Map params) {} public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) { updateClearspaceClientSettings(); } public void certificateDeleted(KeyStore keyStore, String alias) { updateClearspaceClientSettings(); } public void certificateSigned( KeyStore keyStore, String alias, List<X509Certificate> certificates) {} private class ConfigClearspaceTask extends TimerTask { public void run() { try { Log.debug("Trying to configure Clearspace."); doConfigClearspace(); updateClearspaceClientSettings(); } catch (UnauthorizedException e) { Log.warn( "Unauthorization problem trying to configure Clearspace, trying again in 1 minute", e); // TODO: Mark that there is an authorization problem } catch (Exception e) { Log.warn("Unknown problem trying to configure Clearspace, trying again in 1 minute", e); } } } /** Different kind of HTTP request types */ public enum HttpType { /** Represents an HTTP Get request. And it's equivalent to a SQL SELECTE. */ GET, /** Represents an HTTP Post request. And it's equivalent to a SQL UPDATE. */ POST, /** Represents an HTTP Delete request. And it's equivalent to a SQL DELETE. */ DELETE, /** Represents an HTTP Put requests.And it's equivalent to a SQL CREATE. */ PUT } }