/* * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * 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. */ package org.jivesoftware.openfire.server; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.Socket; import java.net.SocketAddress; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.XMPPPacketReader; import org.jivesoftware.openfire.*; import org.jivesoftware.openfire.auth.AuthFactory; import org.jivesoftware.openfire.net.*; import org.jivesoftware.openfire.session.ConnectionSettings; import org.jivesoftware.openfire.session.IncomingServerSession; import org.jivesoftware.openfire.session.LocalIncomingServerSession; import org.jivesoftware.openfire.session.LocalOutgoingServerSession; import org.jivesoftware.openfire.spi.BasicStreamIDFactory; import org.jivesoftware.openfire.spi.ConnectionManagerImpl; import org.jivesoftware.openfire.spi.ConnectionType; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.StringUtils; import org.jivesoftware.util.cache.Cache; import org.jivesoftware.util.cache.CacheFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmpp.packet.JID; import org.xmpp.packet.PacketError; import org.xmpp.packet.StreamError; import javax.net.ssl.SSLHandshakeException; /** * Implementation of the Server Dialback method as defined by the RFC3920. * * The dialback method follows the following logic to validate the remote server: * <ol> * <li>The Originating Server establishes a connection to the Receiving Server.</li> * <li>The Originating Server sends a 'key' value over the connection to the Receiving * Server.</li> * <li>The Receiving Server establishes a connection to the Authoritative Server.</li> * <li>The Receiving Server sends the same 'key' value to the Authoritative Server.</li> * <li>The Authoritative Server replies that key is valid or invalid.</li> * <li>The Receiving Server informs the Originating Server whether it is authenticated or * not.</li> * </ol> * * By default a timeout of 20 seconds will be used for reading packets from remote servers. Use * the property <b>xmpp.server.read.timeout</b> to change that value. The value should be in * milliseconds. * * @author Gaston Dombiak */ public class ServerDialback { private enum VerifyResult { decline, // For some reason, we declined to do the verify. error, // Remote error from the authoritative server. valid, // Explicitly valid. invalid // Invalid. } private static final Logger Log = LoggerFactory.getLogger(ServerDialback.class); /** * The utf-8 charset for decoding and encoding Jabber packet streams. */ protected static String CHARSET = "UTF-8"; /** * Cache (unlimited, never expire) that holds the secret key to be used for * encoding and decoding keys used for authentication. * Key: constant hard coded value, Value: random generated string */ private static Cache<String, String> secretKeyCache; private static XmlPullParserFactory FACTORY = null; static { try { FACTORY = XmlPullParserFactory.newInstance(MXParser.class.getName(), null); } catch (XmlPullParserException e) { Log.error("Error creating a parser factory", e); } secretKeyCache = CacheFactory.createCache("Secret Keys Cache"); } private Connection connection; private String serverName; private SessionManager sessionManager = SessionManager.getInstance(); private RoutingTable routingTable = XMPPServer.getInstance().getRoutingTable(); /** * Returns true if server dialback is enabled. When enabled remote servers may connect to this * server using the server dialback method and this server may try the server dialback method * to connect to remote servers.<p> * * When TLS is enabled between servers and server dialback method is enabled then TLS is going * to be tried first, when connecting to a remote server, and if TLS fails then server dialback * is going to be used as a last resort. If enabled and the remote server offered server-dialback * after TLS and no SASL EXTERNAL then server dialback will be used. * * @return true if server dialback is enabled. */ public static boolean isEnabled() { return JiveGlobals.getBooleanProperty(ConnectionSettings.Server.DIALBACK_ENABLED, true); } /** * Returns true if server dialback can be used when the remote server presented a self-signed * certificate. During TLS the remote server can present a self-signed certificate, if this * setting is enabled then the self-signed certificate will be accepted and if SASL EXTERNAL * is not offered then server dialback will be used for verifying the remote server.<p> * * If self-signed certificates are accepted then server dialback over TLS is enabled. * * @return true if server dialback can be used when the remote server presented a self-signed * certificate. */ public static boolean isEnabledForSelfSigned() { return JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_ACCEPT_SELFSIGNED_CERTS, false); } /** * Sets if server dialback can be used when the remote server presented a self-signed * certificate. During TLS the remote server can present a self-signed certificate, if this * setting is enabled then the self-signed certificate will be accepted and if SASL EXTERNAL * is not offered then server dialback will be used for verifying the remote server.<p> * * If self-signed certificates are accepted then server dialback over TLS is enabled. * * @param enabled if server dialback can be used when the remote server presented a self-signed * certificate. */ public static void setEnabledForSelfSigned(boolean enabled) { JiveGlobals.setProperty(ConnectionSettings.Server.TLS_ACCEPT_SELFSIGNED_CERTS, Boolean.toString(enabled)); } /** * Creates a new instance that will be used for creating {@link IncomingServerSession}, * validating subsequent domains or authenticatig new domains. Use * {@link #createIncomingSession(org.dom4j.io.XMPPPacketReader)} for creating a new server * session used for receiving packets from the remote server. Use * {@link #validateRemoteDomain(org.dom4j.Element, org.jivesoftware.openfire.StreamID)} for * validating subsequent domains and use * {@link #authenticateDomain(OutgoingServerSocketReader, String, String, String)} for * registering new domains that are allowed to send packets to the remote server.<p> * * For validating domains a new TCP connection will be established to the Authoritative Server. * The Authoritative Server may be the same Originating Server or some other machine in the * Originating Server's network. Once the remote domain gets validated the Originating Server * will be allowed for sending packets to this server. However, this server will need to * validate its domain/s with the Originating Server if this server needs to send packets to * the Originating Server. Another TCP connection will be established for validation this * server domain/s and for sending packets to the Originating Server. * * @param connection the connection created by the remote server. * @param serverName the name of the local server. */ public ServerDialback(Connection connection, String serverName) { this.connection = connection; this.serverName = serverName; } public ServerDialback() { } /** * Creates a new connection from the Originating Server to the Receiving Server for * authenticating the specified domain. * * @param localDomain domain of the Originating Server to authenticate with the Receiving Server. * @param remoteDomain IP address or hostname of the Receiving Server. * @param port port of the Receiving Server. * @return an OutgoingServerSession if the domain was authenticated or <tt>null</tt> if none. */ public LocalOutgoingServerSession createOutgoingSession(String localDomain, String remoteDomain, int port) { final Logger log = LoggerFactory.getLogger( Log.getName() + "[Acting as Originating Server: Create Outgoing Session from: " + localDomain + " to RS at: " + remoteDomain + " (port: " + port+ ")]" ); log.debug( "Creating new outgoing session..." ); String hostname = null; int realPort = port; try { // Establish a TCP connection to the Receiving Server final Socket socket = SocketUtil.createSocketToXmppDomain( remoteDomain, port ); if ( socket == null ) { log.info( "Unable to create new outgoing session: Cannot create a plain socket connection with any applicable remote host." ); return null; } connection = new SocketConnection(XMPPServer.getInstance().getPacketDeliverer(), socket, false); log.debug( "Send the stream header and wait for response..." ); StringBuilder stream = new StringBuilder(); stream.append("<stream:stream"); stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\""); stream.append(" xmlns=\"jabber:server\""); stream.append(" to=\"").append(remoteDomain).append("\""); stream.append(" from=\"").append(localDomain).append("\""); stream.append(" xmlns:db=\"jabber:server:dialback\""); stream.append(">"); connection.deliverRawText(stream.toString()); // Set a read timeout (of 5 seconds) so we don't keep waiting forever int soTimeout = socket.getSoTimeout(); socket.setSoTimeout(RemoteServerManager.getSocketTimeout()); XMPPPacketReader reader = new XMPPPacketReader(); reader.setXPPFactory(FACTORY); reader.getXPPParser().setInput(new InputStreamReader( ServerTrafficCounter.wrapInputStream(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(); } log.debug( "Got a response. Check if the remote server supports dialback..." ); if ("jabber:server:dialback".equals(xpp.getNamespace("db"))) { log.debug( "Dialback seems to be supported by the remote server." ); // Restore default timeout socket.setSoTimeout(soTimeout); String id = xpp.getAttributeValue("", "id"); OutgoingServerSocketReader socketReader = new OutgoingServerSocketReader(reader); if (authenticateDomain(socketReader, localDomain, remoteDomain, id)) { log.debug( "Successfully authenticated the connection with dialback." ); // Domain was validated so create a new OutgoingServerSession StreamID streamID = BasicStreamIDFactory.createStreamID(id); LocalOutgoingServerSession session = new LocalOutgoingServerSession(localDomain, connection, socketReader, streamID); connection.init(session); // Set the hostname as the address of the session session.setAddress(new JID(null, remoteDomain, null)); log.debug( "Successfully created new outgoing session!" ); return session; } else { log.debug( "Failed to authenticate the connection with dialback." ); // Close the connection connection.close(); } } else { log.debug("Error! Invalid namespace in packet: '{}'. Closing connection.", xpp.getText() ); // Send an invalid-namespace stream error condition in the response connection.deliverRawText( new StreamError(StreamError.Condition.invalid_namespace).toXML()); // Close the connection connection.close(); } } catch (Exception e) { log.error( "An exception occurred while creating outgoing session to remote server:", e ); // Close the connection if (connection != null) { connection.close(); } } log.warn( "Unable to create a new outgoing session" ); return null; } /** * Authenticates the Originating Server domain with the Receiving Server. Once the domain has * been authenticated the Receiving Server will start accepting packets from the Originating * Server.<p> * * The Receiving Server will connect to the Authoritative Server to verify the dialback key. * Most probably the Originating Server machine will be the Authoritative Server too. * * @param socketReader the reader to use for reading the answer from the Receiving Server. * @param localDomain the domain to authenticate. * @param remoteDomain the domain of the remote server (i.e. Receiving Server). * @param id the stream id to be used for creating the dialback key. * @return true if the Receiving Server authenticated the domain with the Authoritative Server. */ public boolean authenticateDomain(OutgoingServerSocketReader socketReader, String localDomain, String remoteDomain, String id) { final Logger log = LoggerFactory.getLogger( Log.getName() + "[Acting as Originating Server: Authenticate domain: " + localDomain + " with RS: " + remoteDomain + " (id: " + id + ")]" ); log.debug( "Authenticating domain ..." ); String key = AuthFactory.createDigest( id, getSecretkey() ); synchronized (socketReader) { log.debug( "Sending dialback key and wait for the validation response..." ); StringBuilder sb = new StringBuilder(); sb.append("<db:result"); sb.append(" from=\"").append(localDomain).append("\""); sb.append(" to=\"").append(remoteDomain).append("\">"); sb.append(key); sb.append("</db:result>"); connection.deliverRawText(sb.toString()); // Process the answer from the Receiving Server try { while (true) { Element doc = socketReader.getElement(RemoteServerManager.getSocketTimeout(), TimeUnit.MILLISECONDS); if (doc == null) { log.debug( "Failed to authenticate domain: Time out waiting for validation response." ); return false; } else if ("db".equals(doc.getNamespacePrefix()) && "result".equals(doc.getName())) { if ( "valid".equals(doc.attributeValue("type")) ) { log.debug( "Authenticated succeeded!" ); return true; } else { log.debug( "Failed to authenticate domain: the validation response was received, but did not grant authentication." ); return false; } } else { log.warn( "Ignoring unexpected answer while waiting for dialback validation: " + doc.asXML() ); } } } catch (InterruptedException e) { log.debug( "Failed to authenticate domain: An interrupt was received while waiting for validation response (is Openfire shutting down?)" ); return false; } } } /** * Returns a new {@link IncomingServerSession} with a domain validated by the Authoritative * Server. New domains may be added to the returned IncomingServerSession after they have * been validated. See * {@link LocalIncomingServerSession#validateSubsequentDomain(org.dom4j.Element)}. The remote * server will be able to send packets through this session whose domains were previously * validated.<p> * * When acting as an Authoritative Server this method will verify the requested key * and will return null since the underlying TCP connection will be closed after sending the * response to the Receiving Server.<p> * * @param reader reader of DOM documents on the connection to the remote server. * @return an IncomingServerSession that was previously validated against the remote server. * @throws IOException if an I/O error occurs while communicating with the remote server. * @throws XmlPullParserException if an error occurs while parsing XML packets. */ public LocalIncomingServerSession createIncomingSession(XMPPPacketReader reader) throws IOException, XmlPullParserException { XmlPullParser xpp = reader.getXPPParser(); StringBuilder sb; if ("jabber:server:dialback".equals(xpp.getNamespace("db"))) { Log.debug("ServerDialback: Processing incoming session."); StreamID streamID = sessionManager.nextStreamID(); sb = new StringBuilder(); sb.append("<stream:stream"); sb.append(" xmlns:stream=\"http://etherx.jabber.org/streams\""); sb.append(" xmlns=\"jabber:server\" xmlns:db=\"jabber:server:dialback\""); sb.append(" id=\""); sb.append(streamID.toString()); sb.append("\">"); connection.deliverRawText(sb.toString()); try { Element doc = reader.parseDocument().getRootElement(); if ("db".equals(doc.getNamespacePrefix()) && "result".equals(doc.getName())) { String hostname = doc.attributeValue("from"); String recipient = doc.attributeValue("to"); Log.debug("ServerDialback: RS - Validating remote domain for incoming session from {} to {}", hostname, recipient); if (validateRemoteDomain(doc, streamID)) { Log.debug("ServerDialback: RS - Validation of remote domain for incoming session from {} to {} was successful.", hostname, recipient); // Create a server Session for the remote server LocalIncomingServerSession session = sessionManager. createIncomingServerSession(connection, streamID, hostname); // Add the validated domain as a valid domain session.addValidatedDomain(hostname); // Set the domain or subdomain of the local server used when // validating the session session.setLocalDomain(recipient); return session; } else { Log.debug("ServerDialback: RS - Validation of remote domain for incoming session from {} to {} was not successful.", hostname, recipient); return null; } } else if ("db".equals(doc.getNamespacePrefix()) && "verify".equals(doc.getName())) { // When acting as an Authoritative Server the Receiving Server will send a // db:verify packet for verifying a key that was previously sent by this // server when acting as the Originating Server verifyReceivedKey(doc, connection); // Close the underlying connection connection.close(); String verifyFROM = doc.attributeValue("from"); String id = doc.attributeValue("id"); Log.debug("ServerDialback: AS - Connection closed for host: " + verifyFROM + " id: " + id); return null; } else { Log.debug("ServerDialback: Received an invalid/unknown packet while trying to process an incoming session: {}", doc.asXML()); // The remote server sent an invalid/unknown packet connection.deliverRawText( new StreamError(StreamError.Condition.invalid_xml).toXML()); // Close the underlying connection connection.close(); return null; } } catch (Exception e) { Log.error("An error occured while creating a server session", e); // Close the underlying connection connection.close(); return null; } } else { Log.debug("ServerDialback: Received a stanza in an invalid namespace while trying to process an incoming session: {}", xpp.getNamespace("db")); connection.deliverRawText( new StreamError(StreamError.Condition.invalid_namespace).toXML()); // Close the underlying connection connection.close(); return null; } } /** * Send a dialback error. * * @param from From * @param to To * @param err Error type. */ protected void dialbackError(String from, String to, PacketError err) { connection.deliverRawText("<db:result type=\"error\" from=\"" + from + "\" to=\"" + to + "\">" + err.toXML() + "</db:result>"); } /** * Returns true if the domain requested by the remote server was validated by the Authoritative * Server. To validate the domain a new TCP connection will be established to the * Authoritative Server. The Authoritative Server may be the same Originating Server or * some other machine in the Originating Server's network.<p> * * If the domain was not valid or some error occurred while validating the domain then the * underlying TCP connection may be closed. * * @param doc the request for validating the new domain. * @param streamID the stream id generated by this server for the Originating Server. * @return true if the requested domain is valid. */ public boolean validateRemoteDomain(Element doc, StreamID streamID) { StringBuilder sb; String recipient = doc.attributeValue("to"); String remoteDomain = doc.attributeValue("from"); final Logger log = LoggerFactory.getLogger( Log.getName() + "[Acting as Receiving Server: Validate domain:" + recipient + "(id " + streamID + ") for OS: " + remoteDomain + "]" ); log.debug( "Validating domain..."); if (connection.getTlsPolicy() == Connection.TLSPolicy.required && !connection.isSecure()) { connection.deliverRawText(new StreamError(StreamError.Condition.policy_violation).toXML()); // Close the underlying connection connection.close(); return false; } if (!RemoteServerManager.canAccess(remoteDomain)) { connection.deliverRawText(new StreamError(StreamError.Condition.policy_violation).toXML()); // Close the underlying connection connection.close(); log.debug( "Unable to validate domain: Remote domain is not allowed to establish a connection to this server." ); return false; } else if (isHostUnknown(recipient)) { dialbackError(recipient, remoteDomain, new PacketError(PacketError.Condition.item_not_found, PacketError.Type.cancel, "Service not hosted here")); log.debug( "Unable to validate domain: recipient not recognized as a local domain." ); return false; } else { log.debug( "Check if the remote domain already has a connection to the target domain/subdomain" ); boolean alreadyExists = false; for (IncomingServerSession session : sessionManager.getIncomingServerSessions(remoteDomain)) { if (recipient.equals(session.getLocalDomain())) { alreadyExists = true; } } if (alreadyExists && !sessionManager.isMultipleServerConnectionsAllowed()) { dialbackError(recipient, remoteDomain, new PacketError(PacketError.Condition.resource_constraint, PacketError.Type.cancel, "Incoming session already exists")); log.debug( "Unable to validate domain: An incoming connection already exists from this remote domain, and multiple connections are not allowed." ); return false; } else { log.debug( "Checking to see if the remote server provides stronger authentication based on SASL. If that's the case, dialback-based authentication can be skipped." ); if (SASLAuthentication.verifyCertificates(connection.getPeerCertificates(), remoteDomain, true)) { log.debug( "Host authenticated based on SASL. Weaker dialback-based authentication is skipped." ); sb = new StringBuilder(); sb.append("<db:result"); sb.append(" from=\"").append(recipient).append("\""); sb.append(" to=\"").append(remoteDomain).append("\""); sb.append(" type=\"valid\""); sb.append("/>"); connection.deliverRawText(sb.toString()); log.debug( "Domain validated successfully!" ); return true; } log.debug( "Unable to authenticate host based on stronger SASL. Proceeding with dialback..." ); String key = doc.getTextTrim(); Socket socket = SocketUtil.createSocketToXmppDomain( remoteDomain, RemoteServerManager.getPortForServer(remoteDomain) ); if ( socket == null ) { log.debug( "Unable to validate domain: No server available for verifying key of remote server." ); dialbackError(recipient, remoteDomain, new PacketError(PacketError.Condition.remote_server_not_found, PacketError.Type.cancel, "Unable to connect to authoritative server")); return false; } VerifyResult result; try { log.debug( "Verifying dialback key..." ); try { result = verifyKey( key, streamID, recipient, remoteDomain, socket, false ); } catch (SSLHandshakeException e) { log.debug( "Verification of dialback key failed due to TLS failure. Retry without TLS...", e ); // The receiving entity is expected to close the socket *without* sending any more data (<failure/> nor </stream>). // It is probably (see OF-794) best if we, as the initiating entity, therefor don't send any data either. final SocketAddress oldAddress = socket.getRemoteSocketAddress(); socket.close(); log.debug( "Re-opening socket (with the same remote peer)..." ); // Retry, without TLS. socket = new Socket(); socket.connect( oldAddress, RemoteServerManager.getSocketTimeout() ); log.debug( "Successfully re-opened socket! Try to validate dialback key again (without TLS this time)..." ); result = verifyKey( key, streamID, recipient, remoteDomain, socket, true ); } switch(result) { case valid: case invalid: boolean valid = (result == VerifyResult.valid); log.debug( "Dialback key is" + (valid? "valid":"invalid") + ". Sending verification result to remote domain." ); sb = new StringBuilder(); sb.append("<db:result"); sb.append(" from=\"").append(recipient).append("\""); sb.append(" to=\"").append(remoteDomain).append("\""); sb.append(" type=\""); sb.append(valid ? "valid" : "invalid"); sb.append("\"/>"); connection.deliverRawText(sb.toString()); if (!valid) { log.debug( "Close the underlying connection as key verification failed." ); connection.close(); log.debug( "Unable to validate domain: dialback key is invalid." ); return false; } else { log.debug( "Successfully validated domain!" ); return true; } default: break; } log.debug( "Unable to validate domain: key verification did not complete (the AS likely returned an error or a time out occurred)." ); dialbackError( recipient, remoteDomain, new PacketError( PacketError.Condition.remote_server_timeout, PacketError.Type.cancel, "Authoritative server returned error" ) ); return false; } catch (Exception e) { dialbackError(recipient, remoteDomain, new PacketError(PacketError.Condition.remote_server_timeout, PacketError.Type.cancel, "Authoritative server failed")); log.warn( "Unable to validate domain: An exception occurred while verifying the dialback key.", e ); return false; } } } } private boolean isHostUnknown(String recipient) { boolean host_unknown = !serverName.equals(recipient); // If the recipient does not match the serverName then check if it matches a subdomain. This // trick is useful when subdomains of this server are registered in the DNS so remote // servers may establish connections directly to a subdomain of this server if (host_unknown && recipient.contains(serverName)) { host_unknown = !routingTable.hasComponentRoute(new JID(recipient)); } return host_unknown; } private VerifyResult sendVerifyKey(String key, StreamID streamID, String recipient, String remoteDomain, Writer writer, XMPPPacketReader reader, Socket socket, boolean skipTLS) throws IOException, XmlPullParserException, RemoteConnectionFailedException { final Logger log = LoggerFactory.getLogger( Log.getName() + "[Acting as Receiving Server: Verify key with AS: " + remoteDomain + " for OS: " + recipient + " (id " + streamID + ")]" ); VerifyResult result = VerifyResult.error; TLSStreamHandler tlsStreamHandler; log.debug( "Send the Authoritative Server a stream header and wait for answer." ); StringBuilder stream = new StringBuilder(); stream.append("<stream:stream"); stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\""); stream.append(" xmlns=\"jabber:server\""); stream.append(" xmlns:db=\"jabber:server:dialback\""); stream.append(" to=\""); stream.append(remoteDomain); stream.append("\""); stream.append(" from=\""); stream.append(recipient); stream.append("\""); stream.append(" version=\"1.0\">"); writer.write(stream.toString()); writer.flush(); // Get the answer from the Authoritative Server XmlPullParser xpp = reader.getXPPParser(); for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) { eventType = xpp.next(); } log.debug( "Got a response." ); // TODO there's code duplication here with LocalOutgoingServerSession. if ((xpp.getAttributeValue("", "version") != null) && (xpp.getAttributeValue("", "version").equals("1.0"))) { log.debug( "The remote server is XMPP 1.0 compliant (or at least reports to be)."); Document doc; try { doc = reader.parseDocument(); } catch (DocumentException e) { log.warn("Unable to verify key: XML Error!", e); return VerifyResult.error; } Element features = doc.getRootElement(); Element starttls = features.element("starttls"); if (!skipTLS && starttls != null) { writer.write("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"); writer.flush(); try { doc = reader.parseDocument(); } catch (DocumentException e) { log.warn("Unable to verify key: XML Error!", e); return VerifyResult.error; } if (!doc.getRootElement().getName().equals("proceed")) { log.warn("Unable to verify key: Got {} instead of proceed for starttls", doc.getRootElement().getName()); log.debug("Like this: {}", doc.asXML()); return VerifyResult.error; } log.debug( "Negotiating TLS with AS... " ); // Ugly hacks, apparently, copied from SocketConnection. final ConnectionManagerImpl connectionManager = ( (ConnectionManagerImpl) XMPPServer.getInstance().getConnectionManager() ); tlsStreamHandler = new TLSStreamHandler( socket, connectionManager.getListener( ConnectionType.SOCKET_S2S, false ).generateConnectionConfiguration(), true ); // Start handshake tlsStreamHandler.start(); // Use new wrapped writers writer = new BufferedWriter( new OutputStreamWriter( tlsStreamHandler.getOutputStream(), StandardCharsets.UTF_8 ) ); reader.getXPPParser().setInput( new InputStreamReader( tlsStreamHandler.getInputStream(), StandardCharsets.UTF_8 ) ); log.debug( "Successfully negotiated TLS with AS... " ); /// Recurses! return sendVerifyKey( key, streamID, recipient, remoteDomain, writer, reader, socket, skipTLS ); } } if ("jabber:server:dialback".equals(xpp.getNamespace("db"))) { log.debug("Request for verification of the key and wait for response"); StringBuilder sb = new StringBuilder(); sb.append("<db:verify"); sb.append(" from=\"").append(recipient).append("\""); sb.append(" to=\"").append(remoteDomain).append("\""); sb.append(" id=\"").append(streamID.getID()).append("\">"); sb.append(key); sb.append("</db:verify>"); writer.write(sb.toString()); writer.flush(); try { Element doc = reader.parseDocument().getRootElement(); if ("db".equals(doc.getNamespacePrefix()) && "verify".equals(doc.getName())) { if (doc.attributeValue("id") == null || !streamID.equals(BasicStreamIDFactory.createStreamID( doc.attributeValue("id") ))) { // Include the invalid-id stream error condition in the response writer.write(new StreamError(StreamError.Condition.invalid_id).toXML()); writer.flush(); // Thrown an error so <remote-connection-failed/> stream error // condition is sent to the Originating Server throw new RemoteConnectionFailedException("Invalid ID"); } else if (isHostUnknown( doc.attributeValue( "to" ) )) { // Include the host-unknown stream error condition in the response writer.write( new StreamError(StreamError.Condition.host_unknown).toXML()); writer.flush(); // Thrown an error so <remote-connection-failed/> stream error // condition is sent to the Originating Server throw new RemoteConnectionFailedException("Host unknown"); } else if (!remoteDomain.equals(doc.attributeValue("from"))) { // Include the invalid-from stream error condition in the response writer.write( new StreamError(StreamError.Condition.invalid_from).toXML()); writer.flush(); // Thrown an error so <remote-connection-failed/> stream error // condition is sent to the Originating Server throw new RemoteConnectionFailedException("Invalid From"); } else if ("valid".equals(doc.attributeValue("type"))){ log.debug("Key was VERIFIED by the Authoritative Server."); result = VerifyResult.valid; } else if ("invalid".equals(doc.attributeValue("type"))){ log.debug("Key was NOT VERIFIED by the Authoritative Server."); result = VerifyResult.invalid; } else { log.debug("Key was ERRORED by the Authoritative Server."); result = VerifyResult.error; } } else { log.debug("db:verify answer was: " + doc.asXML()); } } catch (DocumentException | RuntimeException e) { log.error("An error occurred while connecting to the Authoritative Server:", e); // Thrown an error so <remote-connection-failed/> stream error condition is // sent to the Originating Server throw new RemoteConnectionFailedException("Error connecting to the Authoritative Server"); } } else { // Include the invalid-namespace stream error condition in the response writer.write(new StreamError(StreamError.Condition.invalid_namespace).toXML()); writer.flush(); // Thrown an error so <remote-connection-failed/> stream error condition is // sent to the Originating Server throw new RemoteConnectionFailedException("Invalid namespace"); } return result; } /** * Verifies the key with the Authoritative Server. */ private VerifyResult verifyKey(String key, StreamID streamID, String recipient, String remoteDomain, Socket socket, boolean skipTLS ) throws IOException, XmlPullParserException, RemoteConnectionFailedException { final Logger log = LoggerFactory.getLogger( Log.getName() + "[Acting as Receiving Server: Verify key with AS: " + remoteDomain + " for OS: " + recipient + " (id " + streamID + ")]" ); log.debug( "Verifying key ..." ); XMPPPacketReader reader; Writer writer = null; // Set a read timeout socket.setSoTimeout(RemoteServerManager.getSocketTimeout()); VerifyResult result = VerifyResult.error; try { reader = new XMPPPacketReader(); reader.setXPPFactory(FACTORY); reader.getXPPParser().setInput(new InputStreamReader(socket.getInputStream(), CHARSET)); // Get a writer for sending the open stream tag writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), CHARSET)); result = sendVerifyKey(key, streamID, recipient, remoteDomain, writer, reader, socket, skipTLS ); } finally { try { // Close the stream StringBuilder sb = new StringBuilder(); sb.append("</stream:stream>"); writer.write(sb.toString()); writer.flush(); // Close the TCP connection socket.close(); } catch (IOException ioe) { // Do nothing } } switch ( result ) { case valid: log.debug( "Successfully verified key!" ); break; case invalid: log.debug( "Unable to verify key: AS reports that the key is invalid." ); break; default: case decline: case error: log.debug( "Unable to verify key: An error occurred." ); break; } return result; } /** * Verifies the key sent by a Receiving Server. This server will be acting as the * Authoritative Server when executing this method. The remote server may have established * a new connection to the Authoritative Server (i.e. this server) for verifying the key * or it may be reusing an existing incoming connection. * * @param doc the Element that contains the key to verify. * @param connection the connection to use for sending the verification result * @return true if the key was verified. */ public static boolean verifyReceivedKey(Element doc, Connection connection) { String verifyFROM = doc.attributeValue("from"); String verifyTO = doc.attributeValue("to"); String key = doc.getTextTrim(); StreamID streamID = BasicStreamIDFactory.createStreamID( doc.attributeValue("id") ); final Logger log = LoggerFactory.getLogger( Log.getName() + "[Acting as Authoritative Server: Verify key sent by RS: " + verifyFROM + " (id " + streamID+ ")]" ); log.debug( "Verifying key... "); // TODO If the value of the 'to' address does not match a recognized hostname, // then generate a <host-unknown/> stream error condition // TODO If the value of the 'from' address does not match the hostname // represented by the Receiving Server when opening the TCP connection, then // generate an <invalid-from/> stream error condition // Verify the received key // Created the expected key based on the received ID value and the shared secret String expectedKey = AuthFactory.createDigest(streamID.getID(), getSecretkey()); boolean verified = expectedKey.equals(key); // Send the result of the key verification StringBuilder sb = new StringBuilder(); sb.append("<db:verify"); sb.append(" from=\"").append(verifyTO).append("\""); sb.append(" to=\"").append(verifyFROM).append("\""); sb.append(" type=\""); sb.append(verified ? "valid" : "invalid"); sb.append("\" id=\"").append(streamID.getID()).append("\"/>"); connection.deliverRawText(sb.toString()); log.debug("Verification successful! Key was: " + (verified ? "VALID" : "INVALID")); return verified; } /** * Returns the secret key that was randomly generated. When running inside of a cluster * the key will be unique to all cluster nodes. * * @return the secret key that was randomly generated. */ private static String getSecretkey() { String key = "secretKey"; Lock lock = CacheFactory.getLock(key, secretKeyCache); try { lock.lock(); String secret = secretKeyCache.get(key); if (secret == null) { secret = StringUtils.randomString(10); secretKeyCache.put(key, secret); } return secret; } finally { lock.unlock(); } } }