rocks.xmpp.extensions.bytestreams.s5b.Socks5ByteStreamManager Maven / Gradle / Ivy
Show all versions of xmpp-extensions-client Show documentation
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.extensions.bytestreams.s5b;
import rocks.xmpp.addr.Jid;
import rocks.xmpp.core.XmppException;
import rocks.xmpp.core.session.XmppSession;
import rocks.xmpp.core.stanza.AbstractIQHandler;
import rocks.xmpp.core.stanza.model.IQ;
import rocks.xmpp.core.stanza.model.errors.Condition;
import rocks.xmpp.extensions.bytestreams.ByteStreamManager;
import rocks.xmpp.extensions.bytestreams.ByteStreamSession;
import rocks.xmpp.extensions.bytestreams.s5b.model.Socks5ByteStream;
import rocks.xmpp.extensions.bytestreams.s5b.model.StreamHost;
import rocks.xmpp.extensions.disco.ServiceDiscoveryManager;
import rocks.xmpp.extensions.disco.model.info.Identity;
import rocks.xmpp.util.XmppUtils;
import rocks.xmpp.util.concurrent.AsyncResult;
import rocks.xmpp.util.concurrent.CompletionStages;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;
/**
* A manager for XEP-0065: SOCKS5 Bytestreams.
*
* This class starts a local SOCKS5 server to support direct connections between two entities.
* You can {@linkplain #setPort(int) set a port} of this local server, if you don't set a port, the default port 1080 is used.
*
* It also allows you to {@linkplain #initiateSession(Jid, String) initiate a byte stream session} with another entity.
*
* @author Christian Schudt
*/
public final class Socks5ByteStreamManager extends ByteStreamManager {
private final ServiceDiscoveryManager serviceDiscoveryManager;
private final LocalSocks5Server localSocks5Server;
/**
* Guarded by "this".
*/
private boolean localHostEnabled;
private Socks5ByteStreamManager(final XmppSession xmppSession) {
super(xmppSession);
this.serviceDiscoveryManager = xmppSession.getManager(ServiceDiscoveryManager.class);
this.localSocks5Server = new LocalSocks5Server();
}
static S5bSession createS5bSession(Jid requester, Jid target, String sessionId, List streamHosts, Duration timeout) throws IOException {
Socket socketUsed = null;
Jid streamHostUsed = null;
IOException ioException = null;
// If the Requester provides more than one StreamHost, the Target SHOULD try to connect to them in the order of the children within the element.
for (StreamHost streamHost : streamHosts) {
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(streamHost.getHostname(), streamHost.getPort()));
// If the Target is able to open a TCP socket on a StreamHost/Requester, it MUST use the SOCKS5 protocol to establish a SOCKS5 connection.
Socks5Protocol.establishClientConnection(socket, Socks5ByteStream.hash(sessionId, requester, target), 0);
socketUsed = socket;
streamHostUsed = streamHost.getJid();
break;
} catch (IOException e) {
// ignore, try next.
ioException = e;
}
}
if (streamHostUsed == null) {
throw new IOException("Unable to connect to any stream host.", ioException);
}
return new S5bSession(sessionId, socketUsed, streamHostUsed, timeout);
}
@Override
protected void initialize() {
super.initialize();
xmppSession.addIQHandler(Socks5ByteStream.class, new AbstractIQHandler(IQ.Type.SET) {
@Override
protected IQ processRequest(IQ iq) {
Socks5ByteStream socks5ByteStream = iq.getExtension(Socks5ByteStream.class);
if (socks5ByteStream.getSessionId() == null) {
// If the request is malformed (e.g., the element does not include the 'sid' attribute), the Target MUST return an error of .
return iq.createError(Condition.BAD_REQUEST);
} else {
XmppUtils.notifyEventListeners(byteStreamListeners, new S5bEvent(Socks5ByteStreamManager.this, socks5ByteStream.getSessionId(), xmppSession, iq, new ArrayList<>(socks5ByteStream.getStreamHosts())));
return null;
}
}
});
}
/**
* Indicates whether the local host is enabled.
*
* @return If enabled.
*/
public synchronized boolean isLocalHostEnabled() {
return localHostEnabled;
}
/**
* Enables or disables the use of a local SOCKS5 host.
*
* @param enabled If enabled.
*/
public synchronized void setLocalHostEnabled(boolean enabled) {
this.localHostEnabled = enabled;
if (!enabled) {
localSocks5Server.stop();
}
}
/**
* Gets the port of the local host.
*
* @return The port.
*/
public int getPort() {
return localSocks5Server.getPort();
}
/**
* Sets the port of the local host.
*
* @param port The port.
*/
public void setPort(int port) {
this.localSocks5Server.setPort(port);
}
@Override
protected void onEnable() {
super.onEnable();
if (isLocalHostEnabled()) {
// Only stop the server here, if we disable support.
// It will be enabled, when needed.
localSocks5Server.start();
}
}
@Override
protected void onDisable() {
super.onDisable();
if (!isLocalHostEnabled()) {
// Only stop the server here, if we disable support.
// It will be enabled, when needed.
localSocks5Server.stop();
}
}
/**
* Discovers the SOCKS5 proxies.
*
* @return The async result with the proxies.
* @see 4. Discovering Proxies
*/
public AsyncResult> discoverProxies() {
// First discover the items which identify the proxy host.
return serviceDiscoveryManager.discoverServices(Identity.proxyByteStreams()).thenCompose(items -> {
// For each proxy service, send a disco request to it (to discover stream hosts)
Collection>> stages = items.stream()
.map(service -> xmppSession.query(IQ.get(service.getJid(), new Socks5ByteStream())).thenApply(result -> {
Socks5ByteStream socks5ByteStream = result.getExtension(Socks5ByteStream.class);
if (socks5ByteStream != null) {
return socks5ByteStream.getStreamHosts();
}
return Collections.emptyList();
}))
.collect(Collectors.toList());
// Combine all discovery of all stream hosts into one future.
return CompletionStages.allOf(stages);
});
}
/**
* Gets a list of available stream hosts, including the discovered proxies and the local host.
*
* @return The async result with the stream hosts.
*/
public AsyncResult> getAvailableStreamHosts() {
return discoverProxies().thenApply(streamHosts -> {
try {
List result = new ArrayList<>();
if (isLocalHostEnabled()) {
// First add the local SOCKS5 server to the list of stream hosts.
Jid requester = xmppSession.getConnectedResource();
result.add(new StreamHost(requester, localSocks5Server.getAddress(), localSocks5Server.getPort()));
}
IOException ioException = null;
// Try, if we -as initiator- can connect to the offered stream proxies, before suggesting them to the receiver.
for (StreamHost streamHost : streamHosts) {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(streamHost.getHostname(), streamHost.getPort()), 5000);
// We can connect, let's add to the stream hosts we send to the receiver.
result.add(streamHost);
} catch (IOException e) {
ioException = e;
}
}
if (result.isEmpty()) {
XmppException xmppException = new XmppException("No stream hosts found.");
if (ioException != null) {
ioException.initCause(ioException);
}
throw xmppException;
}
return result;
} catch (XmppException e) {
throw new CompletionException(e);
}
});
}
/**
* Initiates a SOCKS5 session with a target.
*
* @param target The target.
* @param sessionId The session id.
* @return The async result with the SOCKS5 byte stream session.
*/
public AsyncResult initiateSession(Jid target, String sessionId) {
if (isLocalHostEnabled()) {
localSocks5Server.start();
}
// Discover available stream hosts (which also adds ourselves to the list of available hosts)
return getAvailableStreamHosts().thenCompose(streamHosts -> {
final Jid requester = xmppSession.getConnectedResource();
// Create the hash, which will identify the socket connection.
final String hash = Socks5ByteStream.hash(sessionId, requester, target);
localSocks5Server.allowedAddresses.add(hash);
// 5.3.1 Requester Initiates S5B Negotiation
// 6.3.1 Requester Initiates S5B Negotiation
// Then send the available stream hosts to the receiver, which will then try to connect to one of the hosts.
return xmppSession.query(IQ.set(target, new Socks5ByteStream(sessionId, streamHosts, hash))).whenComplete((a, e) ->
// When the receiver responded (either with success or with error, we can remove the hash)
localSocks5Server.allowedAddresses.remove(hash)
).thenComposeAsync(result -> {
// Then complete the process by either connecting to a SOCKS5 proxy or use the socket which our receiver connected to.
// 5.3.3 Target Acknowledges Bytestream
// 6.3.3 Target Acknowledges Bytestream
Socks5ByteStream socks5ByteStream = result.getExtension(Socks5ByteStream.class);
StreamHost streamHostUsed = null;
for (StreamHost streamHost : streamHosts) {
if (socks5ByteStream.getStreamHostUsed() != null && socks5ByteStream.getStreamHostUsed().equals(streamHost.getJid())) {
streamHostUsed = streamHost;
break;
}
}
final StreamHost usedStreamHost = streamHostUsed;
if (usedStreamHost == null) {
throw new CompletionException(new XmppException("Target did not respond with a stream host."));
}
Socket socket;
if (!usedStreamHost.getJid().equals(requester)) {
socket = new Socket();
// 6.3.4 Requester Establishes SOCKS5 Connection with StreamHost
try {
socket.connect(new InetSocketAddress(usedStreamHost.getHostname(), usedStreamHost.getPort()));
Socks5Protocol.establishClientConnection(socket, hash, 0);
} catch (IOException e) {
throw new CompletionException(e);
}
// 6.3.5 Activation of Bytestream
return xmppSession.query(IQ.set(usedStreamHost.getJid(), Socks5ByteStream.activate(sessionId, target))).thenApply(aVoid -> {
try {
return new S5bSession(sessionId, socket, usedStreamHost.getJid(), xmppSession.getConfiguration().getDefaultResponseTimeout());
} catch (SocketException e) {
throw new CompletionException(e);
}
});
} else {
socket = localSocks5Server.getSocket(hash);
if (socket == null) {
throw new CompletionException(new IOException("Not connected to stream host"));
}
try {
return CompletableFuture.completedFuture(new S5bSession(sessionId, socket, usedStreamHost.getJid(), xmppSession.getConfiguration().getDefaultResponseTimeout()));
} catch (SocketException e) {
throw new CompletionException(e);
}
}
});
}
);
}
@Override
protected void dispose() {
super.dispose();
localSocks5Server.stop();
}
}