package com.rayo.gateway; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Scanner; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import org.apache.commons.collections.CollectionUtils; import org.apache.xerces.dom.CoreDocumentImpl; import org.dom4j.dom.DOMElement; import org.springframework.core.io.Resource; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.rayo.gateway.admin.GatewayAdminService; import com.rayo.gateway.jmx.GatewayStatistics; import com.rayo.server.exception.ErrorMapping; import com.rayo.server.servlet.AbstractRayoServlet; import com.rayo.server.storage.GatewayException; import com.rayo.server.storage.GatewayStorageService; import com.rayo.server.storage.model.Application; import com.rayo.server.storage.model.GatewayMixer; import com.rayo.server.storage.model.GatewayVerb; import com.rayo.server.storage.model.RayoNode; import com.rayo.server.util.JIDUtils; import com.rayo.storage.lb.GatewayLoadBalancingStrategy; import com.voxeo.exceptions.NotFoundException; import com.voxeo.logging.Loggerf; import com.voxeo.moho.util.ParticipantIDParser; import com.voxeo.servlet.xmpp.IQRequest; import com.voxeo.servlet.xmpp.IQResponse; import com.voxeo.servlet.xmpp.JID; import com.voxeo.servlet.xmpp.PresenceMessage; import com.voxeo.servlet.xmpp.StanzaError.Condition; import com.voxeo.servlet.xmpp.StanzaError.Type; import com.voxeo.servlet.xmpp.XmppServletRequest; import com.voxeo.servlet.xmpp.XmppServletResponse; /** * <p>A Gateway Servlet is a particular type of Rayo Servlet that receives * messages from client applications and redirects those messages to Rayo * Nodes, and that receives Calls from Rayo Nodes and redirect those calls * to the corresponding client applications.</p> * * <p>A Gateway Servlet acts like a mere proxy as it does not modify the * content of the XMPP messages. The Gateway will modify and adjust the * different 'from' and 'to' attributes do distribute the calls across all * the different client applications and Rayo Nodes.</p> * * <p>To support its functionality and have a good performance the Rayo * Gateway uses a DHT (Distributed Hash Table) instance in which several * mappings are stored like which calls are being hosted in which nodes, or * which client resources can handle which calls.</p> * * @author martin * */ public class GatewayServlet extends AbstractRayoServlet { private static final String PLATFORM_DIALED = "com.rayo.gateway.platform"; private static final String NODES_DIALED = "com.rayo.gateway.nodesdialed"; private static final String DIAL_RETRIES = "com.rayo.gateway.dialretries"; private static final String ORIGINAL_REQUEST = "com.rayo.gateway.originaRequest"; private static final long serialVersionUID = 1L; private static final Loggerf log = Loggerf .getLogger(GatewayServlet.class); private GatewayStorageService gatewayStorageService; private GatewayLoadBalancingStrategy loadBalancer; private List<String> internalDomains; private List<String> externalDomains; private GatewayStatistics gatewayStatistics; @Override public void init(ServletConfig config) throws ServletException { super.init(config); log.debug("Gateway Servlet initialized"); log.debug("internal domains: %s", internalDomains); log.debug("external domains: %s", externalDomains); } @Override protected void doPresence(PresenceMessage message) throws ServletException, IOException { gatewayStatistics.messageProcessed(); if (getWireLogger().isDebugEnabled()) { getWireLogger().debug("%s :: %s", message, message.getSession().getId()); } try { if (isMyExternalDomain(message.getTo())) { String to = message.getTo().getNode(); GatewayMixer mixer = null; if (to != null) { mixer = gatewayStorageService.getMixer(to); } if (mixer != null) { processClientPresenceToMixer(message, mixer); } else { processClientPresence(message); } } else if (isMyInternalDomain(message.getTo())) { processServerPresence(message); } else { sendPresenceError(message.getTo(), message.getFrom(), Condition.BAD_REQUEST, Type.CANCEL, "Could not map request."); } } catch (Exception e) { log.error(e.getMessage(),e); ErrorMapping error = getExceptionMapper().toXmppError(e); sendPresenceError(message.getTo(), message.getFrom(), error.getCondition(), error.getType(), error.getText()); } } /* * Processes a Presence message from a Rayo Node */ private void processServerPresence(PresenceMessage message) throws Exception { if (message.getFrom().getNode() == null) { processRayoNodePresence(message); } else { // find if presence is from a mixer or from a call GatewayMixer mixer = gatewayStorageService.getMixer(message.getFrom().getNode()); if (mixer != null) { processMixerPresence(message, mixer); } else { processCallPresence(message); } } } /* * Process an "administrative" presence message from a Rayo Node. This * will tipically be sent when a Rayo Node wants to register/unregister * from the Rayo Gateway */ private void processRayoNodePresence(PresenceMessage message) throws Exception { if (null == message.getType() || message.getType().isEmpty()) { switch(message.getShow()) { case CHAT: registerRayoNode(message); break; case AWAY: case DND: case XA: gatewayStorageService.unregisterRayoNode( JIDUtils.getDomain(message.getFrom().toString())); break; } } else if (message.getType().equals("unavailable")) { broadcastEndEvent(message); } else { sendPresenceError(message.getTo(), message.getFrom(), Condition.BAD_REQUEST, Type.CANCEL, "Could not map request."); } } /* * Broadcasts an End event to all calls from a Rayo Node. Basically this will happen * when a Rayo Node status goes to "unavailable" meaning that the Rayo Node has been * shut down or moved to Quiesce mode. */ private void broadcastEndEvent(PresenceMessage message) { Collection<String> calls = gatewayStorageService.getCallsForNode(message.getFrom().toString()); for (String callId : calls) { JID fromJid = createInternalJid(callId, message); String target = gatewayStorageService.getclientJID(callId); JID targetJid = getXmppFactory().createJID(target); CoreDocumentImpl document = new CoreDocumentImpl(false); org.w3c.dom.Element endElement = document.createElementNS("urn:xmpp:rayo:1", "end"); org.w3c.dom.Element errorElement = document.createElement("error"); endElement.appendChild(errorElement); try { PresenceMessage presence = getXmppFactory().createPresence( fromJid, targetJid, null, endElement); presence.send(); gatewayStatistics.errorProcessed(); } catch (Exception e) { log.error("Could not send End event to Jid [%s]", targetJid); log.error(e.getMessage(),e); } } } /* * Links a Rayo node to this Gateway. This normally happens when a * Rayo Node comes online and broadcasts its availability to this gateway */ private void registerRayoNode(PresenceMessage message) throws Exception { Element nodeInfoElement = message.getElement( "node-info", "urn:xmpp:rayo:cluster:1"); List<String> platforms = new ArrayList<String>(); int weight = RayoNode.DEFAULT_WEIGHT; int priority = RayoNode.DEFAULT_PRIORITY; if (nodeInfoElement != null) { NodeList platformElements = nodeInfoElement .getElementsByTagName("platform"); for (int i=0;i<platformElements.getLength();i++) { platforms.add(platformElements.item(i).getTextContent()); } NodeList weightList = nodeInfoElement.getElementsByTagName("weight"); if (weightList.getLength() > 0) { try { weight = Integer.parseInt(weightList.item(0).getTextContent()); } catch (Exception e) { log.error("Unable to parse weight on message [%s]", message); } } NodeList priorityList = nodeInfoElement.getElementsByTagName("priority"); if (priorityList.getLength() > 0) { try { priority = Integer.parseInt(priorityList.item(0).getTextContent()); } catch (Exception e) { log.error("Unable to parse priority on message [%s]", message); } } } RayoNode node = new RayoNode(message.getFrom().toString(), null, new HashSet<String>(platforms)); node.setPriority(priority); node.setWeight(weight); // if a rayo node sends a chat presence, then lets give it a chance if blacklisted node.setBlackListed(false); node.setConsecutiveErrors(0); gatewayStorageService.registerRayoNode(node); } /* * Process an incoming Rayo Node event which is originated from a call id * * @param message Presence Message */ private void processCallPresence(PresenceMessage message) throws Exception { JID fromJid = message.getFrom(); String callId = fromJid.getNode(); if (isOffer(message)) { if (!processOffer(message, callId)) { return; } } else if (isUnjoinedMixer(message)) { processUnjoinedMixer(message); } else if (isJoinedMixer(message)) { processJoinedMixer(message); } JID from = createExternalJid(callId, fromJid.getResource()); String jid = gatewayStorageService.getclientJID(callId); if (jid == null) { log.error("Could not find registered JID for call id [%s]", callId); sendPresenceError(message.getTo(), message.getFrom(), Condition.RECIPIENT_UNAVAILABLE, Type.CANCEL, "Could not find registered JID for id " + callId); gatewayStatistics.errorProcessed(); return; } JID to = getXmppFactory().createJID(jid); if (message.getElement("end", "urn:xmpp:rayo:1") != null) { gatewayStorageService.unregistercall(callId); } forwardPresence(message, from, to, callId); } private void forwardPresence(PresenceMessage message, JID fromJid, JID to, String callId) throws IOException, ServletException { sendPresence(message, fromJid, to, callId); } private void processMixerPresence(PresenceMessage message, GatewayMixer mixer) throws Exception { JID fromJid = message.getFrom(); JID from = createExternalJid(mixer.getName(), fromJid.getResource()); List<String> filteredApplications = gatewayStorageService.getFilteredApplications(mixer.getName()); String resource = message.getFrom().getResource(); if (resource != null) { GatewayVerb verb = gatewayStorageService.getVerb(mixer.getName(), resource); if (verb != null) { if (message.getElement("complete","urn:xmpp:rayo:ext:1") != null) { gatewayStorageService.removeVerbFromMixer(resource, fromJid.getNode()); } if (!filteredApplications.contains(JIDUtils.getBareJid(verb.getAppJid()))) { JID to = getXmppFactory().createJID(verb.getAppJid()); sendPresence(message, from, to, mixer.getName()); } } else { log.error("Received presence [%s] but could not find the application JID for it.", message); return; } } else { // Generic Mixer event (e.g. active speaker). Send it to all apps in the mixer. List<String> appIds = new ArrayList<String>(); List<String> participants = mixer.getParticipants(); // Race condition here. For joined events, we might receive the joined from the mixer when // we haven't received the "joined" from the call yet. Which means the call id is not a participant // yet. So, we need to cope with that and add the id if necessary Element joined = message.getElement("joined", "urn:xmpp:rayo:1"); if (joined != null) { String callId = joined.getAttribute("call-id"); if (!participants.contains(callId)) { participants.add(callId); } } // Same for unjoined Element unjoined = message.getElement("unjoined", "urn:xmpp:rayo:1"); if (unjoined != null) { String callId = unjoined.getAttribute("call-id"); if (!participants.contains(callId)) { participants.add(callId); } } for(String participant: participants) { String jid = gatewayStorageService.getclientJID(participant); if (jid != null && !filteredApplications.contains(JIDUtils.getBareJid(jid))) { if (!appIds.contains(jid)) { appIds.add(jid); JID to = getXmppFactory().createJID(jid); forwardPresence(message, from, to, participant); } } } appIds.clear(); } } private void sendPresence(PresenceMessage message, JID from, JID to, String id) { try { log.debug("Sending presence [%s] from [%s] to [%s]", message, from, to); // Send presence PresenceMessage presence = getXmppFactory().createPresence(from, to, null, message.getElement()); if (presence == null) { log.error("Could not find registered client session for id [%s]", id); sendPresenceError(message.getTo(), message.getFrom(), Condition.RECIPIENT_UNAVAILABLE, Type.CANCEL, "Could not find registered client session for call"); return; } presence.send(); } catch (ServletException se) { if (se.getMessage().startsWith("can't find corresponding client session")) { //TODO: Unregister call. As with Rayo Servlet } } catch (Exception e) { // In the event of an error, continue dispatching to all remaining JIDs log.error("Failed to dispatch event [jid=%s, event=%s]", to.getBareJID(), message, e); } } private boolean isJoinedMixer(PresenceMessage message) { Element joined = message.getElement("joined", "urn:xmpp:rayo:1"); if (joined != null) { return joined.hasAttribute("mixer-name"); } return false; } private boolean isUnjoinedMixer(PresenceMessage message) { Element unjoined = message.getElement("unjoined", "urn:xmpp:rayo:1"); if (unjoined != null) { return unjoined.hasAttribute("mixer-name"); } return false; } private boolean isOffer(PresenceMessage message) { return message.getElement("offer", "urn:xmpp:rayo:1") != null; } private boolean isJoinMixer(IQRequest request) { Element join = request.getElement("join", "urn:xmpp:rayo:1"); if (join != null) { return join.hasAttribute("mixer-name"); } return false; } private void processJoinedMixer(PresenceMessage message) throws Exception { String callId = message.getFrom().getNode(); String mixerName = message.getElement("joined", "urn:xmpp:rayo:1").getAttribute("mixer-name"); gatewayStorageService.addCallToMixer(callId, mixerName); } private void processUnjoinedMixer(PresenceMessage message) throws Exception { String callId = message.getFrom().getNode(); String mixerName = message.getElement("unjoined", "urn:xmpp:rayo:1").getAttribute("mixer-name"); gatewayStorageService.removeCallFromMixer(callId, mixerName); GatewayMixer mixer = gatewayStorageService.getMixer(mixerName); if (mixer.getParticipants().size() == 0) { gatewayStorageService.unregisterMixer(mixerName); gatewayStorageService.removeFilters(mixerName); // Send message to the rayo node JID fromJidInternal = getXmppFactory().createJID(getInternalDomain()); JID toJid = getXmppFactory().createJID(mixerName + "@" + mixer.getNodeJid()); CoreDocumentImpl document = new CoreDocumentImpl(false); org.w3c.dom.Element destroyElement = document.createElementNS("urn:xmpp:rayo:1", "destroy-if-empty"); IQRequest destroyMixerRequest = getXmppFactory().createIQ( fromJidInternal, toJid, XmppServletRequest.TYPE_SET, destroyElement); log.debug("Sending destroy mixer command to mixer %s in node %s", mixerName, mixer.getNodeJid()); destroyMixerRequest.send(); if (getWireLogger().isDebugEnabled()) { getWireLogger().debug("%s :: %s", destroyMixerRequest, destroyMixerRequest.getSession().getId()); } } } private boolean processOffer(PresenceMessage message, String callId) throws Exception { Element offerElement = message.getElement("offer", "urn:xmpp:rayo:1"); JID toJid = message.getTo(); JID fromJid = message.getFrom(); if (getAdminService().isQuiesceMode()) { log.warn("Gateway is on Quiesce mode. Discarding incoming job offer for call id: [%s]", callId); sendPresenceError(message.getTo(), message.getFrom(), Condition.SERVICE_UNAVAILABLE, Type.CANCEL, "Gateway is on Quiesce mode."); return false; } Application application = gatewayStorageService.getApplicationForAddress(offerElement.getAttribute("to")); if (application == null) { String errorMessage = String.format("Could not find application for URI [%s]", offerElement.getAttribute("to")); log.error(errorMessage); sendPresenceError(message.getTo(), message.getFrom(), Condition.RECIPIENT_UNAVAILABLE, Type.CANCEL, errorMessage); return false; } JID callTo = getXmppFactory().createJID(application.getJid()); String resource = loadBalancer.pickClientResource(callTo.getBareJID().toString()); // picks and load balances if (resource == null) { String errorMessage = String.format("Could not find an available resource for JID [%s]", callTo.getBareJID()); log.error(errorMessage); sendPresenceError(toJid, fromJid, Condition.RECIPIENT_UNAVAILABLE, Type.CANCEL, errorMessage); return false; } callTo.setResource(resource); // Register call in DHT gatewayStorageService.registerCall(callId, callTo.toString()); gatewayStatistics.callRegistered(); return true; } private JID createInternalJid(String id, XmppServletRequest request) { String nodeAddress = null; GatewayMixer mixer = gatewayStorageService.getMixer(id); if (mixer != null) { nodeAddress = mixer.getNodeJid(); } else { try { nodeAddress = ParticipantIDParser.getIpAddress(id); } catch (Exception e) { throw new NotFoundException(String.format("Could not find rayo node for id [%s]", id)); } } return getXmppFactory().createJID(id + "@" + nodeAddress); } private JID createExternalJid(String id, String resource) { JID jid = getXmppFactory().createJID(id + "@" + getExternalDomain()); if (resource != null) { jid.setResource(resource); } return jid; } /* * Processes a Presence Message from a Rayo Client */ private void processClientPresence(PresenceMessage message) throws Exception { if (log.isDebugEnabled()) { log.debug("Received client presence message [%s]", message); } JID fromJid = message.getFrom(); if (message.getType() == null || message.getType().isEmpty()) { // client comes online if (message.getShow() == null) { log.warn("Received empty show presence from Client [%s]. Ignoring it.", fromJid); return; } if (validApplicationJid(message.getFrom())) { switch (message.getShow()) { case CHAT: gatewayStorageService.registerClient(message.getFrom()); gatewayStatistics.clientRegistered(message.getFrom().getBareJID()); break; case AWAY: case DND: case XA: gatewayStorageService.unregisterClient(message.getFrom()); break; } } else { log.warn("Application [%s] is not registered as a valid Rayo application", message.getFrom()); sendPresenceError(message.getTo(), message.getFrom(), Condition.RECIPIENT_UNAVAILABLE, Type.CANCEL, "The application does not exist"); } } else if (message.getType().equals("unavailable")) { gatewayStorageService.unregisterClient(message.getFrom()); // Note that the following method does include the resource as we only want to // stop calls for the resource that goes offline Collection<String> callIds = gatewayStorageService.getCallsForClient(fromJid.toString()); for (String callId: callIds) { try { //Clean call in data store gatewayStorageService.unregistercall(callId); String nodeIp = ParticipantIDParser.getIpAddress(callId); JID toJidInternal = getXmppFactory().createJID(callId + "@" + nodeIp); JID fromJidInternal = getXmppFactory().createJID(getInternalDomain()); sendPresenceError(fromJidInternal, toJidInternal); } catch (Exception e) { log.error("Could not hang up call with id [%s]", callId); log.error(e.getMessage(),e); } } } else if (message.getType().equals("subscribed")) { //TODO: } else if (message.getType().equals("subscribe")) { //TODO: } } /* * Processes a Presence Message from a Rayo Client directed to a mixer. Client * applications will use directed presence to mixers for subscribing and unsubscribing * to mixer participant events. */ private void processClientPresenceToMixer(PresenceMessage message, GatewayMixer mixer) throws Exception { if (log.isDebugEnabled()) { log.debug("Received client presence message to mixer: [%s]", message); } if (message.getType().equals("unavailable")) { gatewayStorageService.createFilter(message.getFrom().getBareJID().toString(), mixer.getName()); } else { gatewayStorageService.removeFilter(message.getFrom().getBareJID().toString(), mixer.getName()); } } private boolean validApplicationJid(JID fromJid) { if (((GatewayAdminService)getAdminService()).isBanned(fromJid.getBareJID().toString())) { return false; } /* GatewayClient client = gatewayStorageService.getClient(fromJid); if (client == null) { return false; } Application application = gatewayStorageService.getApplication(client.getAppId()); //TODO: Check permissions */ return true; } @Override protected void processIQRequest(IQRequest request, DOMElement payload) { gatewayStatistics.messageProcessed(); try { if (isMyExternalDomain(request.getTo())) { if (isDial(request)) { processDialRequest(request); } else if (isJoinMixer(request)) { if (!createMixer(request)) { return; } processClientIQRequest(request); } else { processClientIQRequest(request); } } else if (isMyInternalDomain(request.getTo())) { sendIqError(request, Type.CANCEL, Condition.BAD_REQUEST, "Rayo Nodes should not be sending IQ requests to the gateway"); } else { sendIqError(request, Type.CANCEL, Condition.RECIPIENT_UNAVAILABLE, "Unknown domain"); } } catch (Exception e) { log.error(e.getMessage(),e); sendIqError(request, e); } } private boolean createMixer(IQRequest request) throws Exception { String mixerName = request.getElement("join").getAttribute("mixer-name"); GatewayMixer mixer = gatewayStorageService.getMixer(mixerName); if (mixer == null) { // In the current implementation, mixer lives where the first call lives String nodename; try { nodename = ParticipantIDParser.getIpAddress(request.getTo().getNode()); } catch (Exception e) { throw new NotFoundException(String.format("Could not find rayo node for id [%s]", request.getTo())); } gatewayStorageService.registerMixer(mixerName, nodename); gatewayStatistics.mixerRegistered(); } else { log.debug("Mixer [%s] already exists", mixerName); } return true; } /* * It process a Client IQ request that it is not a dial */ private void processClientIQRequest(IQRequest request) throws Exception { String id = request.getTo().getNode(); Element payload = request.getElement(); JID fromJidInternal = getXmppFactory().createJID(getInternalDomain()); JID toJidInternal = createInternalJid(id, request); if (request.getTo().getResource() != null) { toJidInternal.setResource(request.getTo().getResource()); } forwardIQRequest(fromJidInternal, toJidInternal, request, payload); } /* * Processes a dial request from a Rayo Client */ private void processDialRequest(IQRequest request) throws Exception { if (getAdminService().isQuiesceMode()) { log.warn("Gateway is on Quiesce mode. Discarding incoming dial request: [%s]", request); sendIqError(request, Type.CANCEL, Condition.SERVICE_UNAVAILABLE, "Gateway Server is on Quiesce Mode"); return; } String platformId = gatewayStorageService.getPlatformForClient(request.getFrom()); if (platformId != null) { sendDialRequest(request, platformId, new ArrayList<RayoNode>(), 0); } else { sendIqError(request, Type.CANCEL, Condition.SERVICE_UNAVAILABLE, String.format("Could not find associated platform for client JID %s",request.getFrom())); } } @SuppressWarnings("unchecked") private void sendDialRequest(IQRequest request, String platformId, List<RayoNode> nodesDialed, int dialRetries) throws Exception { //TODO: Build full jid as in the doc. Currently blocked on Prism issue. //fromJidInternal = getXmppFactory().createJID( // toJidExternal.getDomain()+"/"+fromJidExternal.getBareJID()); JID fromJidInternal = getXmppFactory().createJID(getInternalDomain()); Element payload = request.getElement(); int maxDialRetries = ((GatewayAdminService)getAdminService()).getMaxDialRetries(); RayoNode firstChoice = null; if (nodesDialed.size() > 0) { firstChoice = nodesDialed.get(0); } do { RayoNode rayoNode = loadBalancer.pickRayoNode(platformId); if (rayoNode == null) { sendIqError(request, Type.CANCEL, Condition.SERVICE_UNAVAILABLE, String.format("Could not find an available Rayo Node in platform %s", platformId)); return; } if (rayoNode.equals(firstChoice)) { List<RayoNode> nodes = gatewayStorageService.getRayoNodes(platformId); if (nodesDialed.size() < nodes.size()) { Collection<RayoNode> missing = CollectionUtils.subtract(nodes, nodesDialed); rayoNode = missing.iterator().next(); } else { // done. No more nodes to dial. log.error("We could not dispatch the dial request [%s] to any of the available Rayo Nodes.", request); sendIqError(request, Type.CANCEL, Condition.SERVICE_UNAVAILABLE, String.format("Could not find an available Rayo Node in platform %s", platformId)); break; } } JID to = getXmppFactory().createJID(rayoNode.getHostname()); try { nodesDialed.add(rayoNode); dialRetries++; log.debug("Dialing node [%s]. Dial attempts: [%s]. Maximum attempts: [%s].", rayoNode, dialRetries, maxDialRetries); forwardIQRequest(fromJidInternal, to, request, payload, platformId, dialRetries, nodesDialed); log.debug("Dial request [%s] dispatched successfully", request); return; } catch (Exception e) { log.error("Error while sending dial request: " + e.getMessage(), e); log.debug("Resending dial request to a different node"); loadBalancer.nodeOperationFailed(rayoNode); } } while (dialRetries < maxDialRetries); log.error("The maximum number of [%s] dial retries was reached for dial request [%s]. This dial request is going to be discarded"); sendIqError(request, Type.CANCEL, Condition.SERVICE_UNAVAILABLE, String.format("Could not find an available Rayo Node in platform %s", platformId)); } private void forwardIQRequest(JID fromJidInternal, JID toJidInternal, IQRequest originalRequest, Element payload) throws Exception { forwardIQRequest(fromJidInternal, toJidInternal, originalRequest, payload, null, null, null); } private void forwardIQRequest(JID fromJidInternal, JID toJidInternal, IQRequest originalRequest, Element payload, String platformId, Integer dialRetries, List<RayoNode> nodesDialed) throws Exception { IQRequest nattedRequest = getXmppFactory().createIQ( fromJidInternal, toJidInternal, originalRequest.getType(),payload); nattedRequest.setAttribute(ORIGINAL_REQUEST, originalRequest); if (dialRetries != null) { nattedRequest.setAttribute(DIAL_RETRIES, dialRetries); } if (nodesDialed != null) { nattedRequest.setAttribute(NODES_DIALED, nodesDialed); } if (platformId != null) { nattedRequest.setAttribute(PLATFORM_DIALED, platformId); } nattedRequest.setID(originalRequest.getId()); nattedRequest.send(); if (getWireLogger().isDebugEnabled()) { getWireLogger().debug("%s :: %s", nattedRequest, nattedRequest.getSession().getId()); } } private boolean isDial(IQRequest request) { if ((request.getTo().getNode() == null) && ("set".equals(request.getType()))) { Element dialElement = request .getElement("dial", "urn:xmpp:rayo:1"); if (dialElement != null) { if (request.getElement("error") == null) { return true; } } } return false; } @SuppressWarnings("unchecked") @Override protected void doResponse(XmppServletResponse response) throws ServletException, IOException { gatewayStatistics.messageProcessed(); // Each Rayo Node will send an IQ Response to the Rayo Gateway for each IQ Request sent super.doResponse(response); //TODO: Depends on this bug https://evolution.voxeo.com/ticket/1553126 so needs a build post 0928 XmppServletRequest nattedRequest = response.getRequest(); IQRequest originalRequest = (IQRequest)nattedRequest.getAttribute(ORIGINAL_REQUEST); if (isDial(originalRequest)) { List<RayoNode> nodesDialed = (List<RayoNode>)nattedRequest.getAttribute(NODES_DIALED); if (response.getElement("error") == null) { // fetch call id and add it to the registry String callId = response.getElement("ref").getAttribute("id"); try { // Note that the original request always has a resource assigned. So this outgoing call // will be linked to that resourc gatewayStorageService.registerCall(callId, originalRequest.getFrom().toString()); gatewayStatistics.callRegistered(); } catch (GatewayException e) { log.error("Could not register call for dial"); log.error(e.getMessage(),e); } } else { Element errorElement = response.getElement("error"); NodeList list = errorElement.getChildNodes(); for (int i=0;i<list.getLength();i++) { Node errorNode = list.item(i); if (!errorNode.getNodeName().equals("text")) { try { // Only retry dial on certain errors that could be caused by a concrete Rayo Node malfunctioning Condition condition = toCondition(errorNode.getNodeName()); if (condition.equals(Condition.SERVICE_UNAVAILABLE) || condition.equals(Condition.GONE) || condition.equals(Condition.INTERNAL_SERVER_ERROR) || condition.equals(Condition.REMOTE_SERVER_NOT_FOUND) || condition.equals(Condition.REMOTE_SERVER_TIMEOUT)) { loadBalancer.nodeOperationFailed(nodesDialed.get(nodesDialed.size()-1)); resendDialRequest(response, nattedRequest, originalRequest, nodesDialed); return; } } catch (Exception e) { log.error("Could not parse condition [%s]", errorNode.getNodeName()); } } } } loadBalancer.nodeOperationSuceeded(nodesDialed.get(nodesDialed.size()-1)); } else { Element refElement = response.getElement("ref"); if (refElement != null) { // Check if the ref element comes from a mixer. In such case we need to track it, so // subsequent events on that resource get forwarded to the appropriate app id String id = response.getFrom().getNode(); GatewayMixer mixer = gatewayStorageService.getMixer(id); if (mixer != null) { String verbId = refElement.getAttribute("id"); String appJid = originalRequest.getFrom().toString(); try { gatewayStorageService.addVerbToMixer(verbId, appJid, mixer.getName()); } catch (Exception e) { log.error(e.getMessage(),e); } } } } forwardResponse(response, originalRequest); } private Condition toCondition(String nodeName) { //TODO: https://evolution.voxeo.com/ticket/1626815 nodeName = nodeName.replaceAll("-", "_").toUpperCase(); return Condition.valueOf(nodeName); } private void resendDialRequest(XmppServletResponse response, XmppServletRequest nattedRequest, IQRequest originalRequest, List<RayoNode> nodesDialed) { Integer dialsRetried = (Integer)nattedRequest.getAttribute(DIAL_RETRIES); if (dialsRetried > ((GatewayAdminService)getAdminService()).getMaxDialRetries()) { log.error("Max number of dial retries reached for request [%s]. Dial request failed.", originalRequest); forwardResponse(response, originalRequest); return; } else { String platformId = (String)nattedRequest.getAttribute(PLATFORM_DIALED); try { sendDialRequest(originalRequest, platformId, nodesDialed, dialsRetried); return; } catch (Exception e) { log.error(e.getMessage(),e); forwardResponse(response, originalRequest); return; } } } /* * Forwards an IQ Response from a Rayo Node to the Rayo Client using an IQ Request */ private void forwardResponse(XmppServletResponse response, IQRequest originalRequest) { JID from = originalRequest.getTo(); JID to = originalRequest.getFrom(); try { IQRequest request = null; List<Element> elements = response.getElements(); if (elements != null && elements.size() > 0) { request = getXmppFactory().createIQ(from,to,response.getType(), elements.toArray(new Element[]{})); } else { request = getXmppFactory().createIQ(from, to, response.getType()); } request.setID(originalRequest.getId()); request.send(); } catch (Exception e) { // In the event of an error, continue dispatching to all remaining JIDs log.error(e.getMessage(),e); } } @Override protected void sendIqError(IQRequest request, Exception e) { super.sendIqError(request, e); gatewayStatistics.errorProcessed(); } @Override protected void sendIqError(IQRequest request, IQResponse response) throws IOException { super.sendIqError(request, response); gatewayStatistics.errorProcessed(); } @Override protected void sendIqError(IQRequest request, String type, String error, String text) throws IOException { super.sendIqError(request, type, error, text); gatewayStatistics.errorProcessed(); } @Override protected void sendIqError(IQRequest request, Type type, Condition error,String text) throws IOException { super.sendIqError(request, type, error, text); gatewayStatistics.errorProcessed(); } @Override protected void sendPresenceError(JID fromJid, JID toJid) throws IOException, ServletException { super.sendPresenceError(fromJid, toJid); gatewayStatistics.errorProcessed(); } @Override protected void sendPresenceError(JID fromJid, JID toJid, Condition condition) throws IOException, ServletException { super.sendPresenceError(fromJid, toJid, condition); gatewayStatistics.errorProcessed(); } @Override protected void sendPresenceError(JID fromJid, JID toJid, Element... elements) throws IOException, ServletException { super.sendPresenceError(fromJid, toJid, elements); gatewayStatistics.errorProcessed(); } @Override protected Loggerf getLog() { return log; } private boolean isMyInternalDomain(JID jid) { return internalDomains.contains(jid.getDomain()); } private boolean isMyExternalDomain(JID jid) { return externalDomains.contains(jid.getDomain()); } private String getInternalDomain() { return internalDomains.iterator().next(); } public String getExternalDomain() { return externalDomains.iterator().next(); } public void GatewayStorageService(GatewayStorageService gatewayStorageService) { this.gatewayStorageService = gatewayStorageService; } public void setInternalDomains(Resource internalDomains) { this.internalDomains = new ArrayList<String>(); readFile(this.internalDomains, internalDomains); if (log.isDebugEnabled()) { log.debug("List of supported internal domains: [%s]", this.internalDomains); } } public void setExternalDomains(Resource externalDomains) { this.externalDomains = new ArrayList<String>(); readFile(this.externalDomains, externalDomains); if (log.isDebugEnabled()) { log.debug("List of supported external domains: [%s]", this.externalDomains); } } private void readFile(List<String> list, Resource resource) { try { Scanner scanner = new Scanner(resource.getFile()); while (scanner.hasNextLine()) { String line = scanner.nextLine(); if (line != null && !line.trim().isEmpty() && !line.startsWith("#")) { list.add(line.trim()); } } } catch (Exception e) { log.error(e.getMessage(),e); } } public void setGatewayStatistics(GatewayStatistics gatewayStatistics) { this.gatewayStatistics = gatewayStatistics; } public void setLoadBalancer(GatewayLoadBalancingStrategy loadBalancer) { this.loadBalancer = loadBalancer; } public void setGatewayStorageService(GatewayStorageService gatewayStorageService) { this.gatewayStorageService = gatewayStorageService; } }