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