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().createStanzaCollectorAndSend(jingle)
141                        .nextResultOrThrow();
142            } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
143                LOGGER.log(Level.WARNING, "Could not send candidate-used.", e);
144            }
145        }
146        connectIfReady();
147    }
148
149    private UsedCandidate chooseFromProposedCandidates(JingleS5BTransport proposal) {
150        for (JingleContentTransportCandidate c : proposal.getCandidates()) {
151            JingleS5BTransportCandidate candidate = (JingleS5BTransportCandidate) c;
152
153            try {
154                return connectToTheirCandidate(candidate);
155            } catch (InterruptedException | TimeoutException | XMPPException | SmackException | IOException e) {
156                LOGGER.log(Level.WARNING, "Could not connect to " + candidate.getHost(), e);
157            }
158        }
159        LOGGER.log(Level.WARNING, "Failed to connect to any candidate.");
160        return null;
161    }
162
163    private UsedCandidate connectToTheirCandidate(JingleS5BTransportCandidate candidate)
164            throws InterruptedException, TimeoutException, SmackException, XMPPException, IOException {
165        Bytestream.StreamHost streamHost = candidate.getStreamHost();
166        InetAddress address = streamHost.getAddress().asInetAddress();
167        Socks5Client socks5Client = new Socks5Client(streamHost, theirProposal.getDestinationAddress());
168        Socket socket = socks5Client.getSocket(10 * 1000);
169        LOGGER.log(Level.INFO, "Connected to their StreamHost " + address + " using dstAddr "
170                + theirProposal.getDestinationAddress());
171        return new UsedCandidate(theirProposal, candidate, socket);
172    }
173
174    private UsedCandidate connectToOurCandidate(JingleS5BTransportCandidate candidate)
175            throws InterruptedException, TimeoutException, SmackException, XMPPException, IOException {
176        Bytestream.StreamHost streamHost = candidate.getStreamHost();
177        InetAddress address = streamHost.getAddress().asInetAddress();
178        Socks5ClientForInitiator socks5Client = new Socks5ClientForInitiator(
179                streamHost, ourProposal.getDestinationAddress(), jingleSession.getConnection(),
180                ourProposal.getStreamId(), jingleSession.getRemote());
181        Socket socket = socks5Client.getSocket(10 * 1000);
182        LOGGER.log(Level.INFO, "Connected to our StreamHost " + address + " using dstAddr "
183                + ourProposal.getDestinationAddress());
184        return new UsedCandidate(ourProposal, candidate, socket);
185    }
186
187    @Override
188    public String getNamespace() {
189        return JingleS5BTransport.NAMESPACE_V1;
190    }
191
192    @Override
193    public IQ handleTransportInfo(Jingle transportInfo) {
194        JingleS5BTransportInfo info = (JingleS5BTransportInfo) transportInfo.getContents().get(0).getTransport().getInfo();
195
196        switch (info.getElementName()) {
197            case JingleS5BTransportInfo.CandidateUsed.ELEMENT:
198                return handleCandidateUsed(transportInfo);
199
200            case JingleS5BTransportInfo.CandidateActivated.ELEMENT:
201                return handleCandidateActivate(transportInfo);
202
203            case JingleS5BTransportInfo.CandidateError.ELEMENT:
204                return handleCandidateError(transportInfo);
205
206            case JingleS5BTransportInfo.ProxyError.ELEMENT:
207                return handleProxyError(transportInfo);
208        }
209        // We should never go here, but lets be gracious...
210        return IQ.createResultIQ(transportInfo);
211    }
212
213    public IQ handleCandidateUsed(Jingle jingle) {
214        JingleS5BTransportInfo info = (JingleS5BTransportInfo) jingle.getContents().get(0).getTransport().getInfo();
215        String candidateId = ((JingleS5BTransportInfo.CandidateUsed) info).getCandidateId();
216        theirChoice = new UsedCandidate(ourProposal, ourProposal.getCandidate(candidateId), null);
217
218        if (theirChoice.candidate == null) {
219            /*
220            TODO: Booooooh illegal candidateId!! Go home!!!!11elf
221             */
222        }
223
224        connectIfReady();
225
226        return IQ.createResultIQ(jingle);
227    }
228
229    public IQ handleCandidateActivate(Jingle jingle) {
230        LOGGER.log(Level.INFO, "handleCandidateActivate");
231        Socks5BytestreamSession bs = new Socks5BytestreamSession(ourChoice.socket,
232                ourChoice.candidate.getJid().asBareJid().equals(jingleSession.getRemote().asBareJid()));
233        callback.onSessionInitiated(bs);
234        return IQ.createResultIQ(jingle);
235    }
236
237    public IQ handleCandidateError(Jingle jingle) {
238        theirChoice = CANDIDATE_FAILURE;
239        connectIfReady();
240        return IQ.createResultIQ(jingle);
241    }
242
243    public IQ handleProxyError(Jingle jingle) {
244        // TODO
245        return IQ.createResultIQ(jingle);
246    }
247
248    /**
249     * Determine, which candidate (ours/theirs) is the nominated one.
250     * Connect to this candidate. If it is a proxy and it is ours, activate it and connect.
251     * If its a proxy and it is theirs, wait for activation.
252     * If it is not a proxy, just connect.
253     */
254    private void connectIfReady() {
255        JingleContent content = jingleSession.getContents().get(0);
256        if (ourChoice == null || theirChoice == null) {
257            // Not yet ready.
258            LOGGER.log(Level.INFO, "Not ready.");
259            return;
260        }
261
262        if (ourChoice == CANDIDATE_FAILURE && theirChoice == CANDIDATE_FAILURE) {
263            LOGGER.log(Level.INFO, "Failure.");
264            jingleSession.onTransportMethodFailed(getNamespace());
265            return;
266        }
267
268        LOGGER.log(Level.INFO, "Ready.");
269
270        // Determine nominated candidate.
271        UsedCandidate nominated;
272        if (ourChoice != CANDIDATE_FAILURE && theirChoice != CANDIDATE_FAILURE) {
273            if (ourChoice.candidate.getPriority() > theirChoice.candidate.getPriority()) {
274                nominated = ourChoice;
275            } else if (ourChoice.candidate.getPriority() < theirChoice.candidate.getPriority()) {
276                nominated = theirChoice;
277            } else {
278                nominated = jingleSession.isInitiator() ? ourChoice : theirChoice;
279            }
280        } else if (ourChoice != CANDIDATE_FAILURE) {
281            nominated = ourChoice;
282        } else {
283            nominated = theirChoice;
284        }
285
286        if (nominated == theirChoice) {
287            LOGGER.log(Level.INFO, "Their choice, so our proposed candidate is used.");
288            boolean isProxy = nominated.candidate.getType() == JingleS5BTransportCandidate.Type.proxy;
289            try {
290                nominated = connectToOurCandidate(nominated.candidate);
291            } catch (InterruptedException | IOException | XMPPException | SmackException | TimeoutException e) {
292                LOGGER.log(Level.INFO, "Could not connect to our candidate.", e);
293                // TODO: Proxy-Error
294                return;
295            }
296
297            if (isProxy) {
298                LOGGER.log(Level.INFO, "Is external proxy. Activate it.");
299                Bytestream activate = new Bytestream(ourProposal.getStreamId());
300                activate.setMode(null);
301                activate.setType(IQ.Type.set);
302                activate.setTo(nominated.candidate.getJid());
303                activate.setToActivate(jingleSession.getRemote());
304                activate.setFrom(jingleSession.getLocal());
305                try {
306                    jingleSession.getConnection().createStanzaCollectorAndSend(activate).nextResultOrThrow();
307                } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
308                    LOGGER.log(Level.WARNING, "Could not activate proxy.", e);
309                    return;
310                }
311
312                LOGGER.log(Level.INFO, "Send candidate-activate.");
313                Jingle candidateActivate = transportManager().createCandidateActivated(
314                        jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(),
315                        content.getSenders(), content.getCreator(), content.getName(), nominated.transport.getStreamId(),
316                        nominated.candidate.getCandidateId());
317                try {
318                    jingleSession.getConnection().createStanzaCollectorAndSend(candidateActivate)
319                            .nextResultOrThrow();
320                } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
321                    LOGGER.log(Level.WARNING, "Could not send candidate-activated", e);
322                    return;
323                }
324            }
325
326            LOGGER.log(Level.INFO, "Start transmission.");
327            Socks5BytestreamSession bs = new Socks5BytestreamSession(nominated.socket, !isProxy);
328            callback.onSessionInitiated(bs);
329
330        }
331        // Our choice
332        else {
333            LOGGER.log(Level.INFO, "Our choice, so their candidate was used.");
334            boolean isProxy = nominated.candidate.getType() == JingleS5BTransportCandidate.Type.proxy;
335            if (!isProxy) {
336                LOGGER.log(Level.INFO, "Direct connection.");
337                Socks5BytestreamSession bs = new Socks5BytestreamSession(nominated.socket, true);
338                callback.onSessionInitiated(bs);
339            } else {
340                LOGGER.log(Level.INFO, "Our choice was their external proxy. wait for candidate-activate.");
341            }
342        }
343    }
344
345    @Override
346    public JingleS5BTransportManager transportManager() {
347        return JingleS5BTransportManager.getInstanceFor(jingleSession.getConnection());
348    }
349
350    private static final class UsedCandidate {
351        private final Socket socket;
352        private final JingleS5BTransport transport;
353        private final JingleS5BTransportCandidate candidate;
354
355        private UsedCandidate(JingleS5BTransport transport, JingleS5BTransportCandidate candidate, Socket socket) {
356            this.socket = socket;
357            this.transport = transport;
358            this.candidate = candidate;
359        }
360    }
361
362    private static final UsedCandidate CANDIDATE_FAILURE = new UsedCandidate(null, null, null);
363}