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