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     * @throws InterruptedException 
094     */
095    public abstract void initialize() throws XMPPException, SmackException, InterruptedException;
096
097    /**
098     * Start a the resolution.
099     * @throws InterruptedException 
100     */
101    public abstract void resolve(JingleSession session) throws XMPPException, SmackException, InterruptedException;
102
103    /**
104     * Clear the list of candidates and start a new resolution process.
105     *
106     * @throws XMPPException
107     */
108    public void clear() throws XMPPException {
109        cancel();
110        candidates.clear();
111    }
112
113    /**
114     * Cancel any asynchronous resolution operation.
115     */
116    public abstract void cancel() throws XMPPException;
117
118    /**
119     * Return true if the resolver is working.
120     *
121     * @return true if the resolver is working.
122     */
123    public boolean isResolving() {
124        return resolving;
125    }
126
127    /**
128     * Return true if the resolver has finished the search for transport
129     * candidates.
130     *
131     * @return true if the search has finished
132     */
133    public boolean isResolved() {
134        return resolved;
135    }
136
137    /**
138     * Set the Transport Resolver as initialized.
139     */
140    public synchronized void setInitialized() {
141        initialized = true;
142    }
143
144    /**
145     * Check if the Transport Resolver is initialized.
146     *
147     * @return true if initialized
148     */
149    public synchronized boolean isInitialized() {
150        return initialized;
151    }
152
153    /**
154     * Indicate the beginning of the resolution process. This method must be
155     * used by subclasses at the beginning of their resolve() method.
156     */
157    protected synchronized void setResolveInit() {
158        resolved = false;
159        resolving = true;
160
161        triggerResolveInit();
162    }
163
164    /**
165     * Indicate the end of the resolution process. This method must be used by
166     * subclasses at the beginning of their resolve() method.
167     */
168    protected synchronized void setResolveEnd() {
169        resolved = true;
170        resolving = false;
171
172        triggerResolveEnd();
173    }
174
175    // Listeners management
176
177    /**
178     * Add a transport resolver listener.
179     *
180     * @param li The transport resolver listener to be added.
181     */
182    public void addListener(TransportResolverListener li) {
183        synchronized (listeners) {
184            listeners.add(li);
185        }
186    }
187
188    /**
189     * Removes a transport resolver listener.
190     *
191     * @param li The transport resolver listener to be removed
192     */
193    public void removeListener(TransportResolverListener li) {
194        synchronized (listeners) {
195            listeners.remove(li);
196        }
197    }
198
199    /**
200     * Get the list of listeners.
201     *
202     * @return the list of listeners
203     */
204    public ArrayList<TransportResolverListener> getListenersList() {
205        synchronized (listeners) {
206            return new ArrayList<>(listeners);
207        }
208    }
209
210    /**
211     * Trigger a new candidate added event.
212     *
213     * @param cand The candidate added to the list of candidates.
214     * @throws NotConnectedException 
215     * @throws InterruptedException 
216     */
217    protected void triggerCandidateAdded(TransportCandidate cand) throws NotConnectedException, InterruptedException {
218        Iterator<TransportResolverListener> iter = getListenersList().iterator();
219        while (iter.hasNext()) {
220            TransportResolverListener trl = iter.next();
221            if (trl instanceof TransportResolverListener.Resolver) {
222                TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl;
223                LOGGER.fine("triggerCandidateAdded : " + cand.getLocalIp());
224                li.candidateAdded(cand);
225            }
226        }
227    }
228
229    /**
230     * Trigger a event notifying the initialization of the resolution process.
231     */
232    private void triggerResolveInit() {
233        Iterator<TransportResolverListener> iter = getListenersList().iterator();
234        while (iter.hasNext()) {
235            TransportResolverListener trl = iter.next();
236            if (trl instanceof TransportResolverListener.Resolver) {
237                TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl;
238                li.init();
239            }
240        }
241    }
242
243    /**
244     * Trigger a event notifying the obtainment of all the candidates.
245     */
246    private void triggerResolveEnd() {
247        Iterator<TransportResolverListener> iter = getListenersList().iterator();
248        while (iter.hasNext()) {
249            TransportResolverListener trl = iter.next();
250            if (trl instanceof TransportResolverListener.Resolver) {
251                TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl;
252                li.end();
253            }
254        }
255    }
256
257    // Candidates management
258
259    /**
260     * Clear the list of candidate
261     */
262    protected void clearCandidates() {
263        synchronized (candidates) {
264            candidates.clear();
265        }
266    }
267
268    /**
269     * Add a new transport candidate
270     *
271     * @param cand The candidate to add
272     * @throws NotConnectedException 
273     * @throws InterruptedException 
274     */
275    protected void addCandidate(TransportCandidate cand) throws NotConnectedException, InterruptedException {
276        synchronized (candidates) {
277            if (!candidates.contains(cand))
278                candidates.add(cand);
279        }
280
281        // Notify the listeners
282        triggerCandidateAdded(cand);
283    }
284
285    /**
286     * Get an iterator for the list of candidates.
287     *
288     * @return an iterator
289     */
290    public Iterator<TransportCandidate> getCandidates() {
291        synchronized (candidates) {
292            return Collections.unmodifiableList(new ArrayList<>(candidates)).iterator();
293        }
294    }
295
296    /**
297     * Get the candidate with the highest preference.
298     *
299     * @return The best candidate, according to the preference order.
300     */
301    public TransportCandidate getPreferredCandidate() {
302        TransportCandidate result = null;
303
304        ArrayList<ICECandidate> cands = new ArrayList<>();
305        for (TransportCandidate tpcan : getCandidatesList()) {
306            if (tpcan instanceof ICECandidate)
307                cands.add((ICECandidate) tpcan);
308        }
309
310        // (ArrayList<ICECandidate>) getCandidatesList();
311        if (cands.size() > 0) {
312            Collections.sort(cands);
313            // Return the last candidate
314            result = cands.get(cands.size() - 1);
315            LOGGER.fine("Result: " + result.getIp());
316        }
317
318        return result;
319    }
320
321    /**
322     * Get the number of transport candidates.
323     *
324     * @return The length of the transport candidates list.
325     */
326    public int getCandidateCount() {
327        synchronized (candidates) {
328            return candidates.size();
329        }
330    }
331
332    /**
333     * Get the list of candidates.
334     *
335     * @return the list of transport candidates
336     */
337    public List<TransportCandidate> getCandidatesList() {
338        List<TransportCandidate> result;
339
340        synchronized (candidates) {
341            result = new ArrayList<>(candidates);
342        }
343
344        return result;
345    }
346
347    /**
348     * Get the n-th candidate.
349     *
350     * @return a transport candidate
351     */
352    public TransportCandidate getCandidate(int i) {
353        TransportCandidate cand;
354
355        synchronized (candidates) {
356            cand = candidates.get(i);
357        }
358        return cand;
359    }
360
361    /**
362     * Initialize Transport Resolver and wait until it is completely uninitialized.
363     * @throws SmackException 
364     * @throws InterruptedException 
365     */
366    public void initializeAndWait() throws XMPPException, SmackException, InterruptedException {
367        this.initialize();
368        try {
369            LOGGER.fine("Initializing transport resolver...");
370            while (!this.isInitialized()) {
371                LOGGER.fine("Resolver init still pending");
372                Thread.sleep(1000);
373            }
374            LOGGER.fine("Transport resolved");
375        }
376        catch (Exception e) {
377            LOGGER.log(Level.WARNING, "exception", e);
378        }
379    }
380
381    /**
382     * Obtain a free port we can use.
383     *
384     * @return A free port number.
385     */
386    protected int getFreePort() {
387        ServerSocket ss;
388        int freePort = 0;
389
390        for (int i = 0; i < 10; i++) {
391            freePort = (int) (10000 + Math.round(Math.random() * 10000));
392            freePort = freePort % 2 == 0 ? freePort : freePort + 1;
393            try {
394                ss = new ServerSocket(freePort);
395                freePort = ss.getLocalPort();
396                ss.close();
397                return freePort;
398            }
399            catch (IOException e) {
400                LOGGER.log(Level.WARNING, "exception", e);
401            }
402        }
403        try {
404            ss = new ServerSocket(0);
405            freePort = ss.getLocalPort();
406            ss.close();
407        }
408        catch (IOException e) {
409            LOGGER.log(Level.WARNING, "exception", e);
410        }
411        return freePort;
412    }
413}