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