001: /**
002: * $Revision$
003: * $Date$
004: *
005: * Copyright (C) 2007 Jive Software. All rights reserved.
006: *
007: * This software is published under the terms of the GNU Public License (GPL),
008: * a copy of which is included in this distribution.
009: */package org.jivesoftware.openfire.mediaproxy;
010:
011: import org.dom4j.Attribute;
012: import org.dom4j.DocumentHelper;
013: import org.dom4j.Element;
014: import org.jivesoftware.openfire.*;
015: import org.jivesoftware.openfire.auth.UnauthorizedException;
016: import org.jivesoftware.openfire.container.BasicModule;
017: import org.jivesoftware.openfire.disco.*;
018: import org.jivesoftware.openfire.forms.spi.XDataFormImpl;
019: import org.jivesoftware.util.JiveGlobals;
020: import org.jivesoftware.util.Log;
021: import org.xmpp.packet.IQ;
022: import org.xmpp.packet.JID;
023: import org.xmpp.packet.Packet;
024: import org.xmpp.packet.PacketError;
025:
026: import java.net.SocketException;
027: import java.net.UnknownHostException;
028: import java.util.*;
029:
030: /**
031: * A proxy service for UDP traffic such as RTP. It provides Jingle transport candidates
032: * to be used for media transmission. The media proxy is especially useful for users
033: * behind NAT devices or firewalls that prevent peer to peer communication..
034: *
035: * @author Thiago Camargo
036: */
037: public class MediaProxyService extends BasicModule implements
038: ServerItemsProvider, RoutableChannelHandler, DiscoInfoProvider,
039: DiscoItemsProvider {
040:
041: private String serviceName;
042: private RoutingTable routingTable;
043: private PacketRouter router;
044: private Echo echo = null;
045: private int echoPort = 10020;
046: private SessionManager sessionManager = null;
047:
048: private MediaProxy mediaProxy = null;
049: private boolean enabled = true;
050:
051: public static final String NAMESPACE = "http://www.jivesoftware.com/protocol/rtpbridge";
052:
053: /**
054: * Constructs a new MediaProxyService.
055: */
056: public MediaProxyService() {
057: super ("Media Proxy Service");
058: }
059:
060: public void initialize(XMPPServer server) {
061: super .initialize(server);
062:
063: sessionManager = server.getSessionManager();
064: // In some cases, the domain name of the server may not be the actual address of the machine
065: // (ie, when using DNS SRV records). In that case, the "mediaproxy.externalip" property should be
066: // set to the IP address of the actual server where the media proxy is listening.
067: String ipAddress = JiveGlobals.getProperty(
068: "mediaproxy.externalip", server.getServerInfo()
069: .getName());
070: mediaProxy = new MediaProxy(ipAddress);
071:
072: String defaultName = "rtpbridge";
073: serviceName = JiveGlobals.getProperty("mediaproxy.serviceName",
074: defaultName);
075: serviceName = serviceName.equals("") ? defaultName
076: : serviceName;
077:
078: echoPort = JiveGlobals.getIntProperty("mediaproxy.echoPort",
079: echoPort);
080:
081: routingTable = server.getRoutingTable();
082: router = server.getPacketRouter();
083:
084: initMediaProxy();
085: }
086:
087: public void start() {
088: if (isEnabled()) {
089:
090: try {
091: echo = new Echo(echoPort);
092: Thread t = new Thread(echo);
093: t.start();
094: } catch (UnknownHostException e) {
095: // Ignore
096: } catch (SocketException e) {
097: // Ignore
098: }
099:
100: routingTable.addComponentRoute(getAddress(), this );
101: XMPPServer.getInstance().getIQDiscoItemsHandler()
102: .addServerItemsProvider(this );
103: } else {
104: if (echo != null)
105: echo.cancel();
106: XMPPServer.getInstance().getIQDiscoItemsHandler()
107: .removeComponentItem(getAddress().toString());
108: }
109: }
110:
111: public void stop() {
112: super .stop();
113: mediaProxy.stopProxy();
114: XMPPServer.getInstance().getIQDiscoItemsHandler()
115: .removeComponentItem(getAddress().toString());
116: routingTable.removeComponentRoute(getAddress());
117: if (echo != null)
118: echo.cancel();
119: }
120:
121: // Component Interface
122:
123: public String getName() {
124: // Get the name from the plugin.xml file.
125: return serviceName;
126: }
127:
128: public Iterator<DiscoItem> getItems(String name, String node,
129: JID senderJID) {
130: // A proxy server has no items
131: return new ArrayList<DiscoItem>().iterator();
132: }
133:
134: public void process(Packet packet) throws UnauthorizedException,
135: PacketException {
136: // Check if user is allowed to send packet to this service
137: if (packet instanceof IQ) {
138: // Handle disco packets
139: IQ iq = (IQ) packet;
140: // Ignore IQs of type ERROR or RESULT
141: if (IQ.Type.error == iq.getType()
142: || IQ.Type.result == iq.getType()) {
143: return;
144: }
145: processIQ(iq);
146: }
147: }
148:
149: private void processIQ(IQ iq) {
150: IQ reply = IQ.createResultIQ(iq);
151: Element childElement = iq.getChildElement();
152: String namespace = childElement.getNamespaceURI();
153: Element childElementCopy = iq.getChildElement().createCopy();
154: reply.setChildElement(childElementCopy);
155:
156: if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
157: reply = XMPPServer.getInstance().getIQDiscoInfoHandler()
158: .handleIQ(iq);
159: router.route(reply);
160: return;
161: } else if ("http://jabber.org/protocol/disco#items"
162: .equals(namespace)) {
163: // a component
164: reply = XMPPServer.getInstance().getIQDiscoItemsHandler()
165: .handleIQ(iq);
166: router.route(reply);
167: return;
168: } else if (NAMESPACE.equals(namespace) && enabled) {
169:
170: Element candidateElement = childElementCopy
171: .element("candidate");
172: String sid = childElementCopy.attribute("sid").getValue()
173: + "-" + iq.getFrom();
174:
175: if (candidateElement != null) {
176: childElementCopy.remove(candidateElement);
177: Element candidate = childElementCopy
178: .addElement("candidate ");
179: ProxyCandidate proxyCandidate = mediaProxy
180: .addRelayAgent(sid, iq.getFrom().toString());
181: Log.debug("MediaProxyService: " + sid);
182: proxyCandidate.start();
183: candidate.addAttribute("name", "voicechannel");
184: candidate.addAttribute("ip", mediaProxy.getPublicIP());
185: candidate.addAttribute("porta", String
186: .valueOf(proxyCandidate.getLocalPortA()));
187: candidate.addAttribute("portb", String
188: .valueOf(proxyCandidate.getLocalPortB()));
189: candidate
190: .addAttribute("pass", proxyCandidate.getPass());
191:
192: } else {
193: candidateElement = childElementCopy.element("relay");
194: if (candidateElement != null) {
195: MediaProxySession session = mediaProxy
196: .getSession(sid);
197: Log.debug("MediaProxyService: " + sid);
198: if (session != null) {
199: Attribute pass = candidateElement
200: .attribute("pass");
201:
202: if (pass != null
203: && pass.getValue().trim().equals(
204: session.getPass().trim())) {
205: Attribute portA = candidateElement
206: .attribute("porta");
207: Attribute portB = candidateElement
208: .attribute("portb");
209: Attribute hostA = candidateElement
210: .attribute("hosta");
211: Attribute hostB = candidateElement
212: .attribute("hostb");
213:
214: try {
215: if (hostA != null && portA != null) {
216: for (int i = 0; i < 2; i++) {
217: session.sendFromPortA(hostB
218: .getValue(), Integer
219: .parseInt(portB
220: .getValue()));
221: }
222: }
223: } catch (Exception e) {
224: Log.error(e);
225: }
226:
227: } else {
228: reply
229: .setError(PacketError.Condition.forbidden);
230: }
231: }
232: childElementCopy.remove(candidateElement);
233: } else {
234: candidateElement = childElementCopy
235: .element("publicip");
236: if (candidateElement != null) {
237: childElementCopy.remove(candidateElement);
238: Element publicIp = childElementCopy
239: .addElement("publicip");
240: try {
241: String ip = sessionManager.getSession(
242: iq.getFrom()).getHostAddress();
243: if (ip != null) {
244: publicIp.addAttribute("ip", ip);
245: }
246: } catch (UnknownHostException e) {
247: Log.error(e);
248: }
249:
250: } else {
251: childElementCopy.remove(candidateElement);
252: reply.setError(PacketError.Condition.forbidden);
253: }
254:
255: }
256: }
257: } else {
258: // Answer an error since the server can't handle the requested namespace
259: reply.setError(PacketError.Condition.service_unavailable);
260: }
261:
262: try {
263: if (Log.isDebugEnabled()) {
264: Log.debug("MediaProxyService: RETURNED:"
265: + reply.toXML());
266: }
267: router.route(reply);
268: } catch (Exception e) {
269: Log.error(e);
270: }
271: }
272:
273: /**
274: * Load config using JiveGlobals
275: */
276: private void initMediaProxy() {
277: try {
278: long idleTime = Long.valueOf(JiveGlobals
279: .getProperty("mediaproxy.idleTimeout"));
280: mediaProxy.setIdleTime(idleTime);
281: } catch (NumberFormatException e) {
282: // Do nothing let the default values to be used.
283: }
284: try {
285: long lifetime = Long.valueOf(JiveGlobals
286: .getProperty("mediaproxy.lifetime"));
287: mediaProxy.setLifetime(lifetime);
288: } catch (NumberFormatException e) {
289: // Do nothing let the default values to be used.
290: }
291: try {
292: int minPort = Integer.valueOf(JiveGlobals
293: .getProperty("mediaproxy.portMin"));
294: mediaProxy.setMinPort(minPort);
295: } catch (NumberFormatException e) {
296: // Do nothing let the default values to be used.
297: }
298: try {
299: int maxPort = JiveGlobals.getIntProperty(
300: "mediaproxy.portMax", mediaProxy.getMaxPort());
301: mediaProxy.setMaxPort(maxPort);
302: } catch (NumberFormatException e) {
303: // Do nothing let the default values to be used.
304: }
305: this .enabled = JiveGlobals
306: .getBooleanProperty("mediaproxy.enabled");
307: }
308:
309: /**
310: * Returns the fully-qualifed domain name of this chat service.
311: * The domain is composed by the service name and the
312: * name of the XMPP server where the service is running.
313: *
314: * @return the file transfer server domain (service name + host name).
315: */
316: public String getServiceDomain() {
317: return serviceName + "."
318: + XMPPServer.getInstance().getServerInfo().getName();
319: }
320:
321: public JID getAddress() {
322: return new JID(null, getServiceDomain(), null);
323: }
324:
325: public Iterator<DiscoServerItem> getItems() {
326: List<DiscoServerItem> items = new ArrayList<DiscoServerItem>();
327: if (!isEnabled()) {
328: return items.iterator();
329: }
330:
331: final DiscoServerItem item = new DiscoServerItem(new JID(
332: getServiceDomain()), "Media Proxy Service", null, null,
333: this , this );
334: items.add(item);
335: return items.iterator();
336: }
337:
338: public Iterator<Element> getIdentities(String name, String node,
339: JID senderJID) {
340: List<Element> identities = new ArrayList<Element>();
341: // Answer the identity of the proxy
342: Element identity = DocumentHelper.createElement("identity");
343: identity.addAttribute("category", "proxy");
344: identity.addAttribute("name", "Media Proxy Service");
345: identity.addAttribute("type", "rtpbridge");
346:
347: identities.add(identity);
348:
349: return identities.iterator();
350: }
351:
352: public Iterator<String> getFeatures(String name, String node,
353: JID senderJID) {
354: return Arrays.asList(NAMESPACE,
355: "http://jabber.org/protocol/disco#info").iterator();
356: }
357:
358: public XDataFormImpl getExtendedInfo(String name, String node,
359: JID senderJID) {
360: return null;
361: }
362:
363: public boolean hasInfo(String name, String node, JID senderJID) {
364: return true;
365: }
366:
367: /**
368: * Return the list of active Agents
369: *
370: * @return list of active agents
371: */
372: public Collection<MediaProxySession> getAgents() {
373: return mediaProxy.getSessions();
374: }
375:
376: /**
377: * Set the keep alive delay of the mediaproxy agents.
378: * When an agent stay more then this delay, the agent is destroyed.
379: *
380: * @param delay time in millis
381: */
382: public void setKeepAliveDelay(long delay) {
383: mediaProxy.setIdleTime(delay);
384: }
385:
386: /**
387: * Returns the maximum amount of time (in milleseconds) that a session can
388: * be idle before it's closed.
389: *
390: * @return the max idle time in millis.
391: */
392: public long getIdleTime() {
393: return mediaProxy.getIdleTime();
394: }
395:
396: /**
397: * Set Minimal port value to listen for incoming packets.
398: *
399: * @param minPort port value to listen for incoming packets
400: */
401: public void setMinPort(int minPort) {
402: mediaProxy.setMinPort(minPort);
403: }
404:
405: /**
406: * Set Maximum port value to listen for incoming packets.
407: *
408: * @param maxPort port value to listen for incoming packets
409: */
410: public void setMaxPort(int maxPort) {
411: mediaProxy.setMaxPort(maxPort);
412: }
413:
414: /**
415: * Get Minimal port value to listen from incoming packets.
416: *
417: * @return minPort
418: */
419: public int getMinPort() {
420: return mediaProxy.getMinPort();
421: }
422:
423: /**
424: * Get Maximum port value to listen from incoming packets.
425: *
426: * @return maxPort
427: */
428: public int getMaxPort() {
429: return mediaProxy.getMaxPort();
430: }
431:
432: /**
433: * Get if the service is enabled.
434: *
435: * @return enabled
436: */
437: public boolean isEnabled() {
438: return enabled;
439: }
440:
441: /**
442: * Set the service enable status.
443: *
444: * @param enabled boolean value setting enabled or disabled
445: */
446: public void setEnabled(boolean enabled) {
447: this .enabled = enabled;
448: if (isEnabled()) {
449: start();
450: } else {
451: stop();
452: }
453: }
454:
455: /**
456: * Stops every running agents
457: */
458: public void stopAgents() {
459: mediaProxy.stopProxy();
460: }
461:
462: /**
463: * Get the Life Time of Sessions
464: *
465: * @return lifetime in seconds
466: */
467: public long getLifetime() {
468: return mediaProxy.getLifetime();
469: }
470:
471: /**
472: * Set the Life time of Sessions
473: *
474: * @param lifetime lifetime in seconds
475: */
476: public void setLifetime(long lifetime) {
477: mediaProxy.setLifetime(lifetime);
478: }
479:
480: /**
481: * Get the Port used to the UDP Echo Test
482: *
483: * @return port number
484: */
485: public int getEchoPort() {
486: return echoPort;
487: }
488:
489: /**
490: * Set the Port used to the UDP Echo Test
491: *
492: * @param echoPort port number
493: */
494: public void setEchoPort(int echoPort) {
495: this.echoPort = echoPort;
496: }
497: }
|