/*
* 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.server.ReceiverTimeoutHandler;
import tigase.server.xmppclient.ClientConnectionManager;
import tigase.stats.StatisticsList;
import tigase.xmpp.Authorization;
import tigase.xmpp.JID;
import tigase.xmpp.PacketErrorTypeException;
import tigase.xmpp.StanzaType;
import tigase.xmpp.XMPPIOService;
import static tigase.server.bosh.Constants.*;
//~--- JDK imports ------------------------------------------------------------
import java.util.ArrayDeque;
import java.util.Map;
import java.util.Queue;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import tigase.xmpp.*;
//~--- classes ----------------------------------------------------------------
/**
* Describe class BoshConnectionManager here.
*
*
* Created: Sat Jun 2 12:24:29 2007
*
* @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a>
* @version $Rev$
*/
public class BoshConnectionManager extends ClientConnectionManager implements
BoshSessionTaskHandler {
/**
* Variable <code>log</code> is a class logger.
*/
private static final Logger log = Logger
.getLogger("tigase.server.bosh.BoshConnectionManager");
// private static final String ROUTINGS_PROP_KEY = "routings";
// private static final String ROUTING_MODE_PROP_KEY = "multi-mode";
// private static final boolean ROUTING_MODE_PROP_VAL = true;
// private static final String ROUTING_ENTRY_PROP_KEY = ".+";
// private static final String ROUTING_ENTRY_PROP_VAL = DEF_SM_NAME + "@localhost";
private static final int DEF_PORT_NO = 5280;
private int[] PORTS = { DEF_PORT_NO };
// private static final String HOSTNAMES_PROP_KEY = "hostnames";
// private String[] HOSTNAMES_PROP_VAL = {"localhost", "hostname"};
// private RoutingsContainer routings = null;
// private Set<String> hostnames = new TreeSet<String>();
private long max_wait = MAX_WAIT_DEF_PROP_VAL;
private long min_polling = MIN_POLLING_PROP_VAL;
private long max_pause = MAX_PAUSE_PROP_VAL;
private long max_inactivity = MAX_INACTIVITY_PROP_VAL;
private int hold_requests = HOLD_REQUESTS_PROP_VAL;
private int concurrent_requests = CONCURRENT_REQUESTS_PROP_VAL;
private ReceiverTimeoutHandler stoppedHandler = newStoppedHandler();
private ReceiverTimeoutHandler startedHandler = newStartedHandler();
// This should be actually a multi-thread save variable.
// Changing it to
private final Map<UUID, BoshSession> sessions =
new ConcurrentSkipListMap<UUID, BoshSession>();
@Override
public void processPacket(final Packet packet) {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "Processing packet: {0}", packet.toString());
}
super.processPacket(packet);
}
/**
* Method description
*
*
* @param packet
* @param bs
*
* @return
*/
@Override
public boolean addOutStreamClosed(Packet packet, BoshSession bs) {
packet.setPacketFrom(getFromAddress(bs.getSid().toString()));
packet.setPacketTo(bs.getDataReceiver());
packet.initVars(packet.getPacketFrom(), packet.getPacketTo());
bs.close();
if (log.isLoggable(Level.FINEST))
log.finest("closing BOSH session with sid = " + bs.getSid().toString());
sessions.remove(bs.getSid());
return addOutPacketWithTimeout(packet, stoppedHandler, 15l, TimeUnit.SECONDS);
}
/**
*
* @param packet
* @param bs
* @return
*/
@Override
public boolean addOutStreamOpen(Packet packet, BoshSession bs) {
packet.initVars(getFromAddress(bs.getSid().toString()), bs.getDataReceiver());
return addOutPacketWithTimeout(packet, startedHandler, 15l, TimeUnit.SECONDS);
}
/**
* Method description
*
*
* @param tt
*/
@Override
public void cancelTask(TimerTask tt) {
tt.cancel();
}
/**
* Method description
*
*
* @param params
*
* @return
*/
@Override
public Map<String, Object> getDefaults(Map<String, Object> params) {
Map<String, Object> props = super.getDefaults(params);
props.put(MAX_WAIT_DEF_PROP_KEY, MAX_WAIT_DEF_PROP_VAL);
props.put(MIN_POLLING_PROP_KEY, MIN_POLLING_PROP_VAL);
props.put(MAX_INACTIVITY_PROP_KEY, MAX_INACTIVITY_PROP_VAL);
props.put(CONCURRENT_REQUESTS_PROP_KEY, CONCURRENT_REQUESTS_PROP_VAL);
props.put(HOLD_REQUESTS_PROP_KEY, HOLD_REQUESTS_PROP_VAL);
props.put(MAX_PAUSE_PROP_KEY, MAX_PAUSE_PROP_VAL);
return props;
}
/**
* Method description
*
*
* @return
*/
@Override
public String getDiscoCategoryType() {
return "c2s";
}
/**
* Method description
*
*
* @return
*/
@Override
public String getDiscoDescription() {
return "Bosh connection manager";
}
// ~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param srv
*
* @return
*/
@Override
public Queue<Packet> processSocketData(XMPPIOService<Object> srv) {
BoshIOService serv = (BoshIOService) srv;
Packet p = null;
while ((p = serv.getReceivedPackets().poll()) != null) {
Queue<Packet> out_results = new ArrayDeque<Packet>(2);
BoshSession bs = null;
String sid_str = null;
synchronized (sessions) {
if (log.isLoggable(Level.FINER)) {
log.log(Level.FINER, "Processing packet: {0}, type: {1}",
new Object[] { p.getElemName(), p.getType() });
}
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "Processing socket data: {0}", p);
}
sid_str = p.getAttribute(SID_ATTR);
UUID sid = null;
if (sid_str == null) {
String hostname = p.getAttribute("to");
if ((hostname != null) && isLocalDomain(hostname)) {
bs =
new BoshSession(getDefVHostItem().getDomain(), JID.jidInstanceNS(routings
.computeRouting(hostname)), this);
sid = bs.getSid();
sessions.put(sid, bs);
} else {
log.info("Invalid hostname. Closing invalid connection");
try {
serv.sendErrorAndStop(Authorization.NOT_ALLOWED, p, "Invalid hostname.");
} catch (Exception e) {
log.log(Level.WARNING, "Problem sending invalid hostname error for sid = "
+ sid, e);
}
}
} else {
sid = UUID.fromString(sid_str);
bs = sessions.get(sid);
}
}
try {
if (bs != null) {
synchronized (bs) {
if (sid_str == null) {
bs.init(p, serv, max_wait, min_polling, max_inactivity,
concurrent_requests, hold_requests, max_pause, out_results);
} else {
bs.processSocketPacket(p, serv, out_results);
}
}
} else {
log.info("There is no session with given SID. Closing invalid connection");
serv.sendErrorAndStop(Authorization.ITEM_NOT_FOUND, p, "Invalid SID");
}
addOutPackets(out_results, bs);
} catch (Exception e) {
log.log(Level.WARNING, "Problem processing socket data for sid = " + sid_str, e);
}
// addOutPackets(out_results);
} // end of while ()
return null;
}
/**
* Method description
*
*
* @param bs
* @param delay
*
* @return
*/
@Override
public TimerTask scheduleTask(BoshSession bs, long delay) {
BoshTask bt = new BoshTask(bs);
addTimerTask(bt, delay);
// boshTasks.schedule(bt, delay);
return bt;
}
/**
* Method description
*
*
* @param service
*/
public void serviceStarted(BoshIOService service) {
super.serviceStarted(service);
}
/**
* Method description
*
*
* @param service
*/
public void serviceStopped(BoshIOService service) {
super.serviceStopped(service);
UUID sid = service.getSid();
if (sid != null) {
BoshSession bs = sessions.get(sid);
if (bs != null) {
bs.disconnected(service);
}
}
}
// ~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param props
*/
@Override
public void setProperties(Map<String, Object> props) {
super.setProperties(props);
if (props.get(MAX_WAIT_DEF_PROP_KEY) != null) {
max_wait = (Long) props.get(MAX_WAIT_DEF_PROP_KEY);
log.info("Setting max_wait to: " + max_wait);
}
if (props.get(MIN_POLLING_PROP_KEY) != null) {
min_polling = (Long) props.get(MIN_POLLING_PROP_KEY);
log.info("Setting min_polling to: " + min_polling);
}
if (props.get(MAX_INACTIVITY_PROP_KEY) != null) {
max_inactivity = (Long) props.get(MAX_INACTIVITY_PROP_KEY);
log.info("Setting max_inactivity to: " + max_inactivity);
}
if (props.get(CONCURRENT_REQUESTS_PROP_KEY) != null) {
concurrent_requests = (Integer) props.get(CONCURRENT_REQUESTS_PROP_KEY);
log.info("Setting concurrent_requests to: " + concurrent_requests);
}
if (props.get(HOLD_REQUESTS_PROP_KEY) != null) {
hold_requests = (Integer) props.get(HOLD_REQUESTS_PROP_KEY);
log.info("Setting hold_requests to: " + hold_requests);
}
if (props.get(MAX_PAUSE_PROP_KEY) != null) {
max_pause = (Long) props.get(MAX_PAUSE_PROP_KEY);
log.info("Setting max_pause to: " + max_pause);
}
}
// ~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param ios
* @param data
*/
@Override
public void writeRawData(BoshIOService ios, String data) {
super.writeRawData(ios, data);
}
/**
* Method description
*
*
* @param serv
*/
public void xmppStreamClosed(BoshIOService serv) {
if (log.isLoggable(Level.FINER)) {
log.finer("Stream closed.");
}
}
/**
* Method description
*
*
* @param serv
* @param attribs
*
* @return
*/
public String xmppStreamOpened(BoshIOService serv, Map<String, String> attribs) {
if (log.isLoggable(Level.FINE)) {
log.fine("Ups, what just happened? Stream open. Hey, this is a Bosh connection manager."
+ " c2s and s2s are not supported on the same port as Bosh yet.");
}
return "<?xml version='1.0'?><stream:stream"
+ " xmlns='jabber:client'"
+ " xmlns:stream='http://etherx.jabber.org/streams'"
+ " id='1'"
+ " from='"
+ getDefVHostItem()
+ "'"
+ " version='1.0' xml:lang='en'>"
+ "<stream:error>"
+ "<invalid-namespace xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>"
+ "<text xmlns='urn:ietf:params:xml:ns:xmpp-streams' xml:lang='langcode'>"
+ "Ups, what just happened? Stream open. Hey, this is a Bosh connection manager. "
+ "c2s and s2s are not supported on the same port... yet." + "</text>"
+ "</stream:error>" + "</stream:stream>";
}
@Override
public BareJID getSeeOtherHostForJID(BareJID fromJID) {
if (see_other_host_strategy == null) {
if (log.isLoggable(Level.FINEST)) {
log.finest("no see-other-host implementation set");
}
return null;
}
BareJID see_other_host =
see_other_host_strategy.findHostForJID(fromJID, getDefHostName());
if (log.isLoggable(Level.FINEST)) {
log.finest("using = " + see_other_host_strategy.getClass().getCanonicalName()
+ "for jid = " + fromJID.toString() + " got = "
+ (see_other_host != null ? see_other_host.toString() : "null"));
}
return (see_other_host != null && !see_other_host.equals(getDefHostName())) ? see_other_host
: null;
}
@Override
protected JID changeDataReceiver(Packet packet, JID newAddress,
String command_sessionId, XMPPIOService<Object> serv) {
BoshSession session = getBoshSession(packet.getTo());
if (session != null) {
String sessionId = session.getSessionId();
if (sessionId.equals(command_sessionId)) {
JID old_receiver = session.getDataReceiver();
session.setDataReceiver(newAddress);
return old_receiver;
} else {
log.info("Incorrect session ID, ignoring data redirect for: " + newAddress);
}
}
return null;
}
// ~--- get methods ----------------------------------------------------------
// public void processPacket(Packet packet) {
// log.finer("Processing packet: " + packet.getElemName()
// + ", type: " + packet.getType());
// log.finest("Processing packet: " + packet.toString());
// if (packet.isCommand() && packet.getCommand() != Command.OTHER) {
// processCommand(packet);
// } else {
// writePacketToSocket(packet);
// }
// }
protected BoshSession getBoshSession(JID jid) {
UUID sid = UUID.fromString(jid.getResource());
return sessions.get(sid);
}
@Override
protected int[] getDefPlainPorts() {
return PORTS;
}
@Override
protected int[] getDefSSLPorts() {
return null;
}
/**
* Method <code>getMaxInactiveTime</code> returns max keep-alive time for
* inactive connection. For Bosh it does not make sense to keep the idle
* connection longer than 10 minutes.
*
* @return a <code>long</code> value
*/
@Override
protected long getMaxInactiveTime() {
return 10 * MINUTE;
}
@Override
protected BoshIOService getXMPPIOServiceInstance() {
return new BoshIOService();
}
@Override
public void getStatistics(StatisticsList list) {
super.getStatistics(list);
if (list.checkLevel(Level.FINEST)) {
// Be careful here, the size() for this map is expensive to count
list.add(getName(), "Bosh sessions", sessions.size(), Level.FINEST);
}
}
@Override
protected ReceiverTimeoutHandler newStartedHandler() {
return new StartedHandler();
}
@Override
protected void processCommand(Packet packet) {
BoshSession session = getBoshSession(packet.getTo());
switch (packet.getCommand()) {
case CLOSE:
if (session != null) {
// log.log(Level.FINE, "Closing session for command CLOSE: {0}",
// session.getSid());
// session.close();
// sessions.remove(session.getSid());
log.log(Level.FINE, "Terminating session for command CLOSE: {0}", session.getSid());
session.terminateBoshSession();
} else {
log.log(Level.INFO, "Session does not exist for packet: {0}", packet);
}
break;
case CHECK_USER_CONNECTION:
if (session != null) {
// It's ok, the session has been found, respond with OK.
addOutPacket(packet.okResult((String) null, 0));
} else {
// Session is no longer active, respond with an error.
try {
addOutPacket(Authorization.ITEM_NOT_FOUND.getResponseMessage(packet,
"Connection gone.", false));
} catch (PacketErrorTypeException e) {
// Hm, error already, ignoring...
log.log(Level.INFO, "Error packet is not really expected here: {0}", packet);
}
}
break;
default:
super.processCommand(packet);
break;
} // end of switch (pc.getCommand())
}
@Override
protected boolean writePacketToSocket(Packet packet) {
BoshSession session = getBoshSession(packet.getTo());
if (session != null) {
synchronized (session) {
Queue<Packet> out_results = new ArrayDeque<Packet>();
session.processPacket(packet, out_results);
addOutPackets(out_results, session);
}
return true;
} else {
log.info("Session does not exist for packet: " + packet.toString());
return false;
}
}
private void addOutPackets(Queue<Packet> out_results, BoshSession bs) {
for (Packet res : out_results) {
res.setPacketFrom(getFromAddress(bs.getSid().toString()));
res.setPacketTo(bs.getDataReceiver());
if (res.getCommand() != null) {
switch (res.getCommand()) {
case STREAM_CLOSED:
case GETFEATURES:
res.initVars(res.getPacketFrom(), res.getPacketTo());
break;
default:
// Do nothing...
}
}
addOutPacket(res);
}
out_results.clear();
}
// ~--- get methods ----------------------------------------------------------
private JID getFromAddress(String id) {
return JID.jidInstanceNS(getName(), getDefHostName().getDomain(), id);
}
// ~--- inner classes --------------------------------------------------------
private class BoshTask extends TimerTask {
private BoshSession bs = null;
// ~--- constructors -------------------------------------------------------
/**
* Constructs ...
*
*
* @param bs
*/
public BoshTask(BoshSession bs) {
this.bs = bs;
}
// ~--- methods ------------------------------------------------------------
/**
* Method description
*
*/
@Override
public void run() {
Queue<Packet> out_results = new ArrayDeque<Packet>();
if (bs.task(out_results, this)) {
log.fine("Closing session for BS task: " + bs.getSid());
sessions.remove(bs.getSid());
}
addOutPackets(out_results, bs);
}
}
private class StartedHandler implements ReceiverTimeoutHandler {
/**
* Method description
*
*
* @param packet
* @param response
*/
@Override
public void responseReceived(Packet packet, Packet response) {
// We are now ready to ask for features....
addOutPacket(Command.GETFEATURES.getPacket(packet.getFrom(), packet.getTo(),
StanzaType.get, UUID.randomUUID().toString(), null));
}
/**
* Method description
*
*
* @param packet
*/
@Override
public void timeOutExpired(Packet packet) {
// If we still haven't received confirmation from the SM then
// the packet either has been lost or the server is overloaded
// In either case we disconnect the connection.
log.warning("No response within time limit received for a packet: "
+ packet.toString());
BoshSession session = getBoshSession(packet.getFrom());
if (session != null) {
log.fine("Closing session for timeout: " + session.getSid());
session.close();
sessions.remove(session.getSid());
} else {
log.info("Session does not exist for packet: " + packet.toString());
}
}
}
}
// ~ Formatted in Sun Code Convention
// ~ Formatted by Jindent --- http://www.jindent.com