/* * 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; //~--- non-JDK imports -------------------------------------------------------- import tigase.net.ConnectionType; //import tigase.net.IOService; import tigase.net.SocketType; import tigase.server.ConnectionManager; import tigase.server.Packet; import tigase.stats.StatisticsList; import tigase.util.Algorithms; import tigase.util.DNSEntry; import tigase.util.DNSResolver; import tigase.xml.Element; import tigase.xmpp.Authorization; import tigase.xmpp.JID; import tigase.xmpp.PacketErrorTypeException; import tigase.xmpp.StanzaType; import tigase.xmpp.XMPPIOService; //~--- JDK imports ------------------------------------------------------------ import java.net.UnknownHostException; import java.security.NoSuchAlgorithmException; import java.util.LinkedHashMap; import java.util.Map; import java.util.Queue; import java.util.TimerTask; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; //~--- classes ---------------------------------------------------------------- /** * Class ServerConnectionManager * * * Created: Tue Nov 22 07:07:11 2005 * * @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a> * @version $Rev$ */ public class ServerConnectionManager extends ConnectionManager<XMPPIOService<Object>> implements ConnectionHandlerIfc<XMPPIOService<Object>> { private static final String DB_RESULT_EL_NAME = "db:result"; private static final String DB_VERIFY_EL_NAME = "db:verify"; //public static final String HOSTNAMES_PROP_KEY = "hostnames"; //public String[] HOSTNAMES_PROP_VAL = {"localhost", "hostname"}; /** Field description */ public static final String MAX_PACKET_WAITING_TIME_PROP_KEY = "max-packet-waiting-time"; private static final String RESULT_EL_NAME = "result"; private static final String VERIFY_EL_NAME = "verify"; private static final String XMLNS_DB_ATT = "xmlns:db"; private static final String XMLNS_DB_VAL = "jabber:server:dialback"; private static final String XMLNS_SERVER_VAL = "jabber:server"; private static final String XMLNS_CLIENT_VAL = "jabber:client"; /** * Variable <code>log</code> is a class logger. */ private static final Logger log = Logger.getLogger(ServerConnectionManager.class.getName()); /** Field description */ public static final long MAX_PACKET_WAITING_TIME_PROP_VAL = 7 * MINUTE; private static Map<String, ConnectionWatchdogTask> waitingTasks = new LinkedHashMap<String, ConnectionWatchdogTask>(); //~--- fields --------------------------------------------------------------- private long new_connection_thread_counter = 0; //private String[] hostnames = HOSTNAMES_PROP_VAL; /** * <code>maxPacketWaitingTime</code> keeps the maximum time packets * can wait for sending in ServerPacketQueue. Packets are put in the * queue only when connection to remote server is not established so * effectively this timeout specifies the maximum time for connecting * to remote server. If this time is exceeded then no more reconnecting * attempts are performed and packets are sent back with error information. * * Default TCP/IP timeout is 300 seconds so we can follow this convention * but administrator can set different timeout in server configuration. */ private long maxPacketWaitingTime = MAX_PACKET_WAITING_TIME_PROP_VAL; /** * Incoming (accept) services by sessionId. Some servers (EJabberd) opens * many connections for each domain, especially when in cluster mode. */ private ConcurrentHashMap<String, XMPPIOService<Object>> incoming = new ConcurrentHashMap<String, XMPPIOService<Object>>(1000); /** * Services connected and authorized/authenticated */ private Map<CID, ServerConnections> connectionsByLocalRemote = new ConcurrentHashMap<CID, ServerConnections>(1000); //~--- methods -------------------------------------------------------------- /** * Method description * * * @param packet * * @return */ @Override public boolean addOutPacket(Packet packet) { return super.addOutPacket(packet); } //~--- get methods ---------------------------------------------------------- /** * Method description * * * @param params * * @return */ @Override public Map<String, Object> getDefaults(Map<String, Object> params) { Map<String, Object> props = super.getDefaults(params); // Usually we want the server to do s2s for the external component too: // if (params.get(GEN_VIRT_HOSTS) != null) { // HOSTNAMES_PROP_VAL = ((String)params.get(GEN_VIRT_HOSTS)).split(","); // } else { // HOSTNAMES_PROP_VAL = DNSResolver.getDefHostNames(); // } // ArrayList<String> vhosts = // new ArrayList<String>(Arrays.asList(HOSTNAMES_PROP_VAL)); // for (Map.Entry<String, Object> entry: params.entrySet()) { // if (entry.getKey().startsWith(GEN_EXT_COMP)) { // String ext_comp = (String)entry.getValue(); // if (ext_comp != null) { // String[] comp_params = ext_comp.split(","); // vhosts.add(comp_params[1]); // } // } // if (entry.getKey().startsWith(GEN_COMP_NAME)) { // String comp_name_suffix = entry.getKey().substring(GEN_COMP_NAME.length()); // String c_name = (String)params.get(GEN_COMP_NAME + comp_name_suffix); // for (String vhost: HOSTNAMES_PROP_VAL) { // vhosts.add(c_name + "." + vhost); // } // } // } // HOSTNAMES_PROP_VAL = vhosts.toArray(new String[0]); // hostnames = HOSTNAMES_PROP_VAL; // props.put(HOSTNAMES_PROP_KEY, HOSTNAMES_PROP_VAL); props.put(MAX_PACKET_WAITING_TIME_PROP_KEY, MAX_PACKET_WAITING_TIME_PROP_VAL); return props; } /** * Method description * * * @return */ @Override public String getDiscoCategoryType() { return "s2s"; } /** * Method description * * * @return */ @Override public String getDiscoDescription() { return "Server connection manager"; } /** * Method description * * * @param list */ @Override public void getStatistics(StatisticsList list) { super.getStatistics(list); int waiting_packets = 0; int open_s2s_connections = incoming.size(); int connected_servers = 0; int server_connections_instances = connectionsByLocalRemote.size(); for (Map.Entry<CID, ServerConnections> entry : connectionsByLocalRemote.entrySet()) { ServerConnections conn = entry.getValue(); waiting_packets += conn.getWaitingPackets().size(); if (conn.isOutgoingConnected()) { ++open_s2s_connections; ++connected_servers; } // if (log.isLoggable(Level.FINEST)) { // log.finest("s2s instance: " + entry.getKey() + // ", waitingQueue: " + conn.getWaitingPackets().size() + // ", outgoingIsNull(): " + conn.outgoingIsNull() + // ", outgoingActive: " + conn.isOutgoingConnected() + // ", OutgoingState: " + conn.getOutgoingState().toString() + // ", db_keys.size(): " + conn.getDBKeysSize()); // } } list.add(getName(), "Open s2s connections", open_s2s_connections, Level.FINE); list.add(getName(), "Packets queued", waiting_packets, Level.FINE); list.add(getName(), "Connected servers", connected_servers, Level.FINE); list.add(getName(), "Connection instances", server_connections_instances, Level.FINER); } //~--- methods -------------------------------------------------------------- /** * Method description * * * @return */ @Override public boolean handlesNonLocalDomains() { return true; } /** * Method description * * * @param packet * * @return */ @Override public int hashCodeForPacket(Packet packet) { // Calculate hash code from the destination domain name to make sure packets for // a single domain are processed by the same thread to avoid race condition // creating new connection data structures for a destination domain if (packet.getStanzaTo() != null) { return packet.getStanzaTo().getDomain().hashCode(); } // Otherwise, it might be a control packet which can be processed by single thread return 1; } //~--- get methods ---------------------------------------------------------- /** * Method description * * * @param session_id * * @return */ public boolean isIncomingValid(String session_id) { if (session_id == null) { return false; } XMPPIOService<Object> serv = incoming.get(session_id); if ((serv == null) || (serv.getSessionData().get("valid") == null)) { return false; } else { return (Boolean) serv.getSessionData().get("valid"); } } //~--- methods -------------------------------------------------------------- /** * Method description * * * @param packet * @param serv */ public synchronized void processDialback(Packet packet, XMPPIOService<Object> serv) { if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", DIALBACK - " + packet); } String local_hostname = packet.getStanzaTo().getDomain(); // Check whether this is correct local host name... if ( !isLocalDomainOrComponent(local_hostname)) { // Ups, this hostname is not served by this server, return stream // error and close the connection.... generateStreamError("host-unknown", serv); return; } String remote_hostname = packet.getStanzaFrom().getDomain(); // And we don't want to accept any connection which is from remote // host name the same as one my localnames....... if (isLocalDomainOrComponent(remote_hostname)) { // Ups, remote hostname is the same as one of local hostname?? // fake server or what? internal loop, we don't want that.... // error and close the connection.... generateStreamError("host-unknown", serv); return; } CID cid = getConnectionId(local_hostname, remote_hostname); ServerConnections serv_conns = getServerConnections(cid); String session_id = (String) serv.getSessionData().get(XMPPIOService.SESSION_ID_KEY); String serv_local_hostname = (String) serv.getSessionData().get("local-hostname"); String serv_remote_hostname = (String) serv.getSessionData().get("remote-hostname"); CID serv_cid = (serv_remote_hostname == null) ? null : getConnectionId(serv_local_hostname, serv_remote_hostname); if ((serv_cid != null) &&!cid.equals(serv_cid)) { log.info(serv + ", Somebody tries to reuse connection?" + " old_cid: " + serv_cid + ", new_cid: " + cid); } // <db:result> if ((packet.getElemName() == RESULT_EL_NAME) || (packet.getElemName() == DB_RESULT_EL_NAME)) { if (packet.getType() == null) { // This is incoming connection with dialback key for verification if (packet.getElemCData() != null) { // db:result with key to validate from accept connection String db_key = packet.getElemCData(); // initServiceMapping(local_hostname, remote_hostname, accept_jid, serv); // <db:result> with CDATA containing KEY Element elem = new Element(DB_VERIFY_EL_NAME, db_key, new String[] { "id", "to", "from", XMLNS_DB_ATT }, new String[] { session_id, remote_hostname, local_hostname, XMLNS_DB_VAL }); Packet result = Packet.packetInstance(elem, null, null); if (serv_conns == null) { serv_conns = createNewServerConnections(cid, null); } // serv_conns.putDBKey(session_id, db_key); serv.getSessionData().put("remote-hostname", remote_hostname); serv.getSessionData().put("local-hostname", local_hostname); if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", cid: " + cid + ", sessionId: " + session_id + ", Counters: ioservices: " + countIOServices() + ", s2s connections: " + countOpenConnections() + (Packet.FULL_DEBUG ? ", all connections: " + connectionsByLocalRemote : "")); } if ( !serv_conns.sendControlPacket(result) && serv_conns.needsConnection()) { createServerConnection(cid, result, serv_conns); } } else { // Incorrect dialback packet, it happens for some servers.... // I don't know yet what software they use. // Let's just disconnect and signal unrecoverable conection error if (log.isLoggable(Level.FINER)) { log.finer(serv + ", Incorrect diablack packet: " + packet); } bouncePacketsBack(Authorization.SERVICE_UNAVAILABLE, cid); generateStreamError("bad-format", serv); } } else { // <db:result> with type 'valid' or 'invalid' // It means that session has been validated now.... // XMPPIOService connect_serv = handshakingByHost_Type.get(connect_jid); switch (packet.getType()) { case valid : if (log.isLoggable(Level.FINER)) { log.finer(serv + ", Connection: " + cid + " is valid, adding to available services."); } serv_conns.handleDialbackSuccess(); break; default : if (log.isLoggable(Level.FINER)) { log.finer(serv + ", Connection: " + cid + " is invalid!! Stopping..."); } serv_conns.handleDialbackFailure(); break; } // end of switch (packet.getType()) } // end of if (packet.getType() != null) else } // end of if (packet != null && packet.getElemName().equals("db:result")) // <db:verify> with type 'valid' or 'invalid' if ((packet.getElemName() == VERIFY_EL_NAME) || (packet.getElemName() == DB_VERIFY_EL_NAME)) { if (packet.getStanzaId() != null) { String forkey_session_id = packet.getStanzaId(); if (packet.getType() == null) { // When type is NULL then it means this packet contains // data for verification if (packet.getElemCData() != null) { String db_key = packet.getElemCData(); // This might be the first dialback packet from remote server // serv.getSessionData().put("remote-hostname", remote_hostname); // serv.getSessionData().put("local-hostname", local_hostname); // serv_conns.addIncoming(session_id, serv); // log.finest("cid: " + cid + ", sessionId: " + session_id // + ", Counters: ioservices: " + countIOServices() // + ", s2s connections: " + countOpenConnections()); // initServiceMapping(local_hostname, remote_hostname, accept_jid, serv); String local_key = getLocalDBKey(cid, db_key, forkey_session_id, session_id); if (local_key == null) { if (log.isLoggable(Level.FINE)) { log.fine(serv + ", db key is not available for session ID: " + forkey_session_id + ", key for validation: " + db_key); } } else { if (log.isLoggable(Level.FINE)) { log.fine(serv + ", Local key for cid=" + cid + " is " + local_key); } sendVerifyResult(local_hostname, remote_hostname, forkey_session_id, db_key.equals(local_key), serv_conns, session_id); } } // end of if (packet.getElemName().equals("db:verify")) } else { // Type is not null so this is packet with verification result. // If the type is valid it means accept connection has been // validated and we can now receive data from this channel. Element elem = new Element(DB_RESULT_EL_NAME, new String[] { "type", "to", "from", XMLNS_DB_ATT }, new String[] { packet.getType().toString(), remote_hostname, local_hostname, XMLNS_DB_VAL }); sendToIncoming(forkey_session_id, Packet.packetInstance(elem, null, null)); validateIncoming(forkey_session_id, (packet.getType() == StanzaType.valid)); } // end of if (packet.getType() == null) else } else { // Incorrect dialback packet, it happens for some servers.... // I don't know yet what software they use. // Let's just disconnect and signal unrecoverable conection error if (log.isLoggable(Level.FINER)) { log.finer(serv + ", Incorrect diablack packet: " + packet); } bouncePacketsBack(Authorization.SERVICE_UNAVAILABLE, cid); generateStreamError("bad-format", serv); } } // end of if (packet != null && packet.getType() != null) } /** * Method description * * * @param packet */ @Override public void processPacket(Packet packet) { if (log.isLoggable(Level.FINEST)) { log.finest("Processing packet: " + packet.toString()); } if ( !packet.isCommand() ||!processCommand(packet)) { if (packet.getStanzaTo() == null) { log.warning("Missing 'to' attribute, ignoring packet..." + packet + "\n This most likely happens due to missconfiguration of components" + " domain names."); return; } if (packet.getStanzaFrom() == null) { log.warning("Missing 'from' attribute, ignoring packet..." + packet); return; } // Check whether addressing is correct: String to_hostname = packet.getStanzaTo().getDomain(); // We don't send packets to local domains trough s2s, there // must be something wrong with configuration if (isLocalDomainOrComponent(to_hostname)) { // Ups, remote hostname is the same as one of local hostname?? // Internal loop possible, we don't want that.... // Let's send the packet back.... if (log.isLoggable(Level.INFO)) { log.info("Packet addresses to localhost, I am not processing it: " + packet); } try { addOutPacket(Authorization.SERVICE_UNAVAILABLE.getResponseMessage(packet, "S2S - not delivered. Server missconfiguration.", true)); } catch (PacketErrorTypeException e) { log.warning("Packet processing exception: " + e); } return; } // I think from_hostname needs to be different from to_hostname at // this point... or s2s doesn't make sense String from_hostname = packet.getStanzaFrom().getDomain(); // All hostnames go through String.intern() if (to_hostname == from_hostname) { log.warning("Dropping incorrect packet - from_hostname == to_hostname: " + packet); return; } CID cid = getConnectionId(packet); if (log.isLoggable(Level.FINEST)) { log.finest("Connection ID is: " + cid); } ServerConnections serv_conn = getServerConnections(cid); Packet server_packet = packet.copyElementOnly(); server_packet.getElement().removeAttribute("xmlns"); // if (server_packet.getXMLNS() == XMLNS_CLIENT_VAL) { // server_packet.getElement().setXMLNS(XMLNS_SERVER_VAL); // } if ((serv_conn == null) || ( !serv_conn.sendPacket(server_packet) && serv_conn.needsConnection())) { if (log.isLoggable(Level.FINEST)) { log.finest("Couldn't send packet, creating a new connection: " + cid); } createServerConnection(cid, server_packet, serv_conn); } else { if (log.isLoggable(Level.FINEST)) { log.finest("Packet seems to be sent correctly: " + server_packet); } } } // end of else } /** * Method description * * * @param serv * * @return */ @Override public Queue<Packet> processSocketData(XMPPIOService<Object> serv) { Queue<Packet> packets = serv.getReceivedPackets(); Packet p = null; while ((p = packets.poll()) != null) { // log.finer("Processing packet: " + p.getElemName() // + ", type: " + p.getType()); if (p.getXMLNS() == XMLNS_SERVER_VAL) { p.getElement().setXMLNS(XMLNS_CLIENT_VAL); } if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", Processing socket data: " + p); } if (p.getXMLNS() == XMLNS_DB_VAL) { processDialback(p, serv); } else { if (p.getElemName() == "error") { processStreamError(p, serv); return null; } else { if (checkPacket(p, serv)) { if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", Adding packet out: " + p); } addOutPacket(p); } else { return null; } } } // end of else } // end of while () return null; } /** * Method description * * * @param port_props */ @Override public void reconnectionFailed(Map<String, Object> port_props) { // TODO: handle this somehow } /** * Method description * * * @param session_id * @param packet * * @return */ public boolean sendToIncoming(String session_id, Packet packet) { XMPPIOService<Object> serv = incoming.get(session_id); if (serv != null) { if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", Sending to incoming connection: " + session_id + " packet: " + packet); } return writePacketToSocket(serv, packet); } else { if (log.isLoggable(Level.FINER)) { log.finer("Trying to send packet: " + packet + " to nonexisten connection with sessionId: " + session_id); } return false; } } /** * Method description * * * @param serv */ @Override public void serviceStarted(XMPPIOService<Object> serv) { super.serviceStarted(serv); if (log.isLoggable(Level.FINEST)) { log.finest("s2s connection opened: " + serv); } switch (serv.connectionType()) { case connect : // Send init xmpp stream here // XMPPIOService serv = (XMPPIOService)service; String data = "<stream:stream" + " xmlns:stream='http://etherx.jabber.org/streams'" + " xmlns='jabber:server'" + " xmlns:db='jabber:server:dialback'" + ">"; if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", sending: " + data); } serv.xmppStreamOpen(data); break; default : // Do nothing, more data should come soon... break; } // end of switch (service.connectionType()) } /** * Method description * * * @param serv * * @return */ @Override public boolean serviceStopped(XMPPIOService<Object> serv) { boolean result = super.serviceStopped(serv); if (result) { switch (serv.connectionType()) { case connect : String local_hostname = (String) serv.getSessionData().get("local-hostname"); String remote_hostname = (String) serv.getSessionData().get("remote-hostname"); if (remote_hostname == null) { // There is something wrong... // It may happen only when remote host connecting to Tigase // closed connection before it send any db:... packet // so remote domain is not known. // Let's do nothing for now. log.info(serv + ", remote-hostname is NULL, local-hostname: " + local_hostname + ", local address: " + serv.getLocalAddress() + ", remote address: " + serv.getRemoteAddress()); } else { CID cid = getConnectionId(local_hostname, remote_hostname); ServerConnections serv_conns = getServerConnections(cid); if (serv_conns == null) { log.warning("There is no ServerConnections for stopped service: " + serv + ", cid: " + cid); if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", Counters: ioservices: " + countIOServices() + ", s2s active conns: " + countOpenConnections() + (Packet.FULL_DEBUG ? ", all connections: " + connectionsByLocalRemote : "")); } return result; } serv_conns.serviceStopped(serv); Queue<Packet> waiting = serv_conns.getWaitingPackets(); if (waiting.size() > 0) { if (serv_conns.waitingTime() > maxPacketWaitingTime) { bouncePacketsBack(Authorization.REMOTE_SERVER_TIMEOUT, cid); } else { createServerConnection(cid, null, serv_conns); } } } break; case accept : String session_id = (String) serv.getSessionData().get(XMPPIOService.SESSION_ID_KEY); if (session_id != null) { XMPPIOService<Object> rem = incoming.remove(session_id); if (rem == null) { if (log.isLoggable(Level.FINE)) { log.fine(serv + ", No service with given SESSION_ID: " + session_id); } } else { if (log.isLoggable(Level.FINER)) { log.finer(serv + ", Connection removed: " + session_id); } } } else { if (log.isLoggable(Level.FINE)) { log.fine(serv + ", session_id is null, didn't remove the connection"); } } break; default : log.severe(serv + ", Warning, program shouldn't reach that point."); break; } // end of switch (serv.connectionType()) if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", Counters: ioservices: " + countIOServices() + ", s2s active conns: " + countOpenConnections() + (Packet.FULL_DEBUG ? ", all connections: " + connectionsByLocalRemote : "")); } } return result; } //~--- set methods ---------------------------------------------------------- /** * Method description * * * @param props */ @Override public void setProperties(Map<String, Object> props) { super.setProperties(props); // hostnames = (String[])props.get(HOSTNAMES_PROP_KEY); // if (hostnames == null || hostnames.length == 0) { // log.warning("Hostnames definition is empty, setting 'localhost'"); // hostnames = new String[] {"localhost"}; // } // end of if (hostnames == null || hostnames.length == 0) // Arrays.sort(hostnames); // addRouting("*"); maxPacketWaitingTime = (Long) props.get(MAX_PACKET_WAITING_TIME_PROP_KEY); } //~--- methods -------------------------------------------------------------- /** * Method description * * * @param service */ @Override public void tlsHandshakeCompleted(XMPPIOService<Object> service) {} /** * Method description * * * @param session_id * @param valid */ public void validateIncoming(String session_id, boolean valid) { XMPPIOService<Object> serv = incoming.get(session_id); if (serv != null) { serv.getSessionData().put("valid", valid); if ( !valid) { serv.stop(); } } } /** * Method description * * * @param serv */ @Override public void xmppStreamClosed(XMPPIOService<Object> serv) { if (log.isLoggable(Level.FINER)) { log.finer(serv + ", Stream closed: " + getConnectionId(serv)); } } /** * Method description * * * @param serv * @param attribs * * @return */ @Override public String xmppStreamOpened(XMPPIOService<Object> serv, Map<String, String> attribs) { if (log.isLoggable(Level.FINER)) { log.finer(serv + ", Stream opened: " + attribs.toString()); } serv.getSessionData().put("xmlns", XMLNS_SERVER_VAL); switch (serv.connectionType()) { case connect : { // It must be always set for connect connection type String remote_hostname = (String) serv.getSessionData().get("remote-hostname"); String local_hostname = (String) serv.getSessionData().get("local-hostname"); CID cid = getConnectionId(local_hostname, remote_hostname); String remote_id = attribs.get("id"); if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", Connect Stream opened for: " + cid + ", session id" + remote_id); } ServerConnections serv_conns = getServerConnections(cid); if (serv_conns == null) { serv_conns = createNewServerConnections(cid, null); } serv_conns.addOutgoing(serv); if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", Counters: ioservices: " + countIOServices() + ", s2s active conns: " + countOpenConnections() + (Packet.FULL_DEBUG ? ", all connections: " + connectionsByLocalRemote : "")); } serv.getSessionData().put(XMPPIOService.SESSION_ID_KEY, remote_id); 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_conns.putDBKey(remote_id, key); Element elem = new Element(DB_RESULT_EL_NAME, key, new String[] { "from", "to", XMLNS_DB_ATT }, new String[] { local_hostname, remote_hostname, XMLNS_DB_VAL }); serv_conns.addControlPacket(Packet.packetInstance(elem, null, null)); serv_conns.sendAllControlPackets(); return null; } case accept : { String remote_hostname = (String) serv.getSessionData().get("remote-hostname"); String local_hostname = (String) serv.getSessionData().get("local-hostname"); CID cid = getConnectionId(local_hostname, remote_hostname); String id = UUID.randomUUID().toString(); if (log.isLoggable(Level.FINEST)) { log.finest(serv + ", Accept Stream opened for: " + cid + ", session id: " + id); } if (remote_hostname != null) { if (log.isLoggable(Level.FINE)) { log.fine(serv + ", Opening stream for already established connection...., trying to turn" + " on TLS????"); } } // We don't know hostname yet so we have to save session-id in // connection temp data serv.getSessionData().put(XMPPIOService.SESSION_ID_KEY, id); incoming.put(id, serv); return "<stream:stream" + " xmlns:stream='http://etherx.jabber.org/streams'" + " xmlns='jabber:server'" + " xmlns:db='jabber:server:dialback'" + " id='" + id + "'" + ">" ; } default : log.severe(serv + ", Warning, program shouldn't reach that point."); break; } // end of switch (serv.connectionType()) return null; } //~--- get methods ---------------------------------------------------------- @Override protected int[] getDefPlainPorts() { return new int[] { 5269 }; } protected String getLocalDBKey(CID cid, String key, String forkey_sessionId, String asking_sessionId) { ServerConnections serv_conns = getServerConnections(cid); return (serv_conns == null) ? null : serv_conns.getDBKey(forkey_sessionId); } /** * Method <code>getMaxInactiveTime</code> returns max keep-alive time * for inactive connection. Let's assume s2s should send something * at least once every 15 minutes.... * * @return a <code>long</code> value */ @Override protected long getMaxInactiveTime() { return 15 * MINUTE; } protected ServerConnections getServerConnections(CID cid) { return connectionsByLocalRemote.get(cid); } @Override protected XMPPIOService<Object> getXMPPIOServiceInstance() { return new XMPPIOService<Object>(); } @Override protected boolean isHighThroughput() { return true; } //~--- methods -------------------------------------------------------------- protected ServerConnections removeServerConnections(CID cid) { return connectionsByLocalRemote.remove(cid); } protected void sendVerifyResult(String from, String to, String forkey_sessionId, boolean valid, ServerConnections serv_conns, String asking_sessionId) { String type = (valid ? "valid" : "invalid"); Element result_el = new Element(DB_VERIFY_EL_NAME, new String[] { "from", "to", "id", "type", XMLNS_DB_ATT }, new String[] { from, to, forkey_sessionId, type, XMLNS_DB_VAL }); Packet result = Packet.packetInstance(result_el, null, null); if ( !sendToIncoming(asking_sessionId, result)) { log.warning("Can not send verification packet back: " + result.toString()); } } private void bouncePacketsBack(Authorization author, CID cid) { ServerConnections serv_conns = getServerConnections(cid); if (serv_conns != null) { Queue<Packet> waiting = serv_conns.getWaitingPackets(); Packet p = null; while ((p = waiting.poll()) != null) { if (log.isLoggable(Level.FINEST)) { log.finest("Sending packet back: " + p); } try { addOutPacket(author.getResponseMessage(p, "S2S - not delivered", true)); } catch (PacketErrorTypeException e) { log.info("Packet processing exception: " + e); } } // end of while (p = waitingPackets.remove(ipAddress) != null) } else { log.info("No ServerConnections for cid: " + cid); } } private boolean checkPacket(Packet packet, XMPPIOService<Object> serv) { JID packet_from = packet.getStanzaFrom(); JID packet_to = packet.getStanzaTo(); if ((packet_from == null) || (packet_to == null)) { generateStreamError("improper-addressing", serv); return false; } String remote_hostname = (String) serv.getSessionData().get("remote-hostname"); if ( !packet_from.getDomain().equals(remote_hostname)) { if (log.isLoggable(Level.FINER)) { log.finer(serv + ", Invalid hostname from the remote server, expected: " + remote_hostname); } generateStreamError("invalid-from", serv); return false; } String local_hostname = (String) serv.getSessionData().get("local-hostname"); if ( !packet_to.getDomain().equals(local_hostname)) { if (log.isLoggable(Level.FINER)) { log.finer(serv + ", Invalid hostname of the local server, expected: " + local_hostname); } generateStreamError("host-unknown", serv); return false; } String session_id = (String) serv.getSessionData().get(XMPPIOService.SESSION_ID_KEY); if ( !isIncomingValid(session_id)) { log.info(serv + ", Incoming connection hasn't been validated"); return false; } return true; } private int countOpenConnections() { // int open_s2s_connections = incoming.size(); int open_s2s_connections = 0; for (Map.Entry<CID, ServerConnections> entry : connectionsByLocalRemote.entrySet()) { ServerConnections conn = entry.getValue(); if (conn.isOutgoingConnected()) { ++open_s2s_connections; } } return open_s2s_connections; } private ServerConnections createNewServerConnections(CID cid, Packet packet) { ServerConnections conns = new ServerConnections(this, cid); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Creating a new ServerConnections instance: {0}", conns); } if (packet != null) { // XMLNS is processed through String.intern() if (packet.getElement().getXMLNS() == XMLNS_DB_VAL) { conns.addControlPacket(packet); } else { conns.addDataPacket(packet); } } connectionsByLocalRemote.put(cid, conns); return conns; } /** * Method <code>createServerConnection</code> is called only when a new * connection is needed for any reason for given hostnames combination. * * @param cid a <code>String</code> s2s connection ID (localhost@remotehost) * @param packet a <code>Packet</code> packet to send, should be passed to the * ServerConnections only when it was null. * @param serv_conn a <code>ServerConnections</code> which was called for * the packet. */ private void createServerConnection(final CID cid, final Packet packet, final ServerConnections serv_conn) { // Create a new connection data structures first if it they does not yet exist // to avoid creating them within a separate thread, which leads to a multiple // instances of such structures and general protocol failure final ServerConnections sconn = ((serv_conn == null) ? createNewServerConnections(cid, packet) : serv_conn); sconn.setConnecting(); new ConnectionWatchdogTask(sconn, cid.getLocalHost(), cid.getRemoteHost()); // Spawning a new thread for each new server connection is not the most // optimal solution but I have no idea how to do it better and solve // the long DNS resolution problem. // On the other hand, new server connections are not opened as often // so it should not be a big problem. Let's see how it works. Thread new_connection_thread = new Thread("NewServerConnection-" + (++new_connection_thread_counter)) { @Override public void run() { createServerConnectionInThread(cid, packet, sconn); } }; new_connection_thread.start(); } private void createServerConnectionInThread(CID cid, Packet packet, ServerConnections serv_conn) { ServerConnections conns = serv_conn; String localhost = cid.getLocalHost(); String remotehost = cid.getRemoteHost(); if (openNewServerConnection(localhost, remotehost)) { // conns.setConnecting(); // new ConnectionWatchdogTask(conns, localhost, remotehost); if (log.isLoggable(Level.FINEST)) { log.finest("Connecting a new s2s service: " + conns); } } else { if (log.isLoggable(Level.FINEST)) { log.finest("Couldn't open a new s2s service: (UknownHost??) " + conns); } // Can't establish connection...., unknown host?? Queue<Packet> waitingPackets = conns.getWaitingPackets(); // Well, is somebody injects a packet with the same sender and // receiver domain and this domain is not valid then we have // infinite loop here.... // Let's try to handle this by dropping such packet. // It may happen as well that the source domain is different from // target domain and both are invalid, what then? // The best option would be to drop the packet if it is already an // error - remote-server-not-found.... // For dialback packet just ignore the error completely as it means // remote server tries to connect from domain which doesn't exist // in DNS so no further action should be performed. Packet p = null; while ((p = waitingPackets.poll()) != null) { if (p.getElement().getXMLNS() != XMLNS_DB_VAL) { try { addOutPacket(Authorization.REMOTE_SERVER_NOT_FOUND.getResponseMessage(p, "S2S - destination host not found", true)); } catch (PacketErrorTypeException e) { log.warning("Packet: " + p.toString() + " processing exception: " + e); } } } conns.stopAll(); // connectionsByLocalRemote.remove(cid); } } private void generateStreamError(String error_el, XMPPIOService<Object> serv) { Element error = new Element("stream:error", new Element[] { new Element(error_el, new String[] { "xmlns" }, new String[] { "urn:ietf:params:xml:ns:xmpp-streams" }) }, null, null); try { writeRawData(serv, error.toString()); // serv.writeRawData(error.toString()); // serv.writeRawData("</stream:stream>"); serv.stop(); } catch (Exception e) { serv.forceStop(); } } //~--- get methods ---------------------------------------------------------- private CID getConnectionId(String localhost, String remotehost) { return new CID(localhost, remotehost); } private CID getConnectionId(Packet packet) { return new CID(packet.getStanzaFrom().getDomain(), packet.getStanzaTo().getDomain()); } private CID getConnectionId(XMPPIOService<Object> service) { String local_hostname = (String) service.getSessionData().get("local-hostname"); String remote_hostname = (String) service.getSessionData().get("remote-hostname"); CID cid = getConnectionId(local_hostname, remote_hostname); return cid; } //~--- methods -------------------------------------------------------------- //private void dumpCurrentStack(StackTraceElement[] stack) { // StringBuilder sb = new StringBuilder(); // for (StackTraceElement st_el: stack) { // sb.append("\n" + st_el.toString()); // } // log.finest(sb.toString()); //} private boolean openNewServerConnection(String localhost, String remotehost) { // dumpCurrentStack(Thread.currentThread().getStackTrace()); try { DNSEntry dns_entry = DNSResolver.getHostSRV_Entry(remotehost); Map<String, Object> port_props = new TreeMap<String, Object>(); port_props.put("remote-ip", dns_entry.getIp()); port_props.put("local-hostname", localhost); port_props.put("remote-hostname", remotehost); port_props.put("ifc", new String[] { dns_entry.getIp() }); port_props.put("socket", SocketType.plain); port_props.put("type", ConnectionType.connect); port_props.put("port-no", dns_entry.getPort()); CID cid = getConnectionId(localhost, remotehost); port_props.put("cid", cid); if (log.isLoggable(Level.FINEST)) { log.finest("STARTING new connection: " + cid); } addWaitingTask(port_props); // reconnectService(port_props, 5*SECOND); return true; } catch (UnknownHostException e) { log.info("UnknownHostException for host: " + remotehost); return false; } // end of try-catch } private boolean processCommand(final Packet packet) { // XMPPIOService serv = getXMPPIOService(packet); switch (packet.getCommand()) { case STARTTLS : break; case STREAM_CLOSED : break; case GETDISCO : break; case CLOSE : break; default : break; } // end of switch (pc.getCommand()) return false; } private void processStreamError(Packet packet, XMPPIOService<Object> serv) { Authorization author = Authorization.RECIPIENT_UNAVAILABLE; if (packet.getElement().getChild("host-unknown") != null) { author = Authorization.REMOTE_SERVER_NOT_FOUND; } CID cid = getConnectionId(serv); bouncePacketsBack(author, cid); serv.stop(); } //~--- inner classes -------------------------------------------------------- private class ConnectionWatchdogTask extends TimerTask { private ServerConnections conns = null; private String localhost = null; private String remotehost = null; //~--- constructors ------------------------------------------------------- private ConnectionWatchdogTask(ServerConnections conns, String localhost, String remotehost) { this.conns = conns; this.localhost = localhost; this.remotehost = remotehost; String key = localhost + remotehost; ConnectionWatchdogTask task = waitingTasks.get(key); if (task != null) { task.cancel(); } addTimerTask(this, 15, TimeUnit.SECONDS); waitingTasks.put(key, this); } //~--- methods ------------------------------------------------------------ /** * Method description * */ @Override public void run() { String key = localhost + remotehost; waitingTasks.remove(key); if (conns.getOutgoingState() != ServerConnections.OutgoingState.OK) { if (log.isLoggable(Level.FINEST)) { log.finest("Connecting timeout expired, still connecting: " + conns); } conns.stopAll(); Queue<Packet> waiting = conns.getWaitingPackets(); if (waiting.size() > 0) { if (conns.waitingTime() > maxPacketWaitingTime) { if (log.isLoggable(Level.FINEST)) { log.finest("Max packets waiting time expired, sending all back: " + conns); } bouncePacketsBack(Authorization.REMOTE_SERVER_TIMEOUT, conns.getCID()); } else { if (log.isLoggable(Level.FINEST)) { log.finest("Reconnecting: " + conns); } createServerConnection(conns.getCID(), null, conns); } } else { if (log.isLoggable(Level.FINEST)) { log.finest("No packets waiting in queue, giving up: " + conns); } } } else { if (log.isLoggable(Level.FINEST)) { log.finest("Connecting timeout expired: " + conns); } } } } } //~ Formatted in Sun Code Convention //~ Formatted by Jindent --- http://www.jindent.com