/**
   * Establishes a new outgoing session to a remote server. If the remote server supports TLS and
   * SASL then the new outgoing connection will be secured with TLS and authenticated using SASL.
   * However, if TLS or SASL is not supported by the remote server or if an error occured while
   * securing or authenticating the connection using SASL then server dialback method will be used.
   *
   * @param domain the local domain to authenticate with the remote server.
   * @param hostname the hostname of the remote server.
   * @param port default port to use to establish the connection.
   * @return new outgoing session to a remote server.
   */
  private static LocalOutgoingServerSession createOutgoingSession(
      String domain, String hostname, int port) {

    String localDomainName = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
    boolean useTLS = JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_ENABLED, true);
    RemoteServerConfiguration configuration = RemoteServerManager.getConfiguration(hostname);
    if (configuration != null) {
      // TODO Use the specific TLS configuration for this remote server
      // useTLS = configuration.isTLSEnabled();
    }

    // Connect to remote server using XMPP 1.0 (TLS + SASL EXTERNAL or TLS + server dialback or
    // server dialback)
    String realHostname = null;
    int realPort = port;
    Socket socket = null;
    // Get a list of real hostnames to connect to using DNS lookup of the specified hostname
    List<DNSUtil.HostAddress> hosts = DNSUtil.resolveXMPPDomain(hostname, port);
    for (Iterator<DNSUtil.HostAddress> it = hosts.iterator(); it.hasNext(); ) {
      try {
        socket = new Socket();
        DNSUtil.HostAddress address = it.next();
        realHostname = address.getHost();
        realPort = address.getPort();
        Log.debug(
            "LocalOutgoingServerSession: OS - Trying to connect to "
                + hostname
                + ":"
                + port
                + "(DNS lookup: "
                + realHostname
                + ":"
                + realPort
                + ")");
        // Establish a TCP connection to the Receiving Server
        socket.connect(
            new InetSocketAddress(realHostname, realPort), RemoteServerManager.getSocketTimeout());
        Log.debug(
            "LocalOutgoingServerSession: OS - Plain connection to "
                + hostname
                + ":"
                + port
                + " successful");
        break;
      } catch (Exception e) {
        Log.warn(
            "Error trying to connect to remote server: "
                + hostname
                + "(DNS lookup: "
                + realHostname
                + ":"
                + realPort
                + "): "
                + e.toString());
        try {
          if (socket != null) {
            socket.close();
          }
        } catch (IOException ex) {
          Log.debug(
              "Additional exception while trying to close socket when connection to remote server failed: "
                  + ex.toString());
        }
      }
    }
    if (!socket.isConnected()) {
      return null;
    }

    SocketConnection connection = null;
    try {
      connection =
          new SocketConnection(XMPPServer.getInstance().getPacketDeliverer(), socket, false);

      // Send the stream header
      StringBuilder openingStream = new StringBuilder();
      openingStream.append("<stream:stream");
      openingStream.append(" xmlns:db=\"jabber:server:dialback\"");
      openingStream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
      openingStream.append(" xmlns=\"jabber:server\"");
      openingStream.append(" from=\"").append(localDomainName).append("\""); // OF-673
      openingStream.append(" to=\"").append(hostname).append("\"");
      openingStream.append(" version=\"1.0\">");
      connection.deliverRawText(openingStream.toString());

      // Set a read timeout (of 5 seconds) so we don't keep waiting forever
      int soTimeout = socket.getSoTimeout();
      socket.setSoTimeout(5000);

      XMPPPacketReader reader = new XMPPPacketReader();
      reader
          .getXPPParser()
          .setInput(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
      // Get the answer from the Receiving Server
      XmlPullParser xpp = reader.getXPPParser();
      for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG; ) {
        eventType = xpp.next();
      }

      String serverVersion = xpp.getAttributeValue("", "version");
      String id = xpp.getAttributeValue("", "id");

      // Check if the remote server is XMPP 1.0 compliant
      if (serverVersion != null && decodeVersion(serverVersion)[0] >= 1) {
        // Restore default timeout
        socket.setSoTimeout(soTimeout);
        // Get the stream features
        Element features = reader.parseDocument().getRootElement();
        if (features != null) {
          // Check if TLS is enabled
          if (useTLS && features.element("starttls") != null) {
            // Secure the connection with TLS and authenticate using SASL
            LocalOutgoingServerSession answer;
            answer = secureAndAuthenticate(hostname, connection, reader, openingStream, domain);
            if (answer != null) {
              // Everything went fine so return the secured and
              // authenticated connection
              return answer;
            }
          }
          // Check if we are going to try server dialback (XMPP 1.0)
          else if (ServerDialback.isEnabled() && features.element("dialback") != null) {
            Log.debug(
                "LocalOutgoingServerSession: OS - About to try connecting using server dialback XMPP 1.0 with: "
                    + hostname);
            ServerDialback method = new ServerDialback(connection, domain);
            OutgoingServerSocketReader newSocketReader = new OutgoingServerSocketReader(reader);
            if (method.authenticateDomain(newSocketReader, domain, hostname, id)) {
              Log.debug(
                  "LocalOutgoingServerSession: OS - SERVER DIALBACK XMPP 1.0 with "
                      + hostname
                      + " was successful");
              StreamID streamID = new BasicStreamIDFactory().createStreamID(id);
              LocalOutgoingServerSession session =
                  new LocalOutgoingServerSession(domain, connection, newSocketReader, streamID);
              connection.init(session);
              // Set the hostname as the address of the session
              session.setAddress(new JID(null, hostname, null));
              return session;
            } else {
              Log.debug(
                  "LocalOutgoingServerSession: OS - Error, SERVER DIALBACK with "
                      + hostname
                      + " failed");
            }
          }
        } else {
          Log.debug("LocalOutgoingServerSession: OS - Error, <starttls> was not received");
        }
      }
      // Something went wrong so close the connection and try server dialback over
      // a plain connection
      if (connection != null) {
        connection.close();
      }
    } catch (SSLHandshakeException e) {
      Log.debug(
          "LocalOutgoingServerSession: Handshake error while creating secured outgoing session to remote "
              + "server: "
              + hostname
              + "(DNS lookup: "
              + realHostname
              + ":"
              + realPort
              + "):",
          e);
      // Close the connection
      if (connection != null) {
        connection.close();
      }
    } catch (Exception e) {
      Log.error(
          "Error creating secured outgoing session to remote server: "
              + hostname
              + "(DNS lookup: "
              + realHostname
              + ":"
              + realPort
              + ")",
          e);
      // Close the connection
      if (connection != null) {
        connection.close();
      }
    }

    if (ServerDialback.isEnabled()) {
      Log.debug(
          "LocalOutgoingServerSession: OS - Going to try connecting using server dialback with: "
              + hostname);
      // Use server dialback (pre XMPP 1.0) over a plain connection
      return new ServerDialback().createOutgoingSession(domain, hostname, port);
    }
    return null;
  }
  /**
   * Creates a new outgoing connection to the specified hostname if no one exists. The port of the
   * remote server could be configured by setting the <b>xmpp.server.socket.remotePort</b> property
   * or otherwise the standard port 5269 will be used. Either a new connection was created or
   * already existed the specified hostname will be authenticated with the remote server. Once
   * authenticated the remote server will start accepting packets from the specified domain.
   *
   * <p>The Server Dialback method is currently the only implemented method for server-to-server
   * authentication. This implies that the remote server will ask the Authoritative Server to verify
   * the domain to authenticate. Most probably this (local) server will act as the Authoritative
   * Server. See {@link IncomingServerSession} for more information.
   *
   * @param domain the local domain to authenticate with the remote server.
   * @param hostname the hostname of the remote server.
   * @return True if the domain was authenticated by the remote server.
   */
  public static OutgoingServerSession authenticateDomain(String domain, String hostname) {
    if (hostname == null || hostname.length() == 0 || hostname.trim().indexOf(' ') > -1) {
      // Do nothing if the target hostname is empty, null or contains whitespaces
      return null;
    }
    try {
      // Check if the remote hostname is in the blacklist
      if (!RemoteServerManager.canAccess(hostname)) {
        return null;
      }

      OutgoingServerSession session;
      // Check if a session, that is using server dialback, already exists to the desired
      // hostname (i.e. remote server). If no one exists then create a new session. The same
      // session will be used for the same hostname for all the domains to authenticate
      SessionManager sessionManager = SessionManager.getInstance();
      if (sessionManager == null) {
        // Server is shutting down while we are trying to create a new s2s connection
        return null;
      }
      session = sessionManager.getOutgoingServerSession(hostname);
      if (session == null) {
        // Try locating if the remote server has previously authenticated with this server
        for (IncomingServerSession incomingSession :
            sessionManager.getIncomingServerSessions(hostname)) {
          for (String otherHostname : incomingSession.getValidatedDomains()) {
            session = sessionManager.getOutgoingServerSession(otherHostname);
            if (session != null) {
              if (session.isUsingServerDialback()) {
                // A session to the same remote server but with different hostname
                // was found. Use this session.
                break;
              } else {
                session = null;
              }
            }
          }
        }
      }
      if (session == null) {
        int port = RemoteServerManager.getPortForServer(hostname);
        session = createOutgoingSession(domain, hostname, port);
        if (session != null) {
          // Add the validated domain as an authenticated domain
          session.addAuthenticatedDomain(domain);
          // Add the new hostname to the list of names that the server may have
          session.addHostname(hostname);
          // Notify the SessionManager that a new session has been created
          sessionManager.outgoingServerSessionCreated((LocalOutgoingServerSession) session);
          return session;
        } else {
          Log.warn("Fail to connect to {} for {}", hostname, domain);
          return null;
        }
      }
      // A session already exists. The session was established using server dialback so
      // it is possible to do piggybacking to authenticate more domains
      if (session.getAuthenticatedDomains().contains(domain)
          && session.getHostnames().contains(hostname)) {
        // Do nothing since the domain has already been authenticated
        return session;
      }
      // A session already exists so authenticate the domain using that session
      if (session.authenticateSubdomain(domain, hostname)) return session;
    } catch (Exception e) {
      Log.error("Error authenticating domain with remote server: " + hostname, e);
    }
    return null;
  }