001/**
002 *
003 * Copyright the original author or authors
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.bytestreams.socks5.packet;
018
019import java.net.InetAddress;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.List;
023
024import javax.xml.namespace.QName;
025
026import org.jivesoftware.smack.packet.ExtensionElement;
027import org.jivesoftware.smack.packet.IQ;
028import org.jivesoftware.smack.util.InternetAddress;
029import org.jivesoftware.smack.util.Objects;
030import org.jivesoftware.smack.util.XmlStringBuilder;
031
032import org.jxmpp.jid.Jid;
033
034/**
035 * A stanza representing part of a SOCKS5 Bytestream negotiation.
036 *
037 * @author Alexander Wenckus
038 */
039public class Bytestream extends IQ {
040
041    public static final String ELEMENT = QUERY_ELEMENT;
042
043    /**
044     * The XMPP namespace of the SOCKS5 Bytestream.
045     */
046    public static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
047
048    private String sessionID;
049
050    private Mode mode = Mode.tcp;
051
052    private final List<StreamHost> streamHosts = new ArrayList<>();
053
054    private StreamHostUsed usedHost;
055
056    private Activate toActivate;
057
058    /**
059     * The default constructor.
060     */
061    public Bytestream() {
062        super(ELEMENT, NAMESPACE);
063    }
064
065    /**
066     * A constructor where the session ID can be specified.
067     *
068     * @param SID The session ID related to the negotiation.
069     * @see #setSessionID(String)
070     */
071    @SuppressWarnings("this-escape")
072    public Bytestream(final String SID) {
073        this();
074        setSessionID(SID);
075    }
076
077    /**
078     * Set the session ID related to the bytestream. The session ID is a unique identifier used to
079     * differentiate between stream negotiations.
080     *
081     * @param sessionID the unique session ID that identifies the transfer.
082     */
083    public void setSessionID(final String sessionID) {
084        this.sessionID = sessionID;
085    }
086
087    /**
088     * Returns the session ID related to the bytestream negotiation.
089     *
090     * @return Returns the session ID related to the bytestream negotiation.
091     * @see #setSessionID(String)
092     */
093    public String getSessionID() {
094        return sessionID;
095    }
096
097    /**
098     * Set the transport mode. This should be put in the initiation of the interaction.
099     *
100     * @param mode the transport mode, either UDP or TCP
101     * @see Mode
102     */
103    public void setMode(final Mode mode) {
104        this.mode = mode;
105    }
106
107    /**
108     * Returns the transport mode.
109     *
110     * @return Returns the transport mode.
111     * @see #setMode(Mode)
112     */
113    public Mode getMode() {
114        return mode;
115    }
116
117    /**
118     * Adds a potential stream host that the remote user can connect to to receive the file.
119     *
120     * @param JID The JID of the stream host.
121     * @param address The internet address of the stream host.
122     * @return The added stream host.
123     */
124    public StreamHost addStreamHost(final Jid JID, String address) {
125        return addStreamHost(JID, address, 0);
126    }
127
128    /**
129     * Adds a potential stream host that the remote user can connect to to receive the file.
130     *
131     * @param JID The JID of the stream host.
132     * @param address The internet address of the stream host.
133     * @param port The port on which the remote host is seeking connections.
134     * @return The added stream host.
135     */
136    public StreamHost addStreamHost(final Jid JID, String address, final int port) {
137        StreamHost host = new StreamHost(JID, address, port);
138        addStreamHost(host);
139
140        return host;
141    }
142
143    /**
144     * Adds a potential stream host that the remote user can transfer the file through.
145     *
146     * @param host The potential stream host.
147     */
148    public void addStreamHost(final StreamHost host) {
149        streamHosts.add(host);
150    }
151
152    /**
153     * Returns the list of stream hosts contained in the packet.
154     *
155     * @return Returns the list of stream hosts contained in the packet.
156     */
157    public List<StreamHost> getStreamHosts() {
158        return Collections.unmodifiableList(streamHosts);
159    }
160
161    /**
162     * Returns the stream host related to the given JID, or null if there is none.
163     *
164     * @param JID The JID of the desired stream host.
165     * @return Returns the stream host related to the given JID, or null if there is none.
166     */
167    public StreamHost getStreamHost(final Jid JID) {
168        if (JID == null) {
169            return null;
170        }
171        for (StreamHost host : streamHosts) {
172            if (host.getJID().equals(JID)) {
173                return host;
174            }
175        }
176
177        return null;
178    }
179
180    /**
181     * Returns the count of stream hosts contained in this packet.
182     *
183     * @return Returns the count of stream hosts contained in this packet.
184     */
185    public int countStreamHosts() {
186        return streamHosts.size();
187    }
188
189    /**
190     * Upon connecting to the stream host the target of the stream replies to the initiator with the
191     * JID of the SOCKS5 host that they used.
192     *
193     * @param JID The JID of the used host.
194     */
195    public void setUsedHost(final Jid JID) {
196        this.usedHost = new StreamHostUsed(JID);
197    }
198
199    /**
200     * Returns the SOCKS5 host connected to by the remote user.
201     *
202     * @return Returns the SOCKS5 host connected to by the remote user.
203     */
204    public StreamHostUsed getUsedHost() {
205        return usedHost;
206    }
207
208    /**
209     * Returns the activate element of the stanza sent to the proxy host to verify the identity of
210     * the initiator and match them to the appropriate stream.
211     *
212     * @return Returns the activate element of the stanza sent to the proxy host to verify the
213     *         identity of the initiator and match them to the appropriate stream.
214     */
215    public Activate getToActivate() {
216        return toActivate;
217    }
218
219    /**
220     * Upon the response from the target of the used host the activate stanza is sent to the SOCKS5
221     * proxy. The proxy will activate the stream or return an error after verifying the identity of
222     * the initiator, using the activate packet.
223     *
224     * @param targetID The JID of the target of the file transfer.
225     */
226    public void setToActivate(final Jid targetID) {
227        this.toActivate = new Activate(targetID);
228    }
229
230    @Override
231    protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
232        switch (getType()) {
233        case set:
234            xml.optAttribute("sid", getSessionID());
235            xml.optAttribute("mode", getMode());
236            xml.rightAngleBracket();
237            if (getToActivate() == null) {
238                for (StreamHost streamHost : getStreamHosts()) {
239                    xml.append(streamHost.toXML());
240                }
241            }
242            else {
243                xml.append(getToActivate().toXML());
244            }
245            break;
246        case result:
247            xml.rightAngleBracket();
248            xml.optAppend(getUsedHost());
249            // TODO Bytestream can include either used host *or* streamHosts. Never both. This should be ensured by the
250            // constructions mechanisms of Bytestream
251            // A result from the server can also contain stream hosts
252            for (StreamHost host : streamHosts) {
253                xml.append(host.toXML());
254            }
255            break;
256        case get:
257            xml.setEmptyElement();
258            break;
259        default:
260            throw new IllegalStateException();
261        }
262
263        return xml;
264    }
265
266    private abstract static class BytestreamExtensionElement implements ExtensionElement {
267        @Override
268        public final String getNamespace() {
269            return NAMESPACE;
270        }
271    }
272
273    /**
274     * Stanza extension that represents a potential SOCKS5 proxy for the file transfer. Stream hosts
275     * are forwarded to the target of the file transfer who then chooses and connects to one.
276     *
277     * @author Alexander Wenckus
278     */
279    public static class StreamHost extends BytestreamExtensionElement {
280
281        public static final String ELEMENT = "streamhost";
282        public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
283
284        private final Jid jid;
285
286        private final InternetAddress address;
287
288        private final int port;
289
290        public StreamHost(Jid jid, String address) {
291            this(jid, address, 0);
292        }
293
294        /**
295         * Default constructor.
296         *
297         * @param jid The JID of the stream host.
298         * @param address The internet address of the stream host.
299         * @param port port of the stream host.
300         */
301        public StreamHost(final Jid jid, final String address, int port) {
302            this(jid, InternetAddress.fromIgnoringZoneId(address), port);
303        }
304
305        public StreamHost(Jid jid, InetAddress address, int port) {
306            this(jid, InternetAddress.from(address), port);
307        }
308
309        /**
310         * Stream Host constructor.
311         *
312         * @param jid The JID of the stream host.
313         * @param address The internet address of the stream host.
314         * @param port port of the stream host.
315         */
316        public StreamHost(Jid jid, InternetAddress address, int port) {
317            this.jid = Objects.requireNonNull(jid, "StreamHost JID must not be null");
318            this.address = Objects.requireNonNull(address);
319            this.port = port;
320        }
321
322        /**
323         * Returns the JID of the stream host.
324         *
325         * @return Returns the JID of the stream host.
326         */
327        public Jid getJID() {
328            return jid;
329        }
330
331        /**
332         * Returns the internet address of the stream host.
333         *
334         * @return Returns the internet address of the stream host.
335         */
336        public InternetAddress getAddress() {
337            return address;
338        }
339
340        /**
341         * Returns the port on which the potential stream host would accept the connection.
342         *
343         * @return Returns the port on which the potential stream host would accept the connection.
344         */
345        public int getPort() {
346            return port;
347        }
348
349        @Override
350        public String getElementName() {
351            return QNAME.getLocalPart();
352        }
353
354        @Override
355        public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
356            XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace);
357            xml.attribute("jid", getJID());
358            xml.attribute("host", address);
359            if (getPort() != 0) {
360                xml.attribute("port", Integer.toString(getPort()));
361            } else {
362                xml.attribute("zeroconf", "_jabber.bytestreams");
363            }
364            xml.closeEmptyElement();
365            return xml;
366        }
367
368        @Override
369        public String toString() {
370            return "SOCKS5 Stream Host: " + jid + "[" + address + ":" + port + "]";
371        }
372    }
373
374    /**
375     * After selected a SOCKS5 stream host and successfully connecting, the target of the file
376     * transfer returns a byte stream stanza with the stream host used extension.
377     *
378     * @author Alexander Wenckus
379     */
380    public static class StreamHostUsed extends BytestreamExtensionElement {
381
382        public static final String ELEMENT = "streamhost-used";
383        public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
384
385        private final Jid jid;
386
387        /**
388         * Default constructor.
389         *
390         * @param jid The JID of the selected stream host.
391         */
392        public StreamHostUsed(final Jid jid) {
393            this.jid = jid;
394        }
395
396        /**
397         * Returns the JID of the selected stream host.
398         *
399         * @return Returns the JID of the selected stream host.
400         */
401        public Jid getJID() {
402            return jid;
403        }
404
405        @Override
406        public String getElementName() {
407            return QNAME.getLocalPart();
408        }
409
410        @Override
411        public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
412            XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace);
413            xml.attribute("jid", getJID());
414            xml.closeEmptyElement();
415            return xml;
416        }
417    }
418
419    /**
420     * The stanza sent by the stream initiator to the stream proxy to activate the connection.
421     *
422     * @author Alexander Wenckus
423     */
424    public static class Activate extends BytestreamExtensionElement {
425
426        public static final String ELEMENT = "activate";
427        public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
428
429        private final Jid target;
430
431        /**
432         * Default constructor specifying the target of the stream.
433         *
434         * @param target The target of the stream.
435         */
436        public Activate(final Jid target) {
437            this.target = target;
438        }
439
440        /**
441         * Returns the target of the activation.
442         *
443         * @return Returns the target of the activation.
444         */
445        public Jid getTarget() {
446            return target;
447        }
448
449        @Override
450        public String getElementName() {
451            return QNAME.getLocalPart();
452        }
453
454        @Override
455        public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
456            XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace);
457            xml.rightAngleBracket();
458            xml.escape(getTarget());
459            xml.closeElement(this);
460            return xml;
461        }
462
463    }
464
465    /**
466     * The stream can be either a TCP stream or a UDP stream.
467     *
468     * @author Alexander Wenckus
469     */
470    public enum Mode {
471
472        /**
473         * A TCP based stream.
474         */
475        tcp,
476
477        /**
478         * A UDP based stream.
479         */
480        udp;
481
482        public static Mode fromName(String name) {
483            Mode mode;
484            try {
485                mode = Mode.valueOf(name);
486            }
487            catch (Exception ex) {
488                mode = tcp;
489            }
490
491            return mode;
492        }
493    }
494}