JingleS5BTransportSession.java

  1. /**
  2.  *
  3.  * Copyright 2017 Paul Schaub
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.jingle.transports.jingle_s5b;

  18. import java.io.IOException;
  19. import java.net.InetAddress;
  20. import java.net.Socket;
  21. import java.util.Collections;
  22. import java.util.List;
  23. import java.util.concurrent.TimeoutException;
  24. import java.util.logging.Level;
  25. import java.util.logging.Logger;

  26. import org.jivesoftware.smack.SmackException;
  27. import org.jivesoftware.smack.XMPPException;
  28. import org.jivesoftware.smack.packet.IQ;

  29. import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession;
  30. import org.jivesoftware.smackx.bytestreams.socks5.Socks5Client;
  31. import org.jivesoftware.smackx.bytestreams.socks5.Socks5ClientForInitiator;
  32. import org.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy;
  33. import org.jivesoftware.smackx.bytestreams.socks5.Socks5Utils;
  34. import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
  35. import org.jivesoftware.smackx.jingle.JingleManager;
  36. import org.jivesoftware.smackx.jingle.JingleSession;
  37. import org.jivesoftware.smackx.jingle.element.Jingle;
  38. import org.jivesoftware.smackx.jingle.element.JingleContent;
  39. import org.jivesoftware.smackx.jingle.element.JingleContentTransport;
  40. import org.jivesoftware.smackx.jingle.element.JingleContentTransportCandidate;
  41. import org.jivesoftware.smackx.jingle.transports.JingleTransportInitiationCallback;
  42. import org.jivesoftware.smackx.jingle.transports.JingleTransportSession;
  43. import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransport;
  44. import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransportCandidate;
  45. import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransportInfo;

  46. /**
  47.  * Handler that handles Jingle Socks5Bytestream transports (XEP-0260).
  48.  */
  49. public class JingleS5BTransportSession extends JingleTransportSession<JingleS5BTransport> {
  50.     private static final Logger LOGGER = Logger.getLogger(JingleS5BTransportSession.class.getName());

  51.     private JingleTransportInitiationCallback callback;

  52.     public JingleS5BTransportSession(JingleSession jingleSession) {
  53.         super(jingleSession);
  54.     }

  55.     private UsedCandidate ourChoice, theirChoice;

  56.     @Override
  57.     public JingleS5BTransport createTransport() {
  58.         if (ourProposal == null) {
  59.             ourProposal = createTransport(JingleManager.randomId(), Bytestream.Mode.tcp);
  60.         }
  61.         return ourProposal;
  62.     }

  63.     @Override
  64.     public void setTheirProposal(JingleContentTransport transport) {
  65.         theirProposal = (JingleS5BTransport) transport;
  66.     }

  67.     public JingleS5BTransport createTransport(String sid, Bytestream.Mode mode) {
  68.         JingleS5BTransport.Builder jb = JingleS5BTransport.getBuilder()
  69.                 .setStreamId(sid).setMode(mode).setDestinationAddress(
  70.                         Socks5Utils.createDigest(sid, jingleSession.getLocal(), jingleSession.getRemote()));

  71.         // Local host
  72.         if (JingleS5BTransportManager.isUseLocalCandidates()) {
  73.             for (Bytestream.StreamHost host : transportManager().getLocalStreamHosts()) {
  74.                 jb.addTransportCandidate(new JingleS5BTransportCandidate(host, 100, JingleS5BTransportCandidate.Type.direct));
  75.             }
  76.         }

  77.         List<Bytestream.StreamHost> remoteHosts = Collections.emptyList();
  78.         if (JingleS5BTransportManager.isUseExternalCandidates()) {
  79.             try {
  80.                 remoteHosts = transportManager().getAvailableStreamHosts();
  81.             } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
  82.                 LOGGER.log(Level.WARNING, "Could not determine available StreamHosts.", e);
  83.             }
  84.         }

  85.         for (Bytestream.StreamHost host : remoteHosts) {
  86.             jb.addTransportCandidate(new JingleS5BTransportCandidate(host, 0, JingleS5BTransportCandidate.Type.proxy));
  87.         }

  88.         return jb.build();
  89.     }

  90.     public void setTheirTransport(JingleContentTransport transport) {
  91.         theirProposal = (JingleS5BTransport) transport;
  92.     }

  93.     @Override
  94.     public void initiateOutgoingSession(JingleTransportInitiationCallback callback) {
  95.         this.callback = callback;
  96.         initiateSession();
  97.     }

  98.     @Override
  99.     public void initiateIncomingSession(JingleTransportInitiationCallback callback) {
  100.         this.callback = callback;
  101.         initiateSession();
  102.     }

  103.     private void initiateSession() {
  104.         Socks5Proxy.getSocks5Proxy().addTransfer(createTransport().getDestinationAddress());
  105.         JingleContent content = jingleSession.getContents().get(0);
  106.         UsedCandidate usedCandidate = chooseFromProposedCandidates(theirProposal);
  107.         if (usedCandidate == null) {
  108.             ourChoice = CANDIDATE_FAILURE;
  109.             Jingle candidateError = transportManager().createCandidateError(
  110.                     jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(),
  111.                     content.getSenders(), content.getCreator(), content.getName(), theirProposal.getStreamId());
  112.             try {
  113.                 jingleSession.getConnection().sendStanza(candidateError);
  114.             } catch (SmackException.NotConnectedException | InterruptedException e) {
  115.                 LOGGER.log(Level.WARNING, "Could not send candidate-error.", e);
  116.             }
  117.         } else {
  118.             ourChoice = usedCandidate;
  119.             Jingle jingle = transportManager().createCandidateUsed(jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(),
  120.                     content.getSenders(), content.getCreator(), content.getName(), theirProposal.getStreamId(), ourChoice.candidate.getCandidateId());
  121.             try {
  122.                 jingleSession.getConnection().sendIqRequestAndWaitForResponse(jingle);
  123.             } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
  124.                 LOGGER.log(Level.WARNING, "Could not send candidate-used.", e);
  125.             }
  126.         }
  127.         connectIfReady();
  128.     }

  129.     private UsedCandidate chooseFromProposedCandidates(JingleS5BTransport proposal) {
  130.         for (JingleContentTransportCandidate c : proposal.getCandidates()) {
  131.             JingleS5BTransportCandidate candidate = (JingleS5BTransportCandidate) c;

  132.             try {
  133.                 return connectToTheirCandidate(candidate);
  134.             } catch (InterruptedException | TimeoutException | XMPPException | SmackException | IOException e) {
  135.                 LOGGER.log(Level.WARNING, "Could not connect to " + candidate.getHost(), e);
  136.             }
  137.         }
  138.         LOGGER.log(Level.WARNING, "Failed to connect to any candidate.");
  139.         return null;
  140.     }

  141.     private UsedCandidate connectToTheirCandidate(JingleS5BTransportCandidate candidate)
  142.             throws InterruptedException, TimeoutException, SmackException, XMPPException, IOException {
  143.         Bytestream.StreamHost streamHost = candidate.getStreamHost();
  144.         InetAddress address = streamHost.getAddress().asInetAddress();
  145.         Socks5Client socks5Client = new Socks5Client(streamHost, theirProposal.getDestinationAddress());
  146.         Socket socket = socks5Client.getSocket(10 * 1000);
  147.         LOGGER.log(Level.INFO, "Connected to their StreamHost " + address + " using dstAddr "
  148.                 + theirProposal.getDestinationAddress());
  149.         return new UsedCandidate(theirProposal, candidate, socket);
  150.     }

  151.     private UsedCandidate connectToOurCandidate(JingleS5BTransportCandidate candidate)
  152.             throws InterruptedException, TimeoutException, SmackException, XMPPException, IOException {
  153.         Bytestream.StreamHost streamHost = candidate.getStreamHost();
  154.         InetAddress address = streamHost.getAddress().asInetAddress();
  155.         Socks5ClientForInitiator socks5Client = new Socks5ClientForInitiator(
  156.                 streamHost, ourProposal.getDestinationAddress(), jingleSession.getConnection(),
  157.                 ourProposal.getStreamId(), jingleSession.getRemote());
  158.         Socket socket = socks5Client.getSocket(10 * 1000);
  159.         LOGGER.log(Level.INFO, "Connected to our StreamHost " + address + " using dstAddr "
  160.                 + ourProposal.getDestinationAddress());
  161.         return new UsedCandidate(ourProposal, candidate, socket);
  162.     }

  163.     @Override
  164.     public String getNamespace() {
  165.         return JingleS5BTransport.NAMESPACE_V1;
  166.     }

  167.     @Override
  168.     public IQ handleTransportInfo(Jingle transportInfo) {
  169.         JingleS5BTransportInfo info = (JingleS5BTransportInfo) transportInfo.getContents().get(0).getTransport().getInfo();

  170.         switch (info.getElementName()) {
  171.             case JingleS5BTransportInfo.CandidateUsed.ELEMENT:
  172.                 return handleCandidateUsed(transportInfo);

  173.             case JingleS5BTransportInfo.CandidateActivated.ELEMENT:
  174.                 return handleCandidateActivate(transportInfo);

  175.             case JingleS5BTransportInfo.CandidateError.ELEMENT:
  176.                 return handleCandidateError(transportInfo);

  177.             case JingleS5BTransportInfo.ProxyError.ELEMENT:
  178.                 return handleProxyError(transportInfo);
  179.         }
  180.         // We should never go here, but lets be gracious...
  181.         return IQ.createResultIQ(transportInfo);
  182.     }

  183.     public IQ handleCandidateUsed(Jingle jingle) {
  184.         JingleS5BTransportInfo info = (JingleS5BTransportInfo) jingle.getContents().get(0).getTransport().getInfo();
  185.         String candidateId = ((JingleS5BTransportInfo.CandidateUsed) info).getCandidateId();
  186.         theirChoice = new UsedCandidate(ourProposal, ourProposal.getCandidate(candidateId), null);

  187.         if (theirChoice.candidate == null) {
  188.             /*
  189.             TODO: Booooooh illegal candidateId!! Go home!!!!11elf
  190.              */
  191.         }

  192.         connectIfReady();

  193.         return IQ.createResultIQ(jingle);
  194.     }

  195.     public IQ handleCandidateActivate(Jingle jingle) {
  196.         LOGGER.log(Level.INFO, "handleCandidateActivate");
  197.         Socks5BytestreamSession bs = new Socks5BytestreamSession(ourChoice.socket,
  198.                 ourChoice.candidate.getJid().asBareJid().equals(jingleSession.getRemote().asBareJid()));
  199.         callback.onSessionInitiated(bs);
  200.         return IQ.createResultIQ(jingle);
  201.     }

  202.     public IQ handleCandidateError(Jingle jingle) {
  203.         theirChoice = CANDIDATE_FAILURE;
  204.         connectIfReady();
  205.         return IQ.createResultIQ(jingle);
  206.     }

  207.     public IQ handleProxyError(Jingle jingle) {
  208.         // TODO
  209.         return IQ.createResultIQ(jingle);
  210.     }

  211.     /**
  212.      * Determine, which candidate (ours/theirs) is the nominated one.
  213.      * Connect to this candidate. If it is a proxy and it is ours, activate it and connect.
  214.      * If its a proxy and it is theirs, wait for activation.
  215.      * If it is not a proxy, just connect.
  216.      */
  217.     private void connectIfReady() {
  218.         JingleContent content = jingleSession.getContents().get(0);
  219.         if (ourChoice == null || theirChoice == null) {
  220.             // Not yet ready.
  221.             LOGGER.log(Level.INFO, "Not ready.");
  222.             return;
  223.         }

  224.         if (ourChoice == CANDIDATE_FAILURE && theirChoice == CANDIDATE_FAILURE) {
  225.             LOGGER.log(Level.INFO, "Failure.");
  226.             jingleSession.onTransportMethodFailed(getNamespace());
  227.             return;
  228.         }

  229.         LOGGER.log(Level.INFO, "Ready.");

  230.         // Determine nominated candidate.
  231.         UsedCandidate nominated;
  232.         if (ourChoice != CANDIDATE_FAILURE && theirChoice != CANDIDATE_FAILURE) {
  233.             if (ourChoice.candidate.getPriority() > theirChoice.candidate.getPriority()) {
  234.                 nominated = ourChoice;
  235.             } else if (ourChoice.candidate.getPriority() < theirChoice.candidate.getPriority()) {
  236.                 nominated = theirChoice;
  237.             } else {
  238.                 nominated = jingleSession.isInitiator() ? ourChoice : theirChoice;
  239.             }
  240.         } else if (ourChoice != CANDIDATE_FAILURE) {
  241.             nominated = ourChoice;
  242.         } else {
  243.             nominated = theirChoice;
  244.         }

  245.         if (nominated == theirChoice) {
  246.             LOGGER.log(Level.INFO, "Their choice, so our proposed candidate is used.");
  247.             boolean isProxy = nominated.candidate.getType() == JingleS5BTransportCandidate.Type.proxy;
  248.             try {
  249.                 nominated = connectToOurCandidate(nominated.candidate);
  250.             } catch (InterruptedException | IOException | XMPPException | SmackException | TimeoutException e) {
  251.                 LOGGER.log(Level.INFO, "Could not connect to our candidate.", e);
  252.                 // TODO: Proxy-Error
  253.                 return;
  254.             }

  255.             if (isProxy) {
  256.                 LOGGER.log(Level.INFO, "Is external proxy. Activate it.");
  257.                 Bytestream activate = new Bytestream(ourProposal.getStreamId());
  258.                 activate.setMode(null);
  259.                 activate.setType(IQ.Type.set);
  260.                 activate.setTo(nominated.candidate.getJid());
  261.                 activate.setToActivate(jingleSession.getRemote());
  262.                 activate.setFrom(jingleSession.getLocal());
  263.                 try {
  264.                     jingleSession.getConnection().sendIqRequestAndWaitForResponse(activate);
  265.                 } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
  266.                     LOGGER.log(Level.WARNING, "Could not activate proxy.", e);
  267.                     return;
  268.                 }

  269.                 LOGGER.log(Level.INFO, "Send candidate-activate.");
  270.                 Jingle candidateActivate = transportManager().createCandidateActivated(
  271.                         jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(),
  272.                         content.getSenders(), content.getCreator(), content.getName(), nominated.transport.getStreamId(),
  273.                         nominated.candidate.getCandidateId());
  274.                 try {
  275.                     jingleSession.getConnection().sendIqRequestAndWaitForResponse(candidateActivate);
  276.                 } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
  277.                     LOGGER.log(Level.WARNING, "Could not send candidate-activated", e);
  278.                     return;
  279.                 }
  280.             }

  281.             LOGGER.log(Level.INFO, "Start transmission.");
  282.             Socks5BytestreamSession bs = new Socks5BytestreamSession(nominated.socket, !isProxy);
  283.             callback.onSessionInitiated(bs);

  284.         }
  285.         // Our choice
  286.         else {
  287.             LOGGER.log(Level.INFO, "Our choice, so their candidate was used.");
  288.             boolean isProxy = nominated.candidate.getType() == JingleS5BTransportCandidate.Type.proxy;
  289.             if (!isProxy) {
  290.                 LOGGER.log(Level.INFO, "Direct connection.");
  291.                 Socks5BytestreamSession bs = new Socks5BytestreamSession(nominated.socket, true);
  292.                 callback.onSessionInitiated(bs);
  293.             } else {
  294.                 LOGGER.log(Level.INFO, "Our choice was their external proxy. wait for candidate-activate.");
  295.             }
  296.         }
  297.     }

  298.     @Override
  299.     public JingleS5BTransportManager transportManager() {
  300.         return JingleS5BTransportManager.getInstanceFor(jingleSession.getConnection());
  301.     }

  302.     private static final class UsedCandidate {
  303.         private final Socket socket;
  304.         private final JingleS5BTransport transport;
  305.         private final JingleS5BTransportCandidate candidate;

  306.         private UsedCandidate(JingleS5BTransport transport, JingleS5BTransportCandidate candidate, Socket socket) {
  307.             this.socket = socket;
  308.             this.transport = transport;
  309.             this.candidate = candidate;
  310.         }
  311.     }

  312.     private static final UsedCandidate CANDIDATE_FAILURE = new UsedCandidate(null, null, null);
  313. }