/* * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.openfire.mediaproxy; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import org.dom4j.Attribute; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.jivesoftware.openfire.PacketException; import org.jivesoftware.openfire.PacketRouter; import org.jivesoftware.openfire.RoutableChannelHandler; import org.jivesoftware.openfire.RoutingTable; import org.jivesoftware.openfire.SessionManager; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.container.BasicModule; import org.jivesoftware.openfire.disco.DiscoInfoProvider; import org.jivesoftware.openfire.disco.DiscoItem; import org.jivesoftware.openfire.disco.DiscoItemsProvider; import org.jivesoftware.openfire.disco.DiscoServerItem; import org.jivesoftware.openfire.disco.ServerItemsProvider; import org.jivesoftware.util.JiveGlobals; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.forms.DataForm; import org.xmpp.packet.IQ; import org.xmpp.packet.JID; import org.xmpp.packet.Packet; import org.xmpp.packet.PacketError; /** * A proxy service for UDP traffic such as RTP. It provides Jingle transport candidates * to be used for media transmission. The media proxy is especially useful for users * behind NAT devices or firewalls that prevent peer to peer communication.. * * @author Thiago Camargo */ public class MediaProxyService extends BasicModule implements ServerItemsProvider, RoutableChannelHandler, DiscoInfoProvider, DiscoItemsProvider { private static final Logger Log = LoggerFactory.getLogger(MediaProxyService.class); private String serviceName; private RoutingTable routingTable; private PacketRouter router; private Echo echo = null; private int echoPort = 10020; private SessionManager sessionManager = null; private MediaProxy mediaProxy = null; private boolean enabled = true; public static final String NAMESPACE = "http://www.jivesoftware.com/protocol/rtpbridge"; /** * Constructs a new MediaProxyService. */ public MediaProxyService() { super("Media Proxy Service"); } @Override public void initialize(XMPPServer server) { super.initialize(server); sessionManager = server.getSessionManager(); // In some cases, the domain name of the server may not be the actual address of the machine // (ie, when using DNS SRV records). In that case, the "mediaproxy.externalip" property should be // set to the IP address of the actual server where the media proxy is listening. String ipAddress = JiveGlobals.getProperty("mediaproxy.externalip", server.getServerInfo().getXMPPDomain()); mediaProxy = new MediaProxy(ipAddress); String defaultName = "rtpbridge"; serviceName = JiveGlobals.getProperty("mediaproxy.serviceName", defaultName); serviceName = serviceName.equals("") ? defaultName : serviceName; echoPort = JiveGlobals.getIntProperty("mediaproxy.echoPort", echoPort); routingTable = server.getRoutingTable(); router = server.getPacketRouter(); initMediaProxy(); } @Override public void start() { if (isEnabled()) { try { echo = new Echo(echoPort); Thread t = new Thread(echo); t.start(); } catch (UnknownHostException | SocketException e) { // Ignore } routingTable.addComponentRoute(getAddress(), this); XMPPServer.getInstance().getIQDiscoItemsHandler().addServerItemsProvider(this); } else { if (echo != null) echo.cancel(); XMPPServer.getInstance().getIQDiscoItemsHandler().removeComponentItem(getAddress().toString()); } } @Override public void stop() { super.stop(); mediaProxy.stopProxy(); XMPPServer.getInstance().getIQDiscoItemsHandler().removeComponentItem(getAddress().toString()); routingTable.removeComponentRoute(getAddress()); if (echo != null) echo.cancel(); } // Component Interface @Override public String getName() { // Get the name from the plugin.xml file. return serviceName; } @Override public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) { // A proxy server has no items return new ArrayList<DiscoItem>().iterator(); } @Override public void process(Packet packet) throws UnauthorizedException, PacketException { // Check if user is allowed to send packet to this service if (packet instanceof IQ) { // Handle disco packets IQ iq = (IQ) packet; // Ignore IQs of type ERROR or RESULT if (IQ.Type.error == iq.getType() || IQ.Type.result == iq.getType()) { return; } processIQ(iq); } } private void processIQ(IQ iq) { IQ reply = IQ.createResultIQ(iq); Element childElement = iq.getChildElement(); String namespace = childElement.getNamespaceURI(); Element childElementCopy = iq.getChildElement().createCopy(); reply.setChildElement(childElementCopy); if ("http://jabber.org/protocol/disco#info".equals(namespace)) { reply = XMPPServer.getInstance().getIQDiscoInfoHandler().handleIQ(iq); router.route(reply); return; } else if ("http://jabber.org/protocol/disco#items".equals(namespace)) { // a component reply = XMPPServer.getInstance().getIQDiscoItemsHandler().handleIQ(iq); router.route(reply); return; } else if (NAMESPACE.equals(namespace) && enabled) { Element candidateElement = childElementCopy.element("candidate"); String sid = childElementCopy.attribute("sid").getValue() + "-" + iq.getFrom(); if (candidateElement != null) { childElementCopy.remove(candidateElement); Element candidate = childElementCopy.addElement("candidate "); ProxyCandidate proxyCandidate = mediaProxy.addRelayAgent(sid, iq.getFrom().toString()); Log.debug("MediaProxyService: "+sid); proxyCandidate.start(); candidate.addAttribute("name", "voicechannel"); candidate.addAttribute("ip", mediaProxy.getPublicIP()); candidate.addAttribute("porta", String.valueOf(proxyCandidate.getLocalPortA())); candidate.addAttribute("portb", String.valueOf(proxyCandidate.getLocalPortB())); candidate.addAttribute("pass", proxyCandidate.getPass()); } else { candidateElement = childElementCopy.element("relay"); if (candidateElement != null) { MediaProxySession session = mediaProxy.getSession(sid); Log.debug("MediaProxyService: "+sid); if (session != null) { Attribute pass = candidateElement.attribute("pass"); if (pass != null && pass.getValue().trim().equals(session.getPass().trim())) { Attribute portA = candidateElement.attribute("porta"); Attribute portB = candidateElement.attribute("portb"); Attribute hostA = candidateElement.attribute("hosta"); Attribute hostB = candidateElement.attribute("hostb"); try { if (hostA != null && portA != null) { for (int i = 0; i < 2; i++) { session.sendFromPortA(hostB.getValue(), Integer.parseInt(portB.getValue())); } } } catch (Exception e) { Log.error(e.getMessage(), e); } } else { reply.setError(PacketError.Condition.forbidden); } } childElementCopy.remove(candidateElement); } else { candidateElement = childElementCopy.element("publicip"); if (candidateElement != null) { childElementCopy.remove(candidateElement); Element publicIp = childElementCopy.addElement("publicip"); try { String ip = sessionManager.getSession(iq.getFrom()).getHostAddress(); if (ip != null) { publicIp.addAttribute("ip", ip); } } catch (UnknownHostException e) { Log.error(e.getMessage(), e); } } else { childElementCopy.remove(candidateElement); reply.setError(PacketError.Condition.forbidden); } } } } else { // Answer an error since the server can't handle the requested namespace reply.setError(PacketError.Condition.service_unavailable); } try { if (Log.isDebugEnabled()) { Log.debug("MediaProxyService: RETURNED:" + reply.toXML()); } router.route(reply); } catch (Exception e) { Log.error(e.getMessage(), e); } } /** * Load config using JiveGlobals */ private void initMediaProxy() { try { long idleTime = Long.valueOf(JiveGlobals.getProperty("mediaproxy.idleTimeout")); mediaProxy.setIdleTime(idleTime); } catch (NumberFormatException e) { // Do nothing let the default values to be used. } try { long lifetime = Long.valueOf(JiveGlobals.getProperty("mediaproxy.lifetime")); mediaProxy.setLifetime(lifetime); } catch (NumberFormatException e) { // Do nothing let the default values to be used. } try { int minPort = Integer.valueOf(JiveGlobals.getProperty("mediaproxy.portMin")); mediaProxy.setMinPort(minPort); } catch (NumberFormatException e) { // Do nothing let the default values to be used. } try { int maxPort = JiveGlobals.getIntProperty("mediaproxy.portMax", mediaProxy.getMaxPort()); mediaProxy.setMaxPort(maxPort); } catch (NumberFormatException e) { // Do nothing let the default values to be used. } this.enabled = JiveGlobals.getBooleanProperty("mediaproxy.enabled"); } /** * Returns the fully-qualifed domain name of this chat service. * The domain is composed by the service name and the * name of the XMPP server where the service is running. * * @return the file transfer server domain (service name + host name). */ public String getServiceDomain() { return serviceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(); } @Override public JID getAddress() { return new JID(null, getServiceDomain(), null); } @Override public Iterator<DiscoServerItem> getItems() { List<DiscoServerItem> items = new ArrayList<>(); if (!isEnabled()) { return items.iterator(); } final DiscoServerItem item = new DiscoServerItem(new JID( getServiceDomain()), "Media Proxy Service", null, null, this, this); items.add(item); return items.iterator(); } @Override public Iterator<Element> getIdentities(String name, String node, JID senderJID) { List<Element> identities = new ArrayList<>(); // Answer the identity of the proxy Element identity = DocumentHelper.createElement("identity"); identity.addAttribute("category", "proxy"); identity.addAttribute("name", "Media Proxy Service"); identity.addAttribute("type", "rtpbridge"); identities.add(identity); return identities.iterator(); } @Override public Iterator<String> getFeatures(String name, String node, JID senderJID) { return Arrays.asList(NAMESPACE, "http://jabber.org/protocol/disco#info").iterator(); } @Override public DataForm getExtendedInfo(String name, String node, JID senderJID) { return null; } @Override public boolean hasInfo(String name, String node, JID senderJID) { return true; } /** * Return the list of active Agents * * @return list of active agents */ public Collection<MediaProxySession> getAgents() { return mediaProxy.getSessions(); } /** * Set the keep alive delay of the mediaproxy agents. * When an agent stay more then this delay, the agent is destroyed. * * @param delay time in millis */ public void setKeepAliveDelay(long delay) { mediaProxy.setIdleTime(delay); } /** * Returns the maximum amount of time (in milleseconds) that a session can * be idle before it's closed. * * @return the max idle time in millis. */ public long getIdleTime() { return mediaProxy.getIdleTime(); } /** * Set Minimal port value to listen for incoming packets. * * @param minPort port value to listen for incoming packets */ public void setMinPort(int minPort) { mediaProxy.setMinPort(minPort); } /** * Set Maximum port value to listen for incoming packets. * * @param maxPort port value to listen for incoming packets */ public void setMaxPort(int maxPort) { mediaProxy.setMaxPort(maxPort); } /** * Get Minimal port value to listen from incoming packets. * * @return minPort */ public int getMinPort() { return mediaProxy.getMinPort(); } /** * Get Maximum port value to listen from incoming packets. * * @return maxPort */ public int getMaxPort() { return mediaProxy.getMaxPort(); } /** * Get if the service is enabled. * * @return enabled */ public boolean isEnabled() { return enabled; } /** * Set the service enable status. * * @param enabled boolean value setting enabled or disabled */ public void setEnabled(boolean enabled) { this.enabled = enabled; if (isEnabled()) { start(); } else { stop(); } } /** * Stops every running agents */ public void stopAgents() { mediaProxy.stopProxy(); } /** * Get the Life Time of Sessions * * @return lifetime in seconds */ public long getLifetime() { return mediaProxy.getLifetime(); } /** * Set the Life time of Sessions * * @param lifetime lifetime in seconds */ public void setLifetime(long lifetime) { mediaProxy.setLifetime(lifetime); } /** * Get the Port used to the UDP Echo Test * * @return port number */ public int getEchoPort() { return echoPort; } /** * Set the Port used to the UDP Echo Test * * @param echoPort port number */ public void setEchoPort(int echoPort) { this.echoPort = echoPort; } }