/* * 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.net; import java.io.IOException; import java.net.Socket; import org.dom4j.DocumentException; import org.dom4j.Element; import org.jivesoftware.openfire.Connection; import org.jivesoftware.openfire.session.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmlpull.v1.XmlPullParserException; import org.xmpp.packet.StreamError; import javax.net.ssl.SSLHandshakeException; /** * Abstract class for {@link BlockingReadingMode}. * * @author Gaston Dombiak */ abstract class SocketReadingMode { private static final Logger Log = LoggerFactory.getLogger(SocketReadingMode.class); /** * The utf-8 charset for decoding and encoding Jabber packet streams. */ protected static String CHARSET = "UTF-8"; protected SocketReader socketReader; protected Socket socket; protected SocketReadingMode(Socket socket, SocketReader socketReader) { this.socket = socket; this.socketReader = socketReader; } /* * This method is invoked when client send data to the channel. */ abstract void run(); /** * Tries to secure the connection using TLS. If the connection is secured then reset * the parser to use the new secured reader. But if the connection failed to be secured * then send a <failure> stanza and close the connection. * * @return true if the connection was secured. */ protected boolean negotiateTLS() { if (socketReader.connection.getTlsPolicy() == Connection.TLSPolicy.disabled) { // Set the not_authorized error StreamError error = new StreamError(StreamError.Condition.not_authorized); // Deliver stanza socketReader.connection.deliverRawText(error.toXML()); // Close the underlying connection socketReader.connection.close(); // Log a warning so that admins can track this case from the server side Log.warn("TLS requested by initiator when TLS was never offered by server. " + "Closing connection : " + socketReader.connection); return false; } // Client requested to secure the connection using TLS. Negotiate TLS. try { // This code is only used for s2s socketReader.connection.startTLS(false); } catch (SSLHandshakeException e) { // RFC3620, section 5.4.3.2 "STARTTLS Failure" - close the socket *without* sending any more data (<failure/> nor </stream>). Log.info( "STARTTLS negotiation (with: {}) failed.", socketReader.connection, e ); socketReader.connection.forceClose(); return false; } catch (IOException | RuntimeException e) { // RFC3620, section 5.4.2.2 "Failure case" - Send a <failure/> element, then close the socket. Log.warn( "An exception occurred while performing STARTTLS negotiation (with: {})", socketReader.connection, e); socketReader.connection.deliverRawText("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>"); socketReader.connection.close(); return false; } return true; } /** * TLS negotiation was successful so open a new stream and offer the new stream features. * The new stream features will include available SASL mechanisms and specific features * depending on the session type such as auth for Non-SASL authentication and register * for in-band registration. */ protected void tlsNegotiated() throws XmlPullParserException, IOException { // Offer stream features including SASL Mechanisms StringBuilder sb = new StringBuilder(620); sb.append(geStreamHeader()); sb.append("<stream:features>"); // Include available SASL Mechanisms sb.append(SASLAuthentication.getSASLMechanisms(socketReader.session)); // Include specific features such as auth and register for client sessions String specificFeatures = socketReader.session.getAvailableStreamFeatures(); if (specificFeatures != null) { sb.append(specificFeatures); } sb.append("</stream:features>"); socketReader.connection.deliverRawText(sb.toString()); } protected boolean authenticateClient(Element doc) throws DocumentException, IOException, XmlPullParserException { // Ensure that connection was secured if TLS was required if (socketReader.connection.getTlsPolicy() == Connection.TLSPolicy.required && !socketReader.connection.isSecure()) { socketReader.closeNeverSecuredConnection(); return false; } boolean isComplete = false; boolean success = false; while (!isComplete) { SASLAuthentication.Status status = SASLAuthentication.handle(socketReader.session, doc); if (status == SASLAuthentication.Status.needResponse) { // Get the next answer since we are not done yet doc = socketReader.reader.parseDocument().getRootElement(); if (doc == null) { // Nothing was read because the connection was closed or dropped isComplete = true; } } else { isComplete = true; success = status == SASLAuthentication.Status.authenticated; } } return success; } /** * After SASL authentication was successful we should open a new stream and offer * new stream features such as resource binding and session establishment. Notice that * resource binding and session establishment should only be offered to clients (i.e. not * to servers or external components) */ protected void saslSuccessful() throws XmlPullParserException, IOException { StringBuilder sb = new StringBuilder(420); sb.append(geStreamHeader()); sb.append("<stream:features>"); // Include specific features such as resource binding and session establishment // for client sessions String specificFeatures = socketReader.session.getAvailableStreamFeatures(); if (specificFeatures != null) { sb.append(specificFeatures); } sb.append("</stream:features>"); socketReader.connection.deliverRawText(sb.toString()); } /** * Start using compression but first check if the connection can and should use compression. * The connection will be closed if the requested method is not supported, if the connection * is already using compression or if client requested to use compression but this feature * is disabled. * * @param doc the element sent by the client requesting compression. Compression method is * included. * @return true if it was possible to use compression. * @throws IOException if an error occurs while starting using compression. */ protected boolean compressClient(Element doc) throws IOException, XmlPullParserException { String error = null; if (socketReader.connection.getCompressionPolicy() == Connection.CompressionPolicy.disabled) { // Client requested compression but this feature is disabled error = "<failure xmlns='http://jabber.org/protocol/compress'><setup-failed/></failure>"; // Log a warning so that admins can track this case from the server side Log.warn("Client requested compression while compression is disabled. Closing " + "connection : " + socketReader.connection); } else if (socketReader.connection.isCompressed()) { // Client requested compression but connection is already compressed error = "<failure xmlns='http://jabber.org/protocol/compress'><setup-failed/></failure>"; // Log a warning so that admins can track this case from the server side Log.warn("Client requested compression and connection is already compressed. Closing " + "connection : " + socketReader.connection); } else { // Check that the requested method is supported String method = doc.elementText("method"); if (!"zlib".equals(method)) { error = "<failure xmlns='http://jabber.org/protocol/compress'><unsupported-method/></failure>"; // Log a warning so that admins can track this case from the server side Log.warn("Requested compression method is not supported: " + method + ". Closing connection : " + socketReader.connection); } } if (error != null) { // Deliver stanza socketReader.connection.deliverRawText(error); return false; } else { // Start using compression for incoming traffic socketReader.connection.addCompression(); // Indicate client that he can proceed and compress the socket socketReader.connection.deliverRawText("<compressed xmlns='http://jabber.org/protocol/compress'/>"); // Start using compression for outgoing traffic socketReader.connection.startCompression(); return true; } } /** * After compression was successful we should open a new stream and offer * new stream features such as resource binding and session establishment. Notice that * resource binding and session establishment should only be offered to clients (i.e. not * to servers or external components) */ protected void compressionSuccessful() throws XmlPullParserException, IOException { StringBuilder sb = new StringBuilder(340); sb.append(geStreamHeader()); sb.append("<stream:features>"); // Include SASL mechanisms only if client has not been authenticated if (socketReader.session.getStatus() != Session.STATUS_AUTHENTICATED) { // Include available SASL Mechanisms sb.append(SASLAuthentication.getSASLMechanisms(socketReader.session)); } // Include specific features such as resource binding and session establishment // for client sessions String specificFeatures = socketReader.session.getAvailableStreamFeatures(); if (specificFeatures != null) { sb.append(specificFeatures); } sb.append("</stream:features>"); socketReader.connection.deliverRawText(sb.toString()); } private String geStreamHeader() { StringBuilder sb = new StringBuilder(200); sb.append("<?xml version='1.0' encoding='"); sb.append(CHARSET); sb.append("'?>"); if (socketReader.connection.isFlashClient()) { sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" "); } else { sb.append("<stream:stream "); } sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\""); sb.append(socketReader.getNamespace()).append('\"'); if (socketReader.getExtraNamespaces() != null) { sb.append(' '); sb.append(socketReader.getExtraNamespaces()); } sb.append(" from=\""); sb.append(socketReader.session.getServerName()); sb.append("\" id=\""); sb.append(socketReader.session.getStreamID().toString()); sb.append("\" xml:lang=\""); sb.append(socketReader.session.getLanguage().toLanguageTag()); sb.append("\" version=\""); sb.append(Session.MAJOR_VERSION).append('.').append(Session.MINOR_VERSION); sb.append("\">"); return sb.toString(); } }