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 serverPort = Integer.parseInt(parser.nextText()); 165 166 // If we have a valid hostname and port, add 167 // it to the list. 168 if (serverName != null && serverPort != -1) { 169 STUNService service = new STUNService(serverName, serverPort); 170 171 serversList.add(service); 172 } 173 } 174 } 175 eventType = parser.next(); 176 177 } 178 while (eventType != XmlPullParser.Event.END_DOCUMENT); 179 180 } 181 catch (XmlPullParserException e) { 182 LOGGER.log(Level.SEVERE, "Exception", e); 183 } 184 catch (IOException e) { 185 LOGGER.log(Level.SEVERE, "Exception", e); 186 } 187 188 currentServer = bestSTUNServer(serversList); 189 190 return serversList; 191 } 192 193 /** 194 * Load a list of services: STUN servers and ports. Some public STUN servers 195 * are: 196 * 197 * <pre> 198 * iphone-stun.freenet.de:3478 199 * larry.gloo.net:3478 200 * stun.xten.net:3478 201 * stun.fwdnet.net 202 * stun.fwd.org (no DNS SRV record) 203 * stun01.sipphone.com (no DNS SRV record) 204 * stun.softjoys.com (no DNS SRV record) 205 * stun.voipbuster.com (no DNS SRV record) 206 * stun.voxgratia.org (no DNS SRV record) 207 * stun.noc.ams-ix.net 208 * </pre> 209 * 210 * This list should be contained in a file in the "META-INF" directory 211 * 212 * @return a list of services 213 */ 214 public ArrayList<STUNService> loadSTUNServers() { 215 ArrayList<STUNService> serversList = new ArrayList<>(); 216 217 // Load the STUN configuration 218 try { 219 // Get an array of class loaders to try loading the config from. 220 ClassLoader[] classLoaders = new ClassLoader[2]; 221 classLoaders[0] = new STUNResolver() { 222 }.getClass().getClassLoader(); 223 classLoaders[1] = Thread.currentThread().getContextClassLoader(); 224 225 for (int i = 0; i < classLoaders.length; i++) { 226 Enumeration<URL> stunConfigEnum = classLoaders[i] 227 .getResources(STUNSERVERS_FILENAME); 228 229 while (stunConfigEnum.hasMoreElements() && serversList.isEmpty()) { 230 URL url = stunConfigEnum.nextElement(); 231 java.io.InputStream stunConfigStream; 232 233 stunConfigStream = url.openStream(); 234 serversList.addAll(loadSTUNServers(stunConfigStream)); 235 stunConfigStream.close(); 236 } 237 } 238 } 239 catch (Exception e) { 240 LOGGER.log(Level.SEVERE, "Exception", e); 241 } 242 243 return serversList; 244 } 245 246 /** 247 * Get the best usable STUN server from a list. 248 * 249 * @return the best STUN server that can be used. 250 */ 251 private static STUNService bestSTUNServer(ArrayList<STUNService> listServers) { 252 if (listServers.isEmpty()) { 253 return null; 254 } else { 255 // TODO: this should use some more advanced criteria... 256 return listServers.get(0); 257 } 258 } 259 260 /** 261 * Resolve the IP and obtain a valid transport method. 262 * @throws NotConnectedException if the XMPP connection is not connected. 263 * @throws InterruptedException if the calling thread was interrupted. 264 */ 265 @Override 266 public synchronized void resolve(JingleSession session) throws XMPPException, NotConnectedException, InterruptedException { 267 268 setResolveInit(); 269 270 clearCandidates(); 271 272 TransportCandidate candidate = new TransportCandidate.Fixed( 273 resolvedPublicIP, getFreePort()); 274 candidate.setLocalIp(resolvedLocalIP); 275 276 LOGGER.fine("RESOLVING : " + resolvedPublicIP + ":" + candidate.getPort()); 277 278 addCandidate(candidate); 279 280 setResolveEnd(); 281 282 } 283 284 /** 285 * Initialize the resolver. 286 * 287 * @throws XMPPException if an XMPP protocol error was received. 288 */ 289 @Override 290 public void initialize() throws XMPPException { 291 LOGGER.fine("Initialized"); 292 if (!isResolving() && !isResolved()) { 293 // Get the best STUN server available 294 if (currentServer.isNull()) { 295 loadSTUNServers(); 296 } 297 // We should have a valid STUN server by now... 298 if (!currentServer.isNull()) { 299 300 clearCandidates(); 301 302 resolverThread = new Thread(new Runnable() { 303 @Override 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 @Override 389 public synchronized void cancel() throws XMPPException { 390 if (isResolving()) { 391 resolverThread.interrupt(); 392 setResolveEnd(); 393 } 394 } 395 396 /** 397 * Clear the list of candidates and start the resolution again. 398 * 399 * @see TransportResolver#clear() 400 */ 401 @Override 402 public synchronized void clear() throws XMPPException { 403 this.defaultPort = 0; 404 super.clear(); 405 } 406 407 /** 408 * STUN service definition. 409 */ 410 protected static class STUNService { 411 412 private String hostname; // The hostname of the service 413 414 private int port; // The port number 415 416 /** 417 * Basic constructor, with the hostname and port 418 * 419 * @param hostname The hostname 420 * @param port The port 421 */ 422 public STUNService(String hostname, int port) { 423 super(); 424 425 this.hostname = hostname; 426 this.port = port; 427 } 428 429 /** 430 * Default constructor, without name and port. 431 */ 432 public STUNService() { 433 this(null, -1); 434 } 435 436 /** 437 * Get the host name of the STUN service. 438 * 439 * @return The host name 440 */ 441 public String getHostname() { 442 return hostname; 443 } 444 445 /** 446 * Set the hostname of the STUN service. 447 * 448 * @param hostname The host name of the service. 449 */ 450 public void setHostname(String hostname) { 451 this.hostname = hostname; 452 } 453 454 /** 455 * Get the port of the STUN service 456 * 457 * @return The port number where the STUN server is waiting. 458 */ 459 public int getPort() { 460 return port; 461 } 462 463 /** 464 * Set the port number for the STUN service. 465 * 466 * @param port The port number. 467 */ 468 public void setPort(int port) { 469 this.port = port; 470 } 471 472 /** 473 * Basic format test: the service is not null. 474 * 475 * @return true if the hostname and port are null 476 */ 477 public boolean isNull() { 478 if (hostname == null) { 479 return true; 480 } else if (hostname.length() == 0) { 481 return true; 482 } else if (port < 0) { 483 return true; 484 } else { 485 return false; 486 } 487 } 488 489 /** 490 * Check a binding with the STUN currentServer. 491 * 492 * Note: this function blocks for some time, waiting for a response. 493 * 494 * @return true if the currentServer is usable. 495 */ 496 public boolean checkBinding() { 497 boolean result = false; 498 499 try { 500 BindingLifetimeTest binding = new BindingLifetimeTest(hostname, port); 501 502 binding.test(); 503 504 while (true) { 505 Thread.sleep(5000); 506 if (binding.getLifetime() != -1) { 507 if (binding.isCompleted()) { 508 return true; 509 } 510 } else { 511 break; 512 } 513 } 514 } 515 catch (Exception e) { 516 LOGGER.log(Level.SEVERE, "Exception in checkBinding", e); 517 } 518 519 return result; 520 } 521 } 522}