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}