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