/** * $RCSfile$ * $Revision$ * $Date$ * * Copyright (C) 2002-2006 Jive Software. All rights reserved. * ==================================================================== * The Jive Software License (based on Apache Software License, Version 1.1) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, * if any, must include the following acknowledgment: * "This product includes software developed by * Jive Software (http://www.jivesoftware.com)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "Smack" and "Jive Software" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please * contact webmaster@jivesoftware.com. * * 5. Products derived from this software may not be called "Smack", * nor may "Smack" appear in their name, without prior written * permission of Jive Software. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== */ package org.jivesoftware.smackx.jingle.nat; import org.jivesoftware.smack.XMPPConnection; import java.io.IOException; import java.net.*; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.Arrays; /** * Transport candidate. *

* A candidate represents the possible transport for data interchange between * the two endpoints. * * @author Thiago Camargo * @author Alvaro Saurin */ public abstract class TransportCandidate { private String name; private String ip; // IP address private int port; // Port to use, or 0 for any port private String localIp; private int generation; protected String password; private String sessionId; private XMPPConnection connection; private TransportCandidate symmetric; private CandidateEcho candidateEcho = null; private Thread echoThread = null; // Listeners for events private final List listeners = new ArrayList(); public void addCandidateEcho() throws SocketException, UnknownHostException { candidateEcho = new CandidateEcho(this); echoThread = new Thread(candidateEcho); echoThread.start(); } public void removeCandidateEcho() { candidateEcho.cancel(); candidateEcho = null; echoThread = null; } public CandidateEcho getCandidateEcho() { return candidateEcho; } public String getIp() { return ip; } /** * Set the IP address. * * @param ip the IP address */ public void setIp(String ip) { this.ip = ip; } /** * Get local IP to bind to this candidate * * @return */ public String getLocalIp() { return localIp == null ? ip : localIp; } /** * Set local IP to bind to this candidate * * @param localIp */ public void setLocalIp(String localIp) { this.localIp = localIp; } /** * Get the symetric candidate for this candidate if it exists. * * @return */ public TransportCandidate getSymmetric() { return symmetric; } /** * Set the symetric candidate for this candidate. * * @param symetric */ public void setSymmetric(TransportCandidate symetric) { this.symmetric = symetric; } /** * Get the password used by ICE or relayed candidate * * @return a password */ public String getPassword() { return password; } /** * Set the password used by ICE or relayed candidate * * @param password a password */ public void setPassword(String password) { this.password = password; } /** * Get the XMPPConnection use to send or receive this candidate * * @return */ public XMPPConnection getConnection() { return connection; } /** * Set the XMPPConnection use to send or receive this candidate * * @param connection */ public void setConnection(XMPPConnection connection) { this.connection = connection; } /** * Get the jingle´s sessionId that is using this candidate * * @return */ public String getSessionId() { return sessionId; } /** * Set the jingle´s sessionId that is using this candidate * * @param sessionId */ public void setSessionId(String sessionId) { this.sessionId = sessionId; } /** * Empty constructor */ public TransportCandidate() { this(null, 0, 0); } /** * Constructor with IP address and port * * @param ip The IP address. * @param port The port number. */ public TransportCandidate(String ip, int port) { this(ip, port, 0); } /** * Constructor with IP address and port * * @param ip The IP address. * @param port The port number. * @param generation The generation */ public TransportCandidate(String ip, int port, int generation) { this.ip = ip; this.port = port; this.generation = generation; } /** * Return true if the candidate is not valid. * * @return true if the candidate is null. */ public boolean isNull() { if (ip == null) { return true; } else if (ip.length() == 0) { return true; } else if (port < 0) { return true; } else { return false; } } /** * Get the port, or 0 for any port. * * @return the port or 0 */ public int getPort() { return port; } /** * Set the port, using 0 for any port * * @param port the port */ public void setPort(int port) { this.port = port; } /** * Get the generation for a transportElement definition * * @return the generation */ public int getGeneration() { return generation; } /** * Set the generation for a transportElement definition. * * @param generation the generation number */ public void setGeneration(int generation) { this.generation = generation; } /** * Get the name used for identifying this transportElement method (optional) * * @return a name used for identifying this transportElement (ie, * "myrtpvoice1") */ public String getName() { return name; } /** * Set a name for identifying this transportElement. * * @param name the name used for the transportElement */ public void setName(String name) { this.name = name; } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final TransportCandidate other = (TransportCandidate) obj; if (generation != other.generation) { return false; } if (getIp() == null) { if (other.getIp() != null) { return false; } } else if (!getIp().equals(other.getIp())) { return false; } if (getName() == null) { if (other.getName() != null) { return false; } } else if (!getName().equals(other.getName())) { return false; } if (getPort() != other.getPort()) { return false; } return true; } /** * Check if a transport candidate is usable. The transport resolver should * check if the transport candidate the other endpoint has provided is * usable. *

* Subclasses should provide better methods if they can... */ public void check() { //TODO candidate is being checked trigger //candidatesChecking.add(cand); Thread checkThread = new Thread(new Runnable() { public void run() { boolean isUsable; InetAddress candAddress; try { candAddress = InetAddress.getByName(getIp()); isUsable = candAddress.isReachable(TransportResolver.CHECK_TIMEOUT); } catch (Exception e) { isUsable = false; } triggerCandidateChecked(isUsable); //TODO candidate is being checked trigger //candidatesChecking.remove(cand); } }, "Transport candidate check"); checkThread.setName("Transport candidate test"); checkThread.start(); } /** * Trigger a new candidate checked event. * * @param result The result. */ private void triggerCandidateChecked(boolean result) { for (TransportResolverListener.Checker trl : getListenersList()) { trl.candidateChecked(this, result); } } /** * Get the list of listeners * * @return the list of listeners */ public List getListenersList() { synchronized (listeners) { return new ArrayList(listeners); } } /** * Add a transport resolver listener. * * @param li The transport resolver listener to be added. */ public void addListener(TransportResolverListener.Checker li) { synchronized (listeners) { listeners.add(li); } } /** * Fixed transport candidate */ public static class Fixed extends TransportCandidate { public Fixed() { super(); } /** * Constructor with IP address and port * * @param ip The IP address. * @param port The port number. */ public Fixed(String ip, int port) { super(ip, port); } /** * Constructor with IP address and port * * @param ip The IP address. * @param port The port number. * @param generation The generation */ public Fixed(String ip, int port, int generation) { super(ip, port, generation); } } /** * Type-safe enum for the transportElement protocol */ public static class Protocol { public static final Protocol UDP = new Protocol("udp"); public static final Protocol TCP = new Protocol("tcp"); public static final Protocol TCPACT = new Protocol("tcp-act"); public static final Protocol TCPPASS = new Protocol("tcp-pass"); public static final Protocol SSLTCP = new Protocol("ssltcp"); private String value; public Protocol(String value) { this.value = value; } public String toString() { return value; } /** * Returns the Protocol constant associated with the String value. */ public static Protocol fromString(String value) { if (value == null) { return UDP; } value = value.toLowerCase(); if (value.equals("udp")) { return UDP; } else if (value.equals("tcp")) { return TCP; } else if (value.equals("tcp-act")) { return TCPACT; } else if (value.equals("tcp-pass")) { return TCPPASS; } else if (value.equals("ssltcp")) { return SSLTCP; } else { return UDP; } } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Protocol other = (Protocol) obj; if (value == null) { if (other.value != null) { return false; } } else if (!value.equals(other.value)) { return false; } return true; } /** * Return true if the protocol is not valid. * * @return true if the protocol is null */ public boolean isNull() { if (value == null) { return true; } else if (value.length() == 0) { return true; } else { return false; } } } /** * Type-safe enum for the transportElement channel */ public static class Channel { public static final Channel MYRTPVOICE = new Channel("myrtpvoice"); public static final Channel MYRTCPVOICE = new Channel("myrtcpvoice"); private String value; public Channel(String value) { this.value = value; } public String toString() { return value; } /** * Returns the MediaChannel constant associated with the String value. */ public static Channel fromString(String value) { if (value == null) { return MYRTPVOICE; } value = value.toLowerCase(); if (value.equals("myrtpvoice")) { return MYRTPVOICE; } else if (value.equals("tcp")) { return MYRTCPVOICE; } else { return MYRTPVOICE; } } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Channel other = (Channel) obj; if (value == null) { if (other.value != null) { return false; } } else if (!value.equals(other.value)) { return false; } return true; } /** * Return true if the channel is not valid. * * @return true if the channel is null */ public boolean isNull() { if (value == null) { return true; } else if (value.length() == 0) { return true; } else { return false; } } } public class CandidateEcho implements Runnable { DatagramSocket socket = null; byte password[] = null; List listeners = new ArrayList(); boolean enabled = true; public CandidateEcho(TransportCandidate candidate) throws UnknownHostException, SocketException { this.socket = new DatagramSocket(candidate.getPort(), InetAddress.getByName("0.0.0.0")); Random r = new Random(); password = longToByteArray((Math.abs(r.nextLong()))); } public void run() { try { System.out.println("Listening for ECHO: " + socket.getLocalAddress().getHostAddress() + ":" + socket.getLocalPort()); while (true) { DatagramPacket packet = new DatagramPacket(new byte[8], 8); socket.receive(packet); System.out.println("ECHO Packet Received in: " + socket.getLocalAddress().getHostAddress() + ":" + socket.getLocalPort() + " From: " + packet.getAddress().getHostAddress() + ":" + packet.getPort()); for (DatagramListener listener : listeners) { listener.datagramReceived(packet); } packet.setAddress(packet.getAddress()); packet.setPort(packet.getPort()); if (!Arrays.equals(packet.getData(), password)) for (int i = 0; i < 3; i++) socket.send(packet); } } catch (UnknownHostException uhe) { if (enabled) { } } catch (SocketException se) { if (enabled) { } } catch (IOException ioe) { if (enabled) { } } } public void cancel() { this.enabled = false; socket.close(); } public boolean test(final InetAddress address, final int port) { return test(address, port, 2000); } public boolean test(final InetAddress address, final int port, int timeout) { final TestResults testResults = new TestResults(); DatagramListener listener = new DatagramListener() { public boolean datagramReceived(DatagramPacket datagramPacket) { if (datagramPacket.getAddress().equals(address) && datagramPacket.getPort() == port) { if (Arrays.equals(datagramPacket.getData(), password)) { testResults.setResult(true); return true; } } testResults.setResult(false); return false; } }; this.addListener(listener); DatagramPacket packet = new DatagramPacket(password, password.length); packet.setAddress(address); packet.setPort(port); try { for (int i = 0; i < 3; i++) socket.send(packet); } catch (IOException e) { e.printStackTrace(); } try { Thread.sleep(timeout); } catch (InterruptedException e) { e.printStackTrace(); } this.removeListener(listener); return testResults.isReachable(); } public void addListener(DatagramListener listener) { listeners.add(listener); } public void removeListener(DatagramListener listener) { listeners.remove(listener); } public class TestResults { private boolean result; public boolean isReachable() { return result; } public void setResult(boolean result) { this.result = result; } } } public static byte[] longToByteArray(long valor) { byte[] result = new byte[8]; for (int i = 0; i < result.length; i++) { result[7 - i] = (byte) (valor & 0xFF); valor = valor >> 8; } return result; } }