001/**
002 *
003 * Copyright 2003-2006 Jive Software.
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.jingle.nat;
018
019import java.io.IOException;
020import java.net.InetAddress;
021import java.net.NetworkInterface;
022import java.net.SocketException;
023import java.net.URL;
024import java.util.ArrayList;
025import java.util.Enumeration;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029import org.jivesoftware.smack.SmackException.NotConnectedException;
030import org.jivesoftware.smack.XMPPException;
031import org.jivesoftware.smackx.jingle.JingleSession;
032import org.xmlpull.v1.XmlPullParserFactory;
033import org.xmlpull.v1.XmlPullParser;
034import org.xmlpull.v1.XmlPullParserException;
035
036import de.javawi.jstun.test.BindingLifetimeTest;
037import de.javawi.jstun.test.DiscoveryInfo;
038import de.javawi.jstun.test.DiscoveryTest;
039
040/**
041 * Transport resolver using the JSTUN library, to discover public IP and use it as a candidate.
042 *
043 * The goal of this resolver is to take possible to establish and manage out-of-band connections between two XMPP entities, even if they are behind Network Address Translators (NATs) or firewalls.
044 *
045 * @author Thiago Camargo
046 */
047public class STUNResolver extends TransportResolver {
048
049        private static final Logger LOGGER = Logger.getLogger(STUNResolver.class.getName());
050
051        // The filename where the STUN servers are stored.
052    public final static String STUNSERVERS_FILENAME = "META-INF/stun-config.xml";
053
054    // Current STUN server we are using
055    protected STUNService currentServer;
056
057    protected Thread resolverThread;
058
059    protected int defaultPort;
060
061    protected String resolvedPublicIP;
062    protected String resolvedLocalIP;
063
064    /**
065     * Constructor with default STUN server.
066     */
067    public STUNResolver() {
068        super();
069
070        this.defaultPort = 0;
071        this.currentServer = new STUNService();
072    }
073
074    /**
075     * Constructor with a default port.
076     *
077     * @param defaultPort Port to use by default.
078     */
079    public STUNResolver(int defaultPort) {
080        this();
081
082        this.defaultPort = defaultPort;
083    }
084
085    /**
086     * Return true if the service is working.
087     *
088     * @see TransportResolver#isResolving()
089     */
090    public boolean isResolving() {
091        return super.isResolving() && resolverThread != null;
092    }
093
094    /**
095     * Set the STUN server name and port
096     *
097     * @param ip   the STUN server name
098     * @param port the STUN server port
099     */
100    public void setSTUNService(String ip, int port) {
101        currentServer = new STUNService(ip, port);
102    }
103
104    /**
105     * Get the name of the current STUN server.
106     *
107     * @return the name of the STUN server
108     */
109    public String getCurrentServerName() {
110        if (!currentServer.isNull()) {
111            return currentServer.getHostname();
112        } else {
113            return null;
114        }
115    }
116
117    /**
118     * Get the port of the current STUN server.
119     *
120     * @return the port of the STUN server
121     */
122    public int getCurrentServerPort() {
123        if (!currentServer.isNull()) {
124            return currentServer.getPort();
125        } else {
126            return 0;
127        }
128    }
129
130    /**
131     * Load the STUN configuration from a stream.
132     *
133     * @param stunConfigStream An InputStream with the configuration file.
134     * @return A list of loaded servers
135     */
136    public ArrayList<STUNService> loadSTUNServers(java.io.InputStream stunConfigStream) {
137        ArrayList<STUNService> serversList = new ArrayList<STUNService>();
138        String serverName;
139        int serverPort;
140
141        try {
142            XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
143            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
144            parser.setInput(stunConfigStream, "UTF-8");
145
146            int eventType = parser.getEventType();
147            do {
148                if (eventType == XmlPullParser.START_TAG) {
149
150                    // Parse a STUN server definition
151                    if (parser.getName().equals("stunServer")) {
152
153                        serverName = null;
154                        serverPort = -1;
155
156                        // Parse the hostname
157                        parser.next();
158                        parser.next();
159                        serverName = parser.nextText();
160
161                        // Parse the port
162                        parser.next();
163                        parser.next();
164                        try {
165                            serverPort = Integer.parseInt(parser.nextText());
166                        }
167                        catch (Exception e) {
168                        }
169
170                        // If we have a valid hostname and port, add
171                        // it to the list.
172                        if (serverName != null && serverPort != -1) {
173                            STUNService service = new STUNService(serverName, serverPort);
174
175                            serversList.add(service);
176                        }
177                    }
178                }
179                eventType = parser.next();
180
181            }
182            while (eventType != XmlPullParser.END_DOCUMENT);
183
184        }
185        catch (XmlPullParserException e) {
186            LOGGER.log(Level.SEVERE, "Exception", e);
187        }
188        catch (IOException e) {
189            LOGGER.log(Level.SEVERE, "Exception", e);
190        }
191
192        currentServer = bestSTUNServer(serversList);
193
194        return serversList;
195    }
196
197    /**
198     * Load a list of services: STUN servers and ports. Some public STUN servers
199     * are:
200     * <p/>
201     * <pre>
202     *               iphone-stun.freenet.de:3478
203     *               larry.gloo.net:3478
204     *               stun.xten.net:3478
205     *               stun.fwdnet.net
206     *               stun.fwd.org (no DNS SRV record)
207     *               stun01.sipphone.com (no DNS SRV record)
208     *               stun.softjoys.com (no DNS SRV record)
209     *               stun.voipbuster.com (no DNS SRV record)
210     *               stun.voxgratia.org (no DNS SRV record)
211     *               stun.noc.ams-ix.net
212     * </pre>
213     * <p/>
214     * This list should be contained in a file in the "META-INF" directory
215     *
216     * @return a list of services
217     */
218    public ArrayList<STUNService> loadSTUNServers() {
219        ArrayList<STUNService> serversList = new ArrayList<STUNService>();
220
221        // Load the STUN configuration
222        try {
223            // Get an array of class loaders to try loading the config from.
224            ClassLoader[] classLoaders = new ClassLoader[2];
225            classLoaders[0] = new STUNResolver() {
226            }.getClass().getClassLoader();
227            classLoaders[1] = Thread.currentThread().getContextClassLoader();
228
229            for (int i = 0; i < classLoaders.length; i++) {
230                Enumeration<URL> stunConfigEnum = classLoaders[i]
231                        .getResources(STUNSERVERS_FILENAME);
232
233                while (stunConfigEnum.hasMoreElements() && serversList.isEmpty()) {
234                    URL url = stunConfigEnum.nextElement();
235                    java.io.InputStream stunConfigStream = null;
236
237                    stunConfigStream = url.openStream();
238                    serversList.addAll(loadSTUNServers(stunConfigStream));
239                    stunConfigStream.close();
240                }
241            }
242        }
243        catch (Exception e) {
244            LOGGER.log(Level.SEVERE, "Exception", e);
245        }
246
247        return serversList;
248    }
249
250    /**
251     * Get the best usable STUN server from a list.
252     *
253     * @return the best STUN server that can be used.
254     */
255    private STUNService bestSTUNServer(ArrayList<STUNService> listServers) {
256        if (listServers.isEmpty()) {
257            return null;
258        } else {
259            // TODO: this should use some more advanced criteria...
260            return listServers.get(0);
261        }
262    }
263
264    /**
265     * Resolve the IP and obtain a valid transport method.
266     * @throws NotConnectedException 
267     */
268    public synchronized void resolve(JingleSession session) throws XMPPException, NotConnectedException {
269
270        setResolveInit();
271
272        clearCandidates();
273
274        TransportCandidate candidate = new TransportCandidate.Fixed(
275                resolvedPublicIP, getFreePort());
276        candidate.setLocalIp(resolvedLocalIP);
277
278        LOGGER.fine("RESOLVING : " + resolvedPublicIP + ":" + candidate.getPort());
279
280        addCandidate(candidate);
281
282        setResolveEnd();
283
284    }
285
286    /**
287     * Initialize the resolver.
288     *
289     * @throws XMPPException
290     */
291    public void initialize() throws XMPPException {
292        LOGGER.fine("Initialized");
293        if (!isResolving()&&!isResolved()) {
294            // Get the best STUN server available
295            if (currentServer.isNull()) {
296                loadSTUNServers();
297            }
298            // We should have a valid STUN server by now...
299            if (!currentServer.isNull()) {
300
301                clearCandidates();
302
303                resolverThread = new Thread(new Runnable() {
304                    public void run() {
305                        // Iterate through the list of interfaces, and ask
306                        // to the STUN server for our address.
307                        try {
308                            Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();
309                            String candAddress;
310                            int candPort;
311
312                            while (ifaces.hasMoreElements()) {
313
314                                NetworkInterface iface =  ifaces.nextElement();
315                                Enumeration<InetAddress> iaddresses = iface.getInetAddresses();
316
317                                while (iaddresses.hasMoreElements()) {
318                                    InetAddress iaddress = iaddresses.nextElement();
319                                    if (!iaddress.isLoopbackAddress()
320                                            && !iaddress.isLinkLocalAddress()) {
321
322                                        // Reset the candidate
323                                        candAddress = null;
324                                        candPort = -1;
325
326                                        DiscoveryTest test = new DiscoveryTest(iaddress,
327                                                currentServer.getHostname(),
328                                                currentServer.getPort());
329                                        try {
330                                            // Run the tests and get the
331                                            // discovery
332                                            // information, where all the
333                                            // info is stored...
334                                            DiscoveryInfo di = test.test();
335
336                                            candAddress = di.getPublicIP() != null ?
337                                                    di.getPublicIP().getHostAddress() : null;
338
339                                            // Get a valid port
340                                            if (defaultPort == 0) {
341                                                candPort = getFreePort();
342                                            } else {
343                                                candPort = defaultPort;
344                                            }
345
346                                            // If we have a valid candidate,
347                                            // add it to the list.
348                                            if (candAddress != null && candPort >= 0) {
349                                                TransportCandidate candidate = new TransportCandidate.Fixed(
350                                                        candAddress, candPort);
351                                                candidate.setLocalIp(iaddress.getHostAddress() != null ? iaddress.getHostAddress() : iaddress.getHostName());
352                                                addCandidate(candidate);
353
354                                                resolvedPublicIP = candidate.getIp();
355                                                resolvedLocalIP = candidate.getLocalIp();
356                                                return;
357                                            }
358                                        }
359                                        catch (Exception e) {
360                                            LOGGER.log(Level.SEVERE, "Exception", e);
361                                        }
362                                    }
363                                }
364                            }
365                        }
366                        catch (SocketException e) {
367                            LOGGER.log(Level.SEVERE, "Exception", e);
368                        }
369                        finally {
370                            setInitialized();
371                        }
372                    }
373                }, "Waiting for all the transport candidates checks...");
374
375                resolverThread.setName("STUN resolver");
376                resolverThread.start();
377            } else {
378                throw new IllegalStateException("No valid STUN server found.");
379            }
380        }
381    }
382
383    /**
384     * Cancel any operation.
385     *
386     * @see TransportResolver#cancel()
387     */
388    public synchronized void cancel() throws XMPPException {
389        if (isResolving()) {
390            resolverThread.interrupt();
391            setResolveEnd();
392        }
393    }
394
395    /**
396     * Clear the list of candidates and start the resolution again.
397     *
398     * @see TransportResolver#clear()
399     */
400    public synchronized void clear() throws XMPPException {
401        this.defaultPort = 0;
402        super.clear();
403    }
404
405    /**
406     * STUN service definition.
407     */
408    protected class STUNService {
409
410        private String hostname; // The hostname of the service
411
412        private int port; // The port number
413
414        /**
415         * Basic constructor, with the hostname and port
416         *
417         * @param hostname The hostname
418         * @param port     The port
419         */
420        public STUNService(String hostname, int port) {
421            super();
422
423            this.hostname = hostname;
424            this.port = port;
425        }
426
427        /**
428         * Default constructor, without name and port.
429         */
430        public STUNService() {
431            this(null, -1);
432        }
433
434        /**
435         * Get the host name of the STUN service.
436         *
437         * @return The host name
438         */
439        public String getHostname() {
440            return hostname;
441        }
442
443        /**
444         * Set the hostname of the STUN service.
445         *
446         * @param hostname The host name of the service.
447         */
448        public void setHostname(String hostname) {
449            this.hostname = hostname;
450        }
451
452        /**
453         * Get the port of the STUN service
454         *
455         * @return The port number where the STUN server is waiting.
456         */
457        public int getPort() {
458            return port;
459        }
460
461        /**
462         * Set the port number for the STUN service.
463         *
464         * @param port The port number.
465         */
466        public void setPort(int port) {
467            this.port = port;
468        }
469
470        /**
471         * Basic format test: the service is not null.
472         *
473         * @return true if the hostname and port are null
474         */
475        public boolean isNull() {
476            if (hostname == null) {
477                return true;
478            } else if (hostname.length() == 0) {
479                return true;
480            } else if (port < 0) {
481                return true;
482            } else {
483                return false;
484            }
485        }
486
487        /**
488         * Check a binding with the STUN currentServer.
489         * <p/>
490         * Note: this function blocks for some time, waiting for a response.
491         *
492         * @return true if the currentServer is usable.
493         */
494        public boolean checkBinding() {
495            boolean result = false;
496
497            try {
498                BindingLifetimeTest binding = new BindingLifetimeTest(hostname, port);
499
500                binding.test();
501
502                while (true) {
503                    Thread.sleep(5000);
504                    if (binding.getLifetime() != -1) {
505                        if (binding.isCompleted()) {
506                            return true;
507                        }
508                    } else {
509                        break;
510                    }
511                }
512            }
513            catch (Exception e) {
514                LOGGER.log(Level.SEVERE, "Exception in checkBinding", e);
515            }
516
517            return result;
518        }
519    }
520}