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