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}