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