diff options
author | pperret | 2006-10-17 19:12:42 +0000 |
---|---|---|
committer | pperret | 2006-10-17 19:12:42 +0000 |
commit | 8deb3e7166aa8524fa2b53da623a558f7683d2bb (patch) | |
tree | a20b7b41c61e2ea63ce4cc83292665888a13204c /protocols/bundles/org.jivesoftware.smack | |
parent | 69287910ce05d48ac3cb72cb648e55ff868381d6 (diff) | |
download | org.eclipse.ecf-8deb3e7166aa8524fa2b53da623a558f7683d2bb.tar.gz org.eclipse.ecf-8deb3e7166aa8524fa2b53da623a558f7683d2bb.tar.xz org.eclipse.ecf-8deb3e7166aa8524fa2b53da623a558f7683d2bb.zip |
xmpp jingle provider
Diffstat (limited to 'protocols/bundles/org.jivesoftware.smack')
14 files changed, 5322 insertions, 0 deletions
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/ContentInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/ContentInfo.java new file mode 100644 index 000000000..d40c772f3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/ContentInfo.java @@ -0,0 +1,58 @@ +package org.jivesoftware.smackx.jingle; + +/** + * Content info. Content info messages are complementary messages that can be + * transmitted for informing of events like "busy", "ringtone", etc. + * + * @author Alvaro Saurin <alvaro.saurin@gmail.com> + */ +public abstract class ContentInfo { + + /** + * Audio conten info messages. + * + * @author Alvaro Saurin <alvaro.saurin@gmail.com> + */ + public static class Audio extends ContentInfo { + + public static final ContentInfo.Audio BUSY = new ContentInfo.Audio("busy"); + + public static final ContentInfo.Audio HOLD = new ContentInfo.Audio("hold"); + + public static final ContentInfo.Audio MUTE = new ContentInfo.Audio("mute"); + + public static final ContentInfo.Audio QUEUED = new ContentInfo.Audio("queued"); + + public static final ContentInfo.Audio RINGING = new ContentInfo.Audio("ringing"); + + private String value; + + public Audio(final String value) { + this.value = value; + } + + public String toString() { + return value; + } + + /** + * Returns the MediaInfo constant associated with the String value. + */ + public static ContentInfo fromString(String value) { + value = value.toLowerCase(); + if (value.equals("busy")) { + return BUSY; + } else if (value.equals("hold")) { + return HOLD; + } else if (value.equals("mute")) { + return MUTE; + } else if (value.equals("queued")) { + return QUEUED; + } else if (value.equals("ringing")) { + return RINGING; + } else { + return null; + } + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/IncomingJingleSession.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/IncomingJingleSession.java new file mode 100644 index 000000000..30a400c26 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/IncomingJingleSession.java @@ -0,0 +1,365 @@ +/** + * $RCSfile: IncomingJingleSession.java,v $ + * $Revision: 1.1 $ + * $Date: 2006/10/17 19:12:42 $ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle; + +import java.util.List; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.nat.TransportCandidate; +import org.jivesoftware.smackx.nat.TransportResolver; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleError; +import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType; + +/** + * An incoming Jingle session. + * + * </p> + * + * This class is not directly used by users. Instead, users should refer to the + * JingleManager class, that will create the appropiate instance... + * + * </p> + * + * @author Alvaro Saurin + */ +public class IncomingJingleSession extends JingleSession { + + // states + private final Accepting accepting; + + private final Pending pending; + + private final Active active; + + /** + * Constructor with the request + * + * @param conn the XMPP connection + * @param responder the responder + * @param resolver The transport resolver + */ + public IncomingJingleSession(final XMPPConnection conn, final String responder, + final List payloadTypes, final TransportResolver resolver) { + + super(conn, responder, conn.getUser()); + + // Create the states... + + accepting = new Accepting(this); + pending = new Pending(this); + active = new Active(this); + + setMediaNeg(new MediaNegotiator(this, payloadTypes)); + setTransportNeg(new TransportNegotiator.RawUdp(this, resolver)); + } + + /** + * Start the session. + * + * @throws XMPPException + */ + public void start(final JingleSessionRequest request) throws XMPPException { + if (invalidState()) { + Jingle jin = request.getJingle(); + if (jin != null) { + + // Initialize the session information + setSid(jin.getSid()); + + // Establish the default state + setState(accepting); + + updatePacketListener(); + respond(jin); + } else { + throw new IllegalStateException( + "Session request with null Jingle packet."); + } + } else { + throw new IllegalStateException("Starting session without null state."); + } + } + + // States + + /** + * First stage when we have received a session request, and we accept the + * request. We start in this stage, as the instance is created when the user + * accepts the connection... + */ + public class Accepting extends JingleNegotiator.State { + + public Accepting(final JingleNegotiator neg) { + super(neg); + } + + /** + * Initiate the incoming session. We have already sent the ACK partially + * accepting the session... + * + * @throws XMPPException + */ + public Jingle eventInitiate(final Jingle inJingle) throws XMPPException { + // Set the new session state + setState(pending); + return super.eventInitiate(inJingle); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(final IQ iq) throws XMPPException { + triggerSessionClosedOnError(new JingleException(iq.getError().getMessage())); + super.eventError(iq); + } + } + + /** + * "Pending" state: we are waiting for the transport and content + * negotiators. + */ + private class Pending extends JingleNegotiator.State { + + JingleListener.Media mediaListener; + + JingleListener.Transport transportListener; + + public Pending(final JingleNegotiator neg) { + super(neg); + + // Create the listeners that will send a "session-accept" when the + // sub-negotiators are done. + mediaListener = new JingleListener.Media() { + public void mediaClosed(final PayloadType cand) { + } + + public void mediaEstablished(final PayloadType pt) { + checkFullyEstablished(); + } + }; + + transportListener = new JingleListener.Transport() { + public void transportEstablished(final TransportCandidate local, + final TransportCandidate remote) { + checkFullyEstablished(); + } + + public void transportClosed(final TransportCandidate cand) { + } + + public void transportClosedOnError(final XMPPException e) { + } + }; + } + + /** + * Enter in the pending state: wait for the sub-negotiators. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + // Add the listeners to the sub-negotiators... + addMediaListener(mediaListener); + addTransportListener(transportListener); + super.eventEnter(); + } + + /** + * Exit of the state + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit() + */ + public void eventExit() { + removeMediaListener(mediaListener); + removeTransportListener(transportListener); + super.eventExit(); + } + + /** + * Check if the session has been fully accepted by all the + * sub-negotiators and, in that case, send an "accept" message... + */ + private void checkFullyEstablished() { + if (isFullyEstablished()) { + + PayloadType.Audio bestCommonAudioPt = getMediaNeg() + .getBestCommonAudioPt(); + TransportCandidate bestRemoteCandidate = getTransportNeg() + .getBestRemoteCandidate(); + TransportCandidate acceptedLocalCandidate = getTransportNeg() + .getAcceptedLocalCandidate(); + + if (bestCommonAudioPt != null && bestRemoteCandidate != null + && acceptedLocalCandidate != null) { + // Ok, send a packet saying that we accept this session + Jingle jout = new Jingle(Jingle.Action.SESSIONACCEPT); + + // ... with the audio payload type and the transport + // candidate + jout.addDescription(new JingleContentDescription.Audio( + new JinglePayloadType(bestCommonAudioPt))); + jout.addTransport(getTransportNeg().getJingleTransport( + bestRemoteCandidate)); + + addExpectedId(jout.getPacketID()); + sendFormattedJingle(jout); + } + } + } + + /** + * The other endpoint has accepted the session. + */ + public Jingle eventAccept(final Jingle jin) throws XMPPException { + + PayloadType acceptedPayloadType = null; + TransportCandidate acceptedLocalCandidate = null; + + // We process the "accepted" if we have finished the + // sub-negotiators. Maybe this is not needed (ie, the other endpoint + // can take the first valid transport candidate), but otherwise we + // must cancel the negotiators... + // + if (isFullyEstablished()) { + acceptedPayloadType = getAcceptedAudioPayloadType(jin); + acceptedLocalCandidate = getAcceptedLocalCandidate(jin); + + if (acceptedPayloadType != null && acceptedLocalCandidate != null) { + if (acceptedPayloadType.equals(getMediaNeg().getBestCommonAudioPt()) + && acceptedLocalCandidate.equals(getTransportNeg() + .getAcceptedLocalCandidate())) { + setState(active); + } + } else { + throw new JingleException(JingleError.MALFORMED_STANZA); + } + } + + return super.eventAccept(jin); + } + + /** + * We have received a confirmation. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(final IQ iq) throws XMPPException { + setState(active); + return super.eventAck(iq); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(final IQ iq) throws XMPPException { + triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage())); + super.eventError(iq); + } + } + + /** + * "Active" state: we have an agreement about the session. + */ + private class Active extends JingleNegotiator.State { + public Active(final JingleNegotiator neg) { + super(neg); + } + + /** + * We have a established session: notify the listeners + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + PayloadType.Audio bestCommonAudioPt = getMediaNeg().getBestCommonAudioPt(); + TransportCandidate bestRemoteCandidate = getTransportNeg() + .getBestRemoteCandidate(); + TransportCandidate acceptedLocalCandidate = getTransportNeg() + .getAcceptedLocalCandidate(); + + // Trigger the session established flag + triggerSessionEstablished(bestCommonAudioPt, bestRemoteCandidate, + acceptedLocalCandidate); + + super.eventEnter(); + } + + /** + * Terminate the connection. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventTerminate(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventTerminate(final Jingle jin) throws XMPPException { + triggerSessionClosed(null); + return super.eventTerminate(jin); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(final IQ iq) throws XMPPException { + triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage())); + super.eventError(iq); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleListener.java new file mode 100644 index 000000000..621435124 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleListener.java @@ -0,0 +1,164 @@ +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.nat.TransportCandidate; + +/** + * Jingle listeners. + * + * </p> + * + * This is the list of events that can be observed from a JingleSession and some + * sub negotiators. This listeners can be added to different elements of the + * Jingle model. + * + * </p> + * + * For example, a JingleManager can notify any JingleListener.SessionRequest + * listener when a new session request is received. In this case, the + * <i>sessionRequested()</i> of the listener will be executed, and the listener + * will be able to <i>accept()</i> or <i>decline()</i> the invitation. + * + * </p> + * + * @author Alvaro Saurin + */ +public interface JingleListener { + + /** + * Jingle session request listener. + * + * @author Alvaro Saurin + */ + public static interface SessionRequest extends JingleListener { + /** + * A request to start a session has been recieved from another user. + * + * @param request The request from the other user. + */ + public void sessionRequested(final JingleSessionRequest request); + } + + /** + * Interface for listening for session events. + */ + public static interface Session extends JingleListener { + /** + * Notification that the session has been established. Arguments specify + * the payload type and transport to use. + * + * @param pt The Payload tyep to use + * @param rc The remote candidate to use for connecting to the remote + * service. + * @param lc The local candidate where we must listen for connections + */ + public void sessionEstablished(final PayloadType pt, final TransportCandidate rc, + final TransportCandidate lc); + + /** + * Notification that the session was declined. + * + * @param reason The reason (if any). + */ + public void sessionDeclined(final String reason); + + /** + * Notification that the session was redirected. + */ + public void sessionRedirected(final String redirection); + + /** + * Notification that the session was closed normally. + * + * @param reason The reason (if any). + */ + public void sessionClosed(final String reason); + + /** + * Notification that the session was closed due to an exception. + * + * @param e the exception. + */ + public void sessionClosedOnError(final XMPPException e); + } + + /** + * Interface for listening to transport events. + */ + public static interface Transport extends JingleListener { + /** + * Notification that the transport has been established. + * + * @param local The transport candidate that has been used for listening + * in the local machine + * @param remote The transport candidate that has been used for + * transmitting to the remote machine + */ + public void transportEstablished(final TransportCandidate local, + final TransportCandidate remote); + + /** + * Notification that a transport must be cancelled. + * + * @param cand The transport candidate that must be cancelled. A value + * of "null" means all the transports for this session. + */ + public void transportClosed(final TransportCandidate cand); + + /** + * Notification that the transport was closed due to an exception. + * + * @param e the exception. + */ + public void transportClosedOnError(final XMPPException e); + } + + /** + * Interface for listening to media events. + */ + public static interface Media extends JingleListener { + /** + * Notification that the media has been negotiated and established. + * + * @param pt The payload type agreed. + */ + public void mediaEstablished(final PayloadType pt); + + /** + * Notification that a payload type must be cancelled + * + * @param cand The payload type that must be closed + */ + public void mediaClosed(final PayloadType cand); + } + + /** + * Interface for listening to media info events. + */ + public static interface MediaInfo extends JingleListener { + /** + * The other end is busy. + */ + public void mediaInfoBusy(); + + /** + * We are on hold. + */ + public void mediaInfoHold(); + + /** + * The media is muted. + */ + public void mediaInfoMute(); + + /** + * We are queued. + */ + public void mediaInfoQueued(); + + /** + * We are ringing. + */ + public void mediaInfoRinging(); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManager.java new file mode 100644 index 000000000..49bf6fa41 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManager.java @@ -0,0 +1,363 @@ +/** + * $RCSfile: JingleManager.java,v $ + * $Revision: 1.1 $ + * $Date: 2006/10/17 19:12:42 $ + * + * Copyright 2003-2005 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.smackx.jingle; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.ConnectionEstablishedListener; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.nat.TransportResolver; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.Jingle; + +/** + * The JingleManager is a facade built upon Jabber Jingle (JEP-166) to allow the + * use of the Jingle extension. This implementation allows the user to simply + * use this class for setting the Jingle parameters. + * + * </p> + * + * This is an example of how to use the Jingle code: + * + * <pre> + * XMPPConnection con = new XMPPConnection("jabber.org"); + * + * TransportResolver tm = new STUNResolver(); + * + * JingleManager jmanager = new JingleManager(conn, tm); + * + * // Insert the payloads in the "mypayloads" list... + * + * OutgoingJingleSession jsession = jmanager.createOutgoingJingleSession(mypayloads); + * + * // Install some session listeners... + * + * </pre> + * + * In order to use the Jingle extension, the user must provide a + * TransportResolver that will handle the resolution of the external address of + * the machine. This resolver can be initialized with several default resolvers, + * including a fixed solver that can be used when the address and port are know + * in advance. See TransportResolver or TransportManager for a complete list of + * resolution services. + * + * </p> + * + * Before creating an outgoing connection, the user must create session + * listeners that will be called when different events happen. The most + * important event is <i>sessionEstablished()</i>, that will be called when all + * the negotiations are finished, providing the payload type for the + * transmission as well as the remote and local addresses and ports for the + * communication. See JingleListener for a complete list of events that can be + * observed. + * + * </p> + * + * @see JingleListener + * @see TransportResolver + * @see TransportManager + * @see OutgoingJingleSession + * @see IncomingJingleSession + * + * </p> + * @author Alvaro Saurin + */ +public class JingleManager { + + // non-static + + // Listeners for manager events (ie, session requests...) + private List listeners; + + // The XMPP connection + private XMPPConnection connection; + + // The Jingle transport manager + private final TransportResolver resolver; + + static { + ProviderManager.addIQProvider("jingle", "http://jabber.org/protocol/jingle", + new org.jivesoftware.smackx.provider.JingleProvider()); + + ProviderManager.addExtensionProvider("description", "http://jabber.org/protocol/jingle/description/audio", + new org.jivesoftware.smackx.provider.JingleContentDescriptionProvider.Audio()); + + ProviderManager.addExtensionProvider("transport", "http://jabber.org/protocol/jingle/transport/ice", + new org.jivesoftware.smackx.provider.JingleTransportProvider.Ice()); + ProviderManager.addExtensionProvider("transport", "http://jabber.org/protocol/jingle/transport/raw-udp", + new org.jivesoftware.smackx.provider.JingleTransportProvider.RawUdp()); + + ProviderManager.addExtensionProvider("busy", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Busy()); + ProviderManager.addExtensionProvider("hold", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Hold()); + ProviderManager.addExtensionProvider("mute", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Mute()); + ProviderManager.addExtensionProvider("queued", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Queued()); + ProviderManager.addExtensionProvider("ringing", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Ringing()); + + + // Enable the Jingle support on every established connection + // The ServiceDiscoveryManager class should have been already + // initialized + XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() { + public void connectionEstablished(final XMPPConnection connection) { + JingleManager.setServiceEnabled(connection, true); + } + }); + } + + /** + * Private constructor + */ + private JingleManager() { + resolver = null; + } + + /** + * Default constructor, with a connection. + * + * @param conn + */ + public JingleManager(final XMPPConnection conn, final TransportResolver res) { + connection = conn; + resolver = res; + } + + /** + * Enables or disables the Jingle support on a given connection. + * <p> + * + * Before starting any Jingle media session, check that the user can handle + * it. Enable the Jingle support to indicate that this client handles Jingle + * messages. + * + * @param connection the connection where the service will be enabled or + * disabled + * @param enabled indicates if the service will be enabled or disabled + */ + public synchronized static void setServiceEnabled(final XMPPConnection connection, + final boolean enabled) { + if (isServiceEnabled(connection) == enabled) { + return; + } + + if (enabled) { + ServiceDiscoveryManager.getInstanceFor(connection).addFeature( + Jingle.NAMESPACE); + } else { + ServiceDiscoveryManager.getInstanceFor(connection).removeFeature( + Jingle.NAMESPACE); + } + } + + /** + * Returns true if the Jingle support is enabled for the given connection. + * + * @param connection the connection to look for Jingle support + * @return a boolean indicating if the Jingle support is enabled for the + * given connection + */ + public static boolean isServiceEnabled(final XMPPConnection connection) { + return ServiceDiscoveryManager.getInstanceFor(connection).includesFeature( + Jingle.NAMESPACE); + } + + /** + * Returns true if the specified user handles Jingle messages. + * + * @param connection the connection to use to perform the service discovery + * @param userID the user to check. A fully qualified xmpp ID, e.g. + * jdoe@example.com + * @return a boolean indicating whether the specified user handles Jingle + * messages + */ + public static boolean isServiceEnabled(final XMPPConnection connection, + final String userID) { + try { + DiscoverInfo result = ServiceDiscoveryManager.getInstanceFor(connection) + .discoverInfo(userID); + return result.containsFeature(Jingle.NAMESPACE); + } catch (XMPPException e) { + e.printStackTrace(); + return false; + } + } + + /** + * Add a Jingle session request listener to listen to incoming session + * requests. + * + * @param li The listener + * + * @see #removeJingleSessionRequestListener(JingleListener.SessionRequest) + * @see JingleListener + */ + public synchronized void addJingleSessionRequestListener( + final JingleListener.SessionRequest li) { + if (li != null) { + if (listeners == null) { + initJingleSessionRequestListeners(); + } + synchronized (listeners) { + listeners.add(li); + } + } + } + + /** + * Removes a Jingle session listener. + * + * @param li The jingle session listener to be removed + * @see #addJingleSessionRequestListener(JingleListener.SessionRequest) + * @see JingleListener + */ + public void removeJingleSessionRequestListener(final JingleListener.SessionRequest li) { + if (listeners == null) { + return; + } + synchronized (listeners) { + listeners.remove(li); + } + } + + /** + * Register the listeners, waiting for a Jingle packet that tries to + * establish a new session. + */ + private void initJingleSessionRequestListeners() { + PacketFilter initRequestFilter = new PacketFilter() { + // Return true if we accept this packet + public boolean accept(Packet pin) { + if (pin instanceof IQ) { + IQ iq = (IQ) pin; + if (iq.getType().equals(IQ.Type.SET)) { + if (iq instanceof Jingle) { + Jingle jin = (Jingle) pin; + if (jin.getAction().equals(Jingle.Action.SESSIONINITIATE)) { + return true; + } + } + } + } + return false; + } + }; + + listeners = new ArrayList(); + + // Start a packet listener for session initiation requests + connection.addPacketListener(new PacketListener() { + public void processPacket(final Packet packet) { + triggerSessionRequested((Jingle) packet); + } + }, initRequestFilter); + } + + /** + * Activates the listeners on a Jingle session request. + * + * @param initJin The packet that must be passed to the listeners. + */ + protected void triggerSessionRequested(final Jingle initJin) { + JingleListener.SessionRequest[] listeners = null; + + // Make a synchronized copy of the listeners + synchronized (this.listeners) { + listeners = new JingleListener.SessionRequest[this.listeners.size()]; + this.listeners.toArray(listeners); + } + + // ... and let them know of the event + JingleSessionRequest request = new JingleSessionRequest(this, initJin); + for (int i = 0; i < listeners.length; i++) { + listeners[i].sessionRequested(request); + } + } + + // Session creation + + /** + * Creates an Jingle session to start a communication with another user. + * + * @param responder The fully qualified jabber ID with resource of the other + * user. + * @return The session on which the negotiation can be run. + */ + public OutgoingJingleSession createOutgoingJingleSession(final String responder, + final List payloadTypes) { + + if (responder == null || StringUtils.parseName(responder).length() <= 0 + || StringUtils.parseServer(responder).length() <= 0 + || StringUtils.parseResource(responder).length() <= 0) { + throw new IllegalArgumentException( + "The provided user id was not fully qualified"); + } + + OutgoingJingleSession session = new OutgoingJingleSession(connection, responder, + payloadTypes, resolver); + return session; + } + + /** + * When the session request is acceptable, this method should be invoked. It + * will create an JingleSession which allows the negotiation to procede. + * + * @param request The remote request that is being accepted. + * @return The session which manages the rest of the negotiation. + */ + public IncomingJingleSession createIncomingJingleSession( + final JingleSessionRequest request, final List payloadTypes) { + if (request == null) { + throw new NullPointerException("Received request cannot be null"); + } + + IncomingJingleSession session = new IncomingJingleSession(connection, request + .getFrom(), payloadTypes, resolver); + + return session; + } + + /** + * Reject the session. If we don't want to accept the new session, send an + * appropriate error packet. + * + * @param request the request. + */ + protected void rejectIncomingJingleSession(final JingleSessionRequest request) { + Jingle initiation = request.getJingle(); + + IQ rejection = JingleSession.createError(initiation.getPacketID(), initiation + .getFrom(), initiation.getTo(), 403, "Declined"); + connection.sendPacket(rejection); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManagerTest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManagerTest.java new file mode 100644 index 000000000..277ac2258 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManagerTest.java @@ -0,0 +1,662 @@ +/** + * $RCSfile: JingleManagerTest.java,v $ + * $Revision: 1.1 $ + * $Date: 2006/10/17 19:12:42 $ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle; + +import java.util.ArrayList; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.nat.FixedResolver; +import org.jivesoftware.smackx.nat.TransportCandidate; +import org.jivesoftware.smackx.nat.TransportResolver; +import org.jivesoftware.smackx.packet.Jingle; + +/** + * Test the Jingle extension using the high level API + * </p> + * + * @author Alvaro Saurin + */ +public class JingleManagerTest extends SmackTestCase { + + private int counter; + + private final Object mutex = new Object(); + + /** + * Constructor for JingleManagerTest. + * + * @param name + */ + public JingleManagerTest(final String name) { + super(name); + + resetCounter(); + } + + // Counter management + + private void resetCounter() { + synchronized (mutex) { + counter = 0; + } + } + + private void incCounter() { + synchronized (mutex) { + counter++; + } + } + + private int valCounter() { + int val; + synchronized (mutex) { + val = counter; + } + return val; + } + + /** + * Generate a list of payload types + * + * @return A testing list + */ + private ArrayList getTestPayloads1() { + ArrayList result = new ArrayList(); + + result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000)); + result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000)); + result.add(new PayloadType.Audio(36, "supercodec-3", 2, 28000)); + result.add(new PayloadType.Audio(45, "supercodec-4", 1, 98000)); + + return result; + } + + private ArrayList getTestPayloads2() { + ArrayList result = new ArrayList(); + + result.add(new PayloadType.Audio(27, "supercodec-3", 2, 28000)); + result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000)); + result.add(new PayloadType.Audio(32, "supercodec-4", 1, 98000)); + result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000)); + + return result; + } + + private ArrayList getTestPayloads3() { + ArrayList result = new ArrayList(); + + result.add(new PayloadType.Audio(91, "badcodec-1", 2, 28000)); + result.add(new PayloadType.Audio(92, "badcodec-2", 1, 44000)); + result.add(new PayloadType.Audio(93, "badcodec-3", 1, 98000)); + result.add(new PayloadType.Audio(94, "badcodec-4", 2, 14000)); + + return result; + } + + /** + * Test for the session request detection. Here, we use the same filter we + * use in the JingleManager... + */ + public void testInitJingleSessionRequestListeners() { + + resetCounter(); + + PacketFilter initRequestFilter = new PacketFilter() { + // Return true if we accept this packet + public boolean accept(Packet pin) { + if (pin instanceof IQ) { + IQ iq = (IQ) pin; + if (iq.getType().equals(IQ.Type.SET)) { + if (iq instanceof Jingle) { + Jingle jin = (Jingle) pin; + if (jin.getAction().equals(Jingle.Action.SESSIONINITIATE)) { + System.out + .println("Session initiation packet accepted... "); + return true; + } + } + } + } + return false; + } + }; + + // Start a packet listener for session initiation requests + getConnection(0).addPacketListener(new PacketListener() { + public void processPacket(final Packet packet) { + System.out.println("Packet detected... "); + incCounter(); + } + }, initRequestFilter); + + // Create a dummy packet for testing... + IQfake iqSent = new IQfake( + " <jingle xmlns='http://jabber.org/protocol/jingle'" + + " initiator=\"gorrino@viejo.com\"" + + " responder=\"colico@hepatico.com\"" + + " action=\"session-initiate\" sid=\"08666555\">" + + " <description xmlns='http://jabber.org/protocol/jingle/content/audio'>" + + " <payload-type id=\"34\" name=\"supercodec-34\"/>" + + " <payload-type id=\"23\" name=\"supercodec-23\"/>" + + " </description>" + + " <transport xmlns='http://jabber.org/protocol/jingle/transport/ice'>" + + " <candidate generation=\"1\"" + " ip=\"192.168.1.1\"" + + " password=\"secret\"" + " port=\"8080\"" + + " username=\"username\"" + " preference=\"1\"/>" + + " </transport>" + "</jingle>"); + + iqSent.setTo(getFullJID(0)); + iqSent.setFrom(getFullJID(0)); + iqSent.setType(IQ.Type.SET); + + System.out.println("Sending packet and waiting... "); + getConnection(0).sendPacket(iqSent); + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + } + + System.out.println("Awake... "); + assertTrue(valCounter() > 0); + } + + /** + * High level API test. This is a simple test to use with a XMPP client and + * check if the client receives the message 1. User_1 will send an + * invitation to user_2. + */ + public void testSendSimpleMessage() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + JingleManager man0 = new JingleManager(getConnection(0), tr1); + JingleManager man1 = new JingleManager(getConnection(1), tr2); + + // Session 1 waits for connections + man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + incCounter(); + System.out.println("Session request detected, from " + + request.getFrom()); + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + session0.start(null); + + Thread.sleep(5000); + + assertTrue(valCounter() > 0); + + } catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * High level API test. This is a simple test to use with a XMPP client and + * check if the client receives the message 1. User_1 will send an + * invitation to user_2. + */ + public void testAcceptJingleSession() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + incCounter(); + System.out.println("Session request detected, from " + + request.getFrom() + ": accepting."); + + // We accept the request + IncomingJingleSession session1 = request.accept(getTestPayloads2()); + try { + session1.start(request); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() > 0); + + } catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * This is a simple test where both endpoints have exactly the same payloads + * and the session is accepted. + */ + public void testEqualPayloadsSetSession() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54213); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54531); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + System.out.println("Session request detected, from " + + request.getFrom() + ": accepting."); + + // We accept the request + IncomingJingleSession session1 = request.accept(getTestPayloads1()); + + session1.addListener(new JingleListener.Session() { + public void sessionClosed(final String reason) { + System.out.println("sessionClosed()."); + } + + public void sessionClosedOnError(final XMPPException e) { + System.out.println("sessionClosedOnError()."); + } + + public void sessionDeclined(final String reason) { + System.out.println("sessionDeclined()."); + } + + public void sessionEstablished(final PayloadType pt, + final TransportCandidate rc, final TransportCandidate lc) { + incCounter(); + System.out + .println("Responder: the session is fully established."); + System.out.println("+ Payload Type: " + pt.getId()); + System.out.println("+ Local IP/port: " + lc.getIP() + ":" + + lc.getPort()); + System.out.println("+ Remote IP/port: " + rc.getIP() + ":" + + rc.getPort()); + } + + public void sessionRedirected(final String redirection) { + } + }); + + try { + session1.start(request); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + // Session 0 starts a request + System.out.println("Starting session request with equal payloads, to " + + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() == 2); + + } catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * This is a simple test where the user_2 rejects the Jingle session. + * + */ + public void testStagesSession() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + System.out.println("Session request detected, from " + + request.getFrom() + ": accepting."); + + // We accept the request + IncomingJingleSession session1 = request.accept(getTestPayloads2()); + + session1.addListener(new JingleListener.Session() { + public void sessionClosed(final String reason) { + System.out.println("sessionClosed()."); + } + + public void sessionClosedOnError(final XMPPException e) { + System.out.println("sessionClosedOnError()."); + } + + public void sessionDeclined(final String reason) { + System.out.println("sessionDeclined()."); + } + + public void sessionEstablished(final PayloadType pt, + final TransportCandidate rc, final TransportCandidate lc) { + incCounter(); + System.out + .println("Responder: the session is fully established."); + System.out.println("+ Payload Type: " + pt.getId()); + System.out.println("+ Local IP/port: " + lc.getIP() + ":" + + lc.getPort()); + System.out.println("+ Remote IP/port: " + rc.getIP() + ":" + + rc.getPort()); + } + + public void sessionRedirected(final String redirection) { + } + }); + + try { + session1.start(request); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + + session0.addListener(new JingleListener.Session() { + public void sessionClosed(final String reason) { + } + + public void sessionClosedOnError(final XMPPException e) { + } + + public void sessionDeclined(final String reason) { + } + + public void sessionEstablished(final PayloadType pt, + final TransportCandidate rc, final TransportCandidate lc) { + incCounter(); + System.out.println("Initiator: the session is fully established."); + System.out.println("+ Payload Type: " + pt.getId()); + System.out.println("+ Local IP/port: " + lc.getIP() + ":" + + lc.getPort()); + System.out.println("+ Remote IP/port: " + rc.getIP() + ":" + + rc.getPort()); + } + + public void sessionRedirected(final String redirection) { + } + }); + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() == 2); + + } catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * This is a simple test where the user_2 rejects the Jingle session. + */ + public void testRejectSession() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + System.out.println("Session request detected, from " + + request.getFrom() + ": rejecting."); + + // We reject the request + request.reject(); + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + + session0.addListener(new JingleListener.Session() { + public void sessionClosed(final String reason) { + } + + public void sessionClosedOnError(final XMPPException e) { + } + + public void sessionDeclined(final String reason) { + incCounter(); + System.out + .println("The session has been detected as rejected with reason: " + + reason); + } + + public void sessionEstablished(final PayloadType pt, + final TransportCandidate rc, final TransportCandidate lc) { + } + + public void sessionRedirected(final String redirection) { + } + }); + + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() > 0); + + } catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * This is a simple test where the user_2 rejects the Jingle session. + */ + public void testIncompatibleCodecs() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + System.out.println("Session request detected, from " + + request.getFrom() + ": accepting."); + + // We reject the request + IncomingJingleSession ses = request.accept(getTestPayloads3()); + try { + ses.start(request); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + + session0.addListener(new JingleListener.Session() { + public void sessionClosed(final String reason) { + } + + public void sessionClosedOnError(final XMPPException e) { + incCounter(); + System.out + .println("The session has been close on error with reason: " + + e.getMessage()); + } + + public void sessionDeclined(final String reason) { + incCounter(); + System.out + .println("The session has been detected as rejected with reason: " + + reason); + } + + public void sessionEstablished(final PayloadType pt, + final TransportCandidate rc, final TransportCandidate lc) { + } + + public void sessionRedirected(final String redirection) { + } + }); + + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() > 0); + + } catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + protected int getMaxConnections() { + return 2; + } + + /** + * Simple class for testing an IQ... + * + * @author Alvaro Saurin + */ + private class IQfake extends IQ { + private String s; + + public IQfake(final String s) { + super(); + this.s = s; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append(s); + return buf.toString(); + } + } +} + diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleNegotiator.java new file mode 100644 index 000000000..f7b1ce54e --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleNegotiator.java @@ -0,0 +1,341 @@ +package org.jivesoftware.smackx.jingle; + +import java.util.ArrayList; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleError; + +/** + * Basic Jingle negotiator. + * + * </p> + * + * JingleNegotiator implements some basic behavior for every Jingle negotiation. + * It implements a "state" pattern: each stage should process Jingle packets and + * act depending on the current state in the negotiation... + * + * </p> + * + * @author Alvaro Saurin + */ +public abstract class JingleNegotiator { + + private State state; // Current negotiation state + + private XMPPConnection connection; // The connection associated + + private final ArrayList listeners = new ArrayList(); + + private String expectedAckId; + + /** + * Default constructor. + */ + public JingleNegotiator() { + this(null); + } + + /** + * Default constructor with a XMPPConnection + * + * @param connection the connection associated + */ + public JingleNegotiator(final XMPPConnection connection) { + this.connection = connection; + state = null; + } + + /** + * Get the XMPP connection associated with this negotiation. + * + * @return the connection + */ + public XMPPConnection getConnection() { + return connection; + } + + /** + * Set the XMPP connection associated. + * + * @param connection the connection to set + */ + public void setConnection(final XMPPConnection connection) { + this.connection = connection; + } + + /** + * Inform if current state is null + * + * @return true if current state is null + */ + public boolean invalidState() { + return state == null; + } + + /** + * Return the current state + * + * @return the state + */ + public State getState() { + return state; + } + + /** + * Return the current state class + * + * @return the state + */ + public Class getStateClass() { + if (state != null) { + return state.getClass(); + } else { + return Object.class; + } + } + + /** + * Set the new state. + * + * @param state the state to set + * @throws XMPPException + */ + protected void setState(final State newState) { + boolean transition = newState != state; + + if (transition && state != null) { + state.eventExit(); + } + + state = newState; + + if (transition && state != null) { + state.eventEnter(); + } + } + + // Acks management + + public void addExpectedId(final String id) { + expectedAckId = id; + } + + public boolean isExpectedId(final String id) { + if (id != null) { + return id.equals(expectedAckId); + } else { + return false; + } + } + + public void removeExpectedId(final String id) { + addExpectedId((String) null); + } + + // Listeners + + /** + * Add a Jingle session listener to listen to incoming session requests. + * + * @param li The listener + * + * @see JingleListener + */ + public void addListener(final JingleListener li) { + synchronized (listeners) { + listeners.add(li); + } + } + + /** + * Removes a Jingle session listener. + * + * @param li The jingle session listener to be removed + * @see JingleListener + */ + public void removeListener(final JingleListener li) { + synchronized (listeners) { + listeners.remove(li); + } + } + + /** + * Get a copy of the listeners + * + * @return a copy of the listeners + */ + protected ArrayList getListenersList() { + ArrayList result; + + synchronized (listeners) { + result = new ArrayList(listeners); + } + + return result; + } + + /** + * Dispatch an incomming packet. This method is responsible for recognizing + * the packet type and, depending on the current state, deliverying the + * packet to the right event handler and wait for a response. + * + * @param iq the packet received + * @param id the ID of the response that will be sent + * @return the new packet to send (either a Jingle or an IQ error). + * @throws XMPPException + */ + public abstract IQ dispatchIncomingPacket(final IQ iq, final String id) + throws XMPPException; + + /** + * Close the negotiation. + */ + public void close() { + setState(null); + } + + /** + * A Jingle exception. + * + * @author Alvaro Saurin <alvaro.saurin@gmail.com> + */ + public static class JingleException extends XMPPException { + + private final JingleError error; + + /** + * Default constructor. + */ + public JingleException() { + super(); + error = null; + } + + /** + * Constructor with an error message. + * + * @param error The message. + */ + public JingleException(final String msg) { + super(msg); + error = null; + } + + /** + * Constructor with an error response. + * + * @param error The error message. + */ + public JingleException(final JingleError error) { + super(); + this.error = error; + } + + /** + * Return the error message. + * + * @return the error + */ + public JingleError getError() { + return error; + } + } + + /** + * Negotiation state and events. + * + * </p> + * + * Describes the negotiation stage. + */ + public static class State { + + private JingleNegotiator neg; // The negotiator + + /** + * Default constructor, with a reference to the negotiator. + * + * @param neg The negotiator instance. + */ + public State(final JingleNegotiator neg) { + this.neg = neg; + } + + /** + * Get the negotiator + * + * @return the negotiator. + */ + public JingleNegotiator getNegotiator() { + return neg; + } + + /** + * Set the negotiator. + * + * @param neg the neg to set + */ + public void setNegotiator(final JingleNegotiator neg) { + this.neg = neg; + } + + // State transition events + + public Jingle eventAck(final IQ iq) throws XMPPException { + // We have received an Ack + return null; + } + + public void eventError(final IQ iq) throws XMPPException { + throw new JingleException(iq.getError().getMessage()); + } + + public Jingle eventInvite() throws XMPPException { + throw new IllegalStateException( + "Negotiation can not be started in this state."); + } + + public Jingle eventInitiate(final Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventAccept(final Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventRedirect(final Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventModify(final Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventDecline(final Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventInfo(final Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventTerminate(final Jingle jin) throws XMPPException { + if (neg != null) { + neg.close(); + } + return null; + } + + public void eventEnter() { + } + + public void eventExit() { + if (neg != null) { + neg.removeExpectedId(null); + } + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSession.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSession.java new file mode 100644 index 000000000..39d686fd3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSession.java @@ -0,0 +1,1020 @@ +/** + * $RCSfile: JingleSession.java,v $ + * $Revision: 1.1 $ + * $Date: 2006/10/17 19:12:42 $ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.nat.TransportCandidate; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleContentInfo; +import org.jivesoftware.smackx.packet.JingleError; +import org.jivesoftware.smackx.packet.JingleTransport; +import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType; +import org.jivesoftware.smackx.packet.JingleTransport.JingleTransportCandidate; + +/** + * A Jingle session. + * + * </p> + * + * This class contains some basic properties of every Jingle session. However, + * the concrete implementation will be found in subclasses. + * + * </p> + * + * @see IncomingJingleSession + * @see OutgoingJingleSession + * + * </p> + * @author Alvaro Saurin + */ +public abstract class JingleSession extends JingleNegotiator { + + // static + + private static final HashMap sessions = new HashMap(); + + private static final Random randomGenerator = new Random(); + + // non-static + + private String initiator; // Who started the communication + + private String responder; // The other endpoint + + private String sid; // A unique id that identifies this session + + private MediaNegotiator mediaNeg; // The description... + + private TransportNegotiator transNeg; // and transport negotiators + + PacketListener packetListener; + + PacketFilter packetFilter; + + /** + * Default constructor. + */ + public JingleSession(final XMPPConnection conn, final String ini, final String res, + final String sessionid) { + super(conn); + + mediaNeg = null; + transNeg = null; + + initiator = ini; + responder = res; + sid = sessionid; + + // Add the session to the list and register the listeneres + registerInstance(); + installConnectionListeners(conn); + } + + /** + * Default constructor without session id. + */ + public JingleSession(final XMPPConnection conn, final String ini, final String res) { + this(conn, ini, res, null); + } + + /** + * Get the session initiator + * + * @return the initiator + */ + public String getInitiator() { + return initiator; + } + + /** + * Set the session initiator + * + * @param initiator the initiator to set + */ + public void setInitiator(final String initiator) { + this.initiator = initiator; + } + + /** + * Get the session responder + * + * @return the responder + */ + public String getResponder() { + return responder; + } + + /** + * Set the session responder. + * + * @param responder the receptor to set + */ + public void setResponder(final String responder) { + this.responder = responder; + } + + /** + * Get the session ID + * + * @return the sid + */ + public String getSid() { + return sid; + } + + /** + * Set the session ID + * + * @param sid the sid to set + */ + protected void setSid(final String sessionId) { + sid = sessionId; + } + + /** + * Generate a unique session ID. + */ + protected String generateSessionId() { + StringBuffer buffer = new StringBuffer(); + buffer.append(Math.abs(randomGenerator.nextLong())); + + return buffer.toString(); + } + + /** + * Obtain the description negotiator for this session + * + * @return the description negotiator + */ + protected MediaNegotiator getMediaNeg() { + return mediaNeg; + } + + /** + * Set the media negotiator. + * + * @param mediaNeg the description negotiator to set + */ + protected void setMediaNeg(final MediaNegotiator mediaNeg) { + destroyMediaNeg(); + this.mediaNeg = mediaNeg; + } + + /** + * Destroy the media negotiator. + */ + protected void destroyMediaNeg() { + if (mediaNeg != null) { + mediaNeg.close(); + mediaNeg = null; + } + } + + /** + * Obtain the transport negotiator for this session. + * + * @return the transport negotiator instance + */ + protected TransportNegotiator getTransportNeg() { + return transNeg; + } + + /** + * @param transNeg the transNeg to set + */ + protected void setTransportNeg(final TransportNegotiator transNeg) { + destroyTransportNeg(); + this.transNeg = transNeg; + } + + /** + * Destroy the transport negotiator. + */ + protected void destroyTransportNeg() { + if (transNeg != null) { + transNeg.close(); + transNeg = null; + } + } + + /** + * Return true if the transport and content negotiators have finished + */ + public boolean isFullyEstablished() { + if (!isValid()) { + return false; + } + if (!getTransportNeg().isFullyEstablished() + || !getMediaNeg().isFullyEstablished()) { + return false; + } + return true; + } + + /** + * Return true if the session is valid (<i>ie</i>, it has all the required + * elements initialized). + * + * @return true if the session is valid. + */ + public boolean isValid() { + return mediaNeg != null && transNeg != null && sid != null && initiator != null; + } + + /** + * Dispatch an incoming packet. The medthod is responsible for recognizing + * the packet type and, depending on the current state, deliverying the + * packet to the right event handler and wait for a response. + * + * @param iq the packet received + * @return the new Jingle packet to send. + * @throws XMPPException + */ + public IQ dispatchIncomingPacket(final IQ iq, final String id) throws XMPPException { + IQ jout = null; + + if (invalidState()) { + throw new IllegalStateException( + "Illegal state in dispatch packet in Session manager."); + } else { + if (iq == null) { + // If there is no input packet, then we must be inviting... + jout = getState().eventInvite(); + } else { + if (iq.getType().equals(IQ.Type.ERROR)) { + // Process errors + getState().eventError(iq); + } else if (iq.getType().equals(IQ.Type.RESULT)) { + // Process ACKs + if (isExpectedId(iq.getPacketID())) { + jout = getState().eventAck(iq); + removeExpectedId(iq.getPacketID()); + } + } else if (iq instanceof Jingle) { + // It is not an error: it is a Jingle packet... + Jingle jin = (Jingle) iq; + Jingle.Action action = jin.getAction(); + + if (action != null) { + if (action.equals(Jingle.Action.SESSIONACCEPT)) { + jout = getState().eventAccept(jin); + } else if (action.equals(Jingle.Action.SESSIONINFO)) { + jout = getState().eventInfo(jin); + } else if (action.equals(Jingle.Action.SESSIONINITIATE)) { + jout = getState().eventInitiate(jin); + } else if (action.equals(Jingle.Action.SESSIONREDIRECT)) { + jout = getState().eventRedirect(jin); + } else if (action.equals(Jingle.Action.SESSIONTERMINATE)) { + jout = getState().eventTerminate(jin); + } + } else { + jout = errorMalformedStanza(iq); + } + } + } + + if (jout != null) { + // Save the packet id, for recognizing ACKs... + addExpectedId(jout.getPacketID()); + } + } + + return jout; + } + + /** + * Process and respond to an incomming packet. + * + * This method is called from the packet listener dispatcher when a new + * packet has arrived. The medthod is responsible for recognizing the packet + * type and, depending on the current state, deliverying it to the right + * event handler and wait for a response. The response will be another + * Jingle packet that will be sent to the other endpoint. + * + * @param iq the packet received + * @return the new Jingle packet to send. + * @throws XMPPException + */ + public synchronized IQ respond(final IQ iq) throws XMPPException { + IQ response = null; + + if (isValid()) { + String responseId = null; + IQ sessionResponse = null; + IQ descriptionResponse = null; + IQ transportResponse = null; + + // Send the packet to the right event handler for the session... + try { + sessionResponse = dispatchIncomingPacket(iq, null); + if (sessionResponse != null) { + responseId = sessionResponse.getPacketID(); + } + + // ... and do the same for the Description and Transport + // parts... + if (mediaNeg != null) { + descriptionResponse = mediaNeg.dispatchIncomingPacket(iq, responseId); + } + + if (transNeg != null) { + transportResponse = transNeg.dispatchIncomingPacket(iq, responseId); + } + + // Acknowledge the IQ reception + sendAck(iq); + + // ... and send all these parts in a Jingle response. + response = sendJingleParts(iq, (Jingle) sessionResponse, + (Jingle) descriptionResponse, (Jingle) transportResponse); + + } catch (JingleException e) { + // Send an error message, if present + JingleError error = e.getError(); + if (error != null) { + sendFormattedError(iq, error); + } + + // Notify the session end and close everything... + triggerSessionClosedOnError(e); + close(); + } + } + + return response; + } + + // Packet formatting and delivery + + /** + * Put together all the parts ina Jingle packet. + * + * @return the new Jingle packet + */ + private Jingle sendJingleParts(final IQ iq, final Jingle jSes, final Jingle jDesc, + final Jingle jTrans) { + Jingle response = null; + + if (jSes != null) { + jSes.addDescriptions(jDesc.getDescriptionsList()); + jSes.addTransports(jTrans.getTransportsList()); + + response = sendFormattedJingle(iq, jSes); + } else { + // If we don't have a valid session message, then we must send + // separated messages for transport and media... + if (jDesc != null) { + response = sendFormattedJingle(iq, jDesc); + } + + if (jTrans != null) { + response = sendFormattedJingle(iq, jTrans); + } + } + + return response; + } + + /** + * Complete and send an error. Complete all the null fields in an IQ error + * reponse, using the sesssion information we have or some info from the + * incoming packet. + * + * @param jin The Jingle packet we are responing to + * @param pout the IQ packet we want to complete and send + */ + protected IQ sendFormattedError(final IQ iq, final JingleError error) { + IQ perror = null; + if (error != null) { + perror = createIQ(getSid(), iq.getFrom(), iq.getTo(), IQ.Type.ERROR); + + // Fill in the fields with the info from the Jingle packet + perror.setPacketID(iq.getPacketID()); + perror.addExtension(error); + + getConnection().sendPacket(perror); + } + return perror; + } + + /** + * Complete and send a packet. Complete all the null fields in a Jingle + * reponse, using the session information we have or some info from the + * incoming packet. + * + * @param jin The Jingle packet we are responing to + * @param jout the Jingle packet we want to complete and send + */ + protected Jingle sendFormattedJingle(final IQ iq, final Jingle jout) { + if (jout != null) { + if (jout.getInitiator() == null) { + jout.setInitiator(getInitiator()); + } + + if (jout.getResponder() == null) { + jout.setResponder(getResponder()); + } + + if (jout.getSid() == null) { + jout.setSid(getSid()); + } + + String me = getConnection().getUser(); + String other = getResponder().equals(me) ? getInitiator() : getResponder(); + + if (jout.getTo() == null) { + if (iq != null) { + jout.setTo(iq.getFrom()); + } else { + jout.setTo(other); + } + } + + if (jout.getFrom() == null) { + if (iq != null) { + jout.setFrom(iq.getTo()); + } else { + jout.setFrom(me); + } + } + + getConnection().sendPacket(jout); + } + return jout; + } + + /** + * Complete and send a packet. Complete all the null fields in a Jingle + * reponse, using the session information we have. + * + * @param jout the Jingle packet we want to complete and send + */ + protected Jingle sendFormattedJingle(final Jingle jout) { + return sendFormattedJingle(null, jout); + } + + /** + * Send an error indicating that the stanza is malformed. + * + * @param iq + */ + protected IQ errorMalformedStanza(final IQ iq) { + // FIXME: implement with the right message... + return createError(iq.getPacketID(), iq.getFrom(), getConnection().getUser(), + 400, "Bad Request"); + } + + /** + * Check if we have an established session and, in that case, send an Accept + * packet. + */ + protected Jingle sendAcceptIfFullyEstablished() { + Jingle result = null; + if (isFullyEstablished()) { + // Ok, send a packet saying that we accept this session + Jingle jout = new Jingle(Jingle.Action.SESSIONACCEPT); + jout.setType(IQ.Type.SET); + + result = sendFormattedJingle(jout); + } + return result; + } + + /** + * Acknowledge a IQ packet. + * + * @param iq The IQ to acknowledge + */ + private IQ sendAck(final IQ iq) { + IQ result = null; + + if (iq != null) { + // Don't acknowledge ACKs, errors... + if (iq.getType().equals(IQ.Type.SET)) { + IQ ack = createIQ(iq.getPacketID(), iq.getFrom(), iq.getTo(), + IQ.Type.RESULT); + + getConnection().sendPacket(ack); + result = ack; + } + } + return result; + } + + /** + * Send a content info message. + */ + public synchronized void sendContentInfo(final ContentInfo ci) { + if (isValid()) { + sendFormattedJingle(new Jingle(new JingleContentInfo(ci))); + } + } + + /** + * Get the content description the other part has accepted. + * + * @param jin The Jingle packet where they have accepted the session. + * @return The audio PayloadType they have accepted. + * @throws XMPPException + */ + protected PayloadType.Audio getAcceptedAudioPayloadType(final Jingle jin) + throws XMPPException { + PayloadType.Audio acceptedPayloadType = null; + ArrayList jda = jin.getDescriptionsList(); + + if (jin.getAction().equals(Jingle.Action.SESSIONACCEPT)) { + + if (jda.size() > 1) { + throw new XMPPException( + "Unsupported feature: the number of accepted content descriptions is greater than 1."); + } else if (jda.size() == 1) { + JingleContentDescription jd = (JingleContentDescription) jda.get(0); + if (jd.getJinglePayloadTypesCount() > 1) { + throw new XMPPException( + "Unsupported feature: the number of accepted payload types is greater than 1."); + } + if (jd.getJinglePayloadTypesCount() == 1) { + JinglePayloadType jpt = (JinglePayloadType) jd + .getJinglePayloadTypesList().get(0); + acceptedPayloadType = (PayloadType.Audio) jpt.getPayloadType(); + } + } + } + return acceptedPayloadType; + } + + /** + * Get the accepted local candidate we have previously offered. + * + * @param jin The jingle packet where they accept the session + * @return The transport candidate they have accepted. + * @throws XMPPException + */ + protected TransportCandidate getAcceptedLocalCandidate(final Jingle jin) + throws XMPPException { + ArrayList jta = jin.getTransportsList(); + TransportCandidate acceptedLocalCandidate = null; + + if (jin.getAction().equals(Jingle.Action.SESSIONACCEPT)) { + if (jta.size() > 1) { + throw new XMPPException( + "Unsupported feature: the number of accepted transports is greater than 1."); + } else if (jta.size() == 1) { + JingleTransport jt = (JingleTransport) jta.get(0); + + if (jt.getCandidatesCount() > 1) { + throw new XMPPException( + "Unsupported feature: the number of accepted transport candidates is greater than 1."); + } else if (jt.getCandidatesCount() == 1) { + JingleTransportCandidate jtc = (JingleTransportCandidate) jt + .getCandidatesList().get(0); + acceptedLocalCandidate = jtc.getMediaTransport(); + } + } + } + + return acceptedLocalCandidate; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + return Jingle.getSessionHash(getSid(), getInitiator()); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final JingleSession other = (JingleSession) obj; + + if (initiator == null) { + if (other.initiator != null) { + return false; + } + } else if (!initiator.equals(other.initiator)) { + return false; + } + + if (responder == null) { + if (other.responder != null) { + return false; + } + } else if (!responder.equals(other.responder)) { + return false; + } + + if (sid == null) { + if (other.sid != null) { + return false; + } + } else if (!sid.equals(other.sid)) { + return false; + } + + return true; + } + + // Instances management + + /** + * Clean a session from the list. + * + * @param connection The connection to clean up + */ + private void unregisterInstanceFor(final XMPPConnection connection) { + synchronized (sessions) { + sessions.remove(connection); + } + } + + /** + * Register this instance. + */ + private void registerInstance() { + synchronized (sessions) { + sessions.put(getConnection(), this); + } + } + + /** + * Returns the JingleSession related to a particular connection. + * + * @param con A XMPP connection + * @return a Jingle session + */ + public static JingleSession getInstanceFor(final XMPPConnection con) { + if (con == null) { + throw new IllegalArgumentException("Connection cannot be null"); + } + + JingleSession result = null; + synchronized (sessions) { + if (sessions.containsKey(con)) { + result = (JingleSession) sessions.get(con); + } + } + + return result; + } + + /** + * Configure a session, setting some action listeners... + * + * @param session The connection to set up + */ + private void installConnectionListeners(final XMPPConnection connection) { + if (connection != null) { + connection.addConnectionListener(new ConnectionListener() { + public void connectionClosed() { + unregisterInstanceFor(connection); + } + + public void connectionClosedOnError(final java.lang.Exception e) { + unregisterInstanceFor(connection); + } + }); + } + } + + /** + * Remove the packet listener used for processing packet. + */ + protected void removePacketListener() { + if (packetListener != null) { + getConnection().removePacketListener(packetListener); + } + } + + /** + * Install the packet listener. The listener is responsible for responding + * to any packet that we receive... + */ + protected void updatePacketListener() { + removePacketListener(); + + packetListener = new PacketListener() { + public void processPacket(final Packet packet) { + try { + respond((IQ) packet); + } catch (XMPPException e) { + e.printStackTrace(); + } + } + }; + + packetFilter = new PacketFilter() { + public boolean accept(final Packet packet) { + if (packet instanceof IQ) { + IQ iq = (IQ) packet; + + String me = getConnection().getUser(); + + if (!iq.getTo().equals(me)) { + return false; + } + + String other = getResponder().equals(me) ? getInitiator() + : getResponder(); + + if (!iq.getFrom().equals(other)) { + return false; + } + + if (iq instanceof Jingle) { + Jingle jin = (Jingle) iq; + + String sid = jin.getSid(); + if (!sid.equals(getSid())) { + return false; + } + String ini = jin.getInitiator(); + if (!ini.equals(getInitiator())) { + return false; + } + } else { + // We accept some non-Jingle IQ packets: ERRORs and ACKs + if (iq.getType().equals(IQ.Type.SET)) { + return false; + } else if (iq.getType().equals(IQ.Type.GET)) { + return false; + } + } + return true; + } + return false; + } + }; + + getConnection().addPacketListener(packetListener, packetFilter); + } + + // Listeners + + /** + * Add a listener for media negotiation events + * + * @param li The listener + */ + public void addMediaListener(final JingleListener.Media li) { + if (getMediaNeg() != null) { + getMediaNeg().addListener(li); + } + } + + /** + * Remove a listener for media negotiation events + * + * @param li The listener + */ + public void removeMediaListener(final JingleListener.Media li) { + if (getMediaNeg() != null) { + getMediaNeg().removeListener(li); + } + } + + /** + * Add a listener for transport negotiation events + * + * @param li The listener + */ + public void addTransportListener(final JingleListener.Transport li) { + if (getTransportNeg() != null) { + getTransportNeg().addListener(li); + } + } + + /** + * Remove a listener for transport negotiation events + * + * @param li The listener + */ + public void removeTransportListener(final JingleListener.Transport li) { + if (getTransportNeg() != null) { + getTransportNeg().removeListener(li); + } + } + + // Triggers + + /** + * Trigger a session closed event. + */ + protected void triggerSessionClosed(final String reason) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleListener.Session) { + JingleListener.Session sli = (JingleListener.Session) li; + sli.sessionClosed(reason); + } + } + } + + /** + * Trigger a session closed event due to an error. + */ + protected void triggerSessionClosedOnError(final XMPPException exc) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleListener.Session) { + JingleListener.Session sli = (JingleListener.Session) li; + sli.sessionClosedOnError(exc); + } + } + } + + /** + * Trigger a session established event. + */ + protected void triggerSessionEstablished(final PayloadType pt, + final TransportCandidate rc, final TransportCandidate lc) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleListener.Session) { + JingleListener.Session sli = (JingleListener.Session) li; + sli.sessionEstablished(pt, rc, lc); + } + } + } + + /** + * Trigger a session redirect event. + */ + protected void triggerSessionRedirect(final String arg) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleListener.Session) { + JingleListener.Session sli = (JingleListener.Session) li; + sli.sessionRedirected(arg); + } + } + } + + /** + * Trigger a session redirect event. + */ + protected void triggerSessionDeclined(final String reason) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleListener.Session) { + JingleListener.Session sli = (JingleListener.Session) li; + sli.sessionDeclined(reason); + } + } + } + + /** + * Start the negotiation. + * + * @throws JingleException + * @throws XMPPException + */ + public abstract void start(final JingleSessionRequest jin) throws XMPPException; + + /** + * Terminate negotiations. + */ + public void close() { + destroyMediaNeg(); + destroyTransportNeg(); + + removePacketListener(); + + super.close(); + } + + // Packet and error creation + + /** + * A convience method to create an IQ packet. + * + * @param ID The packet ID of the + * @param to To whom the packet is addressed. + * @param from From whom the packet is sent. + * @param type The iq type of the packet. + * @return The created IQ packet. + */ + public static IQ createIQ(final String ID, final String to, final String from, + final IQ.Type type) { + IQ iqPacket = new IQ() { + public String getChildElementXML() { + return null; + } + }; + + iqPacket.setPacketID(ID); + iqPacket.setTo(to); + iqPacket.setFrom(from); + iqPacket.setType(type); + + return iqPacket; + } + + /** + * A convience method to create an error packet. + * + * @param ID The packet ID of the + * @param to To whom the packet is addressed. + * @param from From whom the packet is sent. + * @param errCode The error code. + * @param errStr The error string. + * + * @return The created IQ packet. + */ + public static IQ createError(final String ID, final String to, final String from, + final int errCode, final String errStr) { + + IQ iqError = createIQ(ID, to, from, IQ.Type.ERROR); + XMPPError error = new XMPPError(errCode, errStr); + iqError.setError(error); + + return iqError; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionRequest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionRequest.java new file mode 100644 index 000000000..53edbf667 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionRequest.java @@ -0,0 +1,91 @@ +package org.jivesoftware.smackx.jingle; + +import java.util.List; + +import org.jivesoftware.smackx.packet.Jingle; + +/** + * A Jingle session request. + * + * </p> + * + * This class is a facade of a received Jingle request. The user can have direct + * access to the Jingle packet (<i>JingleSessionRequest.getJingle() </i>) of + * the request or can use the convencience methods provided by this class. + * + * </p> + * + * @author Alvaro Saurin + */ +public class JingleSessionRequest { + + private final Jingle jingle; // The Jingle packet + + private final JingleManager manager; // The manager associated to this + + // request + + /** + * A recieve request is constructed from the Jingle Initiation request + * received from the initator. + * + * @param manager The manager handling this request + * @param jingle The jingle IQ recieved from the initiator. + */ + public JingleSessionRequest(final JingleManager manager, final Jingle jingle) { + this.manager = manager; + this.jingle = jingle; + } + + /** + * Returns the fully-qualified jabber ID of the user that requested this + * session. + * + * @return Returns the fully-qualified jabber ID of the user that requested + * this session. + */ + public String getFrom() { + return jingle.getFrom(); + } + + /** + * Returns the session ID that uniquely identifies this session. + * + * @return Returns the session ID that uniquely identifies this session + */ + public String getSessionID() { + return jingle.getSid(); + } + + /** + * Returns the Jingle packet that was sent by the requestor which contains + * the parameters of the session. + */ + protected Jingle getJingle() { + return jingle; + } + + /** + * Accepts this request and creates the incoming Jingle session. + * + * @return Returns the <b><i>IncomingJingleSession</b></i> on which the + * negotiation can be carried out. + */ + public synchronized IncomingJingleSession accept(final List pts) { + IncomingJingleSession session = null; + synchronized (manager) { + session = manager.createIncomingJingleSession(this, + pts); + } + return session; + } + + /** + * Rejects the session request. + */ + public synchronized void reject() { + synchronized (manager) { + manager.rejectIncomingJingleSession(this); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionTest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionTest.java new file mode 100644 index 000000000..6ce8add71 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionTest.java @@ -0,0 +1,43 @@ +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.test.SmackTestCase; + +public class JingleSessionTest extends SmackTestCase { + + public JingleSessionTest(final String name) { + super(name); + } + + public void testEqualsObject() { + JingleSession js1 = new OutgoingJingleSession(getConnection(0), "res1", null, null); + JingleSession js2 = new OutgoingJingleSession(getConnection(1), "res1", null, null); + JingleSession js3 = new OutgoingJingleSession(getConnection(2), "res2", null, null); + + assertEquals(js1, js2); + assertEquals(js2, js1); + + assertFalse(js1.equals(js3)); + } + + public void testGetInstanceFor() { + String ini1 = "initiator1"; + String sid1 = "sid1"; + String ini2 = "initiator2"; + String sid2 = "sid2"; + + JingleSession js1 = new OutgoingJingleSession(getConnection(0), sid1, null, null); + JingleSession js2 = new OutgoingJingleSession(getConnection(1), sid2, null, null); + + // For a packet, we should be able to get a session that handles that... + assertNotNull(JingleSession.getInstanceFor(getConnection(0))); + assertNotNull(JingleSession.getInstanceFor(getConnection(1))); + + assertEquals(JingleSession.getInstanceFor(getConnection(0)), js1); + assertEquals(JingleSession.getInstanceFor(getConnection(1)), js2); + } + + protected int getMaxConnections() { + return 3; + } +} + diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/MediaNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/MediaNegotiator.java new file mode 100644 index 000000000..5cd2354b8 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/MediaNegotiator.java @@ -0,0 +1,553 @@ +package org.jivesoftware.smackx.jingle; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleError; +import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType; + +/** + * Manager for media descriptor negotiation. + * + * </p> + * + * This class is responsible for managing the descriptor negotiation process, + * handling all the packet interchange and the stage control. + * + * </p> + * + * @author Alvaro Saurin + */ +public class MediaNegotiator extends JingleNegotiator { + + private final JingleSession session; // The session this negotiation + + // Local and remote payload types... + + private final List localAudioPts = new ArrayList(); + + private final List remoteAudioPts = new ArrayList(); + + private PayloadType.Audio bestCommonAudioPt; + + // states + + private final Inviting inviting; + + private final Accepting accepting; + + private final Pending pending; + + private final Active active; + + /** + * Default constructor. The constructor establishes some basic parameters, + * but it does not start the negotiation. For starting the negotiation, call + * startNegotiation. + * + * @param js The jingle session. + */ + public MediaNegotiator(final JingleSession js, final List pts) { + super(js.getConnection()); + + session = js; + + bestCommonAudioPt = null; + + if (pts != null) { + if (pts.size() > 0) { + localAudioPts.addAll(pts); + } + } + + // Create the states... + inviting = new Inviting(this); + accepting = new Accepting(this); + pending = new Pending(this); + active = new Active(this); + } + + /** + * Dispatch an incomming packet. The medthod is responsible for recognizing + * the packet type and, depending on the current state, deliverying the + * packet to the right event handler and wait for a response. + * + * @param iq the packet received + * @return the new Jingle packet to send. + * @throws XMPPException + */ + public IQ dispatchIncomingPacket(final IQ iq, final String id) throws XMPPException { + IQ jout = null; + + if (invalidState()) { + if (iq == null) { + // With a null packet, we are just inviting the other end... + setState(inviting); + jout = getState().eventInvite(); + } else { + if (iq instanceof Jingle) { + // If there is no specific media action associated, then we + // are being invited to a new session... + setState(accepting); + jout = getState().eventInitiate((Jingle) iq); + } else { + throw new IllegalStateException( + "Invitation IQ received is not a Jingle packet in Media negotiator."); + } + } + } else { + if (iq == null) { + return null; + } else { + if (iq.getType().equals(IQ.Type.ERROR)) { + // Process errors + getState().eventError(iq); + } else if (iq.getType().equals(IQ.Type.RESULT)) { + // Process ACKs + if (isExpectedId(iq.getPacketID())) { + jout = getState().eventAck(iq); + removeExpectedId(iq.getPacketID()); + } + } else if (iq instanceof Jingle) { + // Get the action from the Jingle packet + Jingle jin = (Jingle) iq; + Jingle.Action action = jin.getAction(); + + if (action != null) { + if (action.equals(Jingle.Action.CONTENTACCEPT)) { + jout = getState().eventAccept(jin); + } else if (action.equals(Jingle.Action.CONTENTDECLINE)) { + jout = getState().eventDecline(jin); + } else if (action.equals(Jingle.Action.CONTENTINFO)) { + jout = getState().eventInfo(jin); + } else if (action.equals(Jingle.Action.CONTENTMODIFY)) { + jout = getState().eventModify(jin); + } + // Any unknown action will be ignored: it is not a msg + // to us... + } + } + } + } + + // Save the Id for any ACK + if (id != null) { + addExpectedId(id); + } else { + if (jout != null) { + addExpectedId(jout.getPacketID()); + } + } + + return jout; + } + + /** + * Return true if the content is negotiated. + * + * @return true if the content is negotiated. + */ + public boolean isEstablished() { + return getBestCommonAudioPt() != null; + } + + /** + * Return true if the content is fully negotiated. + * + * @return true if the content is fully negotiated. + */ + public boolean isFullyEstablished() { + return isEstablished() && getState() == active; + } + + // Payload types + + private PayloadType.Audio calculateBestCommonAudioPt(final List remoteAudioPts) { + final ArrayList commonAudioPtsHere = new ArrayList(); + final ArrayList commonAudioPtsThere = new ArrayList(); + PayloadType.Audio result = null; + + if (!remoteAudioPts.isEmpty()) { + commonAudioPtsHere.addAll(localAudioPts); + commonAudioPtsHere.retainAll(remoteAudioPts); + + commonAudioPtsThere.addAll(remoteAudioPts); + commonAudioPtsThere.retainAll(localAudioPts); + + if (!commonAudioPtsHere.isEmpty() && !commonAudioPtsThere.isEmpty()) { + PayloadType.Audio bestPtHere = (PayloadType.Audio) commonAudioPtsHere + .get(0); + PayloadType.Audio bestPtThere = (PayloadType.Audio) commonAudioPtsThere + .get(0); + + // If both match, use it + if (bestPtHere.equals(bestPtThere)) { + result = bestPtHere; + } else { + // Otherwise, use the one of the initiator... + // FIXME: this is an invented behavior!!! + String initiator = session.getInitiator(); + String me = session.getConnection().getUser(); + + if (initiator.equals(me)) { + result = bestPtHere; + } else { + result = bestPtThere; + } + } + } + } + + return result; + } + + private List obtainPayloads(final Jingle jin) { + List result = new ArrayList(); + Iterator iDescr = jin.getDescriptions(); + + // Add the list of payloads: iterate over the descriptions... + while (iDescr.hasNext()) { + JingleContentDescription.Audio descr = (JingleContentDescription.Audio) iDescr + .next(); + + if (descr != null) { + // ...and, then, over the payloads. + // Note: we use the last "description" in the packet... + result.clear(); + result.addAll(descr.getAudioPayloadTypesList()); + } + } + + return result; + } + + /** + * Adds a payload type to the list of remote payloads. + * + * @param pt the remote payload type + */ + public void addRemoteAudioPayloadType(final PayloadType.Audio pt) { + if (pt != null) { + synchronized (remoteAudioPts) { + remoteAudioPts.add(pt); + } + } + } + + /** + * Create an offer for the list of audio payload types. + * + * @return a new Jingle packet with the list of audio Payload Types + */ + private Jingle getAudioPayloadTypesOffer() { + JingleContentDescription.Audio audioDescr = new JingleContentDescription.Audio(); + + // Add the list of payloads for audio and create a + // JingleContentDescription + // where we announce our payloads... + audioDescr.addAudioPayloadTypes(localAudioPts); + + return new Jingle(audioDescr); + } + + // Predefined messages and Errors + + /** + * Create an IQ "accept" message. + */ + private Jingle createAcceptMessage() { + Jingle jout = null; + + // If we hava a common best codec, send an accept right now... + jout = new Jingle(Jingle.Action.CONTENTACCEPT); + jout.addDescription(new JingleContentDescription.Audio( + new JinglePayloadType.Audio(bestCommonAudioPt))); + + return jout; + } + + // Payloads + + /** + * Get the best common codec between both parts. + * + * @return The best common PayloadType codec. + */ + public PayloadType.Audio getBestCommonAudioPt() { + return bestCommonAudioPt; + } + + // Events + + /** + * Trigger a session established event. + * + * @param best payload type that has been agreed. + */ + protected void triggerMediaEstablished(final PayloadType bestPt) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleListener.Media) { + JingleListener.Media mli = (JingleListener.Media) li; + mli.mediaEstablished(bestPt); + } + } + } + + /** + * Trigger a media closed event. + * + * @param currPt current payload type that is cancelled. + */ + protected void triggerMediaClosed(final PayloadType currPt) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleListener.Media) { + JingleListener.Media mli = (JingleListener.Media) li; + mli.mediaClosed(currPt); + } + } + } + + /** + * Terminate the media negotiator + */ + public void close() { + super.close(); + } + + // States + + /** + * First stage when we send a session request. + */ + public class Inviting extends JingleNegotiator.State { + public Inviting(final MediaNegotiator neg) { + super(neg); + } + + /** + * Create an initial Jingle packet, with the list of payload types that + * we support. The list is in order of preference. + */ + public Jingle eventInvite() { + return getAudioPayloadTypesOffer(); + } + + /** + * We have received the ACK for our invitation. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(final IQ iq) { + setState(pending); + return null; + } + } + + /** + * We are accepting connections. + */ + public class Accepting extends JingleNegotiator.State { + + public Accepting(final MediaNegotiator neg) { + super(neg); + } + + /** + * We have received an invitation! Respond with a list of our payload + * types... + */ + public Jingle eventInitiate(final Jingle jin) { + synchronized (remoteAudioPts) { + remoteAudioPts.addAll(obtainPayloads(jin)); + } + + return getAudioPayloadTypesOffer(); + } + + /** + * Process the ACK of our list of codecs (our offer). + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(final IQ iq) throws XMPPException { + Jingle response = null; + + if (!remoteAudioPts.isEmpty()) { + // Calculate the best common codec + bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); + + // and send an accept if we havee an agreement... + if (bestCommonAudioPt != null) { + response = createAcceptMessage(); + } else { + throw new JingleException(JingleError.NO_COMMON_PAYLOAD); + } + + setState(pending); + } + + return response; + } + } + + /** + * Pending class: we are waiting for the other enpoint, that must say if it + * accepts or not... + */ + public class Pending extends JingleNegotiator.State { + public Pending(final MediaNegotiator neg) { + super(neg); + } + + /** + * A content info has been received. This is done for publishing the + * list of payload types... + * + * @param jin The input packet + * @return a Jingle packet + * @throws JingleException + */ + public Jingle eventInfo(final Jingle jin) throws JingleException { + PayloadType.Audio oldBestCommonAudioPt = bestCommonAudioPt; + List offeredPayloads = new ArrayList(); + Jingle response = null; + boolean ptChange = false; + + offeredPayloads = obtainPayloads(jin); + if (!offeredPayloads.isEmpty()) { + + synchronized (remoteAudioPts) { + remoteAudioPts.clear(); + remoteAudioPts.addAll(offeredPayloads); + } + + // Calculate the best common codec + bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); + if (bestCommonAudioPt != null) { + // and send an accept if we have an agreement... + ptChange = !bestCommonAudioPt.equals(oldBestCommonAudioPt); + if (oldBestCommonAudioPt == null || ptChange) { + response = createAcceptMessage(); + } + } else { + throw new JingleException(JingleError.NO_COMMON_PAYLOAD); + } + } + + // Parse the Jingle and get the payload accepted + return response; + } + + /** + * A media description has been accepted. In this case, we must save the + * accepted payload type and notify any listener... + * + * @param jin The input packet + * @return a Jingle packet + * @throws JingleException + */ + public Jingle eventAccept(final Jingle jin) throws JingleException { + PayloadType.Audio agreedCommonAudioPt; + List offeredPayloads = new ArrayList(); + Jingle response = null; + + if (bestCommonAudioPt == null) { + // Update the best common audio PT + bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); + response = createAcceptMessage(); + } + + offeredPayloads = obtainPayloads(jin); + if (!offeredPayloads.isEmpty()) { + if (offeredPayloads.size() == 1) { + agreedCommonAudioPt = (PayloadType.Audio) offeredPayloads.get(0); + if (bestCommonAudioPt != null) { + // If the accepted PT matches the best payload + // everything is fine + if (!agreedCommonAudioPt.equals(bestCommonAudioPt)) { + throw new JingleException(JingleError.NEGOTIATION_ERROR); + } + } + + } else if (offeredPayloads.size() > 1) { + throw new JingleException(JingleError.MALFORMED_STANZA); + } + } + + return response; + } + + /** + * The other part has declined the our codec... + * + * @throws JingleException + */ + public Jingle eventDecline(final Jingle inJingle) throws JingleException { + triggerMediaClosed(getBestCommonAudioPt()); + throw new JingleException(); + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventError(org.jivesoftware.smack.packet.IQ) + */ + public void eventError(final IQ iq) throws XMPPException { + triggerMediaClosed(getBestCommonAudioPt()); + super.eventError(iq); + } + + /** + * ACK received. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(final IQ iq) { + if (isEstablished()) { + setState(active); + } + + return null; + } + } + + /** + * "Active" state: we have an agreement about the codec... + */ + public class Active extends JingleNegotiator.State { + + public Active(final MediaNegotiator neg) { + super(neg); + } + + /** + * We have an agreement. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + triggerMediaEstablished(getBestCommonAudioPt()); + super.eventEnter(); + } + + /** + * We are breaking the contract... + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit() + */ + public void eventExit() { + triggerMediaClosed(getBestCommonAudioPt()); + super.eventExit(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java new file mode 100644 index 000000000..2589f1d7b --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java @@ -0,0 +1,405 @@ +/** + * $RCSfile: OutgoingJingleSession.java,v $ + * $Revision: 1.1 $ + * $Date: 2006/10/17 19:12:42 $ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle; + +import java.util.List; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.nat.TransportCandidate; +import org.jivesoftware.smackx.nat.TransportResolver; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleError; +import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType; + +/** + * An outgoing Jingle session. + * + * </p> + * + * This class is not directly used by users. Instead, users should refer to the + * JingleManager class, that will create the appropiate instance... + * + * </p> + * + * @author Alvaro Saurin <alvaro.saurin@gmail.com> + */ +public class OutgoingJingleSession extends JingleSession { + + // states + + private final Inviting inviting; + + private final Pending pending; + + private final Active active; + + /** + * Constructor for a Jingle outgoing session. + * + * @param conn the XMPP connection + * @param responder the other endpoint + * @param payloadTypes A list of payload types, in order of preference. + * @param resolver The transport resolver. + */ + public OutgoingJingleSession(final XMPPConnection conn, final String responder, + final List payloadTypes, final TransportResolver resolver) { + + super(conn, conn.getUser(), responder); + + setSid(generateSessionId()); + + // Initialize the states. + inviting = new Inviting(this); + pending = new Pending(this); + active = new Active(this); + + // Create description and transport negotiatiors... + setMediaNeg(new MediaNegotiator(this, payloadTypes)); + setTransportNeg(new TransportNegotiator.RawUdp(this, resolver)); + } + + /** + * Initiate the negotiation with an invitation. This method must be invoked + * for starting all negotiations. It is the initial starting point and, + * afterwards, any other packet processing is done with the packet listener + * callback... + * + * @throws IllegalStateException + */ + public void start(final JingleSessionRequest req) throws IllegalStateException { + if (invalidState()) { + setState(inviting); + + // Use the standard behavior, using a null Jingle packet + try { + updatePacketListener(); + respond((Jingle) null); + } catch (XMPPException e) { + e.printStackTrace(); + close(); + } + } else { + throw new IllegalStateException("Starting session without null state."); + } + } + + // States + + /** + * Current state when we want to invite the other endpoint. + */ + public class Inviting extends JingleNegotiator.State { + + public Inviting(final JingleNegotiator neg) { + super(neg); + } + + /** + * Create an invitation packet. + */ + public Jingle eventInvite() { + // Create an invitation packet, saving the Packet ID, for any ACK + return new Jingle(Jingle.Action.SESSIONINITIATE); + } + + /** + * The receiver has partially accepted our invitation. We go to the + * pending state while the content and transport negotiators work... + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(final IQ iq) { + setState(pending); + return null; + } + + /** + * The other endpoint has declined the invitation with an error. + * + * @throws XMPPException + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventError(org.jivesoftware.smack.packet.IQ) + */ + public void eventError(final IQ iq) throws XMPPException { + triggerSessionDeclined(null); + super.eventError(iq); + } + + /** + * The other endpoint wants to redirect this connection. + */ + public Jingle eventRedirect(final Jingle jin) { + String redirArg = null; + + // TODO: parse the redirection parameters... + + triggerSessionRedirect(redirArg); + return null; + } + } + + /** + * "Pending" state: we are waiting for the transport and content + * negotiators. + * + * Note: the transition from/to this state is done with listeners... + */ + public class Pending extends JingleNegotiator.State { + JingleListener.Media mediaListener; + + JingleListener.Transport transportListener; + + public Pending(final JingleNegotiator neg) { + super(neg); + + // Create the listeners that will send a "session-accept" when + // the sub-negotiators are done. + mediaListener = new JingleListener.Media() { + public void mediaClosed(final PayloadType cand) { + } + + public void mediaEstablished(final PayloadType pt) { + checkFullyEstablished(); + } + }; + + transportListener = new JingleListener.Transport() { + public void transportEstablished(final TransportCandidate local, + final TransportCandidate remote) { + checkFullyEstablished(); + } + + public void transportClosed(final TransportCandidate cand) { + } + + public void transportClosedOnError(final XMPPException e) { + } + }; + } + + /** + * Enter in the pending state: install the listeners. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + // Add the listeners to the sub-negotiators... + addMediaListener(mediaListener); + addTransportListener(transportListener); + } + + /** + * Exit of the state: remove the listeners. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit() + */ + public void eventExit() { + removeMediaListener(mediaListener); + removeTransportListener(transportListener); + } + + /** + * Check if the session has been fully accepted by all the + * sub-negotiators and, in that case, send an "accept" message... + */ + private void checkFullyEstablished() { + + if (isFullyEstablished()) { + + PayloadType.Audio bestCommonAudioPt = getMediaNeg() + .getBestCommonAudioPt(); + TransportCandidate bestRemoteCandidate = getTransportNeg() + .getBestRemoteCandidate(); + + // Ok, send a packet saying that we accept this session + // with the audio payload type and the transport + // candidate + Jingle jout = new Jingle(Jingle.Action.SESSIONACCEPT); + jout.addDescription(new JingleContentDescription.Audio( + new JinglePayloadType(bestCommonAudioPt))); + jout.addTransport(getTransportNeg().getJingleTransport( + bestRemoteCandidate)); + + // Send the "accept" and wait for the ACK + addExpectedId(jout.getPacketID()); + sendFormattedJingle(jout); + } + } + + /** + * The other endpoint has finally accepted our invitation. + * + * @throws XMPPException + */ + public Jingle eventAccept(final Jingle jin) throws XMPPException { + + PayloadType acceptedPayloadType = null; + TransportCandidate acceptedLocalCandidate = null; + + // We process the "accepted" if we have finished the + // sub-negotiators. Maybe this is not needed (ie, the other endpoint + // can take the first valid transport candidate), but otherwise we + // must cancel the negotiators... + // + if (isFullyEstablished()) { + acceptedPayloadType = getAcceptedAudioPayloadType(jin); + acceptedLocalCandidate = getAcceptedLocalCandidate(jin); + + if (acceptedPayloadType != null && acceptedLocalCandidate != null) { + if (acceptedPayloadType.equals(getMediaNeg().getBestCommonAudioPt()) + && acceptedLocalCandidate.equals(getTransportNeg() + .getAcceptedLocalCandidate())) { + setState(active); + } + } else { + throw new JingleException(JingleError.NEGOTIATION_ERROR); + } + } + + return null; + } + + /** + * We have received the Ack of our "accept" + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(final IQ iq) { + setState(active); + return null; + } + + /** + * The other endpoint wants to redirect this connection. + */ + public Jingle eventRedirect(final Jingle jin) { + String redirArg = null; + + // TODO: parse the redirection parameters... + + triggerSessionRedirect(redirArg); + return null; + } + + /** + * The other endpoint has rejected our invitation. + * + * @throws XMPPException + */ + public Jingle eventTerminate(final Jingle jin) throws XMPPException { + triggerSessionDeclined(null); + return super.eventTerminate(jin); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(final IQ iq) throws XMPPException { + triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage())); + super.eventError(iq); + } + } + + /** + * State when we have an established session. + */ + public class Active extends JingleNegotiator.State { + public Active(final JingleNegotiator neg) { + super(neg); + } + + /** + * We have a established session: notify the listeners + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + PayloadType.Audio bestCommonAudioPt = getMediaNeg().getBestCommonAudioPt(); + TransportCandidate bestRemoteCandidate = getTransportNeg() + .getBestRemoteCandidate(); + TransportCandidate acceptedLocalCandidate = getTransportNeg() + .getAcceptedLocalCandidate(); + + // Trigger the session established flag + triggerSessionEstablished(bestCommonAudioPt, bestRemoteCandidate, + acceptedLocalCandidate); + + super.eventEnter(); + } + + /** + * Terminate the connection. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventTerminate(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventTerminate(final Jingle jin) throws XMPPException { + triggerSessionClosed(null); + return super.eventTerminate(jin); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(final IQ iq) throws XMPPException { + triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage())); + super.eventError(iq); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/PayloadType.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/PayloadType.java new file mode 100644 index 000000000..e1a97f1a7 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/PayloadType.java @@ -0,0 +1,289 @@ +package org.jivesoftware.smackx.jingle; + +/** + * Represents a payload type. + * + * @author Alvaro Saurin + */ +public class PayloadType { + + public static int MAX_FIXED_PT = 95; + + public static int INVALID_PT = 65535; + + private int id; + + private String name; + + private int channels; + + /** + * Constructor with Id, name and number of channels + * + * @param id The identifier + * @param name A name + * @param channels The number of channels + */ + public PayloadType(final int id, final String name, final int channels) { + super(); + this.id = id; + this.name = name; + this.channels = channels; + } + + /** + * Default constructor. + */ + public PayloadType() { + this(INVALID_PT, null, 1); + } + + /** + * Constructor with Id and name + * + * @param id The identification + * @param name A name + */ + public PayloadType(final int id, final String name) { + this(id, name, 1); + } + + /** + * Copy constructor + * + * @param pt The other payload type. + */ + public PayloadType(final PayloadType pt) { + this(pt.getId(), pt.getName(), pt.getChannels()); + } + + /** + * Get the ID. + * + * @return the ID + */ + public int getId() { + return id; + } + + /** + * Set the ID. + * + * @param id ID + */ + public void setId(final int id) { + this.id = id; + } + + /** + * Get the printable name. + * + * @return printable name for the payload type + */ + public String getName() { + return name; + } + + /** + * Set the printable name. + * + * @param name the printable name + */ + public void setName(final String name) { + this.name = name; + } + + /** + * Get the number of channels used by this payload type. + * + * @return the number of channels + */ + public int getChannels() { + return channels; + } + + /** + * Set the numer of channels for a payload type. + * + * @param channels The number of channels + */ + public void setChannels(final int channels) { + this.channels = channels; + } + + /** + * Return true if the Payload type is not valid + * + * @return true if the payload type is invalid + */ + public boolean isNull() { + if (getId() == INVALID_PT) { + return true; + } else if (getName() == null) { + return true; + } + return false; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + final int PRIME = 31; + int result = 1; + result = PRIME * result + getChannels(); + result = PRIME * result + getId(); + result = PRIME * result + (getName() == null ? 0 : getName().hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final PayloadType other = (PayloadType) obj; + if (getChannels() != other.getChannels()) { + return false; + } + if (getId() != other.getId()) { + return false; + } + + // Compare names only for dynamic payload types + if (getId() > MAX_FIXED_PT) { + if (getName() == null) { + if (other.getName() != null) { + return false; + } + } else if (!getName().equals(other.getName())) { + return false; + } + } + + return true; + } + + /** + * Audio payload type. + */ + public static class Audio extends PayloadType { + private int clockRate; + + /** + * Constructor with all the attributes of an Audio payload type + * + * @param id The identifier + * @param name The name assigned to this payload type + * @param channels The number of channels + * @param rate The clock rate + */ + public Audio(final int id, final String name, final int channels, final int rate) { + super(id, name, channels); + clockRate = rate; + } + + /** + * Empty constructor. + */ + public Audio() { + super(); + clockRate = 0; + } + + /** + * Constructor with Id and name + * + * @param id the Id for the payload type + * @param name the name of the payload type + */ + public Audio(final int id, final String name) { + super(id, name); + clockRate = 0; + } + + /** + * Copy constructor + * + * @param pt the other payload type + */ + public Audio(final PayloadType pt) { + super(pt); + clockRate = 0; + } + + /** + * Copy constructor + * + * @param pt the other payload type + */ + public Audio(final PayloadType.Audio pt) { + super(pt); + clockRate = pt.getClockRate(); + } + + /** + * Get the sampling clockRate for a payload type + * + * @return The sampling clockRate + */ + public int getClockRate() { + return clockRate; + } + + /** + * Set tha sampling clockRate for a playload type. + * + * @param rate The sampling clockRate + */ + public void setClockRate(final int rate) { + clockRate = rate; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + final int PRIME = 31; + int result = super.hashCode(); + result = PRIME * result + getClockRate(); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Audio other = (Audio) obj; + if (getClockRate() != other.getClockRate()) { + return false; + } + return true; + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportManager.java new file mode 100644 index 000000000..54acebab9 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportManager.java @@ -0,0 +1,171 @@ +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.nat.BasicResolver; +import org.jivesoftware.smackx.nat.FixedResolver; +import org.jivesoftware.smackx.nat.STUNResolver; +import org.jivesoftware.smackx.nat.TransportResolver; +import org.jivesoftware.smackx.packet.JingleTransport; + +/** + * Transport manager for Jingle. + * + * This class makes easier the use of transport resolvers by presenting a simple + * interface for algorithm selection. The transport manager also keeps the match + * between the resolution method and the <transport> element present in + * Jingle packets. + * + * This class must be used with a JingleManager instance in the following way: + * + * <pre> + * TransportManager tm = new TransportManager(); + * tm.useSTUN(); // or some other method... + * + * JingleManager jm = new JingleManager(connection, tm.getResolver()); + * jm.createOutgoingJingleSession(responder, payloads); + * </pre> + * + * @author Alvaro Saurin <alvaro.saurin@gmail.com> + */ +public class TransportManager { + // This class implements the context of a Strategy pattern... + + // Current resolver. + private TransportResolver resolver; + + // An instance of the transport resolver. This doesn't need to match + // the resolver, as we can use a STUNResolver for the JingleTransport.Ice or + // JingleTransport.RawUdp resolvers... + private JingleTransport jingleTransport; + + /** + * Deafult contructor. + */ + public TransportManager() { + useSTUNResolver(); // We use Raw-UDP/STUN by default... + } + + /** + * Contructor with an external resolver. + */ + public TransportManager(final TransportResolver resol, final JingleTransport trans) { + resolver = resol; + jingleTransport = trans; + } + + /** + * Use the simple resolver. + */ + public void useSimpleResolver() { + if (resolver != null && !(resolver instanceof BasicResolver)) { + try { + resolver.cancel(); + } catch (XMPPException e) { + e.printStackTrace(); + } + } + resolver = new BasicResolver(); + + if (jingleTransport == null + || !(jingleTransport instanceof JingleTransport.RawUdp)) { + jingleTransport = new JingleTransport.RawUdp(); + } + } + + /** + * Use a simple resolver, with a fixed IP address and port. + * + * @param ip the IP address + * @param port the port to use (0 for any port) + */ + public void useSimpleResolver(final String ip, final int port) { + if (resolver != null && !(resolver instanceof FixedResolver)) { + try { + resolver.cancel(); + } catch (XMPPException e) { + e.printStackTrace(); + } + } + resolver = new FixedResolver(ip, port); + + if (jingleTransport == null + || !(jingleTransport instanceof JingleTransport.RawUdp)) { + jingleTransport = new JingleTransport.RawUdp(); + } + } + + /** + * Returns true if the transport manager is using the simple resolver + * + * @return true if the transport manager is using the simple resolver + */ + public boolean isUsingSimpleResolver() { + return (resolver instanceof BasicResolver || resolver instanceof FixedResolver) + && jingleTransport instanceof JingleTransport.RawUdp; + } + + /** + * Use the STUN resolver. + */ + public void useSTUNResolver() { + if (resolver != null && !(resolver instanceof STUNResolver)) { + try { + resolver.cancel(); + } catch (XMPPException e) { + e.printStackTrace(); + } + } + resolver = new STUNResolver(); + + if (jingleTransport == null + || !(jingleTransport instanceof JingleTransport.RawUdp)) { + jingleTransport = new JingleTransport.RawUdp(); + } + } + + /** + * Returns true if the transport manager is using the STUN resolver + * + * @return true if the transport manager is using the STUN resolver + */ + public boolean isUsingSTUNResolver() { + return resolver instanceof STUNResolver + && jingleTransport instanceof JingleTransport.RawUdp; + } + + /** + * Use the ICE resolver. + */ + public void useICEResolver() { + // Not implemented yet + resolver = null; + jingleTransport = new JingleTransport.Ice(); + } + + /** + * Returns true if the transport manager is using the ICE resolver + * + * @return true if the transport manager is using the ICE resolver + */ + public boolean isUsingICEResolver() { + return false; + } + + /** + * Obtain the JingleTransport for the current resolver + * + * @return a JingleTransport instance + */ + public JingleTransport getJingleTransport() { + return jingleTransport; + } + + /** + * Obtain the resolver + * + * @return a TransportResolver. + */ + public TransportResolver getResolver() { + return resolver; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportNegotiator.java new file mode 100644 index 000000000..bddb84b5b --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportNegotiator.java @@ -0,0 +1,797 @@ +package org.jivesoftware.smackx.jingle; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.nat.TransportCandidate; +import org.jivesoftware.smackx.nat.TransportResolver; +import org.jivesoftware.smackx.nat.TransportResolverListener; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleTransport; +import org.jivesoftware.smackx.packet.JingleTransport.JingleTransportCandidate; + +/** + * Transport negotiator. + * + * </p> + * + * This class is responsible for managing the transport negotiation process, + * handling all the packet interchange and the stage control. + * + * </p> + * + * @author Alvaro Saurin <alvaro.saurin@gmail.com> + */ +public abstract class TransportNegotiator extends JingleNegotiator { + + // The time we give to the candidates check before we accept or decline the + // transport (in milliseconds) + public final static int CANDIDATES_ACCEPT_PERIOD = 4000; + + // The session this nenotiator belongs to + private JingleSession session; + + // The transport manager + private final TransportResolver resolver; + + // Transport candidates we have offered + private final List offeredCandidates = new ArrayList(); + + // List of remote transport candidates + private final List remoteCandidates = new ArrayList(); + + // Valid remote candidates + private final List validRemoteCandidates = new ArrayList(); + + // The best local candidate we have offered (and accepted by the other part) + private TransportCandidate acceptedLocalCandidate; + + // The thread that will report the result to the other end + private Thread resultThread; + + // Listener for the resolver + private TransportResolverListener.Resolver resolverListener; + + // states + final Inviting inviting; + + final Accepting accepting; + + final Pending pending; + + final Active active; + + /** + * Default constructor. + * + * @param js The Jingle session + * @param transResolver The TransportManager to use + */ + public TransportNegotiator(final JingleSession js, + final TransportResolver transResolver) { + super(js.getConnection()); + + session = js; + resolver = transResolver; + + resultThread = null; + + // Create the states... + inviting = new Inviting(this); + accepting = new Accepting(this); + pending = new Pending(this); + active = new Active(this); + } + + /** + * Get a new instance of the right JingleTransport class with this + * candidate. + * + * @return A JingleTransport instance + */ + protected abstract JingleTransport getJingleTransport(TransportCandidate cand); + + /** + * Return true if the transport candidate is acceptable for the current + * negotiator. + * + * @return true if the transport candidate is acceptable + */ + protected abstract boolean acceptableTransportCandidate(TransportCandidate tc); + + /** + * Obtain the best local candidate we want to offer. + * + * @return the best local candidate + */ + public TransportCandidate getBestLocalCandidate() { + return resolver.getPreferredCandidate(); + } + + /** + * Set the best local transport candidate we have offered and accepted by + * the other endpoint. + * + * @param acceptedLocalCandidate the acceptedLocalCandidate to set + */ + protected void setAcceptedLocalCandidate(final TransportCandidate bestLocalCandidate) + throws XMPPException { + if (resolver.getCandidatesList().contains(bestLocalCandidate)) { + acceptedLocalCandidate = bestLocalCandidate; + } else { + throw new XMPPException("Local transport candidate has not be offered."); + } + } + + /** + * Get the best accepted local candidate we have offered. + * + * @return a transport candidate we have offered. + */ + public TransportCandidate getAcceptedLocalCandidate() { + return acceptedLocalCandidate; + } + + /** + * Obtain the best common transport candidate obtained in the negotiation. + * + * @return the bestRemoteCandidate + */ + public abstract TransportCandidate getBestRemoteCandidate(); + + /** + * Get the list of remote candidates. + * + * @return the remoteCandidates + */ + public List getRemoteCandidates() { + return remoteCandidates; + } + + /** + * Add a remote candidate to the list. The candidate will be checked in + * order to verify if it is usable. + * + * @param rc a remote candidate to add and check. + */ + public void addRemoteCandidate(final TransportCandidate rc) { + // Add the candidate to the list + if (rc != null) { + if (acceptableTransportCandidate(rc)) { + synchronized (remoteCandidates) { + remoteCandidates.add(rc); + } + + // Check if the new candidate can be used. + checkRemoteCandidate(rc); + } + } + } + + /** + * Add a offered candidate to the list. + * + * @param rc a remote candidate we have offered. + */ + public void addOfferedCandidate(final TransportCandidate rc) { + // Add the candidate to the list + if (rc != null) { + synchronized (offeredCandidates) { + offeredCandidates.add(rc); + } + } + } + + /** + * Check asynchronously the new transport candidate. + * + * @param offeredCandidate a transport candidates to check + */ + protected void checkRemoteCandidate(final TransportCandidate offeredCandidate) { + resolver.addListener(new TransportResolverListener.Checker() { + public void candidateChecked(final TransportCandidate cand, + final boolean validCandidate) { + if (validCandidate) { + addValidRemoteCandidate(offeredCandidate); + } + } + }); + resolver.check(offeredCandidate); + } + + /** + * Return true if the transport is established. + * + * @return true if the transport is established. + */ + public boolean isEstablished() { + return getBestRemoteCandidate() != null && getAcceptedLocalCandidate() != null; + } + + /** + * Return true if the transport is fully established. + * + * @return true if the transport is fully established. + */ + public boolean isFullyEstablished() { + return isEstablished() && getState() == active; + } + + /** + * Launch a thread that checks, after some time, if any of the candidates + * offered by the other endpoint is usable. The thread does not check the + * candidates: it just checks if we have got a valid one and sends an Accept + * in that case. + */ + private void delayedCheckBestCandidate(final JingleSession js, final Jingle jin) { + // + // If this is the first insertion in the list, start the thread that + // will send the result of our checks... + // + if (resultThread == null && !getRemoteCandidates().isEmpty()) { + resultThread = new Thread(new Runnable() { + public void run() { + + // Sleep for some time, waiting for the candidates checks + try { + Thread.sleep(CANDIDATES_ACCEPT_PERIOD + + TransportResolver.CHECK_TIMEOUT); + } catch (InterruptedException e) { + } + + // Once we are in pending state, look for any valid remote + // candidate, and send an "accept" if we have one... + TransportCandidate bestRemote = getBestRemoteCandidate(); + State state = getState(); + + if (bestRemote != null && (state == pending || state == active)) { + // Accepting the remote candidate + Jingle jout = new Jingle(Jingle.Action.TRANSPORTACCEPT); + jout.addTransport(getJingleTransport(bestRemote)); + + // Send the packet + js.sendFormattedJingle(jin, jout); + + if (isEstablished()) { + setState(active); + } + } + } + }, "Waiting for all the transport candidates checks..."); + + resultThread.setName("Transport Resolver Result"); + resultThread.start(); + } + } + + /** + * Add a valid remote candidate to the list. The remote candidate has been + * checked, and the remote + * + * @param remoteCandidate a remote candidate to add + */ + public void addValidRemoteCandidate(final TransportCandidate remoteCandidate) { + // Add the candidate to the list + if (remoteCandidate != null) { + synchronized (validRemoteCandidates) { + validRemoteCandidates.add(remoteCandidate); + } + } + } + + /** + * Get the list of valid (ie, checked) remote candidates. + * + * @return The list of valid (ie, already checked) remote candidates. + */ + public ArrayList getValidRemoteCandidatesList() { + synchronized (validRemoteCandidates) { + return new ArrayList(validRemoteCandidates); + } + } + + /** + * Get an iterator for the list of valid (ie, checked) remote candidates. + * + * @return The iterator for the list of valid (ie, already checked) remote + * candidates. + */ + public Iterator getValidRemoteCandidates() { + return Collections.unmodifiableList(getRemoteCandidates()).iterator(); + } + + /** + * Add an offered remote candidate. The transport candidate can be unusable: + * we must check if we can use it. + * + * @param rc the remote candidate to add. + */ + public void addRemoteCandidates(final List rc) { + if (rc != null) { + if (rc.size() > 0) { + Iterator iter = rc.iterator(); + while (iter.hasNext()) { + addRemoteCandidate((TransportCandidate) iter.next()); + } + } + } + } + + /** + * Parse the list of transport candidates from a Jingle packet. + * + * @param jin The input jingle packet + */ + private ArrayList obtainCandidatesList(final Jingle jin) { + ArrayList result = new ArrayList(); + + if (jin != null) { + // Get the list of candidates from the packet + Iterator iTrans = jin.getTransports(); + while (iTrans.hasNext()) { + JingleTransport trans = (JingleTransport) iTrans.next(); + + Iterator iCand = trans.getCandidates(); + while (iCand.hasNext()) { + JingleTransportCandidate cand = (JingleTransportCandidate) iCand + .next(); + TransportCandidate transCand = cand.getMediaTransport(); + result.add(transCand); + } + } + } + + return result; + } + + final private boolean isOfferStarted() { + return resolver.isResolving() || resolver.isResolved(); + } + + /** + * Send an offer for a transport candidate + * + * @param cand + */ + private synchronized void sendTransportCandidateOffer(final TransportCandidate cand) { + if (!cand.isNull()) { + addOfferedCandidate(cand); + + // Offer our new candidate... + session.sendFormattedJingle(new Jingle(getJingleTransport(cand))); + } + } + + /** + * Create a Jingle packet where we announce our transport candidates. + * + * @throws XMPPException + */ + protected void sendTransportCandidatesOffer() throws XMPPException { + List notOffered = resolver.getCandidatesList(); + notOffered.removeAll(offeredCandidates); + + // Send any unset candidate + Iterator iter = notOffered.iterator(); + while (iter.hasNext()) { + sendTransportCandidateOffer((TransportCandidate) iter.next()); + } + + // .. and start a listener that will send any future candidate + if (resolverListener == null) { + // Add a listener that sends the offer when the resolver finishes... + resolverListener = new TransportResolverListener.Resolver() { + public void candidateAdded(final TransportCandidate cand) { + sendTransportCandidateOffer(cand); + } + + public void end() { + } + + public void init() { + } + }; + + resolver.addListener(resolverListener); + } + + if (!(resolver.isResolving() || resolver.isResolved())) { + // Resolve our IP and port + resolver.resolve(); + } + } + + /** + * Dispatch an incoming packet. The method is responsible for recognizing + * the packet type and, depending on the current state, deliverying the + * packet to the right event handler and wait for a response. + * + * @param iq the packet received + * @return the new Jingle packet to send. + * @throws XMPPException + */ + public IQ dispatchIncomingPacket(final IQ iq, final String id) throws XMPPException { + IQ jout = null; + + if (invalidState()) { + if (iq == null) { + // With a null packet, we are just inviting the other end... + setState(inviting); + jout = getState().eventInvite(); + + } else { + if (iq instanceof Jingle) { + // If there is no specific media action associated, then we + // are being invited to a new session... + setState(accepting); + jout = getState().eventInitiate((Jingle) iq); + } else { + throw new IllegalStateException( + "Invitation IQ received is not a Jingle packet in Transport negotiator."); + } + } + } else { + if (iq == null) { + return null; + } else { + if (iq.getType().equals(IQ.Type.ERROR)) { + // Process errors + getState().eventError(iq); + } else if (iq.getType().equals(IQ.Type.RESULT)) { + // Process ACKs + if (isExpectedId(iq.getPacketID())) { + jout = getState().eventAck(iq); + removeExpectedId(iq.getPacketID()); + } + } else if (iq instanceof Jingle) { + // Get the action from the Jingle packet + Jingle jin = (Jingle) iq; + Jingle.Action action = jin.getAction(); + + if (action != null) { + if (action.equals(Jingle.Action.TRANSPORTACCEPT)) { + jout = getState().eventAccept(jin); + } else if (action.equals(Jingle.Action.TRANSPORTDECLINE)) { + jout = getState().eventDecline(jin); + } else if (action.equals(Jingle.Action.TRANSPORTINFO)) { + jout = getState().eventInfo(jin); + } else if (action.equals(Jingle.Action.TRANSPORTMODIFY)) { + jout = getState().eventModify(jin); + } + } + } + } + } + + // Save the Id for any ACK + if (id != null) { + addExpectedId(id); + } else { + if (jout != null) { + addExpectedId(jout.getPacketID()); + } + } + + return jout; + } + + /** + * Trigger a session established event. + * + * @param best payload type that has been agreed. + */ + protected void triggerTransportEstablished(final TransportCandidate local, + final TransportCandidate remote) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleListener.Transport) { + JingleListener.Transport mli = (JingleListener.Transport) li; + mli.transportEstablished(local, remote); + } + } + } + + /** + * Trigger a media closed event. + * + * @param currPt current payload type that is cancelled. + */ + protected void triggerTransportClosed(final TransportCandidate cand) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleListener.Transport) { + JingleListener.Transport mli = (JingleListener.Transport) li; + mli.transportClosed(cand); + } + } + } + + // States + + /** + * First stage when we send a session request. + */ + public class Inviting extends JingleNegotiator.State { + + public Inviting(final TransportNegotiator neg) { + super(neg); + } + + /** + * Create an initial Jingle packet with an empty transport. + */ + public Jingle eventInvite() { + return new Jingle(getJingleTransport(null)); + } + + /** + * We have received some candidates. This can happen _before_ the ACK + * has been recieved... + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventInfo(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventInfo(final Jingle jin) throws XMPPException { + // Parse the Jingle and get any proposed transport candidates + addRemoteCandidates(obtainCandidatesList(jin)); + + // Wait for some time and check if we have a valid candidate to + // use... + delayedCheckBestCandidate(session, jin); + + return super.eventInfo(jin); + } + + /** + * The other endpoint has partially accepted our invitation: start + * offering a list of candidates. + * + * @return an IQ packet + * @throws XMPPException + */ + public Jingle eventAck(final IQ iq) throws XMPPException { + sendTransportCandidatesOffer(); + setState(pending); + return super.eventAck(iq); + } + } + + /** + * We are accepting connections. This is the starting state when we accept a + * connection... + */ + public class Accepting extends JingleNegotiator.State { + public Accepting(final TransportNegotiator neg) { + super(neg); + } + + /** + * We have received an invitation. The packet will be ACKed by lower + * levels... + */ + public Jingle eventInitiate(final Jingle jin) throws XMPPException { + // Parse the Jingle and get any proposed transport candidates + addRemoteCandidates(obtainCandidatesList(jin)); + + // Start offering candidates + sendTransportCandidatesOffer(); + + // All these candidates will be checked asyncronously. Wait for some + // time and check if we have a valid candidate to use... + delayedCheckBestCandidate(session, jin); + + // Set the next state + setState(pending); + + return super.eventInitiate(jin); + } + } + + /** + * We are still receiving candidates + */ + public class Pending extends JingleNegotiator.State { + + public Pending(final TransportNegotiator neg) { + super(neg); + } + + /** + * One of our transport candidates has been accepted. + * + * @param jin The input packet + * @return a Jingle packet + * @throws XMPPException an exception + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAccept(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventAccept(final Jingle jin) throws XMPPException { + Jingle response = null; + + // Parse the Jingle and get the accepted candidate + ArrayList accepted = obtainCandidatesList(jin); + if (!accepted.isEmpty()) { + TransportCandidate cand = (TransportCandidate) accepted.get(0); + + setAcceptedLocalCandidate(cand); + + if (isEstablished()) { + setState(active); + } + } + return response; + } + + /** + * We have received another remote transport candidates. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventInfo(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventInfo(final Jingle jin) throws XMPPException { + + sendTransportCandidatesOffer(); + + // Parse the Jingle and get any proposed transport candidates + addRemoteCandidates(obtainCandidatesList(jin)); + + // Wait for some time and check if we have a valid candidate to + // use... + delayedCheckBestCandidate(session, jin); + + return super.eventInfo(jin); + } + + /** + * None of our transport candidates has been accepted... + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventDecline(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventDecline(final Jingle inJingle) throws JingleException { + throw new JingleException("No common payload found."); + } + } + + /** + * "Active" state: we have an agreement about the codec... + */ + public class Active extends JingleNegotiator.State { + + public Active(final TransportNegotiator neg) { + super(neg); + } + + /** + * We have an agreement. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + triggerTransportEstablished(getAcceptedLocalCandidate(), + getBestRemoteCandidate()); + super.eventEnter(); + } + + /** + * We have finished the transport. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventExit() { + triggerTransportClosed(null); + super.eventExit(); + } + } + + // Subclasses + + /** + * Raw-UDP transport negotiator + * + * @author Alvaro Saurin <alvaro.saurin@gmail.com> + */ + public static class RawUdp extends TransportNegotiator { + + /** + * Default constructor, with a JingleSession and transport manager. + * + * @param js The Jingle session this negotiation belongs to. + * @param res The transport resolver to use. + */ + public RawUdp(final JingleSession js, final TransportResolver res) { + super(js, res); + } + + /** + * Get a JingleTransport instance. + */ + protected JingleTransport getJingleTransport(final TransportCandidate bestRemote) { + JingleTransport.RawUdp jt = new JingleTransport.RawUdp(); + jt.addCandidate(new JingleTransport.RawUdp.Candidate(bestRemote)); + return jt; + } + + /** + * Obtain the best common transport candidate obtained in the + * negotiation. + * + * @return the bestRemoteCandidate + */ + public TransportCandidate getBestRemoteCandidate() { + // Hopefully, we only have one validRemoteCandidate + ArrayList cands = getValidRemoteCandidatesList(); + if (!cands.isEmpty()) { + return (TransportCandidate) cands.get(0); + } else { + return null; + } + } + + /** + * Return true for fixed candidates. + */ + protected boolean acceptableTransportCandidate(final TransportCandidate tc) { + return tc instanceof TransportCandidate.Fixed; + } + } + + /** + * Ice transport negotiator. + * + * @author Alvaro Saurin <alvaro.saurin@gmail.com> + */ + public static class Ice extends TransportNegotiator { + + /** + * Default constructor, with a JingleSession and transport manager. + * + * @param js The Jingle session this negotiation belongs to. + * @param res The transport manager to use. + */ + public Ice(final JingleSession js, final TransportResolver res) { + super(js, res); + } + + /** + * Get a JingleTransport instance. + * + * @param bestRemote + */ + protected JingleTransport getJingleTransport(final TransportCandidate bestRemote) { + JingleTransport.Ice jt = new JingleTransport.Ice(); + jt.addCandidate(new JingleTransport.Ice.Candidate(bestRemote)); + return jt; + } + + /** + * Obtain the best remote candidate obtained in the negotiation so far. + * + * @return the bestRemoteCandidate + */ + public TransportCandidate getBestRemoteCandidate() { + TransportCandidate.Ice result = null; + + ArrayList cands = getValidRemoteCandidatesList(); + if (!cands.isEmpty()) { + Collections.sort(cands); + // Return the last candidate + result = (TransportCandidate.Ice) cands.get(cands.size() - 1); + } + + return result; + } + + /** + * Return true for ICE candidates. + */ + protected boolean acceptableTransportCandidate(final TransportCandidate tc) { + return tc instanceof TransportCandidate.Ice; + } + } +} |