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