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.smack.util.PacketParserUtils;
032import org.jivesoftware.smack.xml.XmlPullParser;
033import org.jivesoftware.smack.xml.XmlPullParserException;
034
035import org.jivesoftware.smackx.jingleold.JingleSession;
036
037import de.javawi.jstun.test.BindingLifetimeTest;
038import de.javawi.jstun.test.DiscoveryInfo;
039import de.javawi.jstun.test.DiscoveryTest;
040
041/**
042 * Transport resolver using the JSTUN library, to discover public IP and use it as a candidate.
043 *
044 * 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.
045 *
046 * @author Thiago Camargo
047 */
048public class STUNResolver extends TransportResolver {
049
050    private static final Logger LOGGER = Logger.getLogger(STUNResolver.class.getName());
051
052    // The filename where the STUN servers are stored.
053    public static final String STUNSERVERS_FILENAME = "META-INF/stun-config.xml";
054
055    // Current STUN server we are using
056    protected STUNService currentServer;
057
058    protected Thread resolverThread;
059
060    protected int defaultPort;
061
062    protected String resolvedPublicIP;
063    protected String resolvedLocalIP;
064
065    /**
066     * Constructor with default STUN server.
067     */
068    public STUNResolver() {
069        super();
070
071        this.defaultPort = 0;
072        this.currentServer = new STUNService();
073    }
074
075    /**
076     * Constructor with a default port.
077     *
078     * @param defaultPort Port to use by default.
079     */
080    public STUNResolver(int defaultPort) {
081        this();
082
083        this.defaultPort = defaultPort;
084    }
085
086    /**
087     * Return true if the service is working.
088     *
089     * @see TransportResolver#isResolving()
090     */
091    @Override
092    public boolean isResolving() {
093        return super.isResolving() && resolverThread != null;
094    }
095
096    /**
097     * Set the STUN server name and port.
098     *
099     * @param ip   the STUN server name
100     * @param port the STUN server port
101     */
102    public void setSTUNService(String ip, int port) {
103        currentServer = new STUNService(ip, port);
104    }
105
106    /**
107     * Get the name of the current STUN server.
108     *
109     * @return the name of the STUN server
110     */
111    public String getCurrentServerName() {
112        if (!currentServer.isNull()) {
113            return currentServer.getHostname();
114        } else {
115            return null;
116        }
117    }
118
119    /**
120     * Get the port of the current STUN server.
121     *
122     * @return the port of the STUN server
123     */
124    public int getCurrentServerPort() {
125        if (!currentServer.isNull()) {
126            return currentServer.getPort();
127        } else {
128            return 0;
129        }
130    }
131
132    /**
133     * Load the STUN configuration from a stream.
134     *
135     * @param stunConfigStream An InputStream with the configuration file.
136     * @return A list of loaded servers
137     */
138    public ArrayList<STUNService> loadSTUNServers(java.io.InputStream stunConfigStream) {
139        ArrayList<STUNService> serversList = new ArrayList<>();
140        String serverName;
141        int serverPort;
142
143        try {
144            XmlPullParser parser = PacketParserUtils.getParserFor(stunConfigStream);
145
146            XmlPullParser.Event eventType = parser.getEventType();
147            do {
148                if (eventType == XmlPullParser.Event.START_ELEMENT) {
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.Event.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     *
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     *
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<>();
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;
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 static 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 if the XMPP connection is not connected.
267     * @throws InterruptedException if the calling thread was interrupted.
268     */
269    @Override
270    public synchronized void resolve(JingleSession session) throws XMPPException, NotConnectedException, InterruptedException {
271
272        setResolveInit();
273
274        clearCandidates();
275
276        TransportCandidate candidate = new TransportCandidate.Fixed(
277                resolvedPublicIP, getFreePort());
278        candidate.setLocalIp(resolvedLocalIP);
279
280        LOGGER.fine("RESOLVING : " + resolvedPublicIP + ":" + candidate.getPort());
281
282        addCandidate(candidate);
283
284        setResolveEnd();
285
286    }
287
288    /**
289     * Initialize the resolver.
290     *
291     * @throws XMPPException if an XMPP protocol error was received.
292     */
293    @Override
294    public void initialize() throws XMPPException {
295        LOGGER.fine("Initialized");
296        if (!isResolving() && !isResolved()) {
297            // Get the best STUN server available
298            if (currentServer.isNull()) {
299                loadSTUNServers();
300            }
301            // We should have a valid STUN server by now...
302            if (!currentServer.isNull()) {
303
304                clearCandidates();
305
306                resolverThread = new Thread(new Runnable() {
307                    @Override
308                    public void run() {
309                        // Iterate through the list of interfaces, and ask
310                        // to the STUN server for our address.
311                        try {
312                            Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();
313                            String candAddress;
314                            int candPort;
315
316                            while (ifaces.hasMoreElements()) {
317
318                                NetworkInterface iface =  ifaces.nextElement();
319                                Enumeration<InetAddress> iaddresses = iface.getInetAddresses();
320
321                                while (iaddresses.hasMoreElements()) {
322                                    InetAddress iaddress = iaddresses.nextElement();
323                                    if (!iaddress.isLoopbackAddress()
324                                            && !iaddress.isLinkLocalAddress()) {
325
326                                        // Reset the candidate
327                                        candAddress = null;
328                                        candPort = -1;
329
330                                        DiscoveryTest test = new DiscoveryTest(iaddress,
331                                                currentServer.getHostname(),
332                                                currentServer.getPort());
333                                        try {
334                                            // Run the tests and get the
335                                            // discovery
336                                            // information, where all the
337                                            // info is stored...
338                                            DiscoveryInfo di = test.test();
339
340                                            candAddress = di.getPublicIP() != null ?
341                                                    di.getPublicIP().getHostAddress() : null;
342
343                                            // Get a valid port
344                                            if (defaultPort == 0) {
345                                                candPort = getFreePort();
346                                            } else {
347                                                candPort = defaultPort;
348                                            }
349
350                                            // If we have a valid candidate,
351                                            // add it to the list.
352                                            if (candAddress != null && candPort >= 0) {
353                                                TransportCandidate candidate = new TransportCandidate.Fixed(
354                                                        candAddress, candPort);
355                                                candidate.setLocalIp(iaddress.getHostAddress() != null ? iaddress.getHostAddress() : iaddress.getHostName());
356                                                addCandidate(candidate);
357
358                                                resolvedPublicIP = candidate.getIp();
359                                                resolvedLocalIP = candidate.getLocalIp();
360                                                return;
361                                            }
362                                        }
363                                        catch (Exception e) {
364                                            LOGGER.log(Level.SEVERE, "Exception", e);
365                                        }
366                                    }
367                                }
368                            }
369                        }
370                        catch (SocketException e) {
371                            LOGGER.log(Level.SEVERE, "Exception", e);
372                        }
373                        finally {
374                            setInitialized();
375                        }
376                    }
377                }, "Waiting for all the transport candidates checks...");
378
379                resolverThread.setName("STUN resolver");
380                resolverThread.start();
381            } else {
382                throw new IllegalStateException("No valid STUN server found.");
383            }
384        }
385    }
386
387    /**
388     * Cancel any operation.
389     *
390     * @see TransportResolver#cancel()
391     */
392    @Override
393    public synchronized void cancel() throws XMPPException {
394        if (isResolving()) {
395            resolverThread.interrupt();
396            setResolveEnd();
397        }
398    }
399
400    /**
401     * Clear the list of candidates and start the resolution again.
402     *
403     * @see TransportResolver#clear()
404     */
405    @Override
406    public synchronized void clear() throws XMPPException {
407        this.defaultPort = 0;
408        super.clear();
409    }
410
411    /**
412     * STUN service definition.
413     */
414    protected static class STUNService {
415
416        private String hostname; // The hostname of the service
417
418        private int port; // The port number
419
420        /**
421         * Basic constructor, with the hostname and port
422         *
423         * @param hostname The hostname
424         * @param port     The port
425         */
426        public STUNService(String hostname, int port) {
427            super();
428
429            this.hostname = hostname;
430            this.port = port;
431        }
432
433        /**
434         * Default constructor, without name and port.
435         */
436        public STUNService() {
437            this(null, -1);
438        }
439
440        /**
441         * Get the host name of the STUN service.
442         *
443         * @return The host name
444         */
445        public String getHostname() {
446            return hostname;
447        }
448
449        /**
450         * Set the hostname of the STUN service.
451         *
452         * @param hostname The host name of the service.
453         */
454        public void setHostname(String hostname) {
455            this.hostname = hostname;
456        }
457
458        /**
459         * Get the port of the STUN service
460         *
461         * @return The port number where the STUN server is waiting.
462         */
463        public int getPort() {
464            return port;
465        }
466
467        /**
468         * Set the port number for the STUN service.
469         *
470         * @param port The port number.
471         */
472        public void setPort(int port) {
473            this.port = port;
474        }
475
476        /**
477         * Basic format test: the service is not null.
478         *
479         * @return true if the hostname and port are null
480         */
481        public boolean isNull() {
482            if (hostname == null) {
483                return true;
484            } else if (hostname.length() == 0) {
485                return true;
486            } else if (port < 0) {
487                return true;
488            } else {
489                return false;
490            }
491        }
492
493        /**
494         * Check a binding with the STUN currentServer.
495         *
496         * Note: this function blocks for some time, waiting for a response.
497         *
498         * @return true if the currentServer is usable.
499         */
500        public boolean checkBinding() {
501            boolean result = false;
502
503            try {
504                BindingLifetimeTest binding = new BindingLifetimeTest(hostname, port);
505
506                binding.test();
507
508                while (true) {
509                    Thread.sleep(5000);
510                    if (binding.getLifetime() != -1) {
511                        if (binding.isCompleted()) {
512                            return true;
513                        }
514                    } else {
515                        break;
516                    }
517                }
518            }
519            catch (Exception e) {
520                LOGGER.log(Level.SEVERE, "Exception in checkBinding", e);
521            }
522
523            return result;
524        }
525    }
526}