Ejemplo n.º 1
0
  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);
    }
  }
Ejemplo n.º 2
0
  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.");
  }
Ejemplo n.º 3
0
/**
 * 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
  }
}