/* * Tigase Jabber/XMPP Server * Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. Look for COPYING file in the top folder. * If not, see http://www.gnu.org/licenses/. * * $Rev$ * Last modified by $Author$ * $Date$ */ package tigase.server.xmppserver.proc; //~--- non-JDK imports -------------------------------------------------------- import tigase.cert.CertCheckResult; import tigase.net.ConnectionType; import tigase.server.Packet; import tigase.server.xmppserver.CID; import tigase.server.xmppserver.CIDConnections; import tigase.server.xmppserver.LocalhostException; import tigase.server.xmppserver.NotLocalhostException; import tigase.server.xmppserver.S2SIOService; import tigase.util.Algorithms; import tigase.xml.Element; import tigase.xmpp.JID; import tigase.xmpp.StanzaType; //~--- JDK imports ------------------------------------------------------------ import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; //~--- classes ---------------------------------------------------------------- /** * Created: Dec 9, 2010 2:00:52 PM * * @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a> * @version $Rev$ */ public class Dialback extends S2SAbstractProcessor { private static final Logger log = Logger.getLogger(Dialback.class.getName()); private static final Element features = new Element("dialback", new String[] { "xmlns" }, new String[] { "urn:xmpp:features:dialback" }); private static final Element features_required = new Element("dialback", new Element[] { new Element("required") }, new String[] { "xmlns" }, new String[] { "urn:xmpp:features:dialback" }); // ~--- fields --------------------------------------------------------------- private long authenticationTimeOut = 30; // Ejabberd does not request dialback after TLS (at least some versions don't) private boolean ejabberd_bug_workaround_active = false; private static final String REQUESTED_RESULT_DOMAINS_KEY = "requested-result-domains-key"; // ~--- constructors --------------------------------------------------------- /** * Constructs ... * */ public Dialback() { super(); if (System.getProperty("s2s-ejabberd-bug-workaround-active") == null) { System.setProperty("s2s-ejabberd-bug-workaround-active", "true"); } ejabberd_bug_workaround_active = Boolean.getBoolean("s2s-ejabberd-bug-workaround-active"); } // ~--- methods -------------------------------------------------------------- /** * Method description * * * @param p * @param serv * @param results * * @return */ @Override public boolean process(Packet p, S2SIOService serv, Queue<Packet> results) { CID cid = (CID) serv.getSessionData().get("cid"); boolean skipTLS = (cid == null) ? false : skipTLSForHost(cid.getRemoteHost()); // If this is a dialback packet, process it accordingly if (p.getXMLNS() == XMLNS_DB_VAL) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0}, Processing dialback packet: {1}", new Object[] { serv, p }); } processDialback(p, serv); return true; } // If this is stream features, then it depends.... if (p.isElement(FEATURES_EL, FEATURES_NS)) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0}, Stream features received packet: {1}", new Object[] { serv, p }); } CertCheckResult certCheckResult = (CertCheckResult) serv.getSessionData().get(S2SIOService.CERT_CHECK_RESULT); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0}, TLS Certificate check: {1}, packet: {2}", new Object[] { serv, certCheckResult, p }); } // If TLS is not yet started (announced in stream features) then it is not // the right time for dialback yet // Some servers send starttls in stream features, even if TLS is already // initialized.... if (p.isXMLNS(FEATURES_EL + "/" + START_TLS_EL, START_TLS_NS) && (certCheckResult == null) && !skipTLS) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0}, Waiting for starttls, packet: {1}", new Object[] { serv, p }); } return true; } // If TLS has been started and it is a trusted peer, we do not need // dialback here // but... sometimes the remote server may request dialback anyway, // especially if they // do not trust us. if ((certCheckResult == CertCheckResult.trusted) && !(p.isXMLNS(FEATURES_EL + "/" + DIALBACK_TLS_EL, DIALBACK_TLS_NS))) { if (ejabberd_bug_workaround_active) { if (log.isLoggable(Level.FINEST)) { log.log( Level.FINEST, "{0}, Ejabberd bug workaround active, proceeding to dialback anyway, packet: {1}", new Object[] { serv, p }); } } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0}, TLS trusted peer, no dialback needed or requested, packet: {1}", new Object[] { serv, p }); } CIDConnections cid_conns; try { cid_conns = handler.getCIDConnections(cid, true); cid_conns.connectionAuthenticated(serv); } catch (NotLocalhostException ex) { // Should not happen.... log.log(Level.INFO, "{0}, Incorrect local hostname, packet: {1}", new Object[] { serv, p }); serv.forceStop(); } catch (LocalhostException ex) { // Should not happen.... log.log(Level.INFO, "{0}, Incorrect remote hostname name, packet: {1}", new Object[] { serv, p }); serv.forceStop(); } return true; } } // Nothing else can be done right now except the dialback if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0}, Initializing dialback, packet: {1}", new Object[] { serv, p }); } initDialback(serv, serv.getSessionId()); } return false; } /** * Method description * * * @param serv */ @Override public void serviceStarted(S2SIOService serv) { handler.addTimerTask(new AuthenticationTimer(serv), authenticationTimeOut, TimeUnit.SECONDS); } /** * Method description * * * * @param serv * @param results */ @Override public void streamFeatures(S2SIOService serv, List<Element> results) { CertCheckResult certCheckResult = (CertCheckResult) serv.getSessionData().get(S2SIOService.CERT_CHECK_RESULT); if (certCheckResult == CertCheckResult.trusted) { results.add(features); } else { results.add(features_required); } } /** * Method description * * * @param serv * @param attribs * * @return */ @Override public String streamOpened(S2SIOService serv, Map<String, String> attribs) { if (attribs.containsKey("version")) { // Let's wait for stream features return null; } switch (serv.connectionType()) { case connect: initDialback(serv, attribs.get("id")); break; default: // Ignore } return null; } private void initDialback(S2SIOService serv, String remote_id) { try { CID cid = (CID) serv.getSessionData().get("cid"); CIDConnections cid_conns = handler.getCIDConnections(cid, false); // It must be always set for connect connection type String uuid = UUID.randomUUID().toString(); String key = null; try { key = Algorithms.hexDigest(remote_id, uuid, "SHA"); } catch (NoSuchAlgorithmException e) { key = uuid; } // end of try-catch serv.setDBKey(key); cid_conns.addDBKey(remote_id, key); if (!serv.isHandshakingOnly()) { Element elem = new Element(DB_RESULT_EL_NAME, key, new String[] { XMLNS_DB_ATT }, new String[] { XMLNS_DB_VAL }); addToResultRequested(serv, cid.getRemoteHost()); serv.getS2SConnection().addControlPacket( Packet.packetInstance(elem, JID.jidInstanceNS(cid.getLocalHost()), JID.jidInstanceNS(cid.getRemoteHost()))); } serv.getS2SConnection().sendAllControlPackets(); } catch (NotLocalhostException ex) { generateStreamError(false, "host-unknown", serv); } catch (LocalhostException ex) { generateStreamError(false, "invalid-from", serv); } } private void processDialback(Packet p, S2SIOService serv) { // Get the cid for which the connection has been created, the cid calculated // from the packet may be different though if the remote server tries to // multiplexing CID cid_main = (CID) serv.getSessionData().get("cid"); CID cid_packet = new CID(p.getStanzaTo().getDomain(), p.getStanzaFrom().getDomain()); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0}, DIALBACK packet: {1}, CID_packet: {2}", new Object[] { serv, p, cid_packet }); } CIDConnections cid_conns = null; // Some servers (ejabberd) do not send from/to attributes in the stream:open // which // violates the spec, they seem not to care though, so here we handle the // case. if (cid_main == null) { // This actually can only happen for 'accept' connection type // what we did not get in stream open we can get from here cid_main = cid_packet; serv.getSessionData().put("cid", cid_main); // For debuging purposes only.... serv.getSessionData().put("local-hostname", cid_main.getLocalHost()); serv.getSessionData().put("remote-hostname", cid_main.getRemoteHost()); } try { cid_conns = handler.getCIDConnections(cid_main, true); } catch (NotLocalhostException ex) { log.log(Level.FINER, "{0} Incorrect local hostname: {1}", new Object[] { serv, p }); generateStreamError(false, "host-unknown", serv); return; } catch (LocalhostException ex) { log.log(Level.FINER, "{0} Incorrect remote hostname: {1}", new Object[] { serv, p }); generateStreamError(false, "invalid-from", serv); return; } if (serv.connectionType() == ConnectionType.accept) { cid_conns.addIncoming(serv); } String remote_key = p.getElemCData(); // Dummy dialback implementation for now.... if ((p.getElemName() == RESULT_EL_NAME) || (p.getElemName() == DB_RESULT_EL_NAME)) { if (p.getType() == null) { String conn_sessionId = serv.getSessionId(); handler.sendVerifyResult(DB_VERIFY_EL_NAME, cid_main, cid_packet, null, conn_sessionId, null, p.getElemCData(), true); } else { if (p.getType() == StanzaType.valid) { if (wasResultRequested(serv, p.getStanzaFrom().toString())) { // serv.addCID(new CID(p.getStanzaTo().getDomain(), // p.getStanzaFrom().getDomain())); cid_conns.connectionAuthenticated(serv); } else if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "Received result with type valid for {0} but it was not requested!", p.getStanzaFrom()); } } else { if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "Invalid result for DB authentication: {0}, stopping connection: {1}", new Object[] { cid_packet, serv }); } serv.stop(); } } } if ((p.getElemName() == VERIFY_EL_NAME) || (p.getElemName() == DB_VERIFY_EL_NAME)) { if (p.getType() == null) { String local_key = handler.getLocalDBKey(cid_main, cid_packet, remote_key, p.getStanzaId(), serv.getSessionId()); if (local_key == null) { if (log.isLoggable(Level.FINER)) { log.log(Level.FINER, "The key is not available for connection CID: {0}, " + "or the packet CID: {1} maybe it is " + "located on a different node...", new Object[] { cid_main, cid_packet }); } } else { handler.sendVerifyResult(DB_VERIFY_EL_NAME, cid_main, cid_packet, local_key.equals(remote_key), p.getStanzaId(), serv.getSessionId(), null, false); } } else { if (wasVerifyRequested(serv, p.getStanzaFrom().toString())) { handler.sendVerifyResult(DB_RESULT_EL_NAME, cid_main, cid_packet, (p.getType() == StanzaType.valid), null, p.getStanzaId(), null, false); cid_conns.connectionAuthenticated(p.getStanzaId()); } else { if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "received verify for {0} but it was not requested!", p.getStanzaFrom()); } } if (serv.isHandshakingOnly()) { serv.stop(); } } } } // ~--- inner classes -------------------------------------------------------- private class AuthenticationTimer extends TimerTask { private S2SIOService serv = null; // ~--- constructors ------------------------------------------------------- private AuthenticationTimer(S2SIOService serv) { this.serv = serv; } // ~--- methods ------------------------------------------------------------ /** * Method description * */ @Override public void run() { if (!serv.isAuthenticated() && serv.isConnected()) { if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "Connection not authenticated within timeout, stopping: {0}", serv); } serv.stop(); } } } /** * Adds domain to list of domains requested for result by service * * @param serv * @param domain */ @SuppressWarnings("unchecked") private void addToResultRequested(S2SIOService serv, String domain) { Set<String> requested = (Set<String>) serv.getSessionData().get(REQUESTED_RESULT_DOMAINS_KEY); if (requested == null) { Set<String> requested_tmp = new CopyOnWriteArraySet<String>(); requested = (Set<String>) serv.getSessionData().putIfAbsent(REQUESTED_RESULT_DOMAINS_KEY, requested_tmp); if (requested == null) { requested = requested_tmp; } } requested.add(domain); } /** * Checks if result request for received domain was sent by service * * @param serv * @param domain * @return */ @SuppressWarnings("unchecked") private boolean wasResultRequested(S2SIOService serv, String domain) { Set<String> requested = (Set<String>) serv.getSessionData().get(REQUESTED_RESULT_DOMAINS_KEY); return requested != null && requested.contains(domain); } /** * Checks if verify request for received domain was sent by service * * @see CIDConnections#sendHandshakingOnly * @param serv * @param domain * @return */ private boolean wasVerifyRequested(S2SIOService serv, String domain) { String requested = (String) serv.getSessionData().get(S2SIOService.HANDSHAKING_DOMAIN_KEY); return requested != null && requested.contains(domain); } } // ~ Formatted in Sun Code Convention // ~ Formatted by Jindent --- http://www.jindent.com