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