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