001/** 002 * 003 * Copyright the original author or authors 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.io.UnsupportedEncodingException; 021import java.net.DatagramPacket; 022import java.net.DatagramSocket; 023import java.net.InetAddress; 024import java.net.SocketException; 025import java.net.UnknownHostException; 026import java.nio.ByteBuffer; 027import java.util.ArrayList; 028import java.util.List; 029import java.util.Locale; 030import java.util.logging.Logger; 031 032import org.jivesoftware.smack.XMPPConnection; 033import org.jivesoftware.smackx.jingle.JingleSession; 034import org.jivesoftware.smackx.jingle.nat.TransportResolverListener.Checker; 035 036/** 037 * Transport candidate. 038 * <p/> 039 * A candidate represents the possible transport for data interchange between 040 * the two endpoints. 041 * 042 * @author Thiago Camargo 043 * @author Alvaro Saurin 044 */ 045public abstract class TransportCandidate { 046 047 private static final Logger LOGGER = Logger.getLogger(TransportCandidate.class.getName()); 048 049 private String name; 050 051 private String ip; // IP address 052 053 private int port; // Port to use, or 0 for any port 054 055 private String localIp; 056 057 private int generation; 058 059 protected String password; 060 061 private String sessionId; 062 063 private XMPPConnection connection; 064 065 private TransportCandidate symmetric; 066 067 private CandidateEcho candidateEcho = null; 068 069 private Thread echoThread = null; 070 071 // Listeners for events 072 private final List<TransportResolverListener.Checker> listeners = new ArrayList<Checker>(); 073 074 public void addCandidateEcho(JingleSession session) throws SocketException, UnknownHostException { 075 candidateEcho = new CandidateEcho(this, session); 076 echoThread = new Thread(candidateEcho); 077 echoThread.start(); 078 } 079 080 public void removeCandidateEcho() { 081 if (candidateEcho != null) 082 candidateEcho.cancel(); 083 candidateEcho = null; 084 echoThread = null; 085 } 086 087 public CandidateEcho getCandidateEcho() { 088 return candidateEcho; 089 } 090 091 public String getIp() { 092 return ip; 093 } 094 095 /** 096 * Set the IP address. 097 * 098 * @param ip the IP address 099 */ 100 public void setIp(String ip) { 101 this.ip = ip; 102 } 103 104 /** 105 * Get local IP to bind to this candidate 106 * 107 * @return the local IP 108 */ 109 public String getLocalIp() { 110 return localIp == null ? ip : localIp; 111 } 112 113 /** 114 * Set local IP to bind to this candidate 115 * 116 * @param localIp 117 */ 118 public void setLocalIp(String localIp) { 119 this.localIp = localIp; 120 } 121 122 /** 123 * Get the symmetric candidate for this candidate if it exists. 124 * 125 * @return the symmetric candidate 126 */ 127 public TransportCandidate getSymmetric() { 128 return symmetric; 129 } 130 131 /** 132 * Set the symetric candidate for this candidate. 133 * 134 * @param symetric 135 */ 136 public void setSymmetric(TransportCandidate symetric) { 137 this.symmetric = symetric; 138 } 139 140 /** 141 * Get the password used by ICE or relayed candidate 142 * 143 * @return a password 144 */ 145 public String getPassword() { 146 return password; 147 } 148 149 /** 150 * Set the password used by ICE or relayed candidate 151 * 152 * @param password a password 153 */ 154 public void setPassword(String password) { 155 this.password = password; 156 } 157 158 /** 159 * Get the XMPPConnection use to send or receive this candidate 160 * 161 * @return the connection 162 */ 163 public XMPPConnection getConnection() { 164 return connection; 165 } 166 167 /** 168 * Set the XMPPConnection use to send or receive this candidate 169 * 170 * @param connection 171 */ 172 public void setConnection(XMPPConnection connection) { 173 this.connection = connection; 174 } 175 176 /** 177 * Get the jingle's sessionId that is using this candidate 178 * 179 * @return the session ID 180 */ 181 public String getSessionId() { 182 return sessionId; 183 } 184 185 /** 186 * Set the jingle's sessionId that is using this candidate 187 * 188 * @param sessionId 189 */ 190 public void setSessionId(String sessionId) { 191 this.sessionId = sessionId; 192 } 193 194 /** 195 * Empty constructor 196 */ 197 public TransportCandidate() { 198 this(null, 0, 0); 199 } 200 201 /** 202 * Constructor with IP address and port 203 * 204 * @param ip The IP address. 205 * @param port The port number. 206 */ 207 public TransportCandidate(String ip, int port) { 208 this(ip, port, 0); 209 } 210 211 /** 212 * Constructor with IP address and port 213 * 214 * @param ip The IP address. 215 * @param port The port number. 216 * @param generation The generation 217 */ 218 public TransportCandidate(String ip, int port, int generation) { 219 this.ip = ip; 220 this.port = port; 221 this.generation = generation; 222 } 223 224 /** 225 * Return true if the candidate is not valid. 226 * 227 * @return true if the candidate is null. 228 */ 229 public boolean isNull() { 230 if (ip == null) { 231 return true; 232 } else if (ip.length() == 0) { 233 return true; 234 } else if (port < 0) { 235 return true; 236 } else { 237 return false; 238 } 239 } 240 241 /** 242 * Get the port, or 0 for any port. 243 * 244 * @return the port or 0 245 */ 246 public int getPort() { 247 return port; 248 } 249 250 /** 251 * Set the port, using 0 for any port 252 * 253 * @param port the port 254 */ 255 public void setPort(int port) { 256 this.port = port; 257 } 258 259 /** 260 * Get the generation for a transportElement definition 261 * 262 * @return the generation 263 */ 264 public int getGeneration() { 265 return generation; 266 } 267 268 /** 269 * Set the generation for a transportElement definition. 270 * 271 * @param generation the generation number 272 */ 273 public void setGeneration(int generation) { 274 this.generation = generation; 275 } 276 277 /** 278 * Get the name used for identifying this transportElement method (optional) 279 * 280 * @return a name used for identifying this transportElement (ie, 281 * "myrtpvoice1") 282 */ 283 public String getName() { 284 return name; 285 } 286 287 /** 288 * Set a name for identifying this transportElement. 289 * 290 * @param name the name used for the transportElement 291 */ 292 public void setName(String name) { 293 this.name = name; 294 } 295 296 /* 297 * (non-Javadoc) 298 * 299 * @see java.lang.Object#equals(java.lang.Object) 300 */ 301 public boolean equals(Object obj) { 302 if (this == obj) { 303 return true; 304 } 305 if (obj == null) { 306 return false; 307 } 308 if (getClass() != obj.getClass()) { 309 return false; 310 } 311 final TransportCandidate other = (TransportCandidate) obj; 312 if (generation != other.generation) { 313 return false; 314 } 315 if (getIp() == null) { 316 if (other.getIp() != null) { 317 return false; 318 } 319 } else if (!getIp().equals(other.getIp())) { 320 return false; 321 } 322 323 if (getPort() != other.getPort()) { 324 return false; 325 } 326 327 if (getName() == null) { 328 if (other.getName() != null) { 329 return false; 330 } 331 } else if (!getName().equals(other.getName())) { 332 return false; 333 } 334 if (getPort() != other.getPort()) { 335 return false; 336 } 337 return true; 338 } 339 340 341 /** 342 * Check if a transport candidate is usable. The transport resolver should 343 * check if the transport candidate the other endpoint has provided is 344 * usable. 345 * <p/> 346 * Subclasses should provide better methods if they can... 347 */ 348 public void check(final List<TransportCandidate> localCandidates) { 349 //TODO candidate is being checked trigger 350 //candidatesChecking.add(cand); 351 352 Thread checkThread = new Thread(new Runnable() { 353 public void run() { 354 boolean isUsable; 355 356 357 try { 358 InetAddress candAddress = InetAddress.getByName(getIp()); 359 isUsable = true;//candAddress.isReachable(TransportResolver.CHECK_TIMEOUT); 360 } 361 catch (Exception e) { 362 isUsable = false; 363 } 364 triggerCandidateChecked(isUsable); 365 366 //TODO candidate is being checked trigger 367 //candidatesChecking.remove(cand); 368 } 369 }, "Transport candidate check"); 370 371 checkThread.setName("Transport candidate test"); 372 checkThread.start(); 373 } 374 375 /** 376 * Trigger a new candidate checked event. 377 * 378 * @param result The result. 379 */ 380 void triggerCandidateChecked(boolean result) { 381 382 for (TransportResolverListener.Checker trl : getListenersList()) { 383 trl.candidateChecked(this, result); 384 } 385 } 386 387 /** 388 * Get the list of listeners 389 * 390 * @return the list of listeners 391 */ 392 public List<TransportResolverListener.Checker> getListenersList() { 393 synchronized (listeners) { 394 return new ArrayList<Checker>(listeners); 395 } 396 } 397 398 /** 399 * Add a transport resolver listener. 400 * 401 * @param li The transport resolver listener to be added. 402 */ 403 public void addListener(TransportResolverListener.Checker li) { 404 synchronized (listeners) { 405 listeners.add(li); 406 } 407 } 408 409 /** 410 * Fixed transport candidate 411 */ 412 public static class Fixed extends TransportCandidate { 413 414 public Fixed() { 415 super(); 416 } 417 418 /** 419 * Constructor with IP address and port 420 * 421 * @param ip The IP address. 422 * @param port The port number. 423 */ 424 public Fixed(String ip, int port) { 425 super(ip, port); 426 } 427 428 /** 429 * Constructor with IP address and port 430 * 431 * @param ip The IP address. 432 * @param port The port number. 433 * @param generation The generation 434 */ 435 public Fixed(String ip, int port, int generation) { 436 super(ip, port, generation); 437 } 438 } 439 440 /** 441 * Type-safe enum for the transportElement protocol 442 */ 443 public static class Protocol { 444 445 public static final Protocol UDP = new Protocol("udp"); 446 447 public static final Protocol TCP = new Protocol("tcp"); 448 449 public static final Protocol TCPACT = new Protocol("tcp-act"); 450 451 public static final Protocol TCPPASS = new Protocol("tcp-pass"); 452 453 public static final Protocol SSLTCP = new Protocol("ssltcp"); 454 455 private String value; 456 457 public Protocol(String value) { 458 this.value = value; 459 } 460 461 public String toString() { 462 return value; 463 } 464 465 /** 466 * Returns the Protocol constant associated with the String value. 467 */ 468 public static Protocol fromString(String value) { 469 if (value == null) { 470 return UDP; 471 } 472 value = value.toLowerCase(Locale.US); 473 if (value.equals("udp")) { 474 return UDP; 475 } else if (value.equals("tcp")) { 476 return TCP; 477 } else if (value.equals("tcp-act")) { 478 return TCPACT; 479 } else if (value.equals("tcp-pass")) { 480 return TCPPASS; 481 } else if (value.equals("ssltcp")) { 482 return SSLTCP; 483 } else { 484 return UDP; 485 } 486 } 487 488 /* 489 * (non-Javadoc) 490 * 491 * @see java.lang.Object#equals(java.lang.Object) 492 */ 493 public boolean equals(Object obj) { 494 if (this == obj) { 495 return true; 496 } 497 if (obj == null) { 498 return false; 499 } 500 if (getClass() != obj.getClass()) { 501 return false; 502 } 503 final Protocol other = (Protocol) obj; 504 if (value == null) { 505 if (other.value != null) { 506 return false; 507 } 508 } else if (!value.equals(other.value)) { 509 return false; 510 } 511 return true; 512 } 513 514 /** 515 * Return true if the protocol is not valid. 516 * 517 * @return true if the protocol is null 518 */ 519 public boolean isNull() { 520 if (value == null) { 521 return true; 522 } else if (value.length() == 0) { 523 return true; 524 } else { 525 return false; 526 } 527 } 528 } 529 530 /** 531 * Type-safe enum for the transportElement channel 532 */ 533 public static class Channel { 534 535 public static final Channel MYRTPVOICE = new Channel("myrtpvoice"); 536 537 public static final Channel MYRTCPVOICE = new Channel("myrtcpvoice"); 538 539 private String value; 540 541 public Channel(String value) { 542 this.value = value; 543 } 544 545 public String toString() { 546 return value; 547 } 548 549 /** 550 * Returns the MediaChannel constant associated with the String value. 551 */ 552 public static Channel fromString(String value) { 553 if (value == null) { 554 return MYRTPVOICE; 555 } 556 value = value.toLowerCase(Locale.US); 557 if (value.equals("myrtpvoice")) { 558 return MYRTPVOICE; 559 } else if (value.equals("tcp")) { 560 return MYRTCPVOICE; 561 } else { 562 return MYRTPVOICE; 563 } 564 } 565 566 /* 567 * (non-Javadoc) 568 * 569 * @see java.lang.Object#equals(java.lang.Object) 570 */ 571 public boolean equals(Object obj) { 572 if (this == obj) { 573 return true; 574 } 575 if (obj == null) { 576 return false; 577 } 578 if (getClass() != obj.getClass()) { 579 return false; 580 } 581 final Channel other = (Channel) obj; 582 if (value == null) { 583 if (other.value != null) { 584 return false; 585 } 586 } else if (!value.equals(other.value)) { 587 return false; 588 } 589 return true; 590 } 591 592 /** 593 * Return true if the channel is not valid. 594 * 595 * @return true if the channel is null 596 */ 597 public boolean isNull() { 598 if (value == null) { 599 return true; 600 } else if (value.length() == 0) { 601 return true; 602 } else { 603 return false; 604 } 605 } 606 } 607 608 public class CandidateEcho implements Runnable { 609 610 DatagramSocket socket = null; 611 String localUser = null; 612 String remoteUser = null; 613 String id = null; 614 byte send[] = null; 615 byte receive[] = null; 616 DatagramPacket sendPacket = null; 617 List<DatagramListener> listeners = new ArrayList<DatagramListener>(); 618 List<ResultListener> resultListeners = new ArrayList<ResultListener>(); 619 boolean enabled = true; 620 boolean ended = false; 621 long replyTries = 2; 622 long tries = 10; 623 TransportCandidate candidate = null; 624 625 public CandidateEcho(TransportCandidate candidate, JingleSession session) throws UnknownHostException, SocketException { 626 this.socket = new DatagramSocket(candidate.getPort(), InetAddress.getByName(candidate.getLocalIp())); 627 this.localUser = session.getInitiator(); 628 this.remoteUser = session.getResponder(); 629 this.id = session.getSid(); 630 this.candidate = candidate; 631 632 int keySplitIndex = ((int) Math.ceil(((float) id.length()) / 2)); 633 634 String local = id.substring(0, keySplitIndex) + ";" + localUser; 635 String remote = id.substring(keySplitIndex) + ";" + remoteUser; 636 637 try { 638 if (session.getConnection().getUser().equals(session.getInitiator())) { 639 640 this.send = local.getBytes("UTF-8"); 641 this.receive = remote.getBytes("UTF-8"); 642 } else { 643 this.receive = local.getBytes("UTF-8"); 644 this.send = remote.getBytes("UTF-8"); 645 } 646 } 647 catch (UnsupportedEncodingException e) { 648 e.printStackTrace(); 649 } 650 651 652 } 653 654 public void run() { 655 try { 656 LOGGER.fine("Listening for ECHO: " + socket.getLocalAddress().getHostAddress() + ":" + socket.getLocalPort()); 657 while (true) { 658 659 DatagramPacket packet = new DatagramPacket(new byte[150], 150); 660 661 socket.receive(packet); 662 663 //LOGGER.fine("ECHO Packet Received in: " + socket.getLocalAddress().getHostAddress() + ":" + socket.getLocalPort() + " From: " + packet.getAddress().getHostAddress() + ":" + packet.getPort()); 664 665 boolean accept = false; 666 667 ByteBuffer buf = ByteBuffer.wrap(packet.getData()); 668 byte[] content = new byte[packet.getLength()]; 669 buf = buf.get(content, 0, packet.getLength()); 670 671 packet.setData(content); 672 673 for (DatagramListener listener : listeners) { 674 accept = listener.datagramReceived(packet); 675 if (accept) break; 676 } 677 678 long delay = 100 / replyTries; 679 680 String str[] = new String(packet.getData(), "UTF-8").split(";"); 681 String pass = str[0]; 682 String address[] = str[1].split(":"); 683 String ip = address[0]; 684 String port = address[1]; 685 686 if (pass.equals(candidate.getPassword()) && !accept) { 687 688 byte[] cont = null; 689 try { 690 cont = (password + ";" + candidate.getIp() + ":" + candidate.getPort()).getBytes("UTF-8"); 691 } 692 catch (UnsupportedEncodingException e) { 693 e.printStackTrace(); 694 } 695 696 packet.setData(cont); 697 packet.setLength(cont.length); 698 packet.setAddress(InetAddress.getByName(ip)); 699 packet.setPort(Integer.parseInt(port)); 700 701 for (int i = 0; i < replyTries; i++) { 702 socket.send(packet); 703 if (!enabled) break; 704 try { 705 Thread.sleep(delay); 706 } 707 catch (InterruptedException e) { 708 e.printStackTrace(); 709 } 710 } 711 } 712 } 713 } 714 catch (UnknownHostException uhe) { 715 if (enabled) { 716 } 717 } 718 catch (SocketException se) { 719 if (enabled) { 720 } 721 } 722 catch (IOException ioe) { 723 if (enabled) { 724 } 725 } 726 catch (Exception e) { 727 if (enabled) { 728 } 729 } 730 } 731 732 public void cancel() { 733 this.enabled = false; 734 socket.close(); 735 } 736 737 private void fireTestResult(TestResult testResult, TransportCandidate candidate) { 738 for (ResultListener resultListener : resultListeners) 739 resultListener.testFinished(testResult, candidate); 740 } 741 742 public void testASync(final TransportCandidate transportCandidate, final String password) { 743 744 Thread thread = new Thread(new Runnable() { 745 746 public void run() { 747 748 DatagramListener listener = new DatagramListener() { 749 public boolean datagramReceived(DatagramPacket datagramPacket) { 750 751 try { 752 LOGGER.fine("ECHO Received to: " + candidate.getIp() + ":" + candidate.getPort() + " data: " + new String(datagramPacket.getData(), "UTF-8")); 753 String str[] = new String(datagramPacket.getData(), "UTF-8").split(";"); 754 String pass = str[0]; 755 String addr[] = str[1].split(":"); 756 String ip = addr[0]; 757 String pt = addr[1]; 758 759 if (pass.equals(password) 760 && transportCandidate.getIp().indexOf(ip) != -1 761 && transportCandidate.getPort() == Integer.parseInt(pt)) { 762 LOGGER.fine("ECHO OK: " + candidate.getIp() + ":" + candidate.getPort() + " <-> " + transportCandidate.getIp() + ":" + transportCandidate.getPort()); 763 TestResult testResult = new TestResult(); 764 testResult.setResult(true); 765 ended = true; 766 fireTestResult(testResult, transportCandidate); 767 return true; 768 } 769 770 } 771 catch (UnsupportedEncodingException e) { 772 e.printStackTrace(); 773 } 774 775 LOGGER.fine("ECHO Wrong Data: " + datagramPacket.getAddress().getHostAddress() + ":" + datagramPacket.getPort()); 776 return false; 777 } 778 }; 779 780 addListener(listener); 781 782 byte[] content = null; 783 try { 784 content = new String(password + ";" + getIp() + ":" + getPort()).getBytes("UTF-8"); 785 } 786 catch (UnsupportedEncodingException e) { 787 e.printStackTrace(); 788 } 789 790 DatagramPacket packet = new DatagramPacket(content, content.length); 791 792 try { 793 packet.setAddress(InetAddress.getByName(transportCandidate.getIp())); 794 } 795 catch (UnknownHostException e) { 796 e.printStackTrace(); 797 } 798 packet.setPort(transportCandidate.getPort()); 799 800 long delay = 200; 801 802 try { 803 for (int i = 0; i < tries; i++) { 804 socket.send(packet); 805 if (ended) break; 806 try { 807 Thread.sleep(delay); 808 } 809 catch (InterruptedException e) { 810 e.printStackTrace(); 811 } 812 } 813 } 814 catch (IOException e) { 815 // Do Nothing 816 } 817 818 try { 819 Thread.sleep(2000); 820 } 821 catch (InterruptedException e) { 822 e.printStackTrace(); 823 } 824 825 removeListener(listener); 826 } 827 }); 828 thread.start(); 829 } 830 831 public void addListener(DatagramListener listener) { 832 listeners.add(listener); 833 } 834 835 public void removeListener(DatagramListener listener) { 836 listeners.remove(listener); 837 } 838 839 public void addResultListener(ResultListener resultListener) { 840 resultListeners.add(resultListener); 841 } 842 843 public void removeResultListener(ResultListener resultListener) { 844 resultListeners.remove(resultListener); 845 } 846 847 } 848 849}