001/**
002 *
003 * Copyright 2017 Paul Schaub
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.jingle.transports.jingle_s5b;
018
019import java.io.IOException;
020import java.net.InetAddress;
021import java.net.Socket;
022import java.util.Collections;
023import java.util.List;
024import java.util.concurrent.TimeoutException;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import org.jivesoftware.smack.SmackException;
029import org.jivesoftware.smack.XMPPException;
030import org.jivesoftware.smack.packet.IQ;
031
032import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession;
033import org.jivesoftware.smackx.bytestreams.socks5.Socks5Client;
034import org.jivesoftware.smackx.bytestreams.socks5.Socks5ClientForInitiator;
035import org.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy;
036import org.jivesoftware.smackx.bytestreams.socks5.Socks5Utils;
037import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
038import org.jivesoftware.smackx.jingle.JingleManager;
039import org.jivesoftware.smackx.jingle.JingleSession;
040import org.jivesoftware.smackx.jingle.element.Jingle;
041import org.jivesoftware.smackx.jingle.element.JingleContent;
042import org.jivesoftware.smackx.jingle.element.JingleContentTransport;
043import org.jivesoftware.smackx.jingle.element.JingleContentTransportCandidate;
044import org.jivesoftware.smackx.jingle.transports.JingleTransportInitiationCallback;
045import org.jivesoftware.smackx.jingle.transports.JingleTransportSession;
046import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransport;
047import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransportCandidate;
048import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransportInfo;
049
050/**
051 * Handler that handles Jingle Socks5Bytestream transports (XEP-0260).
052 */
053public class JingleS5BTransportSession extends JingleTransportSession<JingleS5BTransport> {
054    private static final Logger LOGGER = Logger.getLogger(JingleS5BTransportSession.class.getName());
055
056    private JingleTransportInitiationCallback callback;
057
058    public JingleS5BTransportSession(JingleSession jingleSession) {
059        super(jingleSession);
060    }
061
062    private UsedCandidate ourChoice, theirChoice;
063
064    @Override
065    public JingleS5BTransport createTransport() {
066        if (ourProposal == null) {
067            ourProposal = createTransport(JingleManager.randomId(), Bytestream.Mode.tcp);
068        }
069        return ourProposal;
070    }
071
072    @Override
073    public void setTheirProposal(JingleContentTransport transport) {
074        theirProposal = (JingleS5BTransport) transport;
075    }
076
077    public JingleS5BTransport createTransport(String sid, Bytestream.Mode mode) {
078        JingleS5BTransport.Builder jb = JingleS5BTransport.getBuilder()
079                .setStreamId(sid).setMode(mode).setDestinationAddress(
080                        Socks5Utils.createDigest(sid, jingleSession.getLocal(), jingleSession.getRemote()));
081
082        // Local host
083        if (JingleS5BTransportManager.isUseLocalCandidates()) {
084            for (Bytestream.StreamHost host : transportManager().getLocalStreamHosts()) {
085                jb.addTransportCandidate(new JingleS5BTransportCandidate(host, 100, JingleS5BTransportCandidate.Type.direct));
086            }
087        }
088
089        List<Bytestream.StreamHost> remoteHosts = Collections.emptyList();
090        if (JingleS5BTransportManager.isUseExternalCandidates()) {
091            try {
092                remoteHosts = transportManager().getAvailableStreamHosts();
093            } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
094                LOGGER.log(Level.WARNING, "Could not determine available StreamHosts.", e);
095            }
096        }
097
098        for (Bytestream.StreamHost host : remoteHosts) {
099            jb.addTransportCandidate(new JingleS5BTransportCandidate(host, 0, JingleS5BTransportCandidate.Type.proxy));
100        }
101
102        return jb.build();
103    }
104
105    public void setTheirTransport(JingleContentTransport transport) {
106        theirProposal = (JingleS5BTransport) transport;
107    }
108
109    @Override
110    public void initiateOutgoingSession(JingleTransportInitiationCallback callback) {
111        this.callback = callback;
112        initiateSession();
113    }
114
115    @Override
116    public void initiateIncomingSession(JingleTransportInitiationCallback callback) {
117        this.callback = callback;
118        initiateSession();
119    }
120
121    private void initiateSession() {
122        Socks5Proxy.getSocks5Proxy().addTransfer(createTransport().getDestinationAddress());
123        JingleContent content = jingleSession.getContents().get(0);
124        UsedCandidate usedCandidate = chooseFromProposedCandidates(theirProposal);
125        if (usedCandidate == null) {
126            ourChoice = CANDIDATE_FAILURE;
127            Jingle candidateError = transportManager().createCandidateError(
128                    jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(),
129                    content.getSenders(), content.getCreator(), content.getName(), theirProposal.getStreamId());
130            try {
131                jingleSession.getConnection().sendStanza(candidateError);
132            } catch (SmackException.NotConnectedException | InterruptedException e) {
133                LOGGER.log(Level.WARNING, "Could not send candidate-error.", e);
134            }
135        } else {
136            ourChoice = usedCandidate;
137            Jingle jingle = transportManager().createCandidateUsed(jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(),
138                    content.getSenders(), content.getCreator(), content.getName(), theirProposal.getStreamId(), ourChoice.candidate.getCandidateId());
139            try {
140                jingleSession.getConnection().sendIqRequestAndWaitForResponse(jingle);
141            } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
142                LOGGER.log(Level.WARNING, "Could not send candidate-used.", e);
143            }
144        }
145        connectIfReady();
146    }
147
148    private UsedCandidate chooseFromProposedCandidates(JingleS5BTransport proposal) {
149        for (JingleContentTransportCandidate c : proposal.getCandidates()) {
150            JingleS5BTransportCandidate candidate = (JingleS5BTransportCandidate) c;
151
152            try {
153                return connectToTheirCandidate(candidate);
154            } catch (InterruptedException | TimeoutException | XMPPException | SmackException | IOException e) {
155                LOGGER.log(Level.WARNING, "Could not connect to " + candidate.getHost(), e);
156            }
157        }
158        LOGGER.log(Level.WARNING, "Failed to connect to any candidate.");
159        return null;
160    }
161
162    private UsedCandidate connectToTheirCandidate(JingleS5BTransportCandidate candidate)
163            throws InterruptedException, TimeoutException, SmackException, XMPPException, IOException {
164        Bytestream.StreamHost streamHost = candidate.getStreamHost();
165        InetAddress address = streamHost.getAddress().asInetAddress();
166        Socks5Client socks5Client = new Socks5Client(streamHost, theirProposal.getDestinationAddress());
167        Socket socket = socks5Client.getSocket(10 * 1000);
168        LOGGER.log(Level.INFO, "Connected to their StreamHost " + address + " using dstAddr "
169                + theirProposal.getDestinationAddress());
170        return new UsedCandidate(theirProposal, candidate, socket);
171    }
172
173    private UsedCandidate connectToOurCandidate(JingleS5BTransportCandidate candidate)
174            throws InterruptedException, TimeoutException, SmackException, XMPPException, IOException {
175        Bytestream.StreamHost streamHost = candidate.getStreamHost();
176        InetAddress address = streamHost.getAddress().asInetAddress();
177        Socks5ClientForInitiator socks5Client = new Socks5ClientForInitiator(
178                streamHost, ourProposal.getDestinationAddress(), jingleSession.getConnection(),
179                ourProposal.getStreamId(), jingleSession.getRemote());
180        Socket socket = socks5Client.getSocket(10 * 1000);
181        LOGGER.log(Level.INFO, "Connected to our StreamHost " + address + " using dstAddr "
182                + ourProposal.getDestinationAddress());
183        return new UsedCandidate(ourProposal, candidate, socket);
184    }
185
186    @Override
187    public String getNamespace() {
188        return JingleS5BTransport.NAMESPACE_V1;
189    }
190
191    @Override
192    public IQ handleTransportInfo(Jingle transportInfo) {
193        JingleS5BTransportInfo info = (JingleS5BTransportInfo) transportInfo.getContents().get(0).getTransport().getInfo();
194
195        switch (info.getElementName()) {
196            case JingleS5BTransportInfo.CandidateUsed.ELEMENT:
197                return handleCandidateUsed(transportInfo);
198
199            case JingleS5BTransportInfo.CandidateActivated.ELEMENT:
200                return handleCandidateActivate(transportInfo);
201
202            case JingleS5BTransportInfo.CandidateError.ELEMENT:
203                return handleCandidateError(transportInfo);
204
205            case JingleS5BTransportInfo.ProxyError.ELEMENT:
206                return handleProxyError(transportInfo);
207        }
208        // We should never go here, but lets be gracious...
209        return IQ.createResultIQ(transportInfo);
210    }
211
212    public IQ handleCandidateUsed(Jingle jingle) {
213        JingleS5BTransportInfo info = (JingleS5BTransportInfo) jingle.getContents().get(0).getTransport().getInfo();
214        String candidateId = ((JingleS5BTransportInfo.CandidateUsed) info).getCandidateId();
215        theirChoice = new UsedCandidate(ourProposal, ourProposal.getCandidate(candidateId), null);
216
217        if (theirChoice.candidate == null) {
218            /*
219            TODO: Booooooh illegal candidateId!! Go home!!!!11elf
220             */
221        }
222
223        connectIfReady();
224
225        return IQ.createResultIQ(jingle);
226    }
227
228    public IQ handleCandidateActivate(Jingle jingle) {
229        LOGGER.log(Level.INFO, "handleCandidateActivate");
230        Socks5BytestreamSession bs = new Socks5BytestreamSession(ourChoice.socket,
231                ourChoice.candidate.getJid().asBareJid().equals(jingleSession.getRemote().asBareJid()));
232        callback.onSessionInitiated(bs);
233        return IQ.createResultIQ(jingle);
234    }
235
236    public IQ handleCandidateError(Jingle jingle) {
237        theirChoice = CANDIDATE_FAILURE;
238        connectIfReady();
239        return IQ.createResultIQ(jingle);
240    }
241
242    public IQ handleProxyError(Jingle jingle) {
243        // TODO
244        return IQ.createResultIQ(jingle);
245    }
246
247    /**
248     * Determine, which candidate (ours/theirs) is the nominated one.
249     * Connect to this candidate. If it is a proxy and it is ours, activate it and connect.
250     * If its a proxy and it is theirs, wait for activation.
251     * If it is not a proxy, just connect.
252     */
253    private void connectIfReady() {
254        JingleContent content = jingleSession.getContents().get(0);
255        if (ourChoice == null || theirChoice == null) {
256            // Not yet ready.
257            LOGGER.log(Level.INFO, "Not ready.");
258            return;
259        }
260
261        if (ourChoice == CANDIDATE_FAILURE && theirChoice == CANDIDATE_FAILURE) {
262            LOGGER.log(Level.INFO, "Failure.");
263            jingleSession.onTransportMethodFailed(getNamespace());
264            return;
265        }
266
267        LOGGER.log(Level.INFO, "Ready.");
268
269        // Determine nominated candidate.
270        UsedCandidate nominated;
271        if (ourChoice != CANDIDATE_FAILURE && theirChoice != CANDIDATE_FAILURE) {
272            if (ourChoice.candidate.getPriority() > theirChoice.candidate.getPriority()) {
273                nominated = ourChoice;
274            } else if (ourChoice.candidate.getPriority() < theirChoice.candidate.getPriority()) {
275                nominated = theirChoice;
276            } else {
277                nominated = jingleSession.isInitiator() ? ourChoice : theirChoice;
278            }
279        } else if (ourChoice != CANDIDATE_FAILURE) {
280            nominated = ourChoice;
281        } else {
282            nominated = theirChoice;
283        }
284
285        if (nominated == theirChoice) {
286            LOGGER.log(Level.INFO, "Their choice, so our proposed candidate is used.");
287            boolean isProxy = nominated.candidate.getType() == JingleS5BTransportCandidate.Type.proxy;
288            try {
289                nominated = connectToOurCandidate(nominated.candidate);
290            } catch (InterruptedException | IOException | XMPPException | SmackException | TimeoutException e) {
291                LOGGER.log(Level.INFO, "Could not connect to our candidate.", e);
292                // TODO: Proxy-Error
293                return;
294            }
295
296            if (isProxy) {
297                LOGGER.log(Level.INFO, "Is external proxy. Activate it.");
298                Bytestream activate = new Bytestream(ourProposal.getStreamId());
299                activate.setMode(null);
300                activate.setType(IQ.Type.set);
301                activate.setTo(nominated.candidate.getJid());
302                activate.setToActivate(jingleSession.getRemote());
303                activate.setFrom(jingleSession.getLocal());
304                try {
305                    jingleSession.getConnection().sendIqRequestAndWaitForResponse(activate);
306                } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
307                    LOGGER.log(Level.WARNING, "Could not activate proxy.", e);
308                    return;
309                }
310
311                LOGGER.log(Level.INFO, "Send candidate-activate.");
312                Jingle candidateActivate = transportManager().createCandidateActivated(
313                        jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(),
314                        content.getSenders(), content.getCreator(), content.getName(), nominated.transport.getStreamId(),
315                        nominated.candidate.getCandidateId());
316                try {
317                    jingleSession.getConnection().sendIqRequestAndWaitForResponse(candidateActivate);
318                } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
319                    LOGGER.log(Level.WARNING, "Could not send candidate-activated", e);
320                    return;
321                }
322            }
323
324            LOGGER.log(Level.INFO, "Start transmission.");
325            Socks5BytestreamSession bs = new Socks5BytestreamSession(nominated.socket, !isProxy);
326            callback.onSessionInitiated(bs);
327
328        }
329        // Our choice
330        else {
331            LOGGER.log(Level.INFO, "Our choice, so their candidate was used.");
332            boolean isProxy = nominated.candidate.getType() == JingleS5BTransportCandidate.Type.proxy;
333            if (!isProxy) {
334                LOGGER.log(Level.INFO, "Direct connection.");
335                Socks5BytestreamSession bs = new Socks5BytestreamSession(nominated.socket, true);
336                callback.onSessionInitiated(bs);
337            } else {
338                LOGGER.log(Level.INFO, "Our choice was their external proxy. wait for candidate-activate.");
339            }
340        }
341    }
342
343    @Override
344    public JingleS5BTransportManager transportManager() {
345        return JingleS5BTransportManager.getInstanceFor(jingleSession.getConnection());
346    }
347
348    private static final class UsedCandidate {
349        private final Socket socket;
350        private final JingleS5BTransport transport;
351        private final JingleS5BTransportCandidate candidate;
352
353        private UsedCandidate(JingleS5BTransport transport, JingleS5BTransportCandidate candidate, Socket socket) {
354            this.socket = socket;
355            this.transport = transport;
356            this.candidate = candidate;
357        }
358    }
359
360    private static final UsedCandidate CANDIDATE_FAILURE = new UsedCandidate(null, null, null);
361}