OutgoingServerSession.java 13.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright (C) 2004 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package org.jivesoftware.messenger.server;

import org.jivesoftware.messenger.*;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.regex.Pattern;

/**
 * Server-to-server communication is done using two TCP connections between the servers. One
 * connection is used for sending packets while the other connection is used for receiving packets.
 * The <tt>OutgoingServerSession</tt> represents the connection to a remote server that will only
 * be used for sending packets.<p>
 *
 * Currently only the Server Dialback method is being used for authenticating with the remote
 * server. Use {@link #authenticateDomain(String, String)} to create a new connection to a remote
 * server that will be used for sending packets to the remote server from the specified domain.
 * Only the authenticated domains with the remote server will be able to effectively send packets
 * to the remote server. The remote server will reject and close the connection if a
 * non-authenticated domain tries to send a packet through this connection.<p>
 *
 * Once the connection has been established with the remote server and at least a domain has been
 * authenticated then a new route will be added to the routing table for this connection. For
 * optimization reasons the same outgoing connection will be used even if the remote server has
 * several hostnames. However, different routes will be created in the routing table for each
 * hostname of the remote server.
 *
 * @author Gaston Dombiak
 */
public class OutgoingServerSession extends Session {

    /**
     * Regular expression to ensure that the hostname contains letters.
     */
    private static Pattern pattern = Pattern.compile("[a-zA-Z]");

    private Collection<String> authenticatedDomains = new ArrayList<String>();
    private Collection<String> hostnames = new ArrayList<String>();
    private OutgoingServerSocketReader socketReader;

    /**
     * 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 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 boolean 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 false;
        }
        try {
            // Check if the remote hostname is in the blacklist
            if (!RemoteServerManager.canAccess(hostname)) {
                return false;
            }

            // Check if a session 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();
            OutgoingServerSession session = sessionManager.getOutgoingServerSession(hostname);
            if (session == null) {
                // Try locating if the remote server has previously authenticated with this server
                IncomingServerSession incomingSession = sessionManager.getIncomingServerSession(
                        hostname);
                if (incomingSession != null) {
                    for (String otherHostname : incomingSession.getValidatedDomains()) {
                        session = sessionManager.getOutgoingServerSession(otherHostname);
                        if (session != null) {
                            // A session to the same remote server but with different hostname
                            // was found. Use this session and add the new hostname to the session
                            session.addHostname(hostname);
                            break;
                        }
                    }
                }
            }
            if (session == null) {
                int port = RemoteServerManager.getPortForServer(hostname);
                // No session was found to the remote server so make sure that only one is created
                synchronized (hostname.intern()) {
                    session = sessionManager.getOutgoingServerSession(hostname);
                    if (session == null) {
                        session =
                                new ServerDialback().createOutgoingSession(domain, hostname, port);
                        if (session != null) {
                            // Add the new hostname to the list of names that the server may have
                            session.addHostname(hostname);
                            // Add the validated domain as an authenticated domain
                            session.addAuthenticatedDomain(domain);
                            // Notify the SessionManager that a new session has been created
                            sessionManager.outgoingServerSessionCreated(session);
                            return true;
                        }
                        else {
                            // Ensure that the hostname is not an IP address (i.e. contains chars)
                            if (!pattern.matcher(hostname).find()) {
                                return false;
                            }
                            // Check if hostname is a subdomain of an existing outgoing session
                            for (String otherHost : sessionManager.getOutgoingServers()) {
                                if (hostname.contains(otherHost)) {
                                    session = sessionManager.getOutgoingServerSession(otherHost);
                                    // Add the new hostname to the found session
                                    session.addHostname(hostname);
                                    return true;
                                }
                            }
                            // Try to establish a connection to candidate hostnames. Iterate on the
                            // substring after the . and try to establish a connection. If a
                            // connection is established then the same session will be used for
                            // sending packets to the "candidate hostname" as well as for the
                            // requested hostname (i.e. the subdomain of the candidate hostname)
                            // This trick is useful when remote servers haven't registered in their
                            // DNSs an entry for their subdomains
                            int index = hostname.indexOf('.');
                            while (index > -1 && index < hostname.length()) {
                                String newHostname = hostname.substring(index + 1);
                                String serverName = XMPPServer.getInstance().getServerInfo()
                                        .getName();
                                if ("com".equals(newHostname) || "net".equals(newHostname) ||
                                        "org".equals(newHostname) ||
                                        "gov".equals(newHostname) ||
                                        "edu".equals(newHostname) ||
                                        serverName.equals(newHostname)) {
                                    return false;
                                }
                                session =
                                        new ServerDialback().createOutgoingSession(domain, newHostname, port);
                                if (session != null) {
                                    // Add the new hostname to the list of names that the server may have
                                    session.addHostname(hostname);
                                    // Add the validated domain as an authenticated domain
                                    session.addAuthenticatedDomain(domain);
                                    // Notify the SessionManager that a new session has been created
                                    sessionManager.outgoingServerSessionCreated(session);
                                    // Add the new hostname to the found session
                                    session.addHostname(newHostname);
                                    return true;
                                }
                                else {
                                    index = hostname.indexOf('.', index + 1);
                                }
                            }
                            return false;
                        }
                    }
                }
            }
            if (session.getAuthenticatedDomains().contains(domain)) {
                // Do nothing since the domain has already been authenticated
                return true;
            }
            // A session already exists so authenticate the domain using that session
            ServerDialback method = new ServerDialback(session.getConnection(), domain);
            if (method.authenticateDomain(session.socketReader, domain, hostname,
                    session.getStreamID().getID())) {
                // Add the validated domain as an authenticated domain
                session.addAuthenticatedDomain(domain);
                return true;
            }
        }
        catch (Exception e) {
            Log.error("Error authenticating domain with remote server: " + hostname, e);
        }
        return false;
    }

    OutgoingServerSession(String serverName, Connection connection,
            OutgoingServerSocketReader socketReader, StreamID streamID) {
        super(serverName, connection, streamID);
        this.socketReader = socketReader;
        socketReader.setSession(this);
    }

    public void process(Packet packet) throws UnauthorizedException, PacketException {
        if (conn != null && !conn.isClosed()) {
            try {
                conn.deliver(packet);
            }
            catch (Exception e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
    }

    /**
     * Returns a collection with all the domains, subdomains and virtual hosts that where
     * authenticated. The remote server will accept packets sent from any of these domains,
     * subdomains and virtual hosts.
     *
     * @return domains, subdomains and virtual hosts that where validated.
     */
    public Collection<String> getAuthenticatedDomains() {
        return Collections.unmodifiableCollection(authenticatedDomains);
    }

