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