/* * 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, either 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.bosh; //~--- non-JDK imports -------------------------------------------------------- import tigase.server.Command; import tigase.server.Packet; import tigase.util.TigaseStringprepException; import tigase.xml.Element; import tigase.xmpp.Authorization; import tigase.xmpp.JID; import tigase.xmpp.PacketErrorTypeException; import tigase.xmpp.StanzaType; import static tigase.server.bosh.Constants.*; //~--- JDK imports ------------------------------------------------------------ import java.util.Collections; import java.util.List; import java.util.Queue; import java.util.TimerTask; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import tigase.xmpp.*; //~--- classes ---------------------------------------------------------------- /** * Describe class BoshSession here. * * * Created: Tue Jun 5 18:07:23 2007 * * @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a> * @version $Rev$ */ public class BoshSession { /** * Variable <code>log</code> is a class logger. */ private static final Logger log = Logger.getLogger("tigase.server.bosh.BoshSession"); private static final long SECOND = 1000; private static final String PRESENCE_ELEMENT_NAME = "presence"; private static final String MESSAGE_ELEMENT_NAME = "message"; private static final String IQ_ELEMENT_NAME = "iq"; // ~--- fields --------------------------------------------------------------- private BoshSessionCache cache = null; /** * <code>current_rid</code> is the table with body rids which are waiting for * replies. */ private long[] currentRids = null; private JID dataReceiver = null; private String domain = null; private BoshSessionTaskHandler handler = null; private int[] hashCodes = null; private TimerTask inactivityTimer = null; private long previous_received_rid = -1; private String[] replace_with = { "$1<a href=\"http://$2\" target=\"_blank\">$2</a>", "$1<a href=\"$2\" target=\"_blank\">$2</a>", }; private int rids_head = 0; private int rids_tail = 0; private String sessionId = null; private UUID sid = null; // Old connections which might be reused in keep-alive mode. // Requests have been responded to so in most cases the connection should // be closed unless it is reused in keep-alive mode. // Normally there should be no more than max 2 elements in the queue. private Queue<BoshIOService> old_connections = new LinkedBlockingQueue<BoshIOService>(4); // Active connections with pending requests received private Queue<BoshIOService> connections = new ConcurrentLinkedQueue<BoshIOService>(); // private enum TimedTask { EMPTY_RESP, STOP }; // private Map<TimerTask, TimedTask> task_enum = // new LinkedHashMap<TimerTask, TimedTask>(); // private EnumMap<TimedTask, TimerTask> enum_task = // new EnumMap<TimedTask, TimerTask>(TimedTask.class); private TimerTask waitTimer = null; private Queue<Element> waiting_packets = new ConcurrentLinkedQueue<Element>(); private boolean terminate = false; private long min_polling = MIN_POLLING_PROP_VAL; private long max_wait = MAX_WAIT_DEF_PROP_VAL; private long max_pause = MAX_PAUSE_PROP_VAL; private long max_inactivity = MAX_INACTIVITY_PROP_VAL; private Pattern[] links_regexs = { Pattern.compile("([^>/\";]|^)(www\\.[^ ]+)", Pattern.CASE_INSENSITIVE), Pattern.compile("([^\">;]|^)(http://[^ ]+)", Pattern.CASE_INSENSITIVE), }; private int hold_requests = HOLD_REQUESTS_PROP_VAL; private String content_type = CONTENT_TYPE_DEF; private int concurrent_requests = CONCURRENT_REQUESTS_PROP_VAL; private boolean cache_on = false; // ~--- constructors --------------------------------------------------------- /** * Creates a new <code>BoshSession</code> instance. * * * @param def_domain * @param dataReceiver * @param handler */ public BoshSession(String def_domain, JID dataReceiver, BoshSessionTaskHandler handler) { this.sid = UUID.randomUUID(); this.domain = def_domain; this.dataReceiver = dataReceiver; this.handler = handler; } // ~--- methods -------------------------------------------------------------- /** * Method description * */ public void close() { terminate = true; processPacket(null, null); closeAllConnections(); } private void closeAllConnections() { for (BoshIOService conn : old_connections) { conn.stop(); } for (BoshIOService conn : connections) { conn.stop(); } } /** * Method description * * * @param bios */ public void disconnected(BoshIOService bios) { if (log.isLoggable(Level.FINEST)) { log.finest("Disconnected called for: " + bios.getUniqueId()); } if (bios != null) { connections.remove(bios); } if (inactivityTimer != null) { if (log.isLoggable(Level.FINEST)) { log.finest("Canceling inactivityTimer: " + getSid()); } handler.cancelTask(inactivityTimer); } if (connections.size() == 0) { if (log.isLoggable(Level.FINEST)) { log.finest("Setting inactivityTimer for " + max_inactivity + ": " + getSid()); } inactivityTimer = handler.scheduleTask(this, max_inactivity * SECOND); } } // ~--- get methods ---------------------------------------------------------- /** * Method description * * * @return */ public JID getDataReceiver() { return dataReceiver; } /** * Method description * * * @return */ public String getDomain() { return domain; } /** * Method description * * * @return */ public String getSessionId() { return sessionId; } /** * Method description * * * @return */ public UUID getSid() { return sid; } // ~--- methods -------------------------------------------------------------- /** * Method description * * * @param packet * @param service * @param max_wait * @param min_polling * @param max_inactivity * @param concurrent_requests * @param hold_requests * @param max_pause * @param out_results */ public void init(Packet packet, BoshIOService service, long max_wait, long min_polling, long max_inactivity, int concurrent_requests, int hold_requests, long max_pause, Queue<Packet> out_results) { String cache_action = packet.getAttribute(CACHE_ATTR); if ((cache_action != null) && cache_action.equals(CacheAction.on.toString())) { cache = new BoshSessionCache(); cache_on = true; log.fine("BoshSessionCache set to ON"); } hashCodes = new int[(this.concurrent_requests + 1) * 5]; currentRids = new long[(this.concurrent_requests + 1) * 5]; for (int i = 0; i < currentRids.length; i++) { currentRids[i] = -1; hashCodes[i] = -1; } long wait_l = max_wait; String wait_s = packet.getAttribute(WAIT_ATTR); if (wait_s != null) { try { wait_l = Long.parseLong(wait_s); } catch (NumberFormatException e) { wait_l = max_wait; } } this.max_wait = Math.min(wait_l, max_wait); // this.max_wait = wait_l; int hold_i = hold_requests; String tmp_str = packet.getAttribute(HOLD_ATTR); if (tmp_str != null) { try { hold_i = Integer.parseInt(tmp_str); } catch (NumberFormatException e) { hold_i = hold_requests; } } tmp_str = packet.getAttribute(RID_ATTR); if (tmp_str != null) { try { previous_received_rid = Long.parseLong(tmp_str); currentRids[rids_head++] = previous_received_rid; } catch (NumberFormatException e) { } } this.hold_requests = Math.max(hold_i, hold_requests); if (packet.getAttribute(TO_ATTR) != null) { this.domain = packet.getAttribute(TO_ATTR); } this.min_polling = min_polling; this.max_inactivity = max_inactivity; this.concurrent_requests = concurrent_requests; this.max_pause = max_pause; if (packet.getAttribute(CONTENT_ATTR) != null) { content_type = packet.getAttribute(CONTENT_ATTR); } String lang = packet.getAttribute(LANG_ATTR); if (lang == null) { lang = "en"; } service.setContentType(content_type); Element body = new Element(BODY_EL_NAME, new String[] { WAIT_ATTR, INACTIVITY_ATTR, POLLING_ATTR, REQUESTS_ATTR, HOLD_ATTR, MAXPAUSE_ATTR, SID_ATTR, VER_ATTR, FROM_ATTR, SECURE_ATTR, "xmpp:version", "xmlns:xmpp", "xmlns:stream" }, new String[] { Long.valueOf(this.max_wait).toString(), Long.valueOf(this.max_inactivity).toString(), Long.valueOf(this.min_polling).toString(), Integer.valueOf(this.concurrent_requests).toString(), Integer.valueOf(this.hold_requests).toString(), Long.valueOf(this.max_pause).toString(), this.sid.toString(), BOSH_VERSION, this.domain, "true", "1.0", "urn:xmpp:xbosh", "http://etherx.jabber.org/streams" }); sessionId = UUID.randomUUID().toString(); body.setAttribute(AUTHID_ATTR, sessionId); if (getCurrentRidTail() > 0) { body.setAttribute(ACK_ATTR, "" + takeCurrentRidTail()); } try { BareJID userId = packet.getAttribute("from") != null ? BareJID.bareJIDInstance(packet.getAttribute("from")) : null; if (userId != null) { BareJID hostJid = handler.getSeeOtherHostForJID(userId); if (hostJid != null) { Element error = new Element("stream:error"); Element seeOtherHost = new Element("see-other-host", hostJid.toString()); seeOtherHost.setXMLNS("urn:ietf:params:xml:ns:xmpp-streams"); error.addChild(seeOtherHost); body.addChild(error); } } } catch (TigaseStringprepException ex) { Logger.getLogger(BoshSession.class.getName()).log(Level.SEVERE, null, ex); } body.setXMLNS(BOSH_XMLNS); sendBody(service, body); // service.writeRawData(body.toString()); Packet streamOpen = Command.STREAM_OPENED.getPacket(null, null, StanzaType.set, UUID.randomUUID() .toString(), Command.DataType.submit); Command.addFieldValue(streamOpen, "session-id", sessionId); Command.addFieldValue(streamOpen, "hostname", domain); Command.addFieldValue(streamOpen, LANG_ATTR, lang); handler.addOutStreamOpen(streamOpen, this); // out_results.offer(streamOpen); } /** * Method description * * * @param packet * @param out_results */ public synchronized void processPacket(Packet packet, Queue<Packet> out_results) { if (packet != null) { if (log.isLoggable(Level.FINEST)) { log.finest("[" + connections.size() + "] Processing packet: " + packet.toString()); } if (filterInPacket(packet)) { waiting_packets.offer(packet.getElement()); } else { if (log.isLoggable(Level.FINEST)) { log.finest("[" + connections.size() + "] In packet filtered: " + packet.toString()); } } } if ((connections.size() > 0) && ((waiting_packets.size() > 0) || terminate)) { BoshIOService serv = connections.poll(); sendBody(serv, null); } } /** * Method description * * * @param packet * @param service * @param out_results */ public synchronized void processSocketPacket(Packet packet, BoshIOService service, Queue<Packet> out_results) { if (log.isLoggable(Level.FINEST)) { log.finest("[" + connections.size() + "] Processing socket packet: " + packet.toString()); } if (waitTimer != null) { if (log.isLoggable(Level.FINEST)) { log.finest("Canceling waitTimer: " + getSid()); } handler.cancelTask(waitTimer); } if (inactivityTimer != null) { if (log.isLoggable(Level.FINEST)) { log.finest("Canceling inactivityTimer: " + getSid()); } handler.cancelTask(inactivityTimer); } if ((packet.getElemName() == BODY_EL_NAME) && (packet.getXMLNS() == BOSH_XMLNS)) { List<Element> children = packet.getElemChildren(BODY_EL_NAME); boolean duplicate = false; if (packet.getAttribute(RID_ATTR) != null) { try { long rid = Long.parseLong(packet.getAttribute(RID_ATTR)); if (isDuplicateRid(rid, children)) { log.info("Discovered duplicate client connection, trying to close the " + "old one with RID: " + rid); Element body = getBodyElem(); body.setAttribute("type", StanzaType.terminate.toString()); sendBody(service, body); return; } service.setRid(rid); duplicate = isDuplicateMessage(rid, children); if (!duplicate) { processRid(rid, children); } } catch (NumberFormatException e) { log.warning("Incorrect RID value: " + packet.getAttribute(RID_ATTR)); } } service.setContentType(content_type); service.setSid(sid); connections.offer(service); if (!duplicate) { if ((packet.getType() != null) && (packet.getType() == StanzaType.terminate)) { // We are preparing for session termination. // Some client send IQ stanzas with private data to store some // settings so some confirmation stanzas might be sent back // let's give the client a few secs for session termination max_inactivity = 2; // Max pause changed to 2 secs terminate = true; Packet command = Command.STREAM_CLOSED.getPacket(null, null, StanzaType.set, UUID .randomUUID().toString()); handler.addOutStreamClosed(command, this); // out_results.offer(command); } if ((packet.getAttribute(RESTART_ATTR) != null) && packet.getAttribute(RESTART_ATTR).equals("true")) { log.fine("Found stream restart instruction: " + packet.toString()); out_results.offer(Command.GETFEATURES.getPacket(null, null, StanzaType.get, "restart1", null)); } if (packet.getAttribute(CACHE_ATTR) != null) { try { CacheAction action = CacheAction.valueOf(packet.getAttribute(CACHE_ATTR)); if (cache_on || (action == CacheAction.on)) { processCache(action, packet); } } catch (IllegalArgumentException e) { log.warning("Incorrect cache action: " + packet.getAttribute(CACHE_ATTR)); } } else { if (children != null) { for (Element el : children) { try { if (el.getXMLNS().equals(BOSH_XMLNS)) { el.setXMLNS(XMLNS_CLIENT_VAL); } Packet result = Packet.packetInstance(el); if (filterOutPacket(result)) { if (log.isLoggable(Level.FINEST)) { log.finest("Sending out packet: " + result.toString()); } out_results.offer(result); } else { if (log.isLoggable(Level.FINEST)) { log.finest("Out packet filtered: " + result.toString()); } } } catch (TigaseStringprepException ex) { log.warning("Packet addressing problem, stringprep processing failed, dropping: " + el); } } } } } else { log.info("Duplicated packet: " + packet.toString()); } } else { log.warning("[" + connections.size() + "] Unexpected packet from the network: " + packet.toString()); String er_msg = "Invalid body element"; if (packet.getElemName() != BODY_EL_NAME) { er_msg += ", incorrect root element name, use " + BODY_EL_NAME; } if (packet.getXMLNS() != BOSH_XMLNS) { er_msg += ", incorrect xmlns, use " + BOSH_XMLNS; } try { Packet error = Authorization.BAD_REQUEST.getResponseMessage(packet, er_msg, true); waiting_packets.add(error.getElement()); terminate = true; Packet command = Command.STREAM_CLOSED.getPacket(null, null, StanzaType.set, UUID.randomUUID() .toString()); handler.addOutStreamClosed(command, this); // out_results.offer(command); } catch (PacketErrorTypeException e) { log.info("Error type and incorrect from bosh client? Ignoring..."); } } // Send packets waiting in queue... processPacket(null, out_results); if (connections.size() > hold_requests) { BoshIOService serv = connections.poll(); sendBody(serv, null); } if ((connections.size() > 0) && (waiting_packets.size() == 0)) { if (log.isLoggable(Level.FINEST)) { log.finest("Setting waitTimer for " + max_wait + ": " + getSid()); } waitTimer = handler.scheduleTask(this, max_wait * SECOND); } } // ~--- set methods ---------------------------------------------------------- /** * Method description * * * @param dataReceiver */ public void setDataReceiver(JID dataReceiver) { this.dataReceiver = dataReceiver; } // ~--- methods -------------------------------------------------------------- /** * Method description * * * @param out_results * @param tt * * @return */ public boolean task(Queue<Packet> out_results, TimerTask tt) { if (tt == inactivityTimer) { if (log.isLoggable(Level.FINEST)) { log.finest("inactivityTimer fired: " + getSid()); } if (waitTimer != null) { if (log.isLoggable(Level.FINEST)) { log.finest("Canceling waitTimer: " + getSid()); } handler.cancelTask(waitTimer); } for (Element packet : waiting_packets) { try { // Do not send stream:features back with an error if (packet.getName() != "stream:features") { out_results.offer(Authorization.RECIPIENT_UNAVAILABLE.getResponseMessage( Packet.packetInstance(packet), "Bosh = disconnected", true)); } } catch (TigaseStringprepException ex) { log.warning("Packet addressing problem, stringprep processing failed, dropping: " + packet); } catch (PacketErrorTypeException e) { log.info("Packet processing exception: " + e); } } if (log.isLoggable(Level.FINEST)) { log.finest("Closing session, inactivity timeout expired: " + getSid()); } Packet command = Command.STREAM_CLOSED.getPacket(null, null, StanzaType.set, UUID.randomUUID() .toString()); handler.addOutStreamClosed(command, this); closeAllConnections(); // out_results.offer(command); return true; } if (tt == waitTimer) { if (log.isLoggable(Level.FINEST)) { log.finest("waitTimer fired: " + getSid()); } BoshIOService serv = connections.poll(); if (serv != null) { sendBody(serv, null); } } return false; } private Element applyFilters(Element packet) { Element result = packet.clone(); if (result.getName() == MESSAGE_ELEMENT_NAME) { String body = result.getCData("/message/body"); if (body != null) { int count = 0; // for (Pattern reg: links_regexs) { // body = reg.matcher(body).replaceAll(replace_with[count++]); // } result.getChild("body").setCData(body); } } return result; } private boolean filterInPacket(Packet packet) { if (cache_on) { processAutomaticCache(packet); } return true; } private boolean filterOutPacket(Packet packet) { if (cache_on && (packet.getElemName() == MESSAGE_ELEMENT_NAME)) { cache.addToMessage(packet.getElement()); } return true; } // ~--- get methods ---------------------------------------------------------- private Element getBodyElem() { Element body = new Element(BODY_EL_NAME, new String[] { FROM_ATTR, SECURE_ATTR, "xmpp:version", "xmlns:xmpp", "xmlns:stream" }, new String[] { this.domain, "true", "1.0", "urn:xmpp:xbosh", "http://etherx.jabber.org/streams" }); body.setXMLNS(BOSH_XMLNS); return body; } private long getCurrentRidTail() { synchronized (currentRids) { return currentRids[rids_tail]; } } private boolean isDuplicateMessage(long rid, List<Element> packets) { synchronized (currentRids) { int hashCode = -1; if ((packets != null) && (packets.size() > 0)) { StringBuilder sb = new StringBuilder(); for (Element elem : packets) { sb.append(elem.toString()); } hashCode = sb.toString().hashCode(); } if (hashCode == -1) { return false; } for (int i = 0; i < currentRids.length; ++i) { if (rid == currentRids[i]) { return hashCode == hashCodes[i]; } } } return false; } private boolean isDuplicateRid(long rid, List<Element> packets) { synchronized (currentRids) { int hashCode = -1; if ((packets != null) && (packets.size() > 0)) { StringBuilder sb = new StringBuilder(); for (Element elem : packets) { sb.append(elem.toString()); } hashCode = sb.toString().hashCode(); } for (int i = 0; i < currentRids.length; ++i) { if (rid == currentRids[i]) { return hashCode != hashCodes[i]; } } } return false; } // ~--- methods -------------------------------------------------------------- private void processAutomaticCache(Packet packet) { if (packet.getElemName() == PRESENCE_ELEMENT_NAME) { cache.addPresence(packet.getElement()); } if (packet.getElemName() == MESSAGE_ELEMENT_NAME) { cache.addFromMessage(packet.getElement()); } if (packet.isXMLNS("/iq/query", "jabber:iq:roster")) { cache.addRoster(packet.getElement()); } if (packet.isXMLNS("/iq/bind", "urn:ietf:params:xml:ns:xmpp-bind")) { cache.set(BoshSessionCache.RESOURCE_BIND_ID, Collections.singletonList(packet.getElement())); } } private long cache_reload_counter = 0; private void processCache(CacheAction action, Packet packet) { ++cache_reload_counter; int packet_counter = 0; List<Element> children = packet.getElemChildren(BODY_EL_NAME); String cache_id = packet.getAttribute(CACHE_ID_ATTR); List<Element> cache_res = null; switch (action) { case on: if (cache == null) { cache = new BoshSessionCache(); } cache_on = true; log.fine("BoshSessionCache set to ON"); break; case off: cache_on = false; log.fine("BoshSessionCache set to OFF"); break; case set: cache.set(cache_id, children); break; case add: cache.add(cache_id, children); break; case get: cache_res = cache.get(cache_id); break; case remove: cache.remove(cache_id); break; case get_all: cache_res = cache.getAll(); retireAllOldConnections(); break; default: log.warning("Unknown cache action: " + action.toString()); break; } if (cache_res != null) { for (Element elem : cache_res) { elem.addAttribute("reload-counter", "" + cache_reload_counter); elem.addAttribute("packet-counter", "" + (++packet_counter)); waiting_packets.add(elem); } } } private void processRid(long rid, List<Element> packets) { synchronized (currentRids) { if ((previous_received_rid + 1) != rid) { log.info("Incorrect packet order, last_rid=" + previous_received_rid + ", current_rid=" + rid); } if ((packets != null) && (packets.size() > 0)) { StringBuilder sb = new StringBuilder(); for (Element elem : packets) { sb.append(elem.toString()); } hashCodes[rids_head] = sb.toString().hashCode(); } else { hashCodes[rids_head] = -1; } previous_received_rid = rid; currentRids[rids_head++] = rid; if (rids_head >= currentRids.length) { rids_head = 0; } } } public void terminateBoshSession() { terminate = true; } private synchronized void sendBody(BoshIOService serv, Element body_par) { Element body = body_par; if (body == null) { body = getBodyElem(); long rid = takeCurrentRidTail(); if (rid > 0) { body.setAttribute(ACK_ATTR, "" + rid); } if (waiting_packets.size() > 0) { // body.addChild(applyFilters(waiting_packets.poll())); // Make sure the XMLNS is set correctly for all stanzas to avoid // namespace confusion: // http://forum.ag-software.de/thread/969 Element stanza = waiting_packets.poll(); if (stanza.getXMLNS() == null) { stanza.setXMLNS(XMLNS_CLIENT_VAL); } body.addChild(stanza); while ((waiting_packets.size() > 0) && (body.getChildren().size() < MAX_PACKETS)) { // body.addChild(applyFilters(waiting_packets.poll())); stanza = waiting_packets.poll(); if (stanza.getXMLNS() == null) { stanza.setXMLNS(XMLNS_CLIENT_VAL); } body.addChild(stanza); } } } try { if (terminate) { body.setAttribute("type", StanzaType.terminate.toString()); } handler.writeRawData(serv, body.toString()); retireConnectionService(serv); // serv.writeRawData(body.toString()); // waiting_packets.clear(); // serv.stop(); // } catch (IOException e) { // // I call it anyway at the end of method call // //disconnected(null); // log.log(Level.WARNING, "[" + connections.size() + // "] Exception during writing to socket", e); } catch (Exception e) { log.log(Level.WARNING, "[" + connections.size() + "] Exception during writing to socket", e); } if (waitTimer != null) { if (log.isLoggable(Level.FINEST)) { log.finest("Canceling waitTimer: " + getSid()); } handler.cancelTask(waitTimer); } } private void retireConnectionService(BoshIOService serv) { if (!old_connections.contains(serv)) { while (!old_connections.offer(serv)) { BoshIOService old_serv = old_connections.poll(); if (old_serv != null) { old_serv.stop(); } else { if (log.isLoggable(Level.WARNING)) { log.warning("old_connections queue is empty but can not add new element!: " + getSid()); } break; } } } serv.setSid(null); disconnected(serv); } private void retireAllOldConnections() { while (connections.size() > 1) { BoshIOService serv = connections.poll(); if (serv != null) { retireConnectionService(serv); } else { if (log.isLoggable(Level.WARNING)) { log.warning("connections queue size is greater than 1 but poll returns null" + getSid()); } } } } private long takeCurrentRidTail() { synchronized (currentRids) { int idx = rids_tail++; if (rids_tail >= currentRids.length) { rids_tail = 0; } return currentRids[idx]; } } } // ~ Formatted in Sun Code Convention // ~ Formatted by Jindent --- http://www.jindent.com