/** * * Copyright 2017 Paul Schaub * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.jingle.transports.jingle_s5b; import java.io.IOException; import java.net.Socket; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession; import org.jivesoftware.smackx.bytestreams.socks5.Socks5Client; import org.jivesoftware.smackx.bytestreams.socks5.Socks5ClientForInitiator; import org.jivesoftware.smackx.bytestreams.socks5.Socks5Utils; import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; import org.jivesoftware.smackx.jingle.JingleManager; import org.jivesoftware.smackx.jingle.JingleSession; import org.jivesoftware.smackx.jingle.element.Jingle; import org.jivesoftware.smackx.jingle.element.JingleContent; import org.jivesoftware.smackx.jingle.element.JingleContentTransport; import org.jivesoftware.smackx.jingle.element.JingleContentTransportCandidate; import org.jivesoftware.smackx.jingle.transports.JingleTransportInitiationCallback; import org.jivesoftware.smackx.jingle.transports.JingleTransportSession; import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransport; import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransportCandidate; import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransportInfo; /** * Created by vanitas on 26.06.17. */ public class JingleS5BTransportSession extends JingleTransportSession { private static final Logger LOGGER = Logger.getLogger(JingleS5BTransportSession.class.getName()); private JingleTransportInitiationCallback callback; public JingleS5BTransportSession(JingleSession jingleSession) { super(jingleSession); } private UsedCandidate ourChoice, theirChoice; @Override public JingleS5BTransport createTransport() { if (ourProposal == null) { ourProposal = createTransport(JingleManager.randomId(), Bytestream.Mode.tcp); } return ourProposal; } @Override public void setTheirProposal(JingleContentTransport transport) { theirProposal = (JingleS5BTransport) transport; } public JingleS5BTransport createTransport(String sid, Bytestream.Mode mode) { JingleS5BTransport.Builder jb = JingleS5BTransport.getBuilder() .setStreamId(sid).setMode(mode).setDestinationAddress( Socks5Utils.createDigest(sid, jingleSession.getLocal(), jingleSession.getRemote())); //Local host for (Bytestream.StreamHost host : transportManager().getLocalStreamHosts()) { jb.addTransportCandidate(new JingleS5BTransportCandidate(host, 100)); } List remoteHosts; try { remoteHosts = transportManager().getAvailableStreamHosts(); } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) { LOGGER.log(Level.WARNING, "Could not determine available StreamHosts.", e); remoteHosts = Collections.emptyList(); } for (Bytestream.StreamHost host : remoteHosts) { jb.addTransportCandidate(new JingleS5BTransportCandidate(host, 0)); } return jb.build(); } public void setTheirTransport(JingleContentTransport transport) { theirProposal = (JingleS5BTransport) transport; } @Override public void initiateOutgoingSession(JingleTransportInitiationCallback callback) { this.callback = callback; initiateSession(); } @Override public void initiateIncomingSession(JingleTransportInitiationCallback callback) { this.callback = callback; initiateSession(); } private void initiateSession() { JingleContent content = jingleSession.getContents().get(0); UsedCandidate usedCandidate = chooseFromProposedCandidates(theirProposal); if (usedCandidate == null) { ourChoice = CANDIDATE_FAILURE; Jingle candidateError = transportManager().createCandidateError( jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(), content.getSenders(), content.getCreator(), content.getName(), theirProposal.getStreamId()); try { jingleSession.getConnection().sendStanza(candidateError); } catch (SmackException.NotConnectedException | InterruptedException e) { LOGGER.log(Level.WARNING, "Could not send candidate-error.", e); } } else { ourChoice = usedCandidate; Jingle jingle = transportManager().createCandidateUsed(jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(), content.getSenders(), content.getCreator(), content.getName(), theirProposal.getStreamId(), ourChoice.candidate.getCandidateId()); try { jingleSession.getConnection().createStanzaCollectorAndSend(jingle) .nextResultOrThrow(); } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) { LOGGER.log(Level.WARNING, "Could not send candidate-used.", e); } } connectIfReady(); } private UsedCandidate chooseFromProposedCandidates(JingleS5BTransport proposal) { for (JingleContentTransportCandidate c : proposal.getCandidates()) { JingleS5BTransportCandidate candidate = (JingleS5BTransportCandidate) c; try { return connectToTheirCandidate(candidate); } catch (InterruptedException | TimeoutException | XMPPException | SmackException | IOException e) { LOGGER.log(Level.WARNING, "Could not connect to " + candidate.getHost(), e); } } LOGGER.log(Level.WARNING, "Failed to connect to any candidate."); return null; } private UsedCandidate connectToTheirCandidate(JingleS5BTransportCandidate candidate) throws InterruptedException, TimeoutException, SmackException, XMPPException, IOException { Bytestream.StreamHost streamHost = candidate.getStreamHost(); String address = streamHost.getAddress(); Socks5Client socks5Client = new Socks5Client(streamHost, theirProposal.getDestinationAddress()); Socket socket = socks5Client.getSocket(10 * 1000); LOGGER.log(Level.INFO, "Connected to their StreamHost " + address + " using dstAddr " + theirProposal.getDestinationAddress()); return new UsedCandidate(theirProposal, candidate, socket); } private UsedCandidate connectToOurCandidate(JingleS5BTransportCandidate candidate) throws InterruptedException, TimeoutException, SmackException, XMPPException, IOException { Bytestream.StreamHost streamHost = candidate.getStreamHost(); String address = streamHost.getAddress(); Socks5ClientForInitiator socks5Client = new Socks5ClientForInitiator( streamHost, ourProposal.getDestinationAddress(), jingleSession.getConnection(), jingleSession.getSessionId(), jingleSession.getRemote()); Socket socket = socks5Client.getSocket(10 * 1000); LOGGER.log(Level.INFO, "Connected to our StreamHost " + address + " using dstAddr " + theirProposal.getDestinationAddress()); return new UsedCandidate(ourProposal, candidate, socket); } @Override public String getNamespace() { return JingleS5BTransport.NAMESPACE_V1; } @Override public IQ handleTransportInfo(Jingle transportInfo) { JingleS5BTransportInfo info = (JingleS5BTransportInfo) transportInfo.getContents().get(0).getJingleTransport().getInfo(); switch (info.getElementName()) { case JingleS5BTransportInfo.CandidateUsed.ELEMENT: return handleCandidateUsed(transportInfo); case JingleS5BTransportInfo.CandidateActivated.ELEMENT: return handleCandidateActivate(transportInfo); case JingleS5BTransportInfo.CandidateError.ELEMENT: return handleCandidateError(transportInfo); case JingleS5BTransportInfo.ProxyError.ELEMENT: return handleProxyError(transportInfo); } //We should never go here, but lets be gracious... return IQ.createResultIQ(transportInfo); } public IQ handleCandidateUsed(Jingle jingle) { JingleS5BTransportInfo info = (JingleS5BTransportInfo) jingle.getContents().get(0).getJingleTransport().getInfo(); String candidateId = ((JingleS5BTransportInfo.CandidateUsed) info).getCandidateId(); theirChoice = new UsedCandidate(ourProposal, ourProposal.getCandidate(candidateId), null); if (theirChoice.candidate == null) { /* TODO: Booooooh illegal candidateId!! Go home!!!!11elf */ } connectIfReady(); return IQ.createResultIQ(jingle); } public IQ handleCandidateActivate(Jingle jingle) { Socks5BytestreamSession bs = new Socks5BytestreamSession(ourChoice.socket, ourChoice.candidate.getJid().asBareJid().equals(jingleSession.getRemote().asBareJid())); callback.onSessionInitiated(bs); return IQ.createResultIQ(jingle); } public IQ handleCandidateError(Jingle jingle) { theirChoice = CANDIDATE_FAILURE; connectIfReady(); return IQ.createResultIQ(jingle); } public IQ handleProxyError(Jingle jingle) { //TODO return IQ.createResultIQ(jingle); } private void connectIfReady() { if (ourChoice == null || theirChoice == null) { // Not yet ready. LOGGER.log(Level.INFO, "Not ready."); return; } if (ourChoice == CANDIDATE_FAILURE && theirChoice == CANDIDATE_FAILURE) { // TODO: Transport failed. } else { UsedCandidate nominated; if (ourChoice != CANDIDATE_FAILURE && theirChoice != CANDIDATE_FAILURE) { if (ourChoice.candidate.getPriority() > theirChoice.candidate.getPriority()) { nominated = ourChoice; } else if (ourChoice.candidate.getPriority() < theirChoice.candidate.getPriority()) { nominated = theirChoice; } else { nominated = jingleSession.isInitiator() ? ourChoice : theirChoice; } } else if (ourChoice != CANDIDATE_FAILURE) { nominated = ourChoice; } else { nominated = theirChoice; } // Proxy. Needs activation. if (nominated.candidate.getType() == JingleS5BTransportCandidate.Type.proxy) { //Our proxy. Activate it. if (nominated == theirChoice) { JingleContent content = jingleSession.getContents().get(0); Bytestream activateProxy = new Bytestream(ourProposal.getStreamId()); activateProxy.setToActivate(nominated.candidate.getJid()); activateProxy.setTo(nominated.candidate.getJid()); //Send proxy activation. try { jingleSession.getConnection().createStanzaCollectorAndSend(activateProxy).nextResultOrThrow(); //Connect try { nominated = connectToOurCandidate(nominated.candidate); Socks5BytestreamSession bs = new Socks5BytestreamSession(nominated.socket, nominated.candidate.getJid().asBareJid().equals(jingleSession.getLocal().asBareJid())); callback.onSessionInitiated(bs); } catch (TimeoutException | SmackException | IOException | XMPPException e) { LOGGER.log(Level.WARNING, "Could not connect to our own proxy after activation.", e); //TODO: ??? } } //Could not activate proxy. Send proxy-error catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) { LOGGER.log(Level.WARNING, "Could not activate proxy at " + nominated.candidate.getJid(), e); Jingle proxyError = transportManager().createProxyError( jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(), content.getSenders(), content.getCreator(), content.getName(), nominated.transport.getStreamId()); try { jingleSession.getConnection().sendStanza(proxyError); } //Could not send proxy-error. WTF? catch (SmackException.NotConnectedException | InterruptedException e1) { LOGGER.log(Level.WARNING, "Could not send proxy-error.", e1); } callback.onException(e); } //Send candidate-activate. Jingle candidateActivate = transportManager().createCandidateActivated( jingleSession.getRemote(), jingleSession.getInitiator(), jingleSession.getSessionId(), content.getSenders(), content.getCreator(), content.getName(), nominated.transport.getStreamId(), nominated.candidate.getCandidateId()); try { jingleSession.getConnection().createStanzaCollectorAndSend(candidateActivate) .nextResultOrThrow(); } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) { LOGGER.log(Level.WARNING, "Could not send candidate-activated", e); } } //Else wait for activation. } // Direct connection. Go ahead. else { Socks5BytestreamSession bs; if (nominated == ourChoice) { bs = new Socks5BytestreamSession(nominated.socket, nominated.candidate.getJid().asBareJid().equals(jingleSession.getRemote().asBareJid())); } else { try { nominated = connectToOurCandidate(theirChoice.candidate); } catch (InterruptedException | IOException | XMPPException | SmackException | TimeoutException e) { LOGGER.log(Level.SEVERE, "Failed to connect to our own StreamHost!"); callback.onException(e); return; } bs = new Socks5BytestreamSession(nominated.socket, nominated.candidate.getJid().asBareJid().equals(jingleSession.getLocal().asBareJid())); } callback.onSessionInitiated(bs); } } } @Override public JingleS5BTransportManager transportManager() { return JingleS5BTransportManager.getInstanceFor(jingleSession.getConnection()); } private static class UsedCandidate { private final Socket socket; private final JingleS5BTransport transport; private final JingleS5BTransportCandidate candidate; public UsedCandidate(JingleS5BTransport transport, JingleS5BTransportCandidate candidate, Socket socket) { this.socket = socket; this.transport = transport; this.candidate = candidate; } } private static final UsedCandidate CANDIDATE_FAILURE = new UsedCandidate(null, null, null); }