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}