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}