    /**
     * Adds a new authenticated domain, subdomain or virtual host to the list of
     * authenticated domains for the remote server. The remote server will accept packets
     * sent from this new authenticated domain.
     *
     * @param domain the new authenticated domain, subdomain or virtual host to add.
     */
    public void addAuthenticatedDomain(String domain) {
        authenticatedDomains.add(domain);
    }

    /**
     * Removes an authenticated domain from the list of authenticated domains. The remote
     * server will no longer be able to accept packets sent from the removed domain, subdomain or
     * virtual host.
     *
     * @param domain the domain, subdomain or virtual host to remove from the list of
     *               authenticated domains.
     */
    public void removeAuthenticatedDomain(String domain) {
        authenticatedDomains.remove(domain);
    }

    /**
     * Returns the list of hostnames related to the remote server. This tracking is useful for
     * reusing the same session for the same remote server even if the server has many names.
     *
     * @return the list of hostnames related to the remote server.
     */
    public Collection<String> getHostnames() {
        return Collections.unmodifiableCollection(hostnames);
    }

    /**
     * Adds a new hostname to the list of known hostnames of the remote server. This tracking is
     * useful for reusing the same session for the same remote server even if the server has
     * many names.
     *
     * @param hostname the new known name of the remote server
     */
    private void addHostname(String hostname) {
        if (hostnames.add(hostname)) {
            // Register the outgoing session in the SessionManager. If the session
            // was already registered nothing happens
            sessionManager.registerOutgoingServerSession(hostname, this);
            // Add a new route for this new session
            XMPPServer.getInstance().getRoutingTable().addRoute(new JID(hostname), this);
        }
    }

}