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.net.DatagramPacket;
021import java.net.DatagramSocket;
022import java.net.InetAddress;
023import java.net.SocketException;
024import java.net.UnknownHostException;
025import java.nio.ByteBuffer;
026import java.nio.charset.StandardCharsets;
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    @Override
301    public boolean equals(Object obj) {
302        if (this == obj) {
303            return true;
304        }
305        if (obj == null) {
306            return false;
307        }
308        if (!(obj instanceof TransportCandidate)) {
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     *
346     * Subclasses should provide better methods if they can...
347     *
348     * @param localCandidates a list of local candidates.
349     */
350    @SuppressWarnings("UnusedVariable")
351    public void check(final List<TransportCandidate> localCandidates) {
352        // TODO candidate is being checked trigger
353        // candidatesChecking.add(cand);
354
355        Thread checkThread = new Thread(new Runnable() {
356            @Override
357            public void run() {
358                boolean isUsable;
359
360
361                try {
362                    // CHECKSTYLE:OFF
363                    InetAddress candAddress = InetAddress.getByName(getIp());
364                    // CHECKSTYLE:ON
365                    isUsable = true;//candAddress.isReachable(TransportResolver.CHECK_TIMEOUT);
366                }
367                catch (Exception e) {
368                    isUsable = false;
369                }
370                triggerCandidateChecked(isUsable);
371
372                // TODO candidate is being checked trigger
373                // candidatesChecking.remove(cand);
374            }
375        }, "Transport candidate check");
376
377        checkThread.setName("Transport candidate test");
378        checkThread.start();
379    }
380
381    /**
382     * Trigger a new candidate checked event.
383     *
384     * @param result The result.
385     */
386    void triggerCandidateChecked(boolean result) {
387
388        for (TransportResolverListener.Checker trl : getListenersList()) {
389            trl.candidateChecked(this, result);
390        }
391    }
392
393    /**
394     * Get the list of listeners.
395     *
396     * @return the list of listeners
397     */
398    public List<TransportResolverListener.Checker> getListenersList() {
399        synchronized (listeners) {
400            return new ArrayList<>(listeners);
401        }
402    }
403
404    /**
405     * Add a transport resolver listener.
406     *
407     * @param li The transport resolver listener to be added.
408     */
409    public void addListener(TransportResolverListener.Checker li) {
410        synchronized (listeners) {
411            listeners.add(li);
412        }
413    }
414
415    /**
416     * Fixed transport candidate.
417     */
418    public static class Fixed extends TransportCandidate {
419
420        public Fixed() {
421            super();
422        }
423
424        /**
425         * Constructor with IP address and port.
426         *
427         * @param ip   The IP address.
428         * @param port The port number.
429         */
430        public Fixed(String ip, int port) {
431            super(ip, port);
432        }
433
434        /**
435         * Constructor with IP address and port.
436         *
437         * @param ip         The IP address.
438         * @param port       The port number.
439         * @param generation The generation
440         */
441        public Fixed(String ip, int port, int generation) {
442            super(ip, port, generation);
443        }
444    }
445
446    /**
447     * Type-safe enum for the transportElement protocol.
448     */
449    public static final class Protocol {
450
451        public static final Protocol UDP = new Protocol("udp");
452
453        public static final Protocol TCP = new Protocol("tcp");
454
455        public static final Protocol TCPACT = new Protocol("tcp-act");
456
457        public static final Protocol TCPPASS = new Protocol("tcp-pass");
458
459        public static final Protocol SSLTCP = new Protocol("ssltcp");
460
461        private String value;
462
463        public Protocol(String value) {
464            this.value = value;
465        }
466
467        @Override
468        public String toString() {
469            return value;
470        }
471
472        /**
473         * Returns the Protocol constant associated with the String value.
474         *
475         * @param value the input String.
476         * @return the protocol.
477         */
478        public static Protocol fromString(String value) {
479            if (value == null) {
480                return UDP;
481            }
482            value = value.toLowerCase(Locale.US);
483            if (value.equals("udp")) {
484                return UDP;
485            } else if (value.equals("tcp")) {
486                return TCP;
487            } else if (value.equals("tcp-act")) {
488                return TCPACT;
489            } else if (value.equals("tcp-pass")) {
490                return TCPPASS;
491            } else if (value.equals("ssltcp")) {
492                return SSLTCP;
493            } else {
494                return UDP;
495            }
496        }
497
498        @Override
499        public boolean equals(Object obj) {
500            if (this == obj) {
501                return true;
502            }
503            if (obj == null) {
504                return false;
505            }
506            if (getClass() != obj.getClass()) {
507                return false;
508            }
509            final Protocol other = (Protocol) obj;
510            if (value == null) {
511                if (other.value != null) {
512                    return false;
513                }
514            } else if (!value.equals(other.value)) {
515                return false;
516            }
517            return true;
518        }
519
520        @Override
521        public int hashCode() {
522            if (value == null) {
523                return -1;
524            }
525            return value.hashCode();
526        }
527
528        /**
529         * Return true if the protocol is not valid.
530         *
531         * @return true if the protocol is null
532         */
533        public boolean isNull() {
534            if (value == null) {
535                return true;
536            } else if (value.length() == 0) {
537                return true;
538            } else {
539                return false;
540            }
541        }
542    }
543
544    /**
545     * Type-safe enum for the transportElement channel.
546     */
547    public static class Channel {
548
549        public static final Channel MYRTPVOICE = new Channel("myrtpvoice");
550
551        public static final Channel MYRTCPVOICE = new Channel("myrtcpvoice");
552
553        private String value;
554
555        public Channel(String value) {
556            this.value = value;
557        }
558
559        @Override
560        public String toString() {
561            return value;
562        }
563
564        /**
565         * Returns the MediaChannel constant associated with the String value.
566         *
567         * @param value the input String.
568         * @return the channel.
569         */
570        public static Channel fromString(String value) {
571            if (value == null) {
572                return MYRTPVOICE;
573            }
574            value = value.toLowerCase(Locale.US);
575            if (value.equals("myrtpvoice")) {
576                return MYRTPVOICE;
577            } else if (value.equals("tcp")) {
578                return MYRTCPVOICE;
579            } else {
580                return MYRTPVOICE;
581            }
582        }
583
584        @Override
585        public boolean equals(Object obj) {
586            if (this == obj) {
587                return true;
588            }
589            if (obj == null) {
590                return false;
591            }
592            if (!(obj instanceof Channel)) {
593                return false;
594            }
595            final Channel other = (Channel) obj;
596            if (value == null) {
597                if (other.value != null) {
598                    return false;
599                }
600            } else if (!value.equals(other.value)) {
601                return false;
602            }
603            return true;
604        }
605
606        @Override
607        public int hashCode() {
608            if (value == null) {
609                return -1;
610            }
611            return value.hashCode();
612        }
613
614        /**
615         * Return true if the channel is not valid.
616         *
617         * @return true if the channel is null
618         */
619        public boolean isNull() {
620            if (value == null) {
621                return true;
622            } else if (value.length() == 0) {
623                return true;
624            } else {
625                return false;
626            }
627        }
628    }
629
630    public class CandidateEcho implements Runnable {
631
632        DatagramSocket socket = null;
633        Jid localUser;
634        Jid remoteUser;
635        String id = null;
636        byte[] send = null;
637        byte[] receive = null;
638        DatagramPacket sendStanza = null;
639        List<DatagramListener> listeners = new ArrayList<>();
640        List<ResultListener> resultListeners = new ArrayList<>();
641        boolean enabled = true;
642        boolean ended = false;
643        long replyTries = 2;
644        long tries = 10;
645        TransportCandidate candidate = null;
646
647        public CandidateEcho(TransportCandidate candidate, JingleSession session) throws UnknownHostException, SocketException {
648            this.socket = new DatagramSocket(candidate.getPort(), InetAddress.getByName(candidate.getLocalIp()));
649            this.localUser = session.getInitiator();
650            this.remoteUser = session.getResponder();
651            this.id = session.getSid();
652            this.candidate = candidate;
653
654            int keySplitIndex = (int) Math.ceil(((float) id.length()) / 2);
655
656            String local = id.substring(0, keySplitIndex) + ";" + localUser;
657            String remote = id.substring(keySplitIndex) + ";" + remoteUser;
658
659            if (session.getConnection().getUser().equals(session.getInitiator())) {
660                this.send = local.getBytes(StandardCharsets.UTF_8);
661                this.receive = remote.getBytes(StandardCharsets.UTF_8);
662            } else {
663                this.receive = local.getBytes(StandardCharsets.UTF_8);
664                this.send = remote.getBytes(StandardCharsets.UTF_8);
665            }
666        }
667
668        @SuppressWarnings("UnusedVariable")
669        @Override
670        public void run() {
671            try {
672                LOGGER.fine("Listening for ECHO: " + socket.getLocalAddress().getHostAddress() + ":" + socket.getLocalPort());
673                while (true) {
674
675                    DatagramPacket packet = new DatagramPacket(new byte[150], 150);
676
677                    socket.receive(packet);
678
679                    // LOGGER.fine("ECHO Packet Received in: " + socket.getLocalAddress().getHostAddress() + ":" + socket.getLocalPort() + " From: " + packet.getAddress().getHostAddress() + ":" + packet.getPort());
680
681                    boolean accept = false;
682
683                    ByteBuffer buf = ByteBuffer.wrap(packet.getData());
684                    byte[] content = new byte[packet.getLength()];
685                    buf = buf.get(content, 0, packet.getLength());
686
687                    packet.setData(content);
688
689                    for (DatagramListener listener : listeners) {
690                        accept = listener.datagramReceived(packet);
691                        if (accept) break;
692                    }
693
694                    long delay = 100 / replyTries;
695
696                    String[] str = new String(packet.getData(), StandardCharsets.UTF_8).split(";");
697                    String pass = str[0];
698                    String[] address = str[1].split(":");
699                    String ip = address[0];
700                    String port = address[1];
701
702                    if (pass.equals(candidate.getPassword()) && !accept) {
703
704                        byte[] cont = (password + ";" + candidate.getIp() + ":" + candidate.getPort()).getBytes(StandardCharsets.UTF_8);
705
706                        packet.setData(cont);
707                        packet.setLength(cont.length);
708                        packet.setAddress(InetAddress.getByName(ip));
709                        packet.setPort(Integer.parseInt(port));
710
711                        for (int i = 0; i < replyTries; i++) {
712                            socket.send(packet);
713                            if (!enabled) break;
714                            try {
715                                Thread.sleep(delay);
716                            }
717                            catch (InterruptedException e) {
718                                LOGGER.log(Level.WARNING, "exception", e);
719                            }
720                        }
721                    }
722                }
723            }
724            catch (UnknownHostException uhe) {
725                if (enabled) {
726                }
727            }
728            catch (SocketException se) {
729                if (enabled) {
730                }
731            }
732            catch (IOException ioe) {
733                if (enabled) {
734                }
735            }
736            catch (Exception e) {
737                if (enabled) {
738                }
739            }
740        }
741
742        public void cancel() {
743            this.enabled = false;
744            socket.close();
745        }
746
747        private void fireTestResult(TestResult testResult, TransportCandidate candidate) {
748            for (ResultListener resultListener : resultListeners)
749                resultListener.testFinished(testResult, candidate);
750        }
751
752        public void testASync(final TransportCandidate transportCandidate, final String password) {
753
754            Thread thread = new Thread(new Runnable() {
755
756                @Override
757                public void run() {
758
759                    DatagramListener listener = new DatagramListener() {
760                        @Override
761                        public boolean datagramReceived(DatagramPacket datagramPacket) {
762                            LOGGER.fine("ECHO Received to: " + candidate.getIp() + ":" + candidate.getPort() + "  data: " + new String(datagramPacket.getData(), StandardCharsets.UTF_8));
763                            String[] str = new String(datagramPacket.getData(), StandardCharsets.UTF_8).split(";");
764                            String pass = str[0];
765                            String[] addr = str[1].split(":");
766                            String ip = addr[0];
767                            String pt = addr[1];
768
769                            // CHECKSTYLE:OFF
770                            if (pass.equals(password)
771                                    && transportCandidate.getIp().indexOf(ip) != -1
772                                    && transportCandidate.getPort() == Integer.parseInt(pt)) {
773                                // CHECKSTYLE:ON
774                                LOGGER.fine("ECHO OK: " + candidate.getIp() + ":" + candidate.getPort() + " <-> " + transportCandidate.getIp() + ":" + transportCandidate.getPort());
775                                TestResult testResult = new TestResult();
776                                testResult.setResult(true);
777                                ended = true;
778                                fireTestResult(testResult, transportCandidate);
779                                return true;
780                            }
781
782                            LOGGER.fine("ECHO Wrong Data: " + datagramPacket.getAddress().getHostAddress() + ":" + datagramPacket.getPort());
783                            return false;
784                        }
785                    };
786
787                    addListener(listener);
788
789                    byte[] content = new String(password + ";" + getIp() + ":" + getPort()).getBytes(StandardCharsets.UTF_8);
790
791                    DatagramPacket packet = new DatagramPacket(content, content.length);
792
793                    try {
794                        packet.setAddress(InetAddress.getByName(transportCandidate.getIp()));
795                    }
796                    catch (UnknownHostException e) {
797                        LOGGER.log(Level.WARNING, "exception", e);
798                    }
799                    packet.setPort(transportCandidate.getPort());
800
801                    long delay = 200;
802
803                    try {
804                        for (int i = 0; i < tries; i++) {
805                            socket.send(packet);
806                            if (ended) break;
807                            try {
808                                Thread.sleep(delay);
809                            }
810                            catch (InterruptedException e) {
811                                LOGGER.log(Level.WARNING, "exception", e);
812                            }
813                        }
814                    }
815                    catch (IOException e) {
816                        // Do Nothing
817                    }
818
819                    try {
820                        Thread.sleep(2000);
821                    }
822                    catch (InterruptedException e) {
823                        LOGGER.log(Level.WARNING, "exception", e);
824                    }
825
826                    removeListener(listener);
827                }
828            });
829            thread.start();
830        }
831
832        public void addListener(DatagramListener listener) {
833            listeners.add(listener);
834        }
835
836        public void removeListener(DatagramListener listener) {
837            listeners.remove(listener);
838        }
839
840        public void addResultListener(ResultListener resultListener) {
841            resultListeners.add(resultListener);
842        }
843
844        public void removeResultListener(ResultListener resultListener) {
845            resultListeners.remove(resultListener);
846        }
847
848    }
849
850}