LocalIncomingServerSession.java 16.7 KB
Newer Older
Gaston Dombiak's avatar
Gaston Dombiak committed
1 2 3 4 5
/**
 * $RCSfile: $
 * $Revision: $
 * $Date: $
 *
6
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
Gaston Dombiak's avatar
Gaston Dombiak committed
7
 *
8 9 10 11 12 13 14 15 16 17 18
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
Gaston Dombiak's avatar
Gaston Dombiak committed
19 20 21
 */
package org.jivesoftware.openfire.session;

22
import java.io.IOException;
23
import java.security.KeyStoreException;
24 25 26 27 28 29 30
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

Gaston Dombiak's avatar
Gaston Dombiak committed
31 32 33 34 35 36 37 38 39 40
import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.openfire.server.ServerDialback;
41
import org.jivesoftware.util.CertificateManager;
Gaston Dombiak's avatar
Gaston Dombiak committed
42
import org.jivesoftware.util.JiveGlobals;
43 44
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Gaston Dombiak's avatar
Gaston Dombiak committed
45 46
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
47
import org.xmpp.packet.JID;
Gaston Dombiak's avatar
Gaston Dombiak committed
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
import org.xmpp.packet.Packet;

/**
 * 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>IncomingServerSession</tt> represents the connection to a remote server that will only
 * be used for receiving packets.<p>
 *
 * Currently only the Server Dialback method is being used for authenticating the remote server.
 * Once the remote server has been authenticated incoming packets will be processed by this server.
 * It is also possible for remote servers to authenticate more domains once the session has been
 * established. For optimization reasons the existing connection is used between the servers.
 * Therefore, the incoming server session holds the list of authenticated domains which are allowed
 * to send packets to this server.<p>
 *
 * Using the Server Dialback method it is possible that this server may also act as the
 * Authoritative Server. This implies that an incoming connection will be established with this
 * server for authenticating a domain. This incoming connection will only last for a brief moment
 * and after the domain has been authenticated the connection will be closed and no session will
 * exist.
 *
 * @author Gaston Dombiak
 */
public class LocalIncomingServerSession extends LocalSession implements IncomingServerSession {
72 73 74
	
	private static final Logger Log = LoggerFactory.getLogger(LocalIncomingServerSession.class);

75 76 77 78 79 80
    /**
     * List of domains, subdomains and virtual hostnames of the remote server that were
     * validated with this server. The remote server is allowed to send packets to this
     * server from any of the validated domains.
     */
    private Set<String> validatedDomains = new HashSet<String>();
Gaston Dombiak's avatar
Gaston Dombiak committed
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

    /**
     * Domains or subdomain of this server that was used by the remote server
     * when validating the new connection. This information is useful to prevent
     * many connections from the same remote server to the same local domain.
     */
    private String localDomain = null;

