2017-07-03 10:35:46 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* 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.Socks5Proxy;
|
|
|
|
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;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler that handles Jingle Socks5Bytestream transports (XEP-0260).
|
|
|
|
*/
|
|
|
|
public class JingleS5BTransportSession extends JingleTransportSession<JingleS5BTransport> {
|
|
|
|
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()));
|
|
|
|
|
2017-11-22 08:37:47 +01:00
|
|
|
// Local host
|
2017-07-03 10:35:46 +02:00
|
|
|
if (JingleS5BTransportManager.isUseLocalCandidates()) {
|
|
|
|
for (Bytestream.StreamHost host : transportManager().getLocalStreamHosts()) {
|
|
|
|
jb.addTransportCandidate(new JingleS5BTransportCandidate(host, 100, JingleS5BTransportCandidate.Type.direct));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Bytestream.StreamHost> remoteHosts = Collections.emptyList();
|
|
|
|
if (JingleS5BTransportManager.isUseExternalCandidates()) {
|
|
|
|
try {
|
|
|
|
remoteHosts = transportManager().getAvailableStreamHosts();
|
|
|
|
} catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
|
|
|
|
LOGGER.log(Level.WARNING, "Could not determine available StreamHosts.", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (Bytestream.StreamHost host : remoteHosts) {
|
|
|
|
jb.addTransportCandidate(new JingleS5BTransportCandidate(host, 0, JingleS5BTransportCandidate.Type.proxy));
|
|
|
|
}
|
|
|
|
|
|
|
|
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() {
|
|
|
|
Socks5Proxy.getSocks5Proxy().addTransfer(createTransport().getDestinationAddress());
|
|
|
|
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(),
|
|
|
|
ourProposal.getStreamId(), jingleSession.getRemote());
|
|
|
|
Socket socket = socks5Client.getSocket(10 * 1000);
|
|
|
|
LOGGER.log(Level.INFO, "Connected to our StreamHost " + address + " using dstAddr "
|
|
|
|
+ ourProposal.getDestinationAddress());
|
|
|
|
return new UsedCandidate(ourProposal, candidate, socket);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String getNamespace() {
|
|
|
|
return JingleS5BTransport.NAMESPACE_V1;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public IQ handleTransportInfo(Jingle transportInfo) {
|
2017-07-30 19:15:56 +02:00
|
|
|
JingleS5BTransportInfo info = (JingleS5BTransportInfo) transportInfo.getContents().get(0).getTransport().getInfo();
|
2017-07-03 10:35:46 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2017-11-22 08:37:47 +01:00
|
|
|
// We should never go here, but lets be gracious...
|
2017-07-03 10:35:46 +02:00
|
|
|
return IQ.createResultIQ(transportInfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
public IQ handleCandidateUsed(Jingle jingle) {
|
2017-07-30 19:15:56 +02:00
|
|
|
JingleS5BTransportInfo info = (JingleS5BTransportInfo) jingle.getContents().get(0).getTransport().getInfo();
|
2017-07-03 10:35:46 +02:00
|
|
|
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) {
|
|
|
|
LOGGER.log(Level.INFO, "handleCandidateActivate");
|
|
|
|
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) {
|
2017-11-22 08:37:47 +01:00
|
|
|
// TODO
|
2017-07-03 10:35:46 +02:00
|
|
|
return IQ.createResultIQ(jingle);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine, which candidate (ours/theirs) is the nominated one.
|
|
|
|
* Connect to this candidate. If it is a proxy and it is ours, activate it and connect.
|
|
|
|
* If its a proxy and it is theirs, wait for activation.
|
|
|
|
* If it is not a proxy, just connect.
|
|
|
|
*/
|
|
|
|
private void connectIfReady() {
|
|
|
|
JingleContent content = jingleSession.getContents().get(0);
|
|
|
|
if (ourChoice == null || theirChoice == null) {
|
|
|
|
// Not yet ready.
|
|
|
|
LOGGER.log(Level.INFO, "Not ready.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ourChoice == CANDIDATE_FAILURE && theirChoice == CANDIDATE_FAILURE) {
|
|
|
|
LOGGER.log(Level.INFO, "Failure.");
|
|
|
|
jingleSession.onTransportMethodFailed(getNamespace());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
LOGGER.log(Level.INFO, "Ready.");
|
|
|
|
|
2017-11-22 08:37:47 +01:00
|
|
|
// Determine nominated candidate.
|
2017-07-03 10:35:46 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nominated == theirChoice) {
|
|
|
|
LOGGER.log(Level.INFO, "Their choice, so our proposed candidate is used.");
|
|
|
|
boolean isProxy = nominated.candidate.getType() == JingleS5BTransportCandidate.Type.proxy;
|
|
|
|
try {
|
|
|
|
nominated = connectToOurCandidate(nominated.candidate);
|
|
|
|
} catch (InterruptedException | IOException | XMPPException | SmackException | TimeoutException e) {
|
|
|
|
LOGGER.log(Level.INFO, "Could not connect to our candidate.", e);
|
2017-11-22 08:37:47 +01:00
|
|
|
// TODO: Proxy-Error
|
2017-07-03 10:35:46 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isProxy) {
|
|
|
|
LOGGER.log(Level.INFO, "Is external proxy. Activate it.");
|
|
|
|
Bytestream activate = new Bytestream(ourProposal.getStreamId());
|
|
|
|
activate.setMode(null);
|
|
|
|
activate.setType(IQ.Type.set);
|
|
|
|
activate.setTo(nominated.candidate.getJid());
|
|
|
|
activate.setToActivate(jingleSession.getRemote());
|
|
|
|
activate.setFrom(jingleSession.getLocal());
|
|
|
|
try {
|
|
|
|
jingleSession.getConnection().createStanzaCollectorAndSend(activate).nextResultOrThrow();
|
|
|
|
} catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
|
|
|
|
LOGGER.log(Level.WARNING, "Could not activate proxy.", e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
LOGGER.log(Level.INFO, "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);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
LOGGER.log(Level.INFO, "Start transmission.");
|
|
|
|
Socks5BytestreamSession bs = new Socks5BytestreamSession(nominated.socket, !isProxy);
|
|
|
|
callback.onSessionInitiated(bs);
|
|
|
|
|
|
|
|
}
|
2017-11-22 08:37:47 +01:00
|
|
|
// Our choice
|
2017-07-03 10:35:46 +02:00
|
|
|
else {
|
|
|
|
LOGGER.log(Level.INFO, "Our choice, so their candidate was used.");
|
|
|
|
boolean isProxy = nominated.candidate.getType() == JingleS5BTransportCandidate.Type.proxy;
|
|
|
|
if (!isProxy) {
|
|
|
|
LOGGER.log(Level.INFO, "Direct connection.");
|
|
|
|
Socks5BytestreamSession bs = new Socks5BytestreamSession(nominated.socket, true);
|
|
|
|
callback.onSessionInitiated(bs);
|
|
|
|
} else {
|
|
|
|
LOGGER.log(Level.INFO, "Our choice was their external proxy. wait for candidate-activate.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public JingleS5BTransportManager transportManager() {
|
|
|
|
return JingleS5BTransportManager.getInstanceFor(jingleSession.getConnection());
|
|
|
|
}
|
|
|
|
|
2018-03-28 14:02:21 +02:00
|
|
|
private static final class UsedCandidate {
|
2017-07-03 10:35:46 +02:00
|
|
|
private final Socket socket;
|
|
|
|
private final JingleS5BTransport transport;
|
|
|
|
private final JingleS5BTransportCandidate candidate;
|
|
|
|
|
2018-03-28 14:02:21 +02:00
|
|
|
private UsedCandidate(JingleS5BTransport transport, JingleS5BTransportCandidate candidate, Socket socket) {
|
2017-07-03 10:35:46 +02:00
|
|
|
this.socket = socket;
|
|
|
|
this.transport = transport;
|
|
|
|
this.candidate = candidate;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static final UsedCandidate CANDIDATE_FAILURE = new UsedCandidate(null, null, null);
|
|
|
|
}
|