001/**
002 *
003 * Copyright the original author or authors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.jingleold.nat;
018
019import java.io.IOException;
020import java.net.ServerSocket;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Iterator;
024import java.util.List;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import org.jivesoftware.smack.SmackException;
029import org.jivesoftware.smack.SmackException.NotConnectedException;
030import org.jivesoftware.smack.XMPPException;
031
032import org.jivesoftware.smackx.jingleold.JingleSession;
033
034/**
035 * A TransportResolver is used for obtaining a list of valid transport
036 * candidates. A transport candidate is composed by an IP address and a port number.
037 * It is called candidate, because it can be elected or not.
038 *
039 * @author Thiago Camargo
040 * @author Alvaro Saurin
041 */
042public abstract class TransportResolver {
043
044    private static final Logger LOGGER = Logger.getLogger(TransportResolver.class.getName());
045
046    public enum Type {
047
048        rawupd, ice
049    }
050
051    public Type getType() {
052        return type;
053    }
054
055    public void setType(Type type) {
056        this.type = type;
057    }
058
059    public Type type = Type.rawupd;
060
061    // the time, in milliseconds, before a check aborts
062    public static final int CHECK_TIMEOUT = 3000;
063
064    // Listeners for events
065    private final ArrayList<TransportResolverListener> listeners = new ArrayList<>();
066
067    // TRue if the resolver is working
068    private boolean resolving;
069
070    // This will be true when all the transport candidates have been gathered...
071    private boolean resolved;
072
073    // This indicates that a transport resolver is initialized
074    private boolean initialized = false;
075
076    // We store a list of candidates internally, just in case there are several
077    // possibilities. When the user asks for a transport, we return the best
078    // one.
079    protected final List<TransportCandidate> candidates = new ArrayList<>();
080
081    /**
082     * Default constructor.
083     */
084    protected TransportResolver() {
085        super();
086
087        resolving = false;
088        resolved = false;
089    }
090
091    /**
092     * Initialize the Resolver.
093     *
094     * @throws XMPPException if an XMPP protocol error was received.
095     * @throws SmackException if Smack detected an exceptional situation.
096     * @throws InterruptedException if the calling thread was interrupted.
097     */
098    public abstract void initialize() throws XMPPException, SmackException, InterruptedException;
099
100    /**
101     * Start a the resolution.
102     *
103     * @param session the Jingle session.
104     * @throws XMPPException if an XMPP protocol error was received.
105     * @throws SmackException if Smack detected an exceptional situation.
106     * @throws InterruptedException if the calling thread was interrupted.
107     */
108    public abstract void resolve(JingleSession session) throws XMPPException, SmackException, InterruptedException;
109
110    /**
111     * Clear the list of candidates and start a new resolution process.
112     *
113     * @throws XMPPException if an XMPP protocol error was received.
114     */
115    public void clear() throws XMPPException {
116        cancel();
117        candidates.clear();
118    }
119
120    /**
121     * Cancel any asynchronous resolution operation.
122     *
123     * @throws XMPPException if an XMPP protocol error was received.
124     */
125    public abstract void cancel() throws XMPPException;
126
127    /**
128     * Return true if the resolver is working.
129     *
130     * @return true if the resolver is working.
131     */
132    public boolean isResolving() {
133        return resolving;
134    }
135
136    /**
137     * Return true if the resolver has finished the search for transport
138     * candidates.
139     *
140     * @return true if the search has finished
141     */
142    public boolean isResolved() {
143        return resolved;
144    }
145
146    /**
147     * Set the Transport Resolver as initialized.
148     */
149    public synchronized void setInitialized() {
150        initialized = true;
151    }
152
153    /**
154     * Check if the Transport Resolver is initialized.
155     *
156     * @return true if initialized
157     */
158    public synchronized boolean isInitialized() {
159        return initialized;
160    }
161
162    /**
163     * Indicate the beginning of the resolution process. This method must be
164     * used by subclasses at the beginning of their resolve() method.
165     */
166    protected synchronized void setResolveInit() {
167        resolved = false;
168        resolving = true;
169
170        triggerResolveInit();
171    }
172
173    /**
174     * Indicate the end of the resolution process. This method must be used by
175     * subclasses at the beginning of their resolve() method.
176     */
177    protected synchronized void setResolveEnd() {
178        resolved = true;
179        resolving = false;
180
181        triggerResolveEnd();
182    }
183
184    // Listeners management
185
186    /**
187     * Add a transport resolver listener.
188     *
189     * @param li The transport resolver listener to be added.
190     */
191    public void addListener(TransportResolverListener li) {
192        synchronized (listeners) {
193            listeners.add(li);
194        }
195    }
196
197    /**
198     * Removes a transport resolver listener.
199     *
200     * @param li The transport resolver listener to be removed
201     */
202    public void removeListener(TransportResolverListener li) {
203        synchronized (listeners) {
204            listeners.remove(li);
205        }
206    }
207
208    /**
209     * Get the list of listeners.
210     *
211     * @return the list of listeners
212     */
213    public ArrayList<TransportResolverListener> getListenersList() {
214        synchronized (listeners) {
215            return new ArrayList<>(listeners);
216        }
217    }
218
219    /**
220     * Trigger a new candidate added event.
221     *
222     * @param cand The candidate added to the list of candidates.
223     * @throws NotConnectedException if the XMPP connection is not connected.
224     * @throws InterruptedException if the calling thread was interrupted.
225     */
226    protected void triggerCandidateAdded(TransportCandidate cand) throws NotConnectedException, InterruptedException {
227        Iterator<TransportResolverListener> iter = getListenersList().iterator();
228        while (iter.hasNext()) {
229            TransportResolverListener trl = iter.next();
230            if (trl instanceof TransportResolverListener.Resolver) {
231                TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl;
232                LOGGER.fine("triggerCandidateAdded : " + cand.getLocalIp());
233                li.candidateAdded(cand);
234            }
235        }
236    }
237
238    /**
239     * Trigger a event notifying the initialization of the resolution process.
240     */
241    private void triggerResolveInit() {
242        Iterator<TransportResolverListener> iter = getListenersList().iterator();
243        while (iter.hasNext()) {
244            TransportResolverListener trl = iter.next();
245            if (trl instanceof TransportResolverListener.Resolver) {
246                TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl;
247                li.init();
248            }
249        }
250    }
251
252    /**
253     * Trigger a event notifying the obtainment of all the candidates.
254     */
255    private void triggerResolveEnd() {
256        Iterator<TransportResolverListener> iter = getListenersList().iterator();
257        while (iter.hasNext()) {
258            TransportResolverListener trl = iter.next();
259            if (trl instanceof TransportResolverListener.Resolver) {
260                TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl;
261                li.end();
262            }
263        }
264    }
265
266    // Candidates management
267
268    /**
269     * Clear the list of candidate
270     */
271    protected void clearCandidates() {
272        synchronized (candidates) {
273            candidates.clear();
274        }
275    }
276
277    /**
278     * Add a new transport candidate
279     *
280     * @param cand The candidate to add
281     * @throws NotConnectedException if the XMPP connection is not connected.
282     * @throws InterruptedException if the calling thread was interrupted.
283     */
284    protected void addCandidate(TransportCandidate cand) throws NotConnectedException, InterruptedException {
285        synchronized (candidates) {
286            if (!candidates.contains(cand))
287                candidates.add(cand);
288        }
289
290        // Notify the listeners
291        triggerCandidateAdded(cand);
292    }
293
294    /**
295     * Get an iterator for the list of candidates.
296     *
297     * @return an iterator
298     */
299    public Iterator<TransportCandidate> getCandidates() {
300        synchronized (candidates) {
301            return Collections.unmodifiableList(new ArrayList<>(candidates)).iterator();
302        }
303    }
304
305    /**
306     * Get the candidate with the highest preference.
307     *
308     * @return The best candidate, according to the preference order.
309     */
310    public TransportCandidate getPreferredCandidate() {
311        TransportCandidate result = null;
312
313        ArrayList<ICECandidate> cands = new ArrayList<>();
314        for (TransportCandidate tpcan : getCandidatesList()) {
315            if (tpcan instanceof ICECandidate)
316                cands.add((ICECandidate) tpcan);
317        }
318
319        // (ArrayList<ICECandidate>) getCandidatesList();
320        if (cands.size() > 0) {
321            Collections.sort(cands);
322            // Return the last candidate
323            result = cands.get(cands.size() - 1);
324            LOGGER.fine("Result: " + result.getIp());
325        }
326
327        return result;
328    }
329
330    /**
331     * Get the number of transport candidates.
332     *
333     * @return The length of the transport candidates list.
334     */
335    public int getCandidateCount() {
336        synchronized (candidates) {
337            return candidates.size();
338        }
339    }
340
341    /**
342     * Get the list of candidates.
343     *
344     * @return the list of transport candidates
345     */
346    public List<TransportCandidate> getCandidatesList() {
347        List<TransportCandidate> result;
348
349        synchronized (candidates) {
350            result = new ArrayList<>(candidates);
351        }
352
353        return result;
354    }
355
356    /**
357     * Get the n-th candidate.
358     *
359     * @param i the index of the candidate.
360     * @return a transport candidate
361     */
362    public TransportCandidate getCandidate(int i) {
363        TransportCandidate cand;
364
365        synchronized (candidates) {
366            cand = candidates.get(i);
367        }
368        return cand;
369    }
370
371    /**
372     * Initialize Transport Resolver and wait until it is completely uninitialized.
373     *
374     * @throws XMPPException if an XMPP protocol error was received.
375     * @throws SmackException if Smack detected an exceptional situation.
376     * @throws InterruptedException if the calling thread was interrupted.
377     */
378    public void initializeAndWait() throws XMPPException, SmackException, InterruptedException {
379        this.initialize();
380        try {
381            LOGGER.fine("Initializing transport resolver...");
382            while (!this.isInitialized()) {
383                LOGGER.fine("Resolver init still pending");
384                Thread.sleep(1000);
385            }
386            LOGGER.fine("Transport resolved");
387        }
388        catch (Exception e) {
389            LOGGER.log(Level.WARNING, "exception", e);
390        }
391    }
392
393    /**
394     * Obtain a free port we can use.
395     *
396     * @return A free port number.
397     */
398    protected int getFreePort() {
399        ServerSocket ss;
400        int freePort = 0;
401
402        for (int i = 0; i < 10; i++) {
403            freePort = (int) (10000 + Math.round(Math.random() * 10000));
404            freePort = freePort % 2 == 0 ? freePort : freePort + 1;
405            try {
406                ss = new ServerSocket(freePort);
407                freePort = ss.getLocalPort();
408                ss.close();
409                return freePort;
410            }
411            catch (IOException e) {
412                LOGGER.log(Level.WARNING, "exception", e);
413            }
414        }
415        try {
416            ss = new ServerSocket(0);
417            freePort = ss.getLocalPort();
418            ss.close();
419        }
420        catch (IOException e) {
421            LOGGER.log(Level.WARNING, "exception", e);
422        }
423        return freePort;
424    }
425}