OutgoingServerSession.java 25.1 KB
Newer Older
Gaston Dombiak's avatar
Gaston Dombiak committed
1
/**
2
 * $RCSfile: OutgoingServerSession.java,v $
Gaston Dombiak's avatar
Gaston Dombiak committed
3 4 5 6 7 8 9 10 11 12 13
 * $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;

14 15 16
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.XPPPacketReader;
Gaston Dombiak's avatar
Gaston Dombiak committed
17 18
import org.jivesoftware.messenger.*;
import org.jivesoftware.messenger.auth.UnauthorizedException;
19 20 21 22
import org.jivesoftware.messenger.net.DNSUtil;
import org.jivesoftware.messenger.net.SocketConnection;
import org.jivesoftware.messenger.spi.BasicStreamIDFactory;
import org.jivesoftware.util.JiveGlobals;
Gaston Dombiak's avatar
Gaston Dombiak committed
23 24
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
25 26 27 28
import org.jivesoftware.util.StringUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
Gaston Dombiak's avatar
Gaston Dombiak committed
29 30 31
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;

32 33 34 35
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
Gaston Dombiak's avatar
Gaston Dombiak committed
36 37 38
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
39
import java.util.Iterator;
40
import java.util.regex.Pattern;
Gaston Dombiak's avatar
Gaston Dombiak committed
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64

/**
 * 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 {

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

Gaston Dombiak's avatar
Gaston Dombiak committed
70 71
    private Collection<String> authenticatedDomains = new ArrayList<String>();
    private Collection<String> hostnames = new ArrayList<String>();
72
    private OutgoingServerSocketReader socketReader;
73 74 75 76
    /**
     * Flag that indicates if the session was created usign server-dialback.
     */
    private boolean usingServerDialback = true;
Gaston Dombiak's avatar
Gaston Dombiak committed
77 78 79 80 81 82 83 84 85 86 87 88

    /**
     * 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
Gaston Dombiak's avatar
Gaston Dombiak committed
89
     * Authoritative Server. See {@link IncomingServerSession} for more information.
Gaston Dombiak's avatar
Gaston Dombiak committed
90 91 92 93 94 95
     *
     * @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) {
96 97
        if (hostname == null || hostname.length() == 0 || hostname.trim().indexOf(' ') > -1) {
            // Do nothing if the target hostname is empty, null or contains whitespaces
98 99
            return false;
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
100
        try {
101 102 103 104
            // Check if the remote hostname is in the blacklist
            if (!RemoteServerManager.canAccess(hostname)) {
                return false;
            }
Gaston Dombiak's avatar
Gaston Dombiak committed
105

106 107 108
            // 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
Gaston Dombiak's avatar
Gaston Dombiak committed
109 110 111 112
            SessionManager sessionManager = SessionManager.getInstance();
            OutgoingServerSession session = sessionManager.getOutgoingServerSession(hostname);
            if (session == null) {
                // Try locating if the remote server has previously authenticated with this server
113 114
                for (IncomingServerSession incomingSession : sessionManager
                        .getIncomingServerSessions(hostname)) {
Gaston Dombiak's avatar
Gaston Dombiak committed
115 116 117
                    for (String otherHostname : incomingSession.getValidatedDomains()) {
                        session = sessionManager.getOutgoingServerSession(otherHostname);
                        if (session != null) {
118 119 120 121 122 123 124 125 126 127
                            if (session.usingServerDialback) {
                                // 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;
                            }
                            else {
                                session = null;
                            }
Gaston Dombiak's avatar
Gaston Dombiak committed
128 129 130 131 132
                        }
                    }
                }
            }
            if (session == null) {
133
                int port = RemoteServerManager.getPortForServer(hostname);
Gaston Dombiak's avatar
Gaston Dombiak committed
134 135 136 137
                // 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) {
138
                        session = createOutgoingSession(domain, hostname, port);
Gaston Dombiak's avatar
Gaston Dombiak committed
139 140 141 142 143
                        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);
144 145
                            // Notify the SessionManager that a new session has been created
                            sessionManager.outgoingServerSessionCreated(session);
Gaston Dombiak's avatar
Gaston Dombiak committed
146 147 148
                            return true;
                        }
                        else {
149 150 151 152
                            // Ensure that the hostname is not an IP address (i.e. contains chars)
                            if (!pattern.matcher(hostname).find()) {
                                return false;
                            }
153 154 155 156 157 158 159 160 161
                            // 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;
                                }
                            }
162 163 164 165 166 167 168 169 170 171
                            // 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);
172 173
                                String serverName = XMPPServer.getInstance().getServerInfo()
                                        .getName();
174
                                if ("com".equals(newHostname) || "net".equals(newHostname) ||
Gaston Dombiak's avatar
Gaston Dombiak committed
175 176
                                        "org".equals(newHostname) ||
                                        "gov".equals(newHostname) ||
177 178
                                        "edu".equals(newHostname) ||
                                        serverName.equals(newHostname)) {
179 180
                                    return false;
                                }
181
                                session = createOutgoingSession(domain, newHostname, port);
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
                                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);
                                }
                            }
Gaston Dombiak's avatar
Gaston Dombiak committed
197 198 199 200 201
                            return false;
                        }
                    }
                }
            }
202 203
            // A session already exists. The session was established using server dialback so
            // it is possible to do piggybacking to authenticate more domains
Gaston Dombiak's avatar
Gaston Dombiak committed
204 205 206 207 208 209
            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);
210
            if (method.authenticateDomain(session.socketReader, domain, hostname,
Gaston Dombiak's avatar
Gaston Dombiak committed
211 212 213 214 215 216 217 218 219 220 221 222
                    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;
    }

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 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
    /**
     * 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 OutgoingServerSession createOutgoingSession(String domain, String hostname,
            int port) {
        boolean useTLS = JiveGlobals.getBooleanProperty("xmpp.server.tls.enabled", false);
        RemoteServerConfiguration configuration = RemoteServerManager.getConfiguration(hostname);
        if (configuration != null) {
            // TODO Use the specific TLS configuration for this remote server
            //useTLS = configuration.isTLSEnabled();
        }

        if (useTLS) {
            // Connect to remote server using TLS + SASL
            SocketConnection connection = null;
            String realHostname = null;
            int realPort = port;
            Socket socket = new Socket();
            try {
                Log.debug("OS - Trying to connect to " + hostname + ":" + port);

                // Get the real hostname to connect to using DNS lookup of the specified hostname
                DNSUtil.HostAddress address = DNSUtil.resolveXMPPServerDomain(hostname, port);
                realHostname = address.getHost();
                realPort = address.getPort();
                // Establish a TCP connection to the Receiving Server
                socket.connect(new InetSocketAddress(realHostname, realPort),
                        RemoteServerManager.getSocketTimeout());
                Log.debug("OS - Plain connection to " + hostname + ":" + port + " successful");
            }
            catch (Exception e) {
                Log.error("Error trying to connect to remote server: " + hostname +
                        "(DNS lookup: " + realHostname + ":" + realPort + ")", e);
                return 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:stream=\"http://etherx.jabber.org/streams\"");
                openingStream.append(" xmlns=\"jabber:server\"");
                openingStream.append(" to=\"").append(hostname).append("\"");
                openingStream.append(" version=\"1.0\">");
                connection.deliverRawText(openingStream.toString());

                XPPPacketReader reader = new XPPPacketReader();
                reader.setXPPFactory(XmlPullParserFactory.newInstance());
                reader.getXPPParser().setInput(new InputStreamReader(socket.getInputStream(),
                        CHARSET));
                // 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");

                // Check if the remote server is XMPP 1.0 compliant
                if (serverVersion != null && decodeVersion(serverVersion)[0] >= 1) {
                    // Get the stream features
                    Element features = reader.parseDocument().getRootElement();
                    // Check if TLS is enabled
                    if (features != null && features.element("starttls") != null) {
                        // Secure the connection with TLS and authenticate using SASL
                        OutgoingServerSession answer;
                        answer = secureAndAuthenticate(hostname, connection, reader, openingStream,
                                xpp, domain);
                        if (answer != null) {
                            // Everything went fine so return the secured and
                            // authenticated connection
                            return answer;
                        }
                    }
                    else {
                        Log.debug("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 (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();
                }
            }
        }
        Log.debug("OS - Going to try connecting using server dialback");
        // Use server dialback over a plain connection
        return new ServerDialback().createOutgoingSession(domain, hostname, port);
    }

    private static OutgoingServerSession secureAndAuthenticate(String hostname,
            SocketConnection connection, XPPPacketReader reader, StringBuilder openingStream,
            XmlPullParser xpp, String domain) throws Exception {
        Element features;
        Log.debug("OS - Indicating we want TLS to " + hostname);
        connection.deliverRawText("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");

        // Wait for the <proceed> response
        Element proceed = reader.parseDocument().getRootElement();
        if (proceed != null && proceed.getName().equals("proceed")) {
            Log.debug("OS - Negotiating TLS with " + hostname);
            connection.startTLS(true);
            Log.debug("OS - TLS negotiation with " + hostname + " was successful");

            // TLS negotiation was successful so initiate a new stream
            connection.deliverRawText(openingStream.toString());

            // Reset the parser to use the new secured reader
            xpp.setInput(new InputStreamReader(connection.getTLSStreamHandler().getInputStream(),
                    CHARSET));
            // Skip new stream element
            for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
                eventType = xpp.next();
            }
            // Get new stream features
            features = reader.parseDocument().getRootElement();
            if (features != null && features.element("mechanisms") != null) {
                Iterator it = features.element("mechanisms").elementIterator();
                while (it.hasNext()) {
                    Element mechanism = (Element) it.next();
                    if ("EXTERNAL".equals(mechanism.getTextTrim())) {
                        Log.debug("OS - Starting EXTERNAL SASL with " + hostname);
                        if (doExternalAuthentication(domain, connection, reader)) {
                            Log.debug("OS - EXTERNAL SASL with " + hostname + " was successful");
                            // SASL was successful so initiate a new stream
                            connection.deliverRawText(openingStream.toString());

                            // Reset the parser to use the new secured reader
                            xpp.setInput(new InputStreamReader(
                                    connection.getTLSStreamHandler().getInputStream(), CHARSET));
                            // Skip the opening stream sent by the server
                            for (int eventType = xpp.getEventType();
                                 eventType != XmlPullParser.START_TAG;) {
                                eventType = xpp.next();
                            }

                            // SASL authentication was successful so create new
                            // OutgoingServerSession
                            String id = xpp.getAttributeValue("", "id");
                            StreamID streamID = new BasicStreamIDFactory().createStreamID(id);
                            OutgoingServerSession session = new OutgoingServerSession(domain,
                                    connection, new OutgoingServerSocketReader(reader), streamID);
                            connection.init(session);
                            // Set the hostname as the address of the session
                            session.setAddress(new JID(null, hostname, null));
389 390
                            // Set that the session was created using TLS+SASL (no server dialback)
                            session.usingServerDialback = false;
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
                            return session;
                        }
                        else {
                            Log.debug("OS - Error, EXTERNAL SASL authentication with " + hostname +
                                    " failed");
                        }
                    }
                }
                Log.debug("OS - Error, EXTERNAL SASL was not offered by " + hostname);
            }
            else {
                Log.debug("OS - Error, no SASL mechanisms were offered by " + hostname);
            }
        }
        else {
            Log.debug("OS - Error, <proceed> was not received");
        }
        return null;
    }

    private static boolean doExternalAuthentication(String domain, SocketConnection connection,
            XPPPacketReader reader) throws DocumentException, IOException, XmlPullParserException {

        StringBuilder sb = new StringBuilder();
        sb.append("<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"EXTERNAL\">");
        sb.append(StringUtils.encodeBase64(domain));
        sb.append("</auth>");
        connection.deliverRawText(sb.toString());

        Element response = reader.parseDocument().getRootElement();
        if (response != null && "success".equals(response.getName())) {
            return true;
        }
        return false;
    }

427 428
    OutgoingServerSession(String serverName, Connection connection,
            OutgoingServerSocketReader socketReader, StreamID streamID) {
Gaston Dombiak's avatar
Gaston Dombiak committed
429
        super(serverName, connection, streamID);
430 431
        this.socketReader = socketReader;
        socketReader.setSession(this);
Gaston Dombiak's avatar
Gaston Dombiak committed
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
    }

    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);
        }
    }

}