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