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}