Bytestream.java
/**
*
* Copyright the original author or authors
*
* 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.bytestreams.socks5.packet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jxmpp.jid.Jid;
/**
* A stanza representing part of a SOCKS5 Bytestream negotiation.
*
* @author Alexander Wenckus
*/
public class Bytestream extends IQ {
public static final String ELEMENT = QUERY_ELEMENT;
/**
* The XMPP namespace of the SOCKS5 Bytestream.
*/
public static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
private String sessionID;
private Mode mode = Mode.tcp;
private final List<StreamHost> streamHosts = new ArrayList<>();
private StreamHostUsed usedHost;
private Activate toActivate;
/**
* The default constructor.
*/
public Bytestream() {
super(ELEMENT, NAMESPACE);
}
/**
* A constructor where the session ID can be specified.
*
* @param SID The session ID related to the negotiation.
* @see #setSessionID(String)
*/
public Bytestream(final String SID) {
this();
setSessionID(SID);
}
/**
* Set the session ID related to the bytestream. The session ID is a unique identifier used to
* differentiate between stream negotiations.
*
* @param sessionID the unique session ID that identifies the transfer.
*/
public void setSessionID(final String sessionID) {
this.sessionID = sessionID;
}
/**
* Returns the session ID related to the bytestream negotiation.
*
* @return Returns the session ID related to the bytestream negotiation.
* @see #setSessionID(String)
*/
public String getSessionID() {
return sessionID;
}
/**
* Set the transport mode. This should be put in the initiation of the interaction.
*
* @param mode the transport mode, either UDP or TCP
* @see Mode
*/
public void setMode(final Mode mode) {
this.mode = mode;
}
/**
* Returns the transport mode.
*
* @return Returns the transport mode.
* @see #setMode(Mode)
*/
public Mode getMode() {
return mode;
}
/**
* Adds a potential stream host that the remote user can connect to to receive the file.
*
* @param JID The JID of the stream host.
* @param address The internet address of the stream host.
* @return The added stream host.
*/
public StreamHost addStreamHost(final Jid JID, final String address) {
return addStreamHost(JID, address, 0);
}
/**
* Adds a potential stream host that the remote user can connect to to receive the file.
*
* @param JID The JID of the stream host.
* @param address The internet address of the stream host.
* @param port The port on which the remote host is seeking connections.
* @return The added stream host.
*/
public StreamHost addStreamHost(final Jid JID, final String address, final int port) {
StreamHost host = new StreamHost(JID, address, port);
addStreamHost(host);
return host;
}
/**
* Adds a potential stream host that the remote user can transfer the file through.
*
* @param host The potential stream host.
*/
public void addStreamHost(final StreamHost host) {
streamHosts.add(host);
}
/**
* Returns the list of stream hosts contained in the packet.
*
* @return Returns the list of stream hosts contained in the packet.
*/
public List<StreamHost> getStreamHosts() {
return Collections.unmodifiableList(streamHosts);
}
/**
* Returns the stream host related to the given JID, or null if there is none.
*
* @param JID The JID of the desired stream host.
* @return Returns the stream host related to the given JID, or null if there is none.
*/
public StreamHost getStreamHost(final Jid JID) {
if (JID == null) {
return null;
}
for (StreamHost host : streamHosts) {
if (host.getJID().equals(JID)) {
return host;
}
}
return null;
}
/**
* Returns the count of stream hosts contained in this packet.
*
* @return Returns the count of stream hosts contained in this packet.
*/
public int countStreamHosts() {
return streamHosts.size();
}
/**
* Upon connecting to the stream host the target of the stream replies to the initiator with the
* JID of the SOCKS5 host that they used.
*
* @param JID The JID of the used host.
*/
public void setUsedHost(final Jid JID) {
this.usedHost = new StreamHostUsed(JID);
}
/**
* Returns the SOCKS5 host connected to by the remote user.
*
* @return Returns the SOCKS5 host connected to by the remote user.
*/
public StreamHostUsed getUsedHost() {
return usedHost;
}
/**
* Returns the activate element of the stanza sent to the proxy host to verify the identity of
* the initiator and match them to the appropriate stream.
*
* @return Returns the activate element of the stanza sent to the proxy host to verify the
* identity of the initiator and match them to the appropriate stream.
*/
public Activate getToActivate() {
return toActivate;
}
/**
* Upon the response from the target of the used host the activate stanza is sent to the SOCKS5
* proxy. The proxy will activate the stream or return an error after verifying the identity of
* the initiator, using the activate packet.
*
* @param targetID The JID of the target of the file transfer.
*/
public void setToActivate(final Jid targetID) {
this.toActivate = new Activate(targetID);
}
@Override
protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
switch (getType()) {
case set:
xml.optAttribute("sid", getSessionID());
xml.optAttribute("mode", getMode());
xml.rightAngleBracket();
if (getToActivate() == null) {
for (StreamHost streamHost : getStreamHosts()) {
xml.append(streamHost.toXML(null));
}
}
else {
xml.append(getToActivate().toXML(null));
}
break;
case result:
xml.rightAngleBracket();
xml.optAppend(getUsedHost());
// TODO Bytestream can include either used host *or* streamHosts. Never both. This should be ensured by the
// constructions mechanisms of Bytestream
// A result from the server can also contain stream hosts
for (StreamHost host : streamHosts) {
xml.append(host.toXML(null));
}
break;
case get:
xml.setEmptyElement();
break;
default:
throw new IllegalStateException();
}
return xml;
}
/**
* Stanza extension that represents a potential SOCKS5 proxy for the file transfer. Stream hosts
* are forwarded to the target of the file transfer who then chooses and connects to one.
*
* @author Alexander Wenckus
*/
public static class StreamHost implements NamedElement {
public static String ELEMENTNAME = "streamhost";
private final Jid JID;
private final String addy;
private final int port;
public StreamHost(Jid jid, String address) {
this(jid, address, 0);
}
/**
* Default constructor.
*
* @param JID The JID of the stream host.
* @param address The internet address of the stream host.
* @param port port of the stream host.
*/
public StreamHost(final Jid JID, final String address, int port) {
this.JID = Objects.requireNonNull(JID, "StreamHost JID must not be null");
this.addy = StringUtils.requireNotNullOrEmpty(address, "StreamHost address must not be null");
this.port = port;
}
/**
* Returns the JID of the stream host.
*
* @return Returns the JID of the stream host.
*/
public Jid getJID() {
return JID;
}
/**
* Returns the internet address of the stream host.
*
* @return Returns the internet address of the stream host.
*/
public String getAddress() {
return addy;
}
/**
* Returns the port on which the potential stream host would accept the connection.
*
* @return Returns the port on which the potential stream host would accept the connection.
*/
public int getPort() {
return port;
}
@Override
public String getElementName() {
return ELEMENTNAME;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this);
xml.attribute("jid", getJID());
xml.attribute("host", getAddress());
if (getPort() != 0) {
xml.attribute("port", Integer.toString(getPort()));
} else {
xml.attribute("zeroconf", "_jabber.bytestreams");
}
xml.closeEmptyElement();
return xml;
}
}
/**
* After selected a SOCKS5 stream host and successfully connecting, the target of the file
* transfer returns a byte stream stanza with the stream host used extension.
*
* @author Alexander Wenckus
*/
public static class StreamHostUsed implements NamedElement {
public static String ELEMENTNAME = "streamhost-used";
private final Jid JID;
/**
* Default constructor.
*
* @param JID The JID of the selected stream host.
*/
public StreamHostUsed(final Jid JID) {
this.JID = JID;
}
/**
* Returns the JID of the selected stream host.
*
* @return Returns the JID of the selected stream host.
*/
public Jid getJID() {
return JID;
}
@Override
public String getElementName() {
return ELEMENTNAME;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this);
xml.attribute("jid", getJID());
xml.closeEmptyElement();
return xml;
}
}
/**
* The stanza sent by the stream initiator to the stream proxy to activate the connection.
*
* @author Alexander Wenckus
*/
public static class Activate implements NamedElement {
public static String ELEMENTNAME = "activate";
private final Jid target;
/**
* Default constructor specifying the target of the stream.
*
* @param target The target of the stream.
*/
public Activate(final Jid target) {
this.target = target;
}
/**
* Returns the target of the activation.
*
* @return Returns the target of the activation.
*/
public Jid getTarget() {
return target;
}
@Override
public String getElementName() {
return ELEMENTNAME;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this);
xml.rightAngleBracket();
xml.escape(getTarget());
xml.closeElement(this);
return xml;
}
}
/**
* The stream can be either a TCP stream or a UDP stream.
*
* @author Alexander Wenckus
*/
public enum Mode {
/**
* A TCP based stream.
*/
tcp,
/**
* A UDP based stream.
*/
udp;
public static Mode fromName(String name) {
Mode mode;
try {
mode = Mode.valueOf(name);
}
catch (Exception ex) {
mode = tcp;
}
return mode;
}
}
}