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