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