    /**
     * Creates a new session that will receive packets. The new session will be authenticated
     * before being returned. If the authentication process fails then the answer will be
     * <tt>null</tt>.<p>
     *
     * @param serverName hostname of this server.
     * @param reader reader on the new established connection with the remote server.
     * @param connection the new established connection with the remote server.
     * @return a new session that will receive packets or null if a problem occured while
     *         authenticating the remote server or when acting as the Authoritative Server during
     *         a Server Dialback authentication process.
     * @throws org.xmlpull.v1.XmlPullParserException if an error occurs while parsing the XML.
     * @throws java.io.IOException if an input/output error occurs while using the connection.
     */
    public static LocalIncomingServerSession createSession(String serverName, XMPPPacketReader reader,
            SocketConnection connection) throws XmlPullParserException, IOException {
        XmlPullParser xpp = reader.getXPPParser();
106
                
Gaston Dombiak's avatar
Gaston Dombiak committed
107 108
        String version = xpp.getAttributeValue("", "version");
        int[] serverVersion = version != null ? decodeVersion(version) : new int[] {0,0};
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
        
        try {
            // Get the stream ID for the new session
            StreamID streamID = SessionManager.getInstance().nextStreamID();
            // Create a server Session for the remote server
            LocalIncomingServerSession session =
                    SessionManager.getInstance().createIncomingServerSession(connection, streamID);

            // 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(serverName).append("\"");
            openingStream.append(" id=\"").append(streamID).append("\"");
125 126 127 128 129 130 131 132 133
            
            // OF-443: Not responding with a 1.0 version in the stream header when federating with older
            // implementations appears to reduce connection issues with those domains (patch by Marcin Cieślak).
            if (serverVersion[0] >= 1) {
                openingStream.append(" version=\"1.0\">");
            } else {
                openingStream.append(">");
            }
            
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
            connection.deliverRawText(openingStream.toString());

            if (serverVersion[0] >= 1) {        	
                // Remote server is XMPP 1.0 compliant so offer TLS and SASL to establish the connection (and server dialback)

	            // Indicate the TLS policy to use for this connection
	            Connection.TLSPolicy tlsPolicy =
	                    ServerDialback.isEnabled() ? Connection.TLSPolicy.optional :
	                            Connection.TLSPolicy.required;
	            boolean hasCertificates = false;
	            try {
	                hasCertificates = SSLConfig.getKeyStore().size() > 0;
	            }
	            catch (Exception e) {
	                Log.error(e.getMessage(), e);
	            }
	            if (Connection.TLSPolicy.required == tlsPolicy && !hasCertificates) {
	                Log.error("Server session rejected. TLS is required but no certificates " +
	                        "were created.");
	                return null;
	            }
	            connection.setTlsPolicy(hasCertificates ? tlsPolicy : Connection.TLSPolicy.disabled);
Gaston Dombiak's avatar
Gaston Dombiak committed
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

            // Indicate the compression policy to use for this connection
            String policyName = JiveGlobals.getProperty("xmpp.server.compression.policy",
                    Connection.CompressionPolicy.disabled.toString());
            Connection.CompressionPolicy compressionPolicy =
                    Connection.CompressionPolicy.valueOf(policyName);
            connection.setCompressionPolicy(compressionPolicy);

            StringBuilder sb = new StringBuilder();
            
            if (serverVersion[0] >= 1) {        	
                // Remote server is XMPP 1.0 compliant so offer TLS and SASL to establish the connection (and server dialback)
            	// Don't offer stream-features to pre-1.0 servers, as it confuses them. Sending features to Openfire < 3.7.1 confuses it too - OF-443) 
                sb.append("<stream:features>");

	            if (JiveGlobals.getBooleanProperty("xmpp.server.tls.enabled", true)) {
	                sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
	                if (!ServerDialback.isEnabled()) {
	                    // Server dialback is disabled so TLS is required
	                    sb.append("<required/>");
	                }
	                sb.append("</starttls>");
	            }
	            
	            // Include available SASL Mechanisms
	            sb.append(SASLAuthentication.getSASLMechanisms(session));
	            
	            if (ServerDialback.isEnabled()) {
	                // Also offer server dialback (when TLS is not required). Server dialback may be offered
	                // after TLS has been negotiated and a self-signed certificate is being used
	                sb.append("<dialback xmlns=\"urn:xmpp:features:dialback\"/>");
	            }

	            sb.append("</stream:features>");
191
            }
192 193
            
            connection.deliverRawText(sb.toString());
Gaston Dombiak's avatar
Gaston Dombiak committed
194

195 196 197
            // Set the domain or subdomain of the local server targeted by the remote server
            session.setLocalDomain(serverName);
            return session;
Gaston Dombiak's avatar
Gaston Dombiak committed
198 199
        }
        catch (Exception e) {
200 201
            Log.error("Error establishing connection from remote server:" + connection, e);
            connection.close();
Gaston Dombiak's avatar
Gaston Dombiak committed
202 203 204 205
            return null;
        }
    }

206

Gaston Dombiak's avatar
Gaston Dombiak committed
207 208 209 210
    public LocalIncomingServerSession(String serverName, Connection connection, StreamID streamID) {
        super(serverName, connection, streamID);
    }

211 212
    @Override
	boolean canProcess(Packet packet) {
Gaston Dombiak's avatar
Gaston Dombiak committed
213 214 215 216
        return true;
    }


217 218
    @Override
	void deliver(Packet packet) throws UnauthorizedException {
Gaston Dombiak's avatar
Gaston Dombiak committed
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
        // Do nothing
    }

    /**
     * Returns true if the request of a new domain was valid. Sessions may receive subsequent
     * domain validation request. If the validation of the new domain fails then the session and
     * the underlying TCP connection will be closed.<p>
     *
     * For optimization reasons, the same session may be servicing several domains of a
     * remote server.
     *
     * @param dbResult the DOM stanza requesting the domain validation.
     * @return true if the requested domain was valid.
     */
    public boolean validateSubsequentDomain(Element dbResult) {
        ServerDialback method = new ServerDialback(getConnection(), getServerName());
        if (method.validateRemoteDomain(dbResult, getStreamID())) {
            // Add the validated domain as a valid domain
            addValidatedDomain(dbResult.attributeValue("from"));
            return true;
        }
        return false;
    }

    /**
     * Returns true if the specified domain has been validated for this session. The remote
     * server should send a "db:result" packet for registering new subdomains or even
     * virtual hosts.<p>
     *
     * In the spirit of being flexible we allow remote servers to not register subdomains
     * and even so consider subdomains that include the server domain in their domain part
     * as valid domains.
     *
     * @param domain the domain to validate.
     * @return true if the specified domain has been validated for this session.
     */
    public boolean isValidDomain(String domain) {
        // Check if the specified domain is contained in any of the validated domains
        for (String validatedDomain : getValidatedDomains()) {
            if (domain.contains(validatedDomain)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns a collection with all the domains, subdomains and virtual hosts that where
     * validated. The remote server is allowed to send packets from any of these domains,
     * subdomains and virtual hosts.
     *
     * @return domains, subdomains and virtual hosts that where validated.
     */
    public Collection<String> getValidatedDomains() {
273
        return Collections.unmodifiableCollection(validatedDomains);
Gaston Dombiak's avatar
Gaston Dombiak committed
274 275 276 277 278 279 280 281 282
    }

    /**
     * Adds a new validated domain, subdomain or virtual host to the list of
     * validated domains for the remote server.
     *
     * @param domain the new validated domain, subdomain or virtual host to add.
     */
    public void addValidatedDomain(String domain) {
283
        if (validatedDomains.add(domain)) {
284 285 286 287
            // Set the first validated domain as the address of the session
            if (validatedDomains.size() < 2) {
                setAddress(new JID(null, domain, null));
            }
Gaston Dombiak's avatar
Gaston Dombiak committed
288 289 290 291 292 293 294 295 296 297 298 299 300 301
            // Register the new validated domain for this server session in SessionManager
            SessionManager.getInstance().registerIncomingServerSession(domain, this);
        }
    }

    /**
     * Removes the previously validated domain from the list of validated domains. The remote
     * server will no longer be able to send packets from the removed domain, subdomain or
     * virtual host.
     *
     * @param domain the domain, subdomain or virtual host to remove from the list of
     *        validated domains.
     */
    public void removeValidatedDomain(String domain) {
302
        validatedDomains.remove(domain);
Gaston Dombiak's avatar
Gaston Dombiak committed
303
        // Unregister the validated domain for this server session in SessionManager
304
        SessionManager.getInstance().unregisterIncomingServerSession(domain, this);
Gaston Dombiak's avatar
Gaston Dombiak committed
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
    }

    /**
     * Returns the domain or subdomain of the local server used by the remote server
     * when validating the session. This information is only used to prevent many
     * connections from the same remote server to the same domain or subdomain of
     * the local server.
     *
     * @return the domain or subdomain of the local server used by the remote server
     *         when validating the session.
     */
    public String getLocalDomain() {
        return localDomain;
    }

    /**
     * Sets the domain or subdomain of the local server used by the remote server when asking
     * to validate the session. This information is only used to prevent many connections from
     * the same remote server to the same domain or subdomain of the local server.
     *
     * @param domain the domain or subdomain of the local server used when validating the
     *        session.
     */
    public void setLocalDomain(String domain) {
        localDomain = domain;
    }

    /**
     * Verifies the received key sent by the remote server. This server is trying to generate
     * an outgoing connection to the remote server and the remote server is reusing an incoming
     * connection for validating the key.
     *
     * @param doc the received Element that contains the key to verify.
     */
    public void verifyReceivedKey(Element doc) {
        ServerDialback.verifyReceivedKey(doc, getConnection());
    }

343 344
    @Override
	public String getAvailableStreamFeatures() {
345
        StringBuilder sb = new StringBuilder();
346
        
Gaston Dombiak's avatar
Gaston Dombiak committed
347 348 349
        // Include Stream Compression Mechanism
        if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
                !conn.isCompressed()) {
350
            sb.append("<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>");
Gaston Dombiak's avatar
Gaston Dombiak committed
351
        }
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
        
        // Offer server dialback if using self-signed certificates and no authentication has been done yet
        boolean usingSelfSigned;
        final Certificate[] chain = conn.getLocalCertificates();
        if (chain == null || chain.length == 0) {
        	usingSelfSigned = true;
        } else {
        	try {
				usingSelfSigned = CertificateManager.isSelfSignedCertificate(SSLConfig.getKeyStore(), (X509Certificate) chain[0]);
			} catch (KeyStoreException ex) {
				Log.warn("Exception occurred while trying to determine whether local certificate is self-signed. Proceeding as if it is.", ex);
				usingSelfSigned = true;
			} catch (IOException ex) {
				Log.warn("Exception occurred while trying to determine whether local certificate is self-signed. Proceeding as if it is.", ex);
				usingSelfSigned = true;
			}
368
        }
369
        
370 371 372
        if (usingSelfSigned && ServerDialback.isEnabledForSelfSigned() && validatedDomains.isEmpty()) {
            sb.append("<dialback xmlns=\"urn:xmpp:features:dialback\"/>");
        }
373
        
374
        return sb.toString();
Gaston Dombiak's avatar
Gaston Dombiak committed
375 376
    }
}