mirror of
https://codeberg.org/Mercury-IM/Smack
synced 2025-12-07 21:51:07 +01:00
moved ibb and socks5bytestream packages in the bytestream package
git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/improve_bytestreams@11819 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
parent
8cb01900c9
commit
3e16b35162
58 changed files with 192 additions and 142 deletions
|
|
@ -13,10 +13,10 @@
|
|||
*/
|
||||
package org.jivesoftware.smackx.bytestreams;
|
||||
|
||||
import org.jivesoftware.smackx.ibb.InBandBytestreamListener;
|
||||
import org.jivesoftware.smackx.ibb.InBandBytestreamManager;
|
||||
import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamListener;
|
||||
import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamManager;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamListener;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamListener;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
|
||||
|
||||
/**
|
||||
* BytestreamListener are notified if a remote user wants to initiate a bytestream. Implement this
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ package org.jivesoftware.smackx.bytestreams;
|
|||
import java.io.IOException;
|
||||
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smackx.ibb.InBandBytestreamManager;
|
||||
import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamManager;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
|
||||
|
||||
/**
|
||||
* BytestreamManager provides a generic interface for bytestream managers.
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
package org.jivesoftware.smackx.bytestreams;
|
||||
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smackx.ibb.InBandBytestreamRequest;
|
||||
import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamRequest;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamRequest;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest;
|
||||
|
||||
/**
|
||||
* BytestreamRequest provides an interface to handle incoming bytestream requests.
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.jivesoftware.smackx.ibb.InBandBytestreamSession;
|
||||
import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamSession;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession;
|
||||
|
||||
/**
|
||||
* BytestreamSession provides an interface for established bytestream sessions.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb;
|
||||
|
||||
import org.jivesoftware.smack.PacketListener;
|
||||
import org.jivesoftware.smack.filter.AndFilter;
|
||||
import org.jivesoftware.smack.filter.IQTypeFilter;
|
||||
import org.jivesoftware.smack.filter.PacketFilter;
|
||||
import org.jivesoftware.smack.filter.PacketTypeFilter;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.Packet;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
|
||||
|
||||
/**
|
||||
* CloseListener handles all In-Band Bytestream close requests.
|
||||
* <p>
|
||||
* If a close request is received it looks if a stored In-Band Bytestream
|
||||
* session exists and closes it. If no session with the given session ID exists
|
||||
* an <item-not-found/> error is returned to the sender.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
class CloseListener implements PacketListener {
|
||||
|
||||
/* manager containing the listeners and the XMPP connection */
|
||||
private final InBandBytestreamManager manager;
|
||||
|
||||
/* packet filter for all In-Band Bytestream close requests */
|
||||
private final PacketFilter closeFilter = new AndFilter(new PacketTypeFilter(
|
||||
Close.class), new IQTypeFilter(IQ.Type.SET));
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param manager the In-Band Bytestream manager
|
||||
*/
|
||||
protected CloseListener(InBandBytestreamManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public void processPacket(Packet packet) {
|
||||
Close closeRequest = (Close) packet;
|
||||
InBandBytestreamSession ibbSession = this.manager.getSessions().get(
|
||||
closeRequest.getSessionID());
|
||||
if (ibbSession == null) {
|
||||
this.manager.replyItemNotFoundPacket(closeRequest);
|
||||
}
|
||||
else {
|
||||
ibbSession.closeByPeer(closeRequest);
|
||||
this.manager.getSessions().remove(closeRequest.getSessionID());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packet filter for In-Band Bytestream close requests.
|
||||
*
|
||||
* @return the packet filter for In-Band Bytestream close requests
|
||||
*/
|
||||
protected PacketFilter getFilter() {
|
||||
return this.closeFilter;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb;
|
||||
|
||||
import org.jivesoftware.smack.PacketListener;
|
||||
import org.jivesoftware.smack.filter.AndFilter;
|
||||
import org.jivesoftware.smack.filter.PacketFilter;
|
||||
import org.jivesoftware.smack.filter.PacketTypeFilter;
|
||||
import org.jivesoftware.smack.packet.Packet;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
|
||||
|
||||
/**
|
||||
* DataListener handles all In-Band Bytestream IQ stanzas containing a data
|
||||
* packet extension that don't belong to an existing session.
|
||||
* <p>
|
||||
* If a data packet is received it looks if a stored In-Band Bytestream session
|
||||
* exists. If no session with the given session ID exists an
|
||||
* <item-not-found/> error is returned to the sender.
|
||||
* <p>
|
||||
* Data packets belonging to a running In-Band Bytestream session are processed
|
||||
* by more specific listeners registered when an {@link InBandBytestreamSession}
|
||||
* is created.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
class DataListener implements PacketListener {
|
||||
|
||||
/* manager containing the listeners and the XMPP connection */
|
||||
private final InBandBytestreamManager manager;
|
||||
|
||||
/* packet filter for all In-Band Bytestream data packets */
|
||||
private final PacketFilter dataFilter = new AndFilter(
|
||||
new PacketTypeFilter(Data.class));
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param manager the In-Band Bytestream manager
|
||||
*/
|
||||
public DataListener(InBandBytestreamManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public void processPacket(Packet packet) {
|
||||
Data data = (Data) packet;
|
||||
InBandBytestreamSession ibbSession = this.manager.getSessions().get(
|
||||
data.getDataPacketExtension().getSessionID());
|
||||
if (ibbSession == null) {
|
||||
this.manager.replyItemNotFoundPacket(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packet filter for In-Band Bytestream data packets.
|
||||
*
|
||||
* @return the packet filter for In-Band Bytestream data packets
|
||||
*/
|
||||
protected PacketFilter getFilter() {
|
||||
return this.dataFilter;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb;
|
||||
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
|
||||
|
||||
/**
|
||||
* InBandBytestreamListener are informed if a remote user wants to initiate an In-Band Bytestream.
|
||||
* Implement this interface to handle incoming In-Band Bytestream requests.
|
||||
* <p>
|
||||
* There are two ways to add this listener. See
|
||||
* {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
|
||||
* {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for
|
||||
* further details.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public abstract class InBandBytestreamListener implements BytestreamListener {
|
||||
|
||||
|
||||
|
||||
public void incomingBytestreamRequest(BytestreamRequest request) {
|
||||
incomingBytestreamRequest((InBandBytestreamRequest) request);
|
||||
}
|
||||
|
||||
/**
|
||||
* This listener is notified if an In-Band Bytestream request from another user has been
|
||||
* received.
|
||||
*
|
||||
* @param request the incoming In-Band Bytestream request
|
||||
*/
|
||||
public abstract void incomingBytestreamRequest(InBandBytestreamRequest request);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,546 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.jivesoftware.smack.AbstractConnectionListener;
|
||||
import org.jivesoftware.smack.Connection;
|
||||
import org.jivesoftware.smack.ConnectionCreationListener;
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.XMPPError;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
|
||||
import org.jivesoftware.smackx.filetransfer.FileTransferManager;
|
||||
import org.jivesoftware.smackx.packet.SyncPacketSend;
|
||||
|
||||
/**
|
||||
* The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a
|
||||
* href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.
|
||||
* <p>
|
||||
* The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which
|
||||
* they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism
|
||||
* in case the Socks5 bytestream method of transferring data is not available.
|
||||
* <p>
|
||||
* There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to
|
||||
* send data packets or message stanzas. If IQ stanzas are used every data packet is acknowledged by
|
||||
* the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message
|
||||
* stanzas are not acknowledged because most XMPP server implementation don't support stanza
|
||||
* flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message
|
||||
* Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
|
||||
* <p>
|
||||
* To establish an In-Band Bytestream invoke the {@link #establishSession(String)} method. This will
|
||||
* negotiate an in-band bytestream with the given target JID and return a session.
|
||||
* <p>
|
||||
* If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
|
||||
* transfer) invoke {@link #establishSession(String, String)}.
|
||||
* <p>
|
||||
* To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the
|
||||
* manager. There are two ways to add this listener. If you want to be informed about incoming
|
||||
* In-Band Bytestreams from a specific user add the listener by invoking
|
||||
* {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
|
||||
* respond to all In-Band Bytestream requests invoke
|
||||
* {@link #addIncomingBytestreamListener(BytestreamListener)}.
|
||||
* <p>
|
||||
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
|
||||
* In-Band bytestream requests sent in the context of <a
|
||||
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||
* {@link FileTransferManager})
|
||||
* <p>
|
||||
* If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests
|
||||
* will be rejected by returning a <not-acceptable/> error to the initiator.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class InBandBytestreamManager implements BytestreamManager {
|
||||
|
||||
/**
|
||||
* Stanzas that can be used to encapsulate In-Band Bytestream data packets.
|
||||
*/
|
||||
public enum StanzaType {
|
||||
|
||||
/**
|
||||
* IQ stanza.
|
||||
*/
|
||||
IQ,
|
||||
|
||||
/**
|
||||
* Message stanza.
|
||||
*/
|
||||
MESSAGE
|
||||
}
|
||||
|
||||
/*
|
||||
* create a new InBandBytestreamManager and register its shutdown listener on every established
|
||||
* connection
|
||||
*/
|
||||
static {
|
||||
Connection.addConnectionCreationListener(new ConnectionCreationListener() {
|
||||
public void connectionCreated(Connection connection) {
|
||||
final InBandBytestreamManager manager;
|
||||
manager = InBandBytestreamManager.getByteStreamManager(connection);
|
||||
|
||||
// register shutdown listener
|
||||
connection.addConnectionListener(new AbstractConnectionListener() {
|
||||
|
||||
public void connectionClosed() {
|
||||
manager.disableService();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The XMPP namespace of the In-Band Bytestream
|
||||
*/
|
||||
public static final String NAMESPACE = "http://jabber.org/protocol/ibb";
|
||||
|
||||
/**
|
||||
* Maximum block size that is allowed for In-Band Bytestreams
|
||||
*/
|
||||
public static final int MAXIMUM_BLOCK_SIZE = 65535;
|
||||
|
||||
/* prefix used to generate session IDs */
|
||||
private static final String SESSION_ID_PREFIX = "jibb_";
|
||||
|
||||
/* random generator to create session IDs */
|
||||
private final static Random randomGenerator = new Random();
|
||||
|
||||
/* stores one InBandBytestreamManager for each XMPP connection */
|
||||
private final static Map<Connection, InBandBytestreamManager> managers = new HashMap<Connection, InBandBytestreamManager>();
|
||||
|
||||
/* XMPP connection */
|
||||
private final Connection connection;
|
||||
|
||||
/*
|
||||
* assigns a user to a listener that is informed if an In-Band Bytestream request for this user
|
||||
* is received
|
||||
*/
|
||||
private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
|
||||
|
||||
/*
|
||||
* list of listeners that respond to all In-Band Bytestream requests if there are no user
|
||||
* specific listeners for that request
|
||||
*/
|
||||
private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
|
||||
|
||||
/* listener that handles all incoming In-Band Bytestream requests */
|
||||
private final InitiationListener initiationListener;
|
||||
|
||||
/* listener that handles all incoming In-Band Bytestream IQ data packets */
|
||||
private final DataListener dataListener;
|
||||
|
||||
/* listener that handles all incoming In-Band Bytestream close requests */
|
||||
private final CloseListener closeListener;
|
||||
|
||||
/* assigns a session ID to the In-Band Bytestream session */
|
||||
private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();
|
||||
|
||||
/* block size used for new In-Band Bytestreams */
|
||||
private int defaultBlockSize = 4096;
|
||||
|
||||
/* maximum block size allowed for this connection */
|
||||
private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;
|
||||
|
||||
/* the stanza used to send data packets */
|
||||
private StanzaType stanza = StanzaType.IQ;
|
||||
|
||||
/*
|
||||
* list containing session IDs of In-Band Bytestream open packets that should be ignored by the
|
||||
* InitiationListener
|
||||
*/
|
||||
private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
|
||||
|
||||
/**
|
||||
* Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
|
||||
* {@link Connection}.
|
||||
*
|
||||
* @param connection the XMPP connection
|
||||
* @return the InBandBytestreamManager for the given XMPP connection
|
||||
*/
|
||||
public static synchronized InBandBytestreamManager getByteStreamManager(Connection connection) {
|
||||
if (connection == null)
|
||||
return null;
|
||||
InBandBytestreamManager manager = managers.get(connection);
|
||||
if (manager == null) {
|
||||
manager = new InBandBytestreamManager(connection);
|
||||
managers.put(connection, manager);
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param connection the XMPP connection
|
||||
*/
|
||||
private InBandBytestreamManager(Connection connection) {
|
||||
this.connection = connection;
|
||||
|
||||
// register bytestream open packet listener
|
||||
this.initiationListener = new InitiationListener(this);
|
||||
this.connection.addPacketListener(this.initiationListener,
|
||||
this.initiationListener.getFilter());
|
||||
|
||||
// register bytestream data packet listener
|
||||
this.dataListener = new DataListener(this);
|
||||
this.connection.addPacketListener(this.dataListener, this.dataListener.getFilter());
|
||||
|
||||
// register bytestream close packet listener
|
||||
this.closeListener = new CloseListener(this);
|
||||
this.connection.addPacketListener(this.closeListener, this.closeListener.getFilter());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
|
||||
* unless there is a user specific InBandBytestreamListener registered.
|
||||
* <p>
|
||||
* If no listeners are registered all In-Band Bytestream request are rejected with a
|
||||
* <not-acceptable/> error.
|
||||
* <p>
|
||||
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
|
||||
* Socks5 bytestream requests sent in the context of <a
|
||||
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||
* {@link FileTransferManager})
|
||||
*
|
||||
* @param listener the listener to register
|
||||
*/
|
||||
public void addIncomingBytestreamListener(BytestreamListener listener) {
|
||||
this.allRequestListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given listener from the list of listeners for all incoming In-Band Bytestream
|
||||
* requests.
|
||||
*
|
||||
* @param listener the listener to remove
|
||||
*/
|
||||
public void removeIncomingBytestreamListener(BytestreamListener listener) {
|
||||
this.allRequestListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
|
||||
* from the given user.
|
||||
* <p>
|
||||
* Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
|
||||
* user.
|
||||
* <p>
|
||||
* If no listeners are registered all In-Band Bytestream request are rejected with a
|
||||
* <not-acceptable/> error.
|
||||
* <p>
|
||||
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
|
||||
* Socks5 bytestream requests sent in the context of <a
|
||||
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||
* {@link FileTransferManager})
|
||||
*
|
||||
* @param listener the listener to register
|
||||
* @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream
|
||||
*/
|
||||
public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
|
||||
this.userListeners.put(initiatorJID, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener for the given user.
|
||||
*
|
||||
* @param initiatorJID the JID of the user the listener should be removed
|
||||
*/
|
||||
public void removeIncomingBytestreamListener(String initiatorJID) {
|
||||
this.userListeners.remove(initiatorJID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to ignore the next incoming In-Band Bytestream request containing the given
|
||||
* session ID. No listeners will be notified for this request and and no error will be returned
|
||||
* to the initiator.
|
||||
* <p>
|
||||
* This method should be used if you are awaiting an In-Band Bytestream request as a reply to
|
||||
* another packet (e.g. file transfer).
|
||||
*
|
||||
* @param sessionID to be ignored
|
||||
*/
|
||||
public void ignoreBytestreamRequestOnce(String sessionID) {
|
||||
this.ignoredBytestreamRequests.add(sessionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default block size that is used for all outgoing in-band bytestreams for this
|
||||
* connection.
|
||||
* <p>
|
||||
* The recommended default block size is 4096 bytes. See <a
|
||||
* href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.
|
||||
*
|
||||
* @return the default block size
|
||||
*/
|
||||
public int getDefaultBlockSize() {
|
||||
return defaultBlockSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default block size that is used for all outgoing in-band bytestreams for this
|
||||
* connection.
|
||||
* <p>
|
||||
* The default block size must be between 1 and 65535 bytes. The recommended default block size
|
||||
* is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
|
||||
* Section 5.
|
||||
*
|
||||
* @param defaultBlockSize the default block size to set
|
||||
*/
|
||||
public void setDefaultBlockSize(int defaultBlockSize) {
|
||||
if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
|
||||
throw new IllegalArgumentException("Default block size must be between 1 and "
|
||||
+ MAXIMUM_BLOCK_SIZE);
|
||||
}
|
||||
this.defaultBlockSize = defaultBlockSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
|
||||
* <p>
|
||||
* Incoming In-Band Bytestream open request will be rejected with an
|
||||
* <resource-constraint/> error if the block size is greater then the maximum allowed
|
||||
* block size.
|
||||
* <p>
|
||||
* The default maximum block size is 65535 bytes.
|
||||
*
|
||||
* @return the maximum block size
|
||||
*/
|
||||
public int getMaximumBlockSize() {
|
||||
return maximumBlockSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
|
||||
* <p>
|
||||
* The maximum block size must be between 1 and 65535 bytes.
|
||||
* <p>
|
||||
* Incoming In-Band Bytestream open request will be rejected with an
|
||||
* <resource-constraint/> error if the block size is greater then the maximum allowed
|
||||
* block size.
|
||||
*
|
||||
* @param maximumBlockSize the maximum block size to set
|
||||
*/
|
||||
public void setMaximumBlockSize(int maximumBlockSize) {
|
||||
if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
|
||||
throw new IllegalArgumentException("Maximum block size must be between 1 and "
|
||||
+ MAXIMUM_BLOCK_SIZE);
|
||||
}
|
||||
this.maximumBlockSize = maximumBlockSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stanza used to send data packets.
|
||||
* <p>
|
||||
* Default is {@link StanzaType#IQ}. See <a
|
||||
* href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
|
||||
*
|
||||
* @return the stanza used to send data packets
|
||||
*/
|
||||
public StanzaType getStanza() {
|
||||
return stanza;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stanza used to send data packets.
|
||||
* <p>
|
||||
* The use of {@link StanzaType#IQ} is recommended. See <a
|
||||
* href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
|
||||
*
|
||||
* @param stanza the stanza to set
|
||||
*/
|
||||
public void setStanza(StanzaType stanza) {
|
||||
this.stanza = stanza;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes an In-Band Bytestream with the given user and returns the session to send/receive
|
||||
* data to/from the user.
|
||||
* <p>
|
||||
* Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
|
||||
* Bytestream requests since this method doesn't provide a way to tell the user something about
|
||||
* the data to be sent.
|
||||
* <p>
|
||||
* To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file
|
||||
* transfer) use {@link #establishSession(String, String)}.
|
||||
*
|
||||
* @param targetJID the JID of the user an In-Band Bytestream should be established
|
||||
* @return the session to send/receive data to/from the user
|
||||
* @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
|
||||
* user prefers smaller block sizes
|
||||
*/
|
||||
public InBandBytestreamSession establishSession(String targetJID) throws XMPPException {
|
||||
String sessionID = getNextSessionID();
|
||||
return establishSession(targetJID, sessionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes an In-Band Bytestream with the given user using the given session ID and returns
|
||||
* the session to send/receive data to/from the user.
|
||||
*
|
||||
* @param targetJID the JID of the user an In-Band Bytestream should be established
|
||||
* @param sessionID the session ID for the In-Band Bytestream request
|
||||
* @return the session to send/receive data to/from the user
|
||||
* @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
|
||||
* user prefers smaller block sizes
|
||||
*/
|
||||
public InBandBytestreamSession establishSession(String targetJID, String sessionID)
|
||||
throws XMPPException {
|
||||
Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
|
||||
byteStreamRequest.setTo(targetJID);
|
||||
|
||||
// sending packet will throw exception on timeout or error reply
|
||||
SyncPacketSend.getReply(this.connection, byteStreamRequest);
|
||||
|
||||
InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
|
||||
this.connection, byteStreamRequest, targetJID);
|
||||
this.sessions.put(sessionID, inBandBytestreamSession);
|
||||
|
||||
return inBandBytestreamSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
|
||||
* not accepted.
|
||||
*
|
||||
* @param request IQ packet that should be answered with a not-acceptable error
|
||||
*/
|
||||
protected void replyRejectPacket(IQ request) {
|
||||
XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
|
||||
IQ error = IQ.createErrorResponse(request, xmppError);
|
||||
this.connection.sendPacket(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
|
||||
* request is rejected because its block size is greater than the maximum allowed block size.
|
||||
*
|
||||
* @param request IQ packet that should be answered with a resource-constraint error
|
||||
*/
|
||||
protected void replyResourceConstraintPacket(IQ request) {
|
||||
XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint);
|
||||
IQ error = IQ.createErrorResponse(request, xmppError);
|
||||
this.connection.sendPacket(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
|
||||
* session could not be found.
|
||||
*
|
||||
* @param request IQ packet that should be answered with a item-not-found error
|
||||
*/
|
||||
protected void replyItemNotFoundPacket(IQ request) {
|
||||
XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found);
|
||||
IQ error = IQ.createErrorResponse(request, xmppError);
|
||||
this.connection.sendPacket(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new unique session ID.
|
||||
*
|
||||
* @return a new unique session ID
|
||||
*/
|
||||
private String getNextSessionID() {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
buffer.append(SESSION_ID_PREFIX);
|
||||
buffer.append(Math.abs(randomGenerator.nextLong()));
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the XMPP connection.
|
||||
*
|
||||
* @return the XMPP connection
|
||||
*/
|
||||
protected Connection getConnection() {
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
|
||||
* request from the given initiator JID is received.
|
||||
*
|
||||
* @param initiator the initiator's JID
|
||||
* @return the listener
|
||||
*/
|
||||
protected BytestreamListener getUserListener(String initiator) {
|
||||
return this.userListeners.get(initiator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of {@link InBandBytestreamListener} that are informed if there are no
|
||||
* listeners for a specific initiator.
|
||||
*
|
||||
* @return list of listeners
|
||||
*/
|
||||
protected List<BytestreamListener> getAllRequestListeners() {
|
||||
return this.allRequestListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sessions map.
|
||||
*
|
||||
* @return the sessions map
|
||||
*/
|
||||
protected Map<String, InBandBytestreamSession> getSessions() {
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of session IDs that should be ignored by the InitialtionListener
|
||||
*
|
||||
* @return list of session IDs
|
||||
*/
|
||||
protected List<String> getIgnoredBytestreamRequests() {
|
||||
return ignoredBytestreamRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the InBandBytestreamManager by removing its packet listeners and resetting its
|
||||
* internal status.
|
||||
*/
|
||||
private void disableService() {
|
||||
|
||||
// remove manager from static managers map
|
||||
managers.remove(connection);
|
||||
|
||||
// remove all listeners registered by this manager
|
||||
this.connection.removePacketListener(this.initiationListener);
|
||||
this.connection.removePacketListener(this.dataListener);
|
||||
this.connection.removePacketListener(this.closeListener);
|
||||
|
||||
// shutdown threads
|
||||
this.initiationListener.shutdown();
|
||||
|
||||
// reset internal status
|
||||
this.userListeners.clear();
|
||||
this.allRequestListeners.clear();
|
||||
this.sessions.clear();
|
||||
this.ignoredBytestreamRequests.clear();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb;
|
||||
|
||||
import org.jivesoftware.smack.Connection;
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
|
||||
|
||||
/**
|
||||
* InBandBytestreamRequest class handles incoming In-Band Bytestream requests.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class InBandBytestreamRequest implements BytestreamRequest {
|
||||
|
||||
/* the bytestream initialization request */
|
||||
private final Open byteStreamRequest;
|
||||
|
||||
/*
|
||||
* In-Band Bytestream manager containing the XMPP connection and helper
|
||||
* methods
|
||||
*/
|
||||
private final InBandBytestreamManager manager;
|
||||
|
||||
protected InBandBytestreamRequest(InBandBytestreamManager manager,
|
||||
Open byteStreamRequest) {
|
||||
this.manager = manager;
|
||||
this.byteStreamRequest = byteStreamRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sender of the In-Band Bytestream open request.
|
||||
*
|
||||
* @return the sender of the In-Band Bytestream open request
|
||||
*/
|
||||
public String getFrom() {
|
||||
return this.byteStreamRequest.getFrom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session ID of the In-Band Bytestream open request.
|
||||
*
|
||||
* @return the session ID of the In-Band Bytestream open request
|
||||
*/
|
||||
public String getSessionID() {
|
||||
return this.byteStreamRequest.getSessionID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts the In-Band Bytestream open request and returns the session to
|
||||
* send/receive data.
|
||||
*
|
||||
* @return the session to send/receive data
|
||||
* @throws XMPPException if stream is invalid.
|
||||
*/
|
||||
public InBandBytestreamSession accept() throws XMPPException {
|
||||
Connection connection = this.manager.getConnection();
|
||||
|
||||
// create In-Band Bytestream session and store it
|
||||
InBandBytestreamSession ibbSession = new InBandBytestreamSession(connection,
|
||||
this.byteStreamRequest, this.byteStreamRequest.getFrom());
|
||||
this.manager.getSessions().put(this.byteStreamRequest.getSessionID(), ibbSession);
|
||||
|
||||
// acknowledge request
|
||||
IQ resultIQ = IQ.createResultIQ(this.byteStreamRequest);
|
||||
connection.sendPacket(resultIQ);
|
||||
|
||||
return ibbSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects the In-Band Bytestream request by sending a reject error to the
|
||||
* initiator.
|
||||
*/
|
||||
public void reject() {
|
||||
this.manager.replyRejectPacket(this.byteStreamRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,795 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.jivesoftware.smack.Connection;
|
||||
import org.jivesoftware.smack.PacketListener;
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smack.filter.AndFilter;
|
||||
import org.jivesoftware.smack.filter.PacketFilter;
|
||||
import org.jivesoftware.smack.filter.PacketTypeFilter;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.Message;
|
||||
import org.jivesoftware.smack.packet.Packet;
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smack.packet.XMPPError;
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamSession;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
|
||||
import org.jivesoftware.smackx.packet.SyncPacketSend;
|
||||
|
||||
/**
|
||||
* InBandBytestreamSession class represents an In-Band Bytestream session.
|
||||
* <p>
|
||||
* In-band bytestreams are bidirectional and this session encapsulates the streams for both
|
||||
* directions.
|
||||
* <p>
|
||||
* Note that closing the In-Band Bytestream session will close both streams. If both streams are
|
||||
* closed individually the session will be closed automatically once the second stream is closed.
|
||||
* Use the {@link #setCloseBothStreamsEnabled(boolean)} method if both streams should be closed
|
||||
* automatically if one of them is closed.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class InBandBytestreamSession implements BytestreamSession {
|
||||
|
||||
/* XMPP connection */
|
||||
private final Connection connection;
|
||||
|
||||
/* the In-Band Bytestream open request for this session */
|
||||
private final Open byteStreamRequest;
|
||||
|
||||
/*
|
||||
* the input stream for this session (either IQIBBInputStream or MessageIBBInputStream)
|
||||
*/
|
||||
private IBBInputStream inputStream;
|
||||
|
||||
/*
|
||||
* the output stream for this session (either IQIBBOutputStream or MessageIBBOutputStream)
|
||||
*/
|
||||
private IBBOutputStream outputStream;
|
||||
|
||||
/* JID of the remote peer */
|
||||
private String remoteJID;
|
||||
|
||||
/* flag to close both streams if one of them is closed */
|
||||
private boolean closeBothStreamsEnabled = false;
|
||||
|
||||
/* flag to indicate if session is closed */
|
||||
private boolean isClosed = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param connection the XMPP connection
|
||||
* @param byteStreamRequest the In-Band Bytestream open request for this session
|
||||
* @param remoteJID JID of the remote peer
|
||||
*/
|
||||
protected InBandBytestreamSession(Connection connection, Open byteStreamRequest,
|
||||
String remoteJID) {
|
||||
this.connection = connection;
|
||||
this.byteStreamRequest = byteStreamRequest;
|
||||
this.remoteJID = remoteJID;
|
||||
|
||||
// initialize streams dependent to the uses stanza type
|
||||
switch (byteStreamRequest.getStanza()) {
|
||||
case IQ:
|
||||
this.inputStream = new IQIBBInputStream();
|
||||
this.outputStream = new IQIBBOutputStream();
|
||||
break;
|
||||
case MESSAGE:
|
||||
this.inputStream = new MessageIBBInputStream();
|
||||
this.outputStream = new MessageIBBOutputStream();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public InputStream getInputStream() {
|
||||
return this.inputStream;
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() {
|
||||
return this.outputStream;
|
||||
}
|
||||
|
||||
public int getReadTimeout() {
|
||||
return this.inputStream.readTimeout;
|
||||
}
|
||||
|
||||
public void setReadTimeout(int timeout) {
|
||||
if (timeout < 0) {
|
||||
throw new IllegalArgumentException("Timeout must be >= 0");
|
||||
}
|
||||
this.inputStream.readTimeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether both streams should be closed automatically if one of the streams is closed.
|
||||
* Default is <code>false</code>.
|
||||
*
|
||||
* @return <code>true</code> if both streams will be closed if one of the streams is closed,
|
||||
* <code>false</code> if both streams can be closed independently.
|
||||
*/
|
||||
public boolean isCloseBothStreamsEnabled() {
|
||||
return closeBothStreamsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether both streams should be closed automatically if one of the streams is closed.
|
||||
* Default is <code>false</code>.
|
||||
*
|
||||
* @param closeBothStreamsEnabled <code>true</code> if both streams should be closed if one of
|
||||
* the streams is closed, <code>false</code> if both streams should be closed
|
||||
* independently
|
||||
*/
|
||||
public void setCloseBothStreamsEnabled(boolean closeBothStreamsEnabled) {
|
||||
this.closeBothStreamsEnabled = closeBothStreamsEnabled;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
closeByLocal(true); // close input stream
|
||||
closeByLocal(false); // close output stream
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is invoked if a request to close the In-Band Bytestream has been received.
|
||||
*
|
||||
* @param closeRequest the close request from the remote peer
|
||||
*/
|
||||
protected void closeByPeer(Close closeRequest) {
|
||||
|
||||
/*
|
||||
* close streams without flushing them, because stream is already considered closed on the
|
||||
* remote peers side
|
||||
*/
|
||||
this.inputStream.closeInternal();
|
||||
this.inputStream.cleanup();
|
||||
this.outputStream.closeInternal(false);
|
||||
|
||||
// acknowledge close request
|
||||
IQ confirmClose = IQ.createResultIQ(closeRequest);
|
||||
this.connection.sendPacket(confirmClose);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is invoked if one of the streams has been closed locally, if an error occurred
|
||||
* locally or if the whole session should be closed.
|
||||
*
|
||||
* @throws IOException if an error occurs while sending the close request
|
||||
*/
|
||||
protected synchronized void closeByLocal(boolean in) throws IOException {
|
||||
if (this.isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.closeBothStreamsEnabled) {
|
||||
this.inputStream.closeInternal();
|
||||
this.outputStream.closeInternal(true);
|
||||
}
|
||||
else {
|
||||
if (in) {
|
||||
this.inputStream.closeInternal();
|
||||
}
|
||||
else {
|
||||
// close stream but try to send any data left
|
||||
this.outputStream.closeInternal(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.inputStream.isClosed && this.outputStream.isClosed) {
|
||||
this.isClosed = true;
|
||||
|
||||
// send close request
|
||||
Close close = new Close(this.byteStreamRequest.getSessionID());
|
||||
close.setTo(this.remoteJID);
|
||||
try {
|
||||
SyncPacketSend.getReply(this.connection, close);
|
||||
}
|
||||
catch (XMPPException e) {
|
||||
throw new IOException("Error while closing stream: " + e.getMessage());
|
||||
}
|
||||
|
||||
this.inputStream.cleanup();
|
||||
|
||||
// remove session from manager
|
||||
InBandBytestreamManager.getByteStreamManager(this.connection).getSessions().remove(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* IBBInputStream class is the base implementation of an In-Band Bytestream input stream.
|
||||
* Subclasses of this input stream must provide a packet listener along with a packet filter to
|
||||
* collect the In-Band Bytestream data packets.
|
||||
*/
|
||||
private abstract class IBBInputStream extends InputStream {
|
||||
|
||||
/* the data packet listener to fill the data queue */
|
||||
private final PacketListener dataPacketListener;
|
||||
|
||||
/* queue containing received In-Band Bytestream data packets */
|
||||
protected final BlockingQueue<DataPacketExtension> dataQueue = new LinkedBlockingQueue<DataPacketExtension>();
|
||||
|
||||
/* buffer containing the data from one data packet */
|
||||
private byte[] buffer;
|
||||
|
||||
/* pointer to the next byte to read from buffer */
|
||||
private int bufferPointer = -1;
|
||||
|
||||
/* data packet sequence (range from 0 to 65535) */
|
||||
private long seq = -1;
|
||||
|
||||
/* flag to indicate if input stream is closed */
|
||||
private boolean isClosed = false;
|
||||
|
||||
/* flag to indicate if close method was invoked */
|
||||
private boolean closeInvoked = false;
|
||||
|
||||
/* timeout for read operations */
|
||||
private int readTimeout = 0;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public IBBInputStream() {
|
||||
// add data packet listener to connection
|
||||
this.dataPacketListener = getDataPacketListener();
|
||||
connection.addPacketListener(this.dataPacketListener, getDataPacketFilter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packet listener that processes In-Band Bytestream data packets.
|
||||
*
|
||||
* @return the data packet listener
|
||||
*/
|
||||
protected abstract PacketListener getDataPacketListener();
|
||||
|
||||
/**
|
||||
* Returns the packet filter that accepts In-Band Bytestream data packets.
|
||||
*
|
||||
* @return the data packet filter
|
||||
*/
|
||||
protected abstract PacketFilter getDataPacketFilter();
|
||||
|
||||
public synchronized int read() throws IOException {
|
||||
checkClosed();
|
||||
|
||||
// if nothing read yet or whole buffer has been read fill buffer
|
||||
if (bufferPointer == -1 || bufferPointer >= buffer.length) {
|
||||
// if no data available and stream was closed return -1
|
||||
if (!loadBuffer()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// return byte and increment buffer pointer
|
||||
return (int) buffer[bufferPointer++];
|
||||
}
|
||||
|
||||
public synchronized int read(byte[] b, int off, int len) throws IOException {
|
||||
if (b == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
|
||||
|| ((off + len) < 0)) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
else if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
checkClosed();
|
||||
|
||||
// if nothing read yet or whole buffer has been read fill buffer
|
||||
if (bufferPointer == -1 || bufferPointer >= buffer.length) {
|
||||
// if no data available and stream was closed return -1
|
||||
if (!loadBuffer()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// if more bytes wanted than available return all available
|
||||
int bytesAvailable = buffer.length - bufferPointer;
|
||||
if (len > bytesAvailable) {
|
||||
len = bytesAvailable;
|
||||
}
|
||||
|
||||
System.arraycopy(buffer, bufferPointer, b, off, len);
|
||||
bufferPointer += len;
|
||||
return len;
|
||||
}
|
||||
|
||||
public synchronized int read(byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method blocks until a data packet is received, the stream is closed or the current
|
||||
* thread is interrupted.
|
||||
*
|
||||
* @return <code>true</code> if data was received, otherwise <code>false</code>
|
||||
* @throws IOException if data packets are out of sequence
|
||||
*/
|
||||
private synchronized boolean loadBuffer() throws IOException {
|
||||
|
||||
// wait until data is available or stream is closed
|
||||
DataPacketExtension data = null;
|
||||
try {
|
||||
if (this.readTimeout == 0) {
|
||||
while (data == null) {
|
||||
if (isClosed && this.dataQueue.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
data = this.dataQueue.poll(1000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
else {
|
||||
data = this.dataQueue.poll(this.readTimeout, TimeUnit.MILLISECONDS);
|
||||
if (data == null) {
|
||||
throw new SocketTimeoutException();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
// Restore the interrupted status
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
|
||||
// handle sequence overflow
|
||||
if (this.seq == 65535) {
|
||||
this.seq = -1;
|
||||
}
|
||||
|
||||
// check if data packets sequence is successor of last seen sequence
|
||||
long seq = data.getSeq();
|
||||
if (seq - 1 != this.seq) {
|
||||
// packets out of order; close stream/session
|
||||
InBandBytestreamSession.this.close();
|
||||
throw new IOException("Packets out of sequence");
|
||||
}
|
||||
else {
|
||||
this.seq = seq;
|
||||
}
|
||||
|
||||
// set buffer to decoded data
|
||||
buffer = data.getDecodedData();
|
||||
bufferPointer = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this stream is closed and throws an IOException if necessary
|
||||
*
|
||||
* @throws IOException if stream is closed and no data should be read anymore
|
||||
*/
|
||||
private void checkClosed() throws IOException {
|
||||
/* throw no exception if there is data available, but not if close method was invoked */
|
||||
if ((isClosed && this.dataQueue.isEmpty()) || closeInvoked) {
|
||||
// clear data queue in case additional data was received after stream was closed
|
||||
this.dataQueue.clear();
|
||||
throw new IOException("Stream is closed");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeInvoked = true;
|
||||
|
||||
InBandBytestreamSession.this.closeByLocal(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sets the close flag and removes the data packet listener.
|
||||
*/
|
||||
private void closeInternal() {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
isClosed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked if the session is closed.
|
||||
*/
|
||||
private void cleanup() {
|
||||
connection.removePacketListener(this.dataPacketListener);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* IQIBBInputStream class implements IBBInputStream to be used with IQ stanzas encapsulating the
|
||||
* data packets.
|
||||
*/
|
||||
private class IQIBBInputStream extends IBBInputStream {
|
||||
|
||||
protected PacketListener getDataPacketListener() {
|
||||
return new PacketListener() {
|
||||
|
||||
private long lastSequence = -1;
|
||||
|
||||
public void processPacket(Packet packet) {
|
||||
// get data packet extension
|
||||
DataPacketExtension data = (DataPacketExtension) packet.getExtension(
|
||||
DataPacketExtension.ELEMENT_NAME,
|
||||
InBandBytestreamManager.NAMESPACE);
|
||||
|
||||
/*
|
||||
* check if sequence was not used already (see XEP-0047 Section 2.2)
|
||||
*/
|
||||
if (data.getSeq() <= this.lastSequence) {
|
||||
IQ unexpectedRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(
|
||||
XMPPError.Condition.unexpected_request));
|
||||
connection.sendPacket(unexpectedRequest);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// check if encoded data is valid (see XEP-0047 Section 2.2)
|
||||
if (data.getDecodedData() == null) {
|
||||
// data is invalid; respond with bad-request error
|
||||
IQ badRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(
|
||||
XMPPError.Condition.bad_request));
|
||||
connection.sendPacket(badRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// data is valid; add to data queue
|
||||
dataQueue.offer(data);
|
||||
|
||||
// confirm IQ
|
||||
IQ confirmData = IQ.createResultIQ((IQ) packet);
|
||||
connection.sendPacket(confirmData);
|
||||
|
||||
// set last seen sequence
|
||||
this.lastSequence = data.getSeq();
|
||||
if (this.lastSequence == 65535) {
|
||||
this.lastSequence = -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
protected PacketFilter getDataPacketFilter() {
|
||||
/*
|
||||
* filter all IQ stanzas having type 'SET' (represented by Data class), containing a
|
||||
* data packet extension, matching session ID and recipient
|
||||
*/
|
||||
return new AndFilter(new PacketTypeFilter(Data.class), new IBBDataPacketFilter());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* MessageIBBInputStream class implements IBBInputStream to be used with message stanzas
|
||||
* encapsulating the data packets.
|
||||
*/
|
||||
private class MessageIBBInputStream extends IBBInputStream {
|
||||
|
||||
protected PacketListener getDataPacketListener() {
|
||||
return new PacketListener() {
|
||||
|
||||
public void processPacket(Packet packet) {
|
||||
// get data packet extension
|
||||
DataPacketExtension data = (DataPacketExtension) packet.getExtension(
|
||||
DataPacketExtension.ELEMENT_NAME,
|
||||
InBandBytestreamManager.NAMESPACE);
|
||||
|
||||
// check if encoded data is valid
|
||||
if (data.getDecodedData() == null) {
|
||||
/*
|
||||
* TODO once a majority of XMPP server implementation support XEP-0079
|
||||
* Advanced Message Processing the invalid message could be answered with an
|
||||
* appropriate error. For now we just ignore the packet. Subsequent packets
|
||||
* with an increased sequence will cause the input stream to close the
|
||||
* stream/session.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
// data is valid; add to data queue
|
||||
dataQueue.offer(data);
|
||||
|
||||
// TODO confirm packet once XMPP servers support XEP-0079
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PacketFilter getDataPacketFilter() {
|
||||
/*
|
||||
* filter all message stanzas containing a data packet extension, matching session ID
|
||||
* and recipient
|
||||
*/
|
||||
return new AndFilter(new PacketTypeFilter(Message.class), new IBBDataPacketFilter());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* IBBDataPacketFilter class filters all packets from the remote peer of this session,
|
||||
* containing an In-Band Bytestream data packet extension whose session ID matches this sessions
|
||||
* ID.
|
||||
*/
|
||||
private class IBBDataPacketFilter implements PacketFilter {
|
||||
|
||||
public boolean accept(Packet packet) {
|
||||
// sender equals remote peer
|
||||
if (!packet.getFrom().equalsIgnoreCase(remoteJID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// stanza contains data packet extension
|
||||
PacketExtension packetExtension = packet.getExtension(DataPacketExtension.ELEMENT_NAME,
|
||||
InBandBytestreamManager.NAMESPACE);
|
||||
if (packetExtension == null || !(packetExtension instanceof DataPacketExtension)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// session ID equals this session ID
|
||||
DataPacketExtension data = (DataPacketExtension) packetExtension;
|
||||
if (!data.getSessionID().equals(byteStreamRequest.getSessionID())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* IBBOutputStream class is the base implementation of an In-Band Bytestream output stream.
|
||||
* Subclasses of this output stream must provide a method to send data over XMPP stream.
|
||||
*/
|
||||
private abstract class IBBOutputStream extends OutputStream {
|
||||
|
||||
/* buffer with the size of this sessions block size */
|
||||
protected final byte[] buffer;
|
||||
|
||||
/* pointer to next byte to write to buffer */
|
||||
protected int bufferPointer = 0;
|
||||
|
||||
/* data packet sequence (range from 0 to 65535) */
|
||||
protected long seq = 0;
|
||||
|
||||
/* flag to indicate if output stream is closed */
|
||||
protected boolean isClosed = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public IBBOutputStream() {
|
||||
this.buffer = new byte[byteStreamRequest.getBlockSize()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given data packet to the XMPP stream.
|
||||
*
|
||||
* @param data the data packet
|
||||
* @throws IOException if an I/O error occurred while sending or if the stream is closed
|
||||
*/
|
||||
protected abstract void writeToXML(DataPacketExtension data) throws IOException;
|
||||
|
||||
public synchronized void write(int b) throws IOException {
|
||||
if (this.isClosed) {
|
||||
throw new IOException("Stream is closed");
|
||||
}
|
||||
|
||||
// if buffer is full flush buffer
|
||||
if (bufferPointer >= buffer.length) {
|
||||
flushBuffer();
|
||||
}
|
||||
|
||||
buffer[bufferPointer++] = (byte) b;
|
||||
}
|
||||
|
||||
public synchronized void write(byte b[], int off, int len) throws IOException {
|
||||
if (b == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
|
||||
|| ((off + len) < 0)) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
else if (len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isClosed) {
|
||||
throw new IOException("Stream is closed");
|
||||
}
|
||||
|
||||
// is data to send greater than buffer size
|
||||
if (len >= buffer.length) {
|
||||
|
||||
// "byte" off the first chunk to write out
|
||||
writeOut(b, off, buffer.length);
|
||||
|
||||
// recursively call this method with the lesser amount
|
||||
write(b, off + buffer.length, len - buffer.length);
|
||||
}
|
||||
else {
|
||||
writeOut(b, off, len);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void write(byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the buffer with the given data and sends it over the XMPP stream if the buffers
|
||||
* capacity has been reached. This method is only called from this class so it is assured
|
||||
* that the amount of data to send is <= buffer capacity
|
||||
*
|
||||
* @param b the data
|
||||
* @param off the data
|
||||
* @param len the number of bytes to write
|
||||
* @throws IOException if an I/O error occurred while sending or if the stream is closed
|
||||
*/
|
||||
private synchronized void writeOut(byte b[], int off, int len) throws IOException {
|
||||
if (this.isClosed) {
|
||||
throw new IOException("Stream is closed");
|
||||
}
|
||||
|
||||
// set to 0 in case the next 'if' block is not executed
|
||||
int available = 0;
|
||||
|
||||
// is data to send greater that buffer space left
|
||||
if (len > buffer.length - bufferPointer) {
|
||||
// fill buffer to capacity and send it
|
||||
available = buffer.length - bufferPointer;
|
||||
System.arraycopy(b, off, buffer, bufferPointer, available);
|
||||
bufferPointer += available;
|
||||
flushBuffer();
|
||||
}
|
||||
|
||||
// copy the data left to buffer
|
||||
System.arraycopy(b, off + available, buffer, bufferPointer, len - available);
|
||||
bufferPointer += len - available;
|
||||
}
|
||||
|
||||
public synchronized void flush() throws IOException {
|
||||
if (this.isClosed) {
|
||||
throw new IOException("Stream is closed");
|
||||
}
|
||||
flushBuffer();
|
||||
}
|
||||
|
||||
private synchronized void flushBuffer() throws IOException {
|
||||
|
||||
// do nothing if no data to send available
|
||||
if (bufferPointer == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create data packet
|
||||
String enc = StringUtils.encodeBase64(buffer, 0, bufferPointer, false);
|
||||
DataPacketExtension data = new DataPacketExtension(byteStreamRequest.getSessionID(),
|
||||
this.seq, enc);
|
||||
|
||||
// write to XMPP stream
|
||||
writeToXML(data);
|
||||
|
||||
// reset buffer pointer
|
||||
bufferPointer = 0;
|
||||
|
||||
// increment sequence, considering sequence overflow
|
||||
this.seq = (this.seq + 1 == 65535 ? 0 : this.seq + 1);
|
||||
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
InBandBytestreamSession.this.closeByLocal(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the close flag and optionally flushes the stream.
|
||||
*
|
||||
* @param flush if <code>true</code> flushes the stream
|
||||
*/
|
||||
protected void closeInternal(boolean flush) {
|
||||
if (this.isClosed) {
|
||||
return;
|
||||
}
|
||||
this.isClosed = true;
|
||||
|
||||
try {
|
||||
if (flush) {
|
||||
flushBuffer();
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
/*
|
||||
* ignore, because writeToXML() will not throw an exception if stream is already
|
||||
* closed
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* IQIBBOutputStream class implements IBBOutputStream to be used with IQ stanzas encapsulating
|
||||
* the data packets.
|
||||
*/
|
||||
private class IQIBBOutputStream extends IBBOutputStream {
|
||||
|
||||
@Override
|
||||
protected synchronized void writeToXML(DataPacketExtension data) throws IOException {
|
||||
// create IQ stanza containing data packet
|
||||
IQ iq = new Data(data);
|
||||
iq.setTo(remoteJID);
|
||||
|
||||
try {
|
||||
SyncPacketSend.getReply(connection, iq);
|
||||
}
|
||||
catch (XMPPException e) {
|
||||
// close session unless it is already closed
|
||||
if (!this.isClosed) {
|
||||
InBandBytestreamSession.this.close();
|
||||
throw new IOException("Error while sending Data: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* MessageIBBOutputStream class implements IBBOutputStream to be used with message stanzas
|
||||
* encapsulating the data packets.
|
||||
*/
|
||||
private class MessageIBBOutputStream extends IBBOutputStream {
|
||||
|
||||
@Override
|
||||
protected synchronized void writeToXML(DataPacketExtension data) {
|
||||
// create message stanza containing data packet
|
||||
Message message = new Message(remoteJID);
|
||||
message.addExtension(data);
|
||||
|
||||
connection.sendPacket(message);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.jivesoftware.smack.PacketListener;
|
||||
import org.jivesoftware.smack.filter.AndFilter;
|
||||
import org.jivesoftware.smack.filter.IQTypeFilter;
|
||||
import org.jivesoftware.smack.filter.PacketFilter;
|
||||
import org.jivesoftware.smack.filter.PacketTypeFilter;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.Packet;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
|
||||
|
||||
/**
|
||||
* InitiationListener handles all incoming In-Band Bytestream open requests. If there are no
|
||||
* listeners for a In-Band Bytestream request InitiationListener will always refuse the request and
|
||||
* reply with a <not-acceptable/> error (<a
|
||||
* href="http://xmpp.org/extensions/xep-0047.html#example-5" >XEP-0047</a> Section 2.1).
|
||||
* <p>
|
||||
* All In-Band Bytestream request having a block size greater than the maximum allowed block size
|
||||
* for this connection are rejected with an <resource-constraint/> error. The maximum block
|
||||
* size can be set by invoking {@link InBandBytestreamManager#setMaximumBlockSize(int)}.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
class InitiationListener implements PacketListener {
|
||||
|
||||
/* manager containing the listeners and the XMPP connection */
|
||||
private final InBandBytestreamManager manager;
|
||||
|
||||
/* packet filter for all In-Band Bytestream requests */
|
||||
private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Open.class),
|
||||
new IQTypeFilter(IQ.Type.SET));
|
||||
|
||||
/* executor service to process incoming requests concurrently */
|
||||
private final ExecutorService initiationListenerExecutor;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param manager the In-Band Bytestream manager
|
||||
*/
|
||||
protected InitiationListener(InBandBytestreamManager manager) {
|
||||
this.manager = manager;
|
||||
initiationListenerExecutor = Executors.newCachedThreadPool();
|
||||
}
|
||||
|
||||
public void processPacket(final Packet packet) {
|
||||
initiationListenerExecutor.execute(new Runnable() {
|
||||
|
||||
public void run() {
|
||||
processRequest(packet);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void processRequest(Packet packet) {
|
||||
Open ibbRequest = (Open) packet;
|
||||
|
||||
// validate that block size is within allowed range
|
||||
if (ibbRequest.getBlockSize() > this.manager.getMaximumBlockSize()) {
|
||||
this.manager.replyResourceConstraintPacket(ibbRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore request if in ignore list
|
||||
if (this.manager.getIgnoredBytestreamRequests().remove(ibbRequest.getSessionID()))
|
||||
return;
|
||||
|
||||
// build bytestream request from packet
|
||||
InBandBytestreamRequest request = new InBandBytestreamRequest(this.manager, ibbRequest);
|
||||
|
||||
// notify listeners for bytestream initiation from a specific user
|
||||
BytestreamListener userListener = this.manager.getUserListener(ibbRequest.getFrom());
|
||||
if (userListener != null) {
|
||||
userListener.incomingBytestreamRequest(request);
|
||||
|
||||
}
|
||||
else if (!this.manager.getAllRequestListeners().isEmpty()) {
|
||||
/*
|
||||
* if there is no user specific listener inform listeners for all initiation requests
|
||||
*/
|
||||
for (BytestreamListener listener : this.manager.getAllRequestListeners()) {
|
||||
listener.incomingBytestreamRequest(request);
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
/*
|
||||
* if there is no listener for this initiation request, reply with reject message
|
||||
*/
|
||||
this.manager.replyRejectPacket(ibbRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packet filter for In-Band Bytestream open requests.
|
||||
*
|
||||
* @return the packet filter for In-Band Bytestream open requests
|
||||
*/
|
||||
protected PacketFilter getFilter() {
|
||||
return this.initFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the listeners executor service.
|
||||
*/
|
||||
protected void shutdown() {
|
||||
this.initiationListenerExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb.packet;
|
||||
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
|
||||
|
||||
/**
|
||||
* Represents a request to close an In-Band Bytestream.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class Close extends IQ {
|
||||
|
||||
/* unique session ID identifying this In-Band Bytestream */
|
||||
private final String sessionID;
|
||||
|
||||
/**
|
||||
* Creates a new In-Band Bytestream close request packet.
|
||||
*
|
||||
* @param sessionID unique session ID identifying this In-Band Bytestream
|
||||
*/
|
||||
public Close(String sessionID) {
|
||||
if (sessionID == null || "".equals(sessionID)) {
|
||||
throw new IllegalArgumentException("Session ID must not be null or empty");
|
||||
}
|
||||
this.sessionID = sessionID;
|
||||
setType(Type.SET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique session ID identifying this In-Band Bytestream.
|
||||
*
|
||||
* @return the unique session ID identifying this In-Band Bytestream
|
||||
*/
|
||||
public String getSessionID() {
|
||||
return sessionID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChildElementXML() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("<close ");
|
||||
buf.append("xmlns=\"");
|
||||
buf.append(InBandBytestreamManager.NAMESPACE);
|
||||
buf.append("\" ");
|
||||
buf.append("sid=\"");
|
||||
buf.append(sessionID);
|
||||
buf.append("\"");
|
||||
buf.append("/>");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb.packet;
|
||||
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
|
||||
/**
|
||||
* Represents a chunk of data sent over an In-Band Bytestream encapsulated in an
|
||||
* IQ stanza.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class Data extends IQ {
|
||||
|
||||
/* the data packet extension */
|
||||
private final DataPacketExtension dataPacketExtension;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param data data packet extension containing the encoded data
|
||||
*/
|
||||
public Data(DataPacketExtension data) {
|
||||
if (data == null) {
|
||||
throw new IllegalArgumentException("Data must not be null");
|
||||
}
|
||||
this.dataPacketExtension = data;
|
||||
|
||||
/*
|
||||
* also set as packet extension so that data packet extension can be
|
||||
* retrieved from IQ stanza and message stanza in the same way
|
||||
*/
|
||||
addExtension(data);
|
||||
setType(IQ.Type.SET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data packet extension.
|
||||
* <p>
|
||||
* Convenience method for <code>packet.getExtension("data",
|
||||
* "http://jabber.org/protocol/ibb")</code>.
|
||||
*
|
||||
* @return the data packet extension
|
||||
*/
|
||||
public DataPacketExtension getDataPacketExtension() {
|
||||
return this.dataPacketExtension;
|
||||
}
|
||||
|
||||
public String getChildElementXML() {
|
||||
return this.dataPacketExtension.toXML();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb.packet;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
|
||||
|
||||
/**
|
||||
* Represents a chunk of data of an In-Band Bytestream within an IQ stanza or a
|
||||
* message stanza
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class DataPacketExtension implements PacketExtension {
|
||||
|
||||
/**
|
||||
* The element name of the data packet extension.
|
||||
*/
|
||||
public final static String ELEMENT_NAME = "data";
|
||||
|
||||
/* unique session ID identifying this In-Band Bytestream */
|
||||
private final String sessionID;
|
||||
|
||||
/* sequence of this packet in regard to the other data packets */
|
||||
private final long seq;
|
||||
|
||||
/* the data contained in this packet */
|
||||
private final String data;
|
||||
|
||||
private byte[] decodedData;
|
||||
|
||||
/**
|
||||
* Creates a new In-Band Bytestream data packet.
|
||||
*
|
||||
* @param sessionID unique session ID identifying this In-Band Bytestream
|
||||
* @param seq sequence of this packet in regard to the other data packets
|
||||
* @param data the base64 encoded data contained in this packet
|
||||
*/
|
||||
public DataPacketExtension(String sessionID, long seq, String data) {
|
||||
if (sessionID == null || "".equals(sessionID)) {
|
||||
throw new IllegalArgumentException("Session ID must not be null or empty");
|
||||
}
|
||||
if (seq < 0 || seq > 65535) {
|
||||
throw new IllegalArgumentException("Sequence must not be between 0 and 65535");
|
||||
}
|
||||
if (data == null) {
|
||||
throw new IllegalArgumentException("Data must not be null");
|
||||
}
|
||||
this.sessionID = sessionID;
|
||||
this.seq = seq;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique session ID identifying this In-Band Bytestream.
|
||||
*
|
||||
* @return the unique session ID identifying this In-Band Bytestream
|
||||
*/
|
||||
public String getSessionID() {
|
||||
return sessionID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sequence of this packet in regard to the other data packets.
|
||||
*
|
||||
* @return the sequence of this packet in regard to the other data packets.
|
||||
*/
|
||||
public long getSeq() {
|
||||
return seq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data contained in this packet.
|
||||
*
|
||||
* @return the data contained in this packet.
|
||||
*/
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decoded data or null if data could not be decoded.
|
||||
* <p>
|
||||
* The encoded data is invalid if it contains bad Base64 input characters or
|
||||
* if it contains the pad ('=') character on a position other than the last
|
||||
* character(s) of the data. See <a
|
||||
* href="http://xmpp.org/extensions/xep-0047.html#sec">XEP-0047</a> Section
|
||||
* 6.
|
||||
*
|
||||
* @return the decoded data
|
||||
*/
|
||||
public byte[] getDecodedData() {
|
||||
// return cached decoded data
|
||||
if (this.decodedData != null) {
|
||||
return this.decodedData;
|
||||
}
|
||||
|
||||
// data must not contain the pad (=) other than end of data
|
||||
if (data.matches(".*={1,2}+.+")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// decodeBase64 will return null if bad characters are included
|
||||
this.decodedData = StringUtils.decodeBase64(data);
|
||||
return this.decodedData;
|
||||
}
|
||||
|
||||
public String getElementName() {
|
||||
return ELEMENT_NAME;
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return InBandBytestreamManager.NAMESPACE;
|
||||
}
|
||||
|
||||
public String toXML() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("<");
|
||||
buf.append(getElementName());
|
||||
buf.append(" ");
|
||||
buf.append("xmlns=\"");
|
||||
buf.append(InBandBytestreamManager.NAMESPACE);
|
||||
buf.append("\" ");
|
||||
buf.append("seq=\"");
|
||||
buf.append(seq);
|
||||
buf.append("\" ");
|
||||
buf.append("sid=\"");
|
||||
buf.append(sessionID);
|
||||
buf.append("\">");
|
||||
buf.append(data);
|
||||
buf.append("</");
|
||||
buf.append(getElementName());
|
||||
buf.append(">");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
}
|
||||
126
source/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java
Normal file
126
source/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb.packet;
|
||||
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType;
|
||||
|
||||
/**
|
||||
* Represents a request to open an In-Band Bytestream.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class Open extends IQ {
|
||||
|
||||
/* unique session ID identifying this In-Band Bytestream */
|
||||
private final String sessionID;
|
||||
|
||||
/* block size in which the data will be fragmented */
|
||||
private final int blockSize;
|
||||
|
||||
/* stanza type used to encapsulate the data */
|
||||
private final StanzaType stanza;
|
||||
|
||||
/**
|
||||
* Creates a new In-Band Bytestream open request packet.
|
||||
* <p>
|
||||
* The data sent over this In-Band Bytestream will be fragmented in blocks
|
||||
* with the given block size. The block size should not be greater than
|
||||
* 65535. A recommended default value is 4096.
|
||||
* <p>
|
||||
* The data can be sent using IQ stanzas or message stanzas.
|
||||
*
|
||||
* @param sessionID unique session ID identifying this In-Band Bytestream
|
||||
* @param blockSize block size in which the data will be fragmented
|
||||
* @param stanza stanza type used to encapsulate the data
|
||||
*/
|
||||
public Open(String sessionID, int blockSize, StanzaType stanza) {
|
||||
if (sessionID == null || "".equals(sessionID)) {
|
||||
throw new IllegalArgumentException("Session ID must not be null or empty");
|
||||
}
|
||||
if (blockSize <= 0) {
|
||||
throw new IllegalArgumentException("Block size must be greater than zero");
|
||||
}
|
||||
|
||||
this.sessionID = sessionID;
|
||||
this.blockSize = blockSize;
|
||||
this.stanza = stanza;
|
||||
setType(Type.SET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new In-Band Bytestream open request packet.
|
||||
* <p>
|
||||
* The data sent over this In-Band Bytestream will be fragmented in blocks
|
||||
* with the given block size. The block size should not be greater than
|
||||
* 65535. A recommended default value is 4096.
|
||||
* <p>
|
||||
* The data will be sent using IQ stanzas.
|
||||
*
|
||||
* @param sessionID unique session ID identifying this In-Band Bytestream
|
||||
* @param blockSize block size in which the data will be fragmented
|
||||
*/
|
||||
public Open(String sessionID, int blockSize) {
|
||||
this(sessionID, blockSize, StanzaType.IQ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique session ID identifying this In-Band Bytestream.
|
||||
*
|
||||
* @return the unique session ID identifying this In-Band Bytestream
|
||||
*/
|
||||
public String getSessionID() {
|
||||
return sessionID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the block size in which the data will be fragmented.
|
||||
*
|
||||
* @return the block size in which the data will be fragmented
|
||||
*/
|
||||
public int getBlockSize() {
|
||||
return blockSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stanza type used to encapsulate the data.
|
||||
*
|
||||
* @return the stanza type used to encapsulate the data
|
||||
*/
|
||||
public StanzaType getStanza() {
|
||||
return stanza;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChildElementXML() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("<open ");
|
||||
buf.append("xmlns=\"");
|
||||
buf.append(InBandBytestreamManager.NAMESPACE);
|
||||
buf.append("\" ");
|
||||
buf.append("block-size=\"");
|
||||
buf.append(blockSize);
|
||||
buf.append("\" ");
|
||||
buf.append("sid=\"");
|
||||
buf.append(sessionID);
|
||||
buf.append("\" ");
|
||||
buf.append("stanza=\"");
|
||||
buf.append(stanza.toString().toLowerCase());
|
||||
buf.append("\"");
|
||||
buf.append("/>");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb.provider;
|
||||
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.provider.IQProvider;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
/**
|
||||
* Parses a close In-Band Bytestream packet.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class CloseIQProvider implements IQProvider {
|
||||
|
||||
public IQ parseIQ(XmlPullParser parser) throws Exception {
|
||||
String sid = parser.getAttributeValue("", "sid");
|
||||
return new Close(sid);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb.provider;
|
||||
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smack.provider.IQProvider;
|
||||
import org.jivesoftware.smack.provider.PacketExtensionProvider;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
/**
|
||||
* Parses an In-Band Bytestream data packet which can be a packet extension of
|
||||
* either an IQ stanza or a message stanza.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class DataPacketProvider implements PacketExtensionProvider, IQProvider {
|
||||
|
||||
public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
|
||||
String sessionID = parser.getAttributeValue("", "sid");
|
||||
long seq = Long.parseLong(parser.getAttributeValue("", "seq"));
|
||||
String data = parser.nextText();
|
||||
return new DataPacketExtension(sessionID, seq, data);
|
||||
}
|
||||
|
||||
public IQ parseIQ(XmlPullParser parser) throws Exception {
|
||||
DataPacketExtension data = (DataPacketExtension) parseExtension(parser);
|
||||
IQ iq = new Data(data);
|
||||
return iq;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* All rights reserved. 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.ibb.provider;
|
||||
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.provider.IQProvider;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType;
|
||||
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
/**
|
||||
* Parses an In-Band Bytestream open packet.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class OpenIQProvider implements IQProvider {
|
||||
|
||||
public IQ parseIQ(XmlPullParser parser) throws Exception {
|
||||
String sessionID = parser.getAttributeValue("", "sid");
|
||||
int blockSize = Integer.parseInt(parser.getAttributeValue("", "block-size"));
|
||||
|
||||
String stanzaValue = parser.getAttributeValue("", "stanza");
|
||||
StanzaType stanza = null;
|
||||
if (stanzaValue == null) {
|
||||
stanza = StanzaType.IQ;
|
||||
}
|
||||
else {
|
||||
stanza = StanzaType.valueOf(stanzaValue.toUpperCase());
|
||||
}
|
||||
|
||||
return new Open(sessionID, blockSize, stanza);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* All rights reserved. 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;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.jivesoftware.smack.PacketListener;
|
||||
import org.jivesoftware.smack.filter.AndFilter;
|
||||
import org.jivesoftware.smack.filter.IQTypeFilter;
|
||||
import org.jivesoftware.smack.filter.PacketFilter;
|
||||
import org.jivesoftware.smack.filter.PacketTypeFilter;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.Packet;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
|
||||
|
||||
/**
|
||||
* InitiationListener handles all incoming SOCKS5 Bytestream initiation requests. If there are no
|
||||
* listeners for a SOCKS5 bytestream request InitiationListener will always refuse the request and
|
||||
* reply with a <not-acceptable/> error (<a
|
||||
* href="http://xmpp.org/extensions/xep-0065.html#usecase-alternate">XEP-0065</a> Section 5.2.A2).
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
final class InitiationListener implements PacketListener {
|
||||
|
||||
/* manager containing the listeners and the XMPP connection */
|
||||
private final Socks5BytestreamManager manager;
|
||||
|
||||
/* packet filter for all SOCKS5 Bytestream requests */
|
||||
private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Bytestream.class),
|
||||
new IQTypeFilter(IQ.Type.SET));
|
||||
|
||||
/* executor service to process incoming requests concurrently */
|
||||
private final ExecutorService initiationListenerExecutor;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param manager the SOCKS5 Bytestream manager
|
||||
*/
|
||||
protected InitiationListener(Socks5BytestreamManager manager) {
|
||||
this.manager = manager;
|
||||
initiationListenerExecutor = Executors.newCachedThreadPool();
|
||||
}
|
||||
|
||||
public void processPacket(final Packet packet) {
|
||||
initiationListenerExecutor.execute(new Runnable() {
|
||||
|
||||
public void run() {
|
||||
processRequest(packet);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void processRequest(Packet packet) {
|
||||
Bytestream byteStreamRequest = (Bytestream) packet;
|
||||
|
||||
// ignore request if in ignore list
|
||||
if (this.manager.getIgnoredBytestreamRequests().remove(byteStreamRequest.getSessionID())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// build bytestream request from packet
|
||||
Socks5BytestreamRequest request = new Socks5BytestreamRequest(this.manager,
|
||||
byteStreamRequest);
|
||||
|
||||
// notify listeners for bytestream initiation from a specific user
|
||||
BytestreamListener userListener = this.manager.getUserListener(byteStreamRequest.getFrom());
|
||||
if (userListener != null) {
|
||||
userListener.incomingBytestreamRequest(request);
|
||||
|
||||
}
|
||||
else if (!this.manager.getAllRequestListeners().isEmpty()) {
|
||||
/*
|
||||
* if there is no user specific listener inform listeners for all initiation requests
|
||||
*/
|
||||
for (BytestreamListener listener : this.manager.getAllRequestListeners()) {
|
||||
listener.incomingBytestreamRequest(request);
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
/*
|
||||
* if there is no listener for this initiation request, reply with reject message
|
||||
*/
|
||||
this.manager.replyRejectPacket(byteStreamRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packet filter for SOCKS5 Bytestream initialization requests.
|
||||
*
|
||||
* @return the packet filter for SOCKS5 Bytestream initialization requests
|
||||
*/
|
||||
protected PacketFilter getFilter() {
|
||||
return this.initFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the listeners executor service.
|
||||
*/
|
||||
protected void shutdown() {
|
||||
this.initiationListenerExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* All rights reserved. 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;
|
||||
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
|
||||
|
||||
/**
|
||||
* Socks5BytestreamListener are informed if a remote user wants to initiate a SOCKS5 Bytestream.
|
||||
* Implement this interface to handle incoming SOCKS5 Bytestream requests.
|
||||
* <p>
|
||||
* There are two ways to add this listener. See
|
||||
* {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
|
||||
* {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for
|
||||
* further details.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public abstract class Socks5BytestreamListener implements BytestreamListener {
|
||||
|
||||
public void incomingBytestreamRequest(BytestreamRequest request) {
|
||||
incomingBytestreamRequest((Socks5BytestreamRequest) request);
|
||||
}
|
||||
|
||||
/**
|
||||
* This listener is notified if a SOCKS5 Bytestream request from another user has been received.
|
||||
*
|
||||
* @param request the incoming SOCKS5 Bytestream request
|
||||
*/
|
||||
public abstract void incomingBytestreamRequest(Socks5BytestreamRequest request);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,760 @@
|
|||
/**
|
||||
* All rights reserved. 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.jivesoftware.smack.AbstractConnectionListener;
|
||||
import org.jivesoftware.smack.Connection;
|
||||
import org.jivesoftware.smack.ConnectionCreationListener;
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.Packet;
|
||||
import org.jivesoftware.smack.packet.XMPPError;
|
||||
import org.jivesoftware.smackx.ServiceDiscoveryManager;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;
|
||||
import org.jivesoftware.smackx.filetransfer.FileTransferManager;
|
||||
import org.jivesoftware.smackx.packet.DiscoverInfo;
|
||||
import org.jivesoftware.smackx.packet.DiscoverItems;
|
||||
import org.jivesoftware.smackx.packet.SyncPacketSend;
|
||||
import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
|
||||
import org.jivesoftware.smackx.packet.DiscoverItems.Item;
|
||||
|
||||
/**
|
||||
* The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as specified in the <a
|
||||
* href="http://xmpp.org/extensions/xep-0065.html">XEP-0065</a>.
|
||||
* <p>
|
||||
* A SOCKS5 Bytestream is negotiated partly over the XMPP XML stream and partly over a separate
|
||||
* socket. The actual transfer though takes place over a separately created socket.
|
||||
* <p>
|
||||
* A SOCKS5 Bytestream generally has three parties, the initiator, the target, and the stream host.
|
||||
* The stream host is a specialized SOCKS5 proxy setup on a server, or, the initiator can act as the
|
||||
* stream host.
|
||||
* <p>
|
||||
* To establish a SOCKS5 Bytestream invoke the {@link #establishSession(String)} method. This will
|
||||
* negotiate a SOCKS5 Bytestream with the given target JID and return a socket.
|
||||
* <p>
|
||||
* If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while negotiating a file
|
||||
* transfer) invoke {@link #establishSession(String, String)}.
|
||||
* <p>
|
||||
* To handle incoming SOCKS5 Bytestream requests add an {@link Socks5BytestreamListener} to the
|
||||
* manager. There are two ways to add this listener. If you want to be informed about incoming
|
||||
* SOCKS5 Bytestreams from a specific user add the listener by invoking
|
||||
* {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
|
||||
* respond to all SOCKS5 Bytestream requests invoke
|
||||
* {@link #addIncomingBytestreamListener(BytestreamListener)}.
|
||||
* <p>
|
||||
* Note that the registered {@link Socks5BytestreamListener} will NOT be notified on incoming Socks5
|
||||
* bytestream requests sent in the context of <a
|
||||
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||
* {@link FileTransferManager})
|
||||
* <p>
|
||||
* If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5 Bytestream requests
|
||||
* will be rejected by returning a <not-acceptable/> error to the initiator.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public final class Socks5BytestreamManager implements BytestreamManager {
|
||||
|
||||
/*
|
||||
* create a new Socks5BytestreamManager and register a shutdown listener on every established
|
||||
* connection
|
||||
*/
|
||||
static {
|
||||
Connection.addConnectionCreationListener(new ConnectionCreationListener() {
|
||||
|
||||
public void connectionCreated(Connection connection) {
|
||||
final Socks5BytestreamManager manager;
|
||||
manager = Socks5BytestreamManager.getBytestreamManager(connection);
|
||||
|
||||
// register shutdown listener
|
||||
connection.addConnectionListener(new AbstractConnectionListener() {
|
||||
|
||||
public void connectionClosed() {
|
||||
manager.disableService();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The XMPP namespace of the SOCKS5 Bytestream
|
||||
*/
|
||||
public static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
|
||||
|
||||
/* prefix used to generate session IDs */
|
||||
private static final String SESSION_ID_PREFIX = "js5_";
|
||||
|
||||
/* random generator to create session IDs */
|
||||
private final static Random randomGenerator = new Random();
|
||||
|
||||
/* stores one Socks5BytestreamManager for each XMPP connection */
|
||||
private final static Map<Connection, Socks5BytestreamManager> managers = new HashMap<Connection, Socks5BytestreamManager>();
|
||||
|
||||
/* XMPP connection */
|
||||
private final Connection connection;
|
||||
|
||||
/*
|
||||
* assigns a user to a listener that is informed if a bytestream request for this user is
|
||||
* received
|
||||
*/
|
||||
private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
|
||||
|
||||
/*
|
||||
* list of listeners that respond to all bytestream requests if there are not user specific
|
||||
* listeners for that request
|
||||
*/
|
||||
private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
|
||||
|
||||
/* listener that handles all incoming bytestream requests */
|
||||
private final InitiationListener initiationListener;
|
||||
|
||||
/* timeout to wait for the response to the SOCKS5 Bytestream initialization request */
|
||||
private int targetResponseTimeout = 10000;
|
||||
|
||||
/* timeout for connecting to the SOCKS5 proxy selected by the target */
|
||||
private int proxyConnectionTimeout = 10000;
|
||||
|
||||
/* blacklist of errornous SOCKS5 proxies */
|
||||
private final List<String> proxyBlacklist = Collections.synchronizedList(new LinkedList<String>());
|
||||
|
||||
/* remember the last proxy that worked to prioritize it */
|
||||
private String lastWorkingProxy = null;
|
||||
|
||||
/* flag to enable/disable prioritization of last working proxy */
|
||||
private boolean proxyPrioritizationEnabled = true;
|
||||
|
||||
/*
|
||||
* list containing session IDs of SOCKS5 Bytestream initialization packets that should be
|
||||
* ignored by the InitiationListener
|
||||
*/
|
||||
private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
|
||||
|
||||
/**
|
||||
* Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given
|
||||
* {@link Connection}.
|
||||
* <p>
|
||||
* If no manager exists a new is created and initialized.
|
||||
*
|
||||
* @param connection the XMPP connection or <code>null</code> if given connection is
|
||||
* <code>null</code>
|
||||
* @return the Socks5BytestreamManager for the given XMPP connection
|
||||
*/
|
||||
public static synchronized Socks5BytestreamManager getBytestreamManager(Connection connection) {
|
||||
if (connection == null) {
|
||||
return null;
|
||||
}
|
||||
Socks5BytestreamManager manager = managers.get(connection);
|
||||
if (manager == null) {
|
||||
manager = new Socks5BytestreamManager(connection);
|
||||
managers.put(connection, manager);
|
||||
manager.activate();
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*
|
||||
* @param connection the XMPP connection
|
||||
*/
|
||||
private Socks5BytestreamManager(Connection connection) {
|
||||
this.connection = connection;
|
||||
this.initiationListener = new InitiationListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request unless
|
||||
* there is a user specific BytestreamListener registered.
|
||||
* <p>
|
||||
* If no listeners are registered all SOCKS5 Bytestream request are rejected with a
|
||||
* <not-acceptable/> error.
|
||||
* <p>
|
||||
* Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
|
||||
* bytestream requests sent in the context of <a
|
||||
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||
* {@link FileTransferManager})
|
||||
*
|
||||
* @param listener the listener to register
|
||||
*/
|
||||
public void addIncomingBytestreamListener(BytestreamListener listener) {
|
||||
this.allRequestListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given listener from the list of listeners for all incoming SOCKS5 Bytestream
|
||||
* requests.
|
||||
*
|
||||
* @param listener the listener to remove
|
||||
*/
|
||||
public void removeIncomingBytestreamListener(BytestreamListener listener) {
|
||||
this.allRequestListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request from the
|
||||
* given user.
|
||||
* <p>
|
||||
* Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific
|
||||
* user.
|
||||
* <p>
|
||||
* If no listeners are registered all SOCKS5 Bytestream request are rejected with a
|
||||
* <not-acceptable/> error.
|
||||
* <p>
|
||||
* Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
|
||||
* bytestream requests sent in the context of <a
|
||||
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||
* {@link FileTransferManager})
|
||||
*
|
||||
* @param listener the listener to register
|
||||
* @param initiatorJID the JID of the user that wants to establish a SOCKS5 Bytestream
|
||||
*/
|
||||
public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
|
||||
this.userListeners.put(initiatorJID, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener for the given user.
|
||||
*
|
||||
* @param initiatorJID the JID of the user the listener should be removed
|
||||
*/
|
||||
public void removeIncomingBytestreamListener(String initiatorJID) {
|
||||
this.userListeners.remove(initiatorJID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to ignore the next incoming SOCKS5 Bytestream request containing the given
|
||||
* session ID. No listeners will be notified for this request and and no error will be returned
|
||||
* to the initiator.
|
||||
* <p>
|
||||
* This method should be used if you are awaiting a SOCKS5 Bytestream request as a reply to
|
||||
* another packet (e.g. file transfer).
|
||||
*
|
||||
* @param sessionID to be ignored
|
||||
*/
|
||||
public void ignoreBytestreamRequestOnce(String sessionID) {
|
||||
this.ignoredBytestreamRequests.add(sessionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream feature from the
|
||||
* service discovery, disabling the listener for SOCKS5 Bytestream initiation requests and
|
||||
* resetting its internal state.
|
||||
* <p>
|
||||
* To re-enable the SOCKS5 Bytestream feature invoke {@link #getBytestreamManager(Connection)}.
|
||||
* Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature.
|
||||
*/
|
||||
public synchronized void disableService() {
|
||||
|
||||
// remove initiation packet listener
|
||||
this.connection.removePacketListener(this.initiationListener);
|
||||
|
||||
// shutdown threads
|
||||
this.initiationListener.shutdown();
|
||||
|
||||
// clear listeners
|
||||
this.allRequestListeners.clear();
|
||||
this.userListeners.clear();
|
||||
|
||||
// reset internal state
|
||||
this.lastWorkingProxy = null;
|
||||
this.proxyBlacklist.clear();
|
||||
this.ignoredBytestreamRequests.clear();
|
||||
|
||||
// remove manager from static managers map
|
||||
managers.remove(this.connection);
|
||||
|
||||
// shutdown local SOCKS5 proxy if there are no more managers for other connections
|
||||
if (managers.size() == 0) {
|
||||
Socks5Proxy.getSocks5Proxy().stop();
|
||||
}
|
||||
|
||||
// remove feature from service discovery
|
||||
ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
||||
|
||||
// check if service discovery is not already disposed by connection shutdown
|
||||
if (serviceDiscoveryManager != null) {
|
||||
serviceDiscoveryManager.removeFeature(NAMESPACE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
|
||||
* Default is 10000ms.
|
||||
*
|
||||
* @return the timeout to wait for the response to the SOCKS5 Bytestream initialization request
|
||||
*/
|
||||
public int getTargetResponseTimeout() {
|
||||
if (this.targetResponseTimeout <= 0) {
|
||||
this.targetResponseTimeout = 10000;
|
||||
}
|
||||
return targetResponseTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
|
||||
* Default is 10000ms.
|
||||
*
|
||||
* @param targetResponseTimeout the timeout to set
|
||||
*/
|
||||
public void setTargetResponseTimeout(int targetResponseTimeout) {
|
||||
this.targetResponseTimeout = targetResponseTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
|
||||
* 10000ms.
|
||||
*
|
||||
* @return the timeout for connecting to the SOCKS5 proxy selected by the target
|
||||
*/
|
||||
public int getProxyConnectionTimeout() {
|
||||
if (this.proxyConnectionTimeout <= 0) {
|
||||
this.proxyConnectionTimeout = 10000;
|
||||
}
|
||||
return proxyConnectionTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
|
||||
* 10000ms.
|
||||
*
|
||||
* @param proxyConnectionTimeout the timeout to set
|
||||
*/
|
||||
public void setProxyConnectionTimeout(int proxyConnectionTimeout) {
|
||||
this.proxyConnectionTimeout = proxyConnectionTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the prioritization of the last working SOCKS5 proxy on successive SOCKS5
|
||||
* Bytestream connections is enabled. Default is <code>true</code>.
|
||||
*
|
||||
* @return <code>true</code> if prioritization is enabled, <code>false</code> otherwise
|
||||
*/
|
||||
public boolean isProxyPrioritizationEnabled() {
|
||||
return proxyPrioritizationEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable the prioritization of the last working SOCKS5 proxy on successive SOCKS5
|
||||
* Bytestream connections.
|
||||
*
|
||||
* @param proxyPrioritizationEnabled enable/disable the prioritization of the last working
|
||||
* SOCKS5 proxy
|
||||
*/
|
||||
public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {
|
||||
this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes a SOCKS5 Bytestream with the given user and returns the Socket to send/receive
|
||||
* data to/from the user.
|
||||
* <p>
|
||||
* Use this method to establish SOCKS5 Bytestreams to users accepting all incoming Socks5
|
||||
* bytestream requests since this method doesn't provide a way to tell the user something about
|
||||
* the data to be sent.
|
||||
* <p>
|
||||
* To establish a SOCKS5 Bytestream after negotiation the kind of data to be sent (e.g. file
|
||||
* transfer) use {@link #establishSession(String, String)}.
|
||||
*
|
||||
* @param targetJID the JID of the user a SOCKS5 Bytestream should be established
|
||||
* @return the Socket to send/receive data to/from the user
|
||||
* @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
|
||||
* Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
|
||||
* @throws IOException if the bytestream could not be established
|
||||
* @throws InterruptedException if the current thread was interrupted while waiting
|
||||
*/
|
||||
public Socks5BytestreamSession establishSession(String targetJID) throws XMPPException,
|
||||
IOException, InterruptedException {
|
||||
String sessionID = getNextSessionID();
|
||||
return establishSession(targetJID, sessionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes a SOCKS5 Bytestream with the given user using the given session ID and returns
|
||||
* the Socket to send/receive data to/from the user.
|
||||
*
|
||||
* @param targetJID the JID of the user a SOCKS5 Bytestream should be established
|
||||
* @param sessionID the session ID for the SOCKS5 Bytestream request
|
||||
* @return the Socket to send/receive data to/from the user
|
||||
* @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
|
||||
* Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
|
||||
* @throws IOException if the bytestream could not be established
|
||||
* @throws InterruptedException if the current thread was interrupted while waiting
|
||||
*/
|
||||
public Socks5BytestreamSession establishSession(String targetJID, String sessionID)
|
||||
throws XMPPException, IOException, InterruptedException {
|
||||
|
||||
// check if target supports SOCKS5 Bytestream
|
||||
if (!supportsSocks5(targetJID)) {
|
||||
throw new XMPPException(targetJID + " doesn't support SOCKS5 Bytestream");
|
||||
}
|
||||
|
||||
// determine SOCKS5 proxies from XMPP-server
|
||||
List<String> proxies = determineProxies();
|
||||
|
||||
// determine address and port of each proxy
|
||||
List<StreamHost> streamHosts = determineStreamHostInfos(proxies);
|
||||
|
||||
// compute digest
|
||||
String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID);
|
||||
|
||||
if (streamHosts.isEmpty()) {
|
||||
throw new XMPPException("no SOCKS5 proxies available");
|
||||
}
|
||||
|
||||
// prioritize last working SOCKS5 proxy if exists
|
||||
if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {
|
||||
StreamHost selectedStreamHost = null;
|
||||
for (StreamHost streamHost : streamHosts) {
|
||||
if (streamHost.getJID().equals(this.lastWorkingProxy)) {
|
||||
selectedStreamHost = streamHost;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedStreamHost != null) {
|
||||
streamHosts.remove(selectedStreamHost);
|
||||
streamHosts.add(0, selectedStreamHost);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
|
||||
try {
|
||||
|
||||
// add transfer digest to local proxy to make transfer valid
|
||||
socks5Proxy.addTransfer(digest);
|
||||
|
||||
// create initiation packet
|
||||
Bytestream initiation = createBytestreamInitiation(sessionID, targetJID, streamHosts);
|
||||
|
||||
// send initiation packet
|
||||
Packet response = SyncPacketSend.getReply(this.connection, initiation,
|
||||
getTargetResponseTimeout());
|
||||
|
||||
// extract used stream host from response
|
||||
StreamHostUsed streamHostUsed = ((Bytestream) response).getUsedHost();
|
||||
StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed.getJID());
|
||||
|
||||
if (usedStreamHost == null) {
|
||||
throw new XMPPException("Remote user responded with unknown host");
|
||||
}
|
||||
|
||||
// build SOCKS5 client
|
||||
Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest,
|
||||
this.connection, sessionID, targetJID);
|
||||
|
||||
// establish connection to proxy
|
||||
Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());
|
||||
|
||||
// remember last working SOCKS5 proxy to prioritize it for next request
|
||||
this.lastWorkingProxy = usedStreamHost.getJID();
|
||||
|
||||
// negotiation successful, return the output stream
|
||||
return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals(
|
||||
this.connection.getUser()));
|
||||
|
||||
}
|
||||
catch (TimeoutException e) {
|
||||
throw new IOException("Timeout while connecting to SOCKS5 proxy");
|
||||
}
|
||||
finally {
|
||||
|
||||
// remove transfer digest if output stream is returned or an exception
|
||||
// occurred
|
||||
socks5Proxy.removeTransfer(digest);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if the given target JID supports feature SOCKS5 Bytestream.
|
||||
*
|
||||
* @param targetJID the target JID
|
||||
* @return <code>true</code> if the given target JID supports feature SOCKS5 Bytestream
|
||||
* otherwise <code>false</code>
|
||||
* @throws XMPPException if there was an error querying target for supported features
|
||||
*/
|
||||
private boolean supportsSocks5(String targetJID) throws XMPPException {
|
||||
ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
||||
DiscoverInfo discoverInfo = serviceDiscoveryManager.discoverInfo(targetJID);
|
||||
return discoverInfo.containsFeature(NAMESPACE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The SOCKS5 proxies are
|
||||
* in the same order as returned by the XMPP server.
|
||||
*
|
||||
* @return list of JIDs of SOCKS5 proxies
|
||||
* @throws XMPPException if there was an error querying the XMPP server for SOCKS5 proxies
|
||||
*/
|
||||
private List<String> determineProxies() throws XMPPException {
|
||||
ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
||||
|
||||
List<String> proxies = new ArrayList<String>();
|
||||
|
||||
// get all items form XMPP server
|
||||
DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName());
|
||||
Iterator<Item> itemIterator = discoverItems.getItems();
|
||||
|
||||
// query all items if they are SOCKS5 proxies
|
||||
while (itemIterator.hasNext()) {
|
||||
Item item = itemIterator.next();
|
||||
|
||||
// skip blacklisted servers
|
||||
if (this.proxyBlacklist.contains(item.getEntityID())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
DiscoverInfo proxyInfo;
|
||||
proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID());
|
||||
Iterator<Identity> identities = proxyInfo.getIdentities();
|
||||
|
||||
// item must have category "proxy" and type "bytestream"
|
||||
while (identities.hasNext()) {
|
||||
Identity identity = identities.next();
|
||||
|
||||
if ("proxy".equalsIgnoreCase(identity.getCategory())
|
||||
&& "bytestreams".equalsIgnoreCase(identity.getType())) {
|
||||
proxies.add(item.getEntityID());
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* server is not a SOCKS5 proxy, blacklist server to skip next time a Socks5
|
||||
* bytestream should be established
|
||||
*/
|
||||
this.proxyBlacklist.add(item.getEntityID());
|
||||
|
||||
}
|
||||
}
|
||||
catch (XMPPException e) {
|
||||
// blacklist errornous server
|
||||
this.proxyBlacklist.add(item.getEntityID());
|
||||
}
|
||||
}
|
||||
|
||||
return proxies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of stream hosts containing the IP address an the port for the given list of
|
||||
* SOCKS5 proxy JIDs. The order of the returned list is the same as the given list of JIDs
|
||||
* excluding all SOCKS5 proxies who's network settings could not be determined. If a local
|
||||
* SOCKS5 proxy is running it will be the first item in the list returned.
|
||||
*
|
||||
* @param proxies a list of SOCKS5 proxy JIDs
|
||||
* @return a list of stream hosts containing the IP address an the port
|
||||
*/
|
||||
private List<StreamHost> determineStreamHostInfos(List<String> proxies) {
|
||||
List<StreamHost> streamHosts = new ArrayList<StreamHost>();
|
||||
|
||||
// add local proxy on first position if exists
|
||||
List<StreamHost> localProxies = getLocalStreamHost();
|
||||
if (localProxies != null) {
|
||||
streamHosts.addAll(localProxies);
|
||||
}
|
||||
|
||||
// query SOCKS5 proxies for network settings
|
||||
for (String proxy : proxies) {
|
||||
Bytestream streamHostRequest = createStreamHostRequest(proxy);
|
||||
try {
|
||||
Bytestream response = (Bytestream) SyncPacketSend.getReply(this.connection,
|
||||
streamHostRequest);
|
||||
streamHosts.addAll(response.getStreamHosts());
|
||||
}
|
||||
catch (XMPPException e) {
|
||||
// blacklist errornous proxies
|
||||
this.proxyBlacklist.add(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
return streamHosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a IQ packet to query a SOCKS5 proxy its network settings.
|
||||
*
|
||||
* @param proxy the proxy to query
|
||||
* @return IQ packet to query a SOCKS5 proxy its network settings
|
||||
*/
|
||||
private Bytestream createStreamHostRequest(String proxy) {
|
||||
Bytestream request = new Bytestream();
|
||||
request.setType(IQ.Type.GET);
|
||||
request.setTo(proxy);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stream host information of the local SOCKS5 proxy containing the IP address and
|
||||
* the port or null if local SOCKS5 proxy is not running.
|
||||
*
|
||||
* @return the stream host information of the local SOCKS5 proxy or null if local SOCKS5 proxy
|
||||
* is not running
|
||||
*/
|
||||
private List<StreamHost> getLocalStreamHost() {
|
||||
|
||||
// get local proxy singleton
|
||||
Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
|
||||
|
||||
if (socks5Server.isRunning()) {
|
||||
List<String> addresses = socks5Server.getLocalAddresses();
|
||||
int port = socks5Server.getPort();
|
||||
|
||||
if (addresses.size() >= 1) {
|
||||
List<StreamHost> streamHosts = new ArrayList<StreamHost>();
|
||||
for (String address : addresses) {
|
||||
StreamHost streamHost = new StreamHost(this.connection.getUser(), address);
|
||||
streamHost.setPort(port);
|
||||
streamHosts.add(streamHost);
|
||||
}
|
||||
return streamHosts;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// server is not running or local address could not be determined
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SOCKS5 Bytestream initialization request packet with the given session ID
|
||||
* containing the given stream hosts for the given target JID.
|
||||
*
|
||||
* @param sessionID the session ID for the SOCKS5 Bytestream
|
||||
* @param targetJID the target JID of SOCKS5 Bytestream request
|
||||
* @param streamHosts a list of SOCKS5 proxies the target should connect to
|
||||
* @return a SOCKS5 Bytestream initialization request packet
|
||||
*/
|
||||
private Bytestream createBytestreamInitiation(String sessionID, String targetJID,
|
||||
List<StreamHost> streamHosts) {
|
||||
Bytestream initiation = new Bytestream(sessionID);
|
||||
|
||||
// add all stream hosts
|
||||
for (StreamHost streamHost : streamHosts) {
|
||||
initiation.addStreamHost(streamHost);
|
||||
}
|
||||
|
||||
initiation.setType(IQ.Type.SET);
|
||||
initiation.setTo(targetJID);
|
||||
|
||||
return initiation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responses to the given packet's sender with a XMPP error that a SOCKS5 Bytestream is not
|
||||
* accepted.
|
||||
*
|
||||
* @param packet Packet that should be answered with a not-acceptable error
|
||||
*/
|
||||
protected void replyRejectPacket(IQ packet) {
|
||||
XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
|
||||
IQ errorIQ = IQ.createErrorResponse(packet, xmppError);
|
||||
this.connection.sendPacket(errorIQ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the Socks5BytestreamManager by registering the SOCKS5 Bytestream initialization
|
||||
* listener and enabling the SOCKS5 Bytestream feature.
|
||||
*/
|
||||
private void activate() {
|
||||
// register bytestream initiation packet listener
|
||||
this.connection.addPacketListener(this.initiationListener,
|
||||
this.initiationListener.getFilter());
|
||||
|
||||
// enable SOCKS5 feature
|
||||
enableService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the SOCKS5 Bytestream feature to the service discovery.
|
||||
*/
|
||||
private void enableService() {
|
||||
ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
||||
if (!manager.includesFeature(NAMESPACE)) {
|
||||
manager.addFeature(NAMESPACE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new unique session ID.
|
||||
*
|
||||
* @return a new unique session ID
|
||||
*/
|
||||
private String getNextSessionID() {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
buffer.append(SESSION_ID_PREFIX);
|
||||
buffer.append(Math.abs(randomGenerator.nextLong()));
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the XMPP connection.
|
||||
*
|
||||
* @return the XMPP connection
|
||||
*/
|
||||
protected Connection getConnection() {
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link BytestreamListener} that should be informed if a SOCKS5 Bytestream request
|
||||
* from the given initiator JID is received.
|
||||
*
|
||||
* @param initiator the initiator's JID
|
||||
* @return the listener
|
||||
*/
|
||||
protected BytestreamListener getUserListener(String initiator) {
|
||||
return this.userListeners.get(initiator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of {@link BytestreamListener} that are informed if there are no listeners for
|
||||
* a specific initiator.
|
||||
*
|
||||
* @return list of listeners
|
||||
*/
|
||||
protected List<BytestreamListener> getAllRequestListeners() {
|
||||
return this.allRequestListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of session IDs that should be ignored by the InitialtionListener
|
||||
*
|
||||
* @return list of session IDs
|
||||
*/
|
||||
protected List<String> getIgnoredBytestreamRequests() {
|
||||
return ignoredBytestreamRequests;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
/**
|
||||
* All rights reserved. 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.XMPPError;
|
||||
import org.jivesoftware.smack.util.Cache;
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
|
||||
|
||||
/**
|
||||
* Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class Socks5BytestreamRequest implements BytestreamRequest {
|
||||
|
||||
/* lifetime of an Item in the blacklist */
|
||||
private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;
|
||||
|
||||
/* size of the blacklist */
|
||||
private static final int BLACKLIST_MAX_SIZE = 100;
|
||||
|
||||
/* blacklist of addresses of SOCKS5 proxies */
|
||||
private static final Cache<String, Integer> ADDRESS_BLACKLIST = new Cache<String, Integer>(
|
||||
BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME);
|
||||
|
||||
/*
|
||||
* The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.
|
||||
* When a proxy is blacklisted no more connection attempts will be made to it for a period of 2
|
||||
* hours.
|
||||
*/
|
||||
private static int CONNECTION_FAILURE_THRESHOLD = 2;
|
||||
|
||||
/* the bytestream initialization request */
|
||||
private Bytestream bytestreamRequest;
|
||||
|
||||
/* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */
|
||||
private Socks5BytestreamManager manager;
|
||||
|
||||
/* timeout to connect to all SOCKS5 proxies */
|
||||
private int totalConnectTimeout = 10000;
|
||||
|
||||
/* minimum timeout to connect to one SOCKS5 proxy */
|
||||
private int minimumConnectTimeout = 2000;
|
||||
|
||||
/**
|
||||
* Returns the number of connection failures it takes for a particular SOCKS5 proxy to be
|
||||
* blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
|
||||
* period of 2 hours. Default is 2.
|
||||
*
|
||||
* @return the number of connection failures it takes for a particular SOCKS5 proxy to be
|
||||
* blacklisted
|
||||
*/
|
||||
public static int getConnectFailureThreshold() {
|
||||
return CONNECTION_FAILURE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of connection failures it takes for a particular SOCKS5 proxy to be
|
||||
* blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
|
||||
* period of 2 hours. Default is 2.
|
||||
* <p>
|
||||
* Setting the connection failure threshold to zero disables the blacklisting.
|
||||
*
|
||||
* @param connectFailureThreshold the number of connection failures it takes for a particular
|
||||
* SOCKS5 proxy to be blacklisted
|
||||
*/
|
||||
public static void setConnectFailureThreshold(int connectFailureThreshold) {
|
||||
CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Socks5BytestreamRequest.
|
||||
*
|
||||
* @param manager the SOCKS5 Bytestream manager
|
||||
* @param bytestreamRequest the SOCKS5 Bytestream initialization packet
|
||||
*/
|
||||
protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {
|
||||
this.manager = manager;
|
||||
this.bytestreamRequest = bytestreamRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
|
||||
* <p>
|
||||
* When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
|
||||
* by the initiator until a connection is established. This timeout divided by the number of
|
||||
* SOCKS5 proxies determines the timeout for every connection attempt.
|
||||
* <p>
|
||||
* You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
|
||||
* {@link #setMinimumConnectTimeout(int)}.
|
||||
*
|
||||
* @return the maximum timeout to connect to SOCKS5 proxies
|
||||
*/
|
||||
public int getTotalConnectTimeout() {
|
||||
if (this.totalConnectTimeout <= 0) {
|
||||
return 10000;
|
||||
}
|
||||
return this.totalConnectTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
|
||||
* <p>
|
||||
* When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
|
||||
* by the initiator until a connection is established. This timeout divided by the number of
|
||||
* SOCKS5 proxies determines the timeout for every connection attempt.
|
||||
* <p>
|
||||
* You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
|
||||
* {@link #setMinimumConnectTimeout(int)}.
|
||||
*
|
||||
* @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies
|
||||
*/
|
||||
public void setTotalConnectTimeout(int totalConnectTimeout) {
|
||||
this.totalConnectTimeout = totalConnectTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
|
||||
* request. Default is 2000ms.
|
||||
*
|
||||
* @return the timeout to connect to one SOCKS5 proxy
|
||||
*/
|
||||
public int getMinimumConnectTimeout() {
|
||||
if (this.minimumConnectTimeout <= 0) {
|
||||
return 2000;
|
||||
}
|
||||
return this.minimumConnectTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
|
||||
* request. Default is 2000ms.
|
||||
*
|
||||
* @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy
|
||||
*/
|
||||
public void setMinimumConnectTimeout(int minimumConnectTimeout) {
|
||||
this.minimumConnectTimeout = minimumConnectTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sender of the SOCKS5 Bytestream initialization request.
|
||||
*
|
||||
* @return the sender of the SOCKS5 Bytestream initialization request.
|
||||
*/
|
||||
public String getFrom() {
|
||||
return this.bytestreamRequest.getFrom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session ID of the SOCKS5 Bytestream initialization request.
|
||||
*
|
||||
* @return the session ID of the SOCKS5 Bytestream initialization request.
|
||||
*/
|
||||
public String getSessionID() {
|
||||
return this.bytestreamRequest.getSessionID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
|
||||
* data.
|
||||
* <p>
|
||||
* Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking
|
||||
* {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
|
||||
*
|
||||
* @return the socket to send/receive data
|
||||
* @throws XMPPException if connection to all SOCKS5 proxies failed or if stream is invalid.
|
||||
* @throws InterruptedException if the current thread was interrupted while waiting
|
||||
*/
|
||||
public Socks5BytestreamSession accept() throws XMPPException, InterruptedException {
|
||||
Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();
|
||||
|
||||
// throw exceptions if request contains no stream hosts
|
||||
if (streamHosts.size() == 0) {
|
||||
cancelRequest();
|
||||
}
|
||||
|
||||
StreamHost selectedHost = null;
|
||||
Socket socket = null;
|
||||
|
||||
String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(),
|
||||
this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser());
|
||||
|
||||
/*
|
||||
* determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of
|
||||
* time so that the first does not consume the whole timeout
|
||||
*/
|
||||
int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(),
|
||||
getMinimumConnectTimeout());
|
||||
|
||||
for (StreamHost streamHost : streamHosts) {
|
||||
String address = streamHost.getAddress() + ":" + streamHost.getPort();
|
||||
|
||||
// check to see if this address has been blacklisted
|
||||
int failures = getConnectionFailures(address);
|
||||
if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// establish socket
|
||||
try {
|
||||
|
||||
// build SOCKS5 client
|
||||
final Socks5Client socks5Client = new Socks5Client(streamHost, digest);
|
||||
|
||||
// connect to SOCKS5 proxy with a timeout
|
||||
socket = socks5Client.getSocket(timeout);
|
||||
|
||||
// set selected host
|
||||
selectedHost = streamHost;
|
||||
break;
|
||||
|
||||
}
|
||||
catch (TimeoutException e) {
|
||||
incrementConnectionFailures(address);
|
||||
}
|
||||
catch (IOException e) {
|
||||
incrementConnectionFailures(address);
|
||||
}
|
||||
catch (XMPPException e) {
|
||||
incrementConnectionFailures(address);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// throw exception if connecting to all SOCKS5 proxies failed
|
||||
if (selectedHost == null || socket == null) {
|
||||
cancelRequest();
|
||||
}
|
||||
|
||||
// send used-host confirmation
|
||||
Bytestream response = createUsedHostResponse(selectedHost);
|
||||
this.manager.getConnection().sendPacket(response);
|
||||
|
||||
return new Socks5BytestreamSession(socket, selectedHost.getJID().equals(
|
||||
this.bytestreamRequest.getFrom()));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
|
||||
*/
|
||||
public void reject() {
|
||||
this.manager.replyRejectPacket(this.bytestreamRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
|
||||
* XMPP exception.
|
||||
*
|
||||
* @throws XMPPException XMPP exception containing the XMPP error
|
||||
*/
|
||||
private void cancelRequest() throws XMPPException {
|
||||
String errorMessage = "Could not establish socket with any provided host";
|
||||
XMPPError error = new XMPPError(XMPPError.Condition.item_not_found, errorMessage);
|
||||
IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
|
||||
this.manager.getConnection().sendPacket(errorIQ);
|
||||
throw new XMPPException(errorMessage, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
|
||||
*
|
||||
* @param selectedHost the used SOCKS5 proxy
|
||||
* @return the response to the SOCKS5 Bytestream request
|
||||
*/
|
||||
private Bytestream createUsedHostResponse(StreamHost selectedHost) {
|
||||
Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
|
||||
response.setTo(this.bytestreamRequest.getFrom());
|
||||
response.setType(IQ.Type.RESULT);
|
||||
response.setPacketID(this.bytestreamRequest.getPacketID());
|
||||
response.setUsedHost(selectedHost.getJID());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the connection failure counter by one for the given address.
|
||||
*
|
||||
* @param address the address the connection failure counter should be increased
|
||||
*/
|
||||
private void incrementConnectionFailures(String address) {
|
||||
Integer count = ADDRESS_BLACKLIST.get(address);
|
||||
ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how often the connection to the given address failed.
|
||||
*
|
||||
* @param address the address
|
||||
* @return number of connection failures
|
||||
*/
|
||||
private int getConnectionFailures(String address) {
|
||||
Integer count = ADDRESS_BLACKLIST.get(address);
|
||||
return count != null ? count : 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package org.jivesoftware.smackx.bytestreams.socks5;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
|
||||
import org.jivesoftware.smackx.bytestreams.BytestreamSession;
|
||||
|
||||
/**
|
||||
* Socks5BytestreamSession class represents a SOCKS5 Bytestream session.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class Socks5BytestreamSession implements BytestreamSession {
|
||||
|
||||
/* the underlying socket of the SOCKS5 Bytestream */
|
||||
private final Socket socket;
|
||||
|
||||
/* flag to indicate if this session is a direct or mediated connection */
|
||||
private final boolean isDirect;
|
||||
|
||||
protected Socks5BytestreamSession(Socket socket, boolean isDirect) {
|
||||
this.socket = socket;
|
||||
this.isDirect = isDirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if the session is established through a direct connection between
|
||||
* the initiator and target, <code>false</code> if the session is mediated over a SOCKS proxy.
|
||||
*
|
||||
* @return <code>true</code> if session is a direct connection, <code>false</code> if session is
|
||||
* mediated over a SOCKS5 proxy
|
||||
*/
|
||||
public boolean isDirect() {
|
||||
return this.isDirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if the session is mediated over a SOCKS proxy, <code>false</code>
|
||||
* if this session is established through a direct connection between the initiator and target.
|
||||
*
|
||||
* @return <code>true</code> if session is mediated over a SOCKS5 proxy, <code>false</code> if
|
||||
* session is a direct connection
|
||||
*/
|
||||
public boolean isMediated() {
|
||||
return !this.isDirect;
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return this.socket.getInputStream();
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return this.socket.getOutputStream();
|
||||
}
|
||||
|
||||
public int getReadTimeout() throws IOException {
|
||||
try {
|
||||
return this.socket.getSoTimeout();
|
||||
}
|
||||
catch (SocketException e) {
|
||||
throw new IOException("Error on underlying Socket");
|
||||
}
|
||||
}
|
||||
|
||||
public void setReadTimeout(int timeout) throws IOException {
|
||||
try {
|
||||
this.socket.setSoTimeout(timeout);
|
||||
}
|
||||
catch (SocketException e) {
|
||||
throw new IOException("Error on underlying Socket");
|
||||
}
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
this.socket.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
/**
|
||||
* All rights reserved. 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;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
|
||||
|
||||
/**
|
||||
* The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a
|
||||
* SOCKS5 proxy requires authentication. This implementation only supports the no-authentication
|
||||
* authentication method.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
class Socks5Client {
|
||||
|
||||
/* stream host containing network settings and name of the SOCKS5 proxy */
|
||||
protected StreamHost streamHost;
|
||||
|
||||
/* SHA-1 digest identifying the SOCKS5 stream */
|
||||
protected String digest;
|
||||
|
||||
/**
|
||||
* Constructor for a SOCKS5 client.
|
||||
*
|
||||
* @param streamHost containing network settings of the SOCKS5 proxy
|
||||
* @param digest identifying the SOCKS5 Bytestream
|
||||
*/
|
||||
public Socks5Client(StreamHost streamHost, String digest) {
|
||||
this.streamHost = streamHost;
|
||||
this.digest = digest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the initialized socket that can be used to transfer data between peers via the SOCKS5
|
||||
* proxy.
|
||||
*
|
||||
* @param timeout timeout to connect to SOCKS5 proxy in milliseconds
|
||||
* @return socket the initialized socket
|
||||
* @throws IOException if initializing the socket failed due to a network error
|
||||
* @throws XMPPException if establishing connection to SOCKS5 proxy failed
|
||||
* @throws TimeoutException if connecting to SOCKS5 proxy timed out
|
||||
* @throws InterruptedException if the current thread was interrupted while waiting
|
||||
*/
|
||||
public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
|
||||
TimeoutException {
|
||||
|
||||
// wrap connecting in future for timeout
|
||||
FutureTask<Socket> futureTask = new FutureTask<Socket>(new Callable<Socket>() {
|
||||
|
||||
public Socket call() throws Exception {
|
||||
|
||||
// initialize socket
|
||||
Socket socket = new Socket();
|
||||
SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress(),
|
||||
streamHost.getPort());
|
||||
socket.connect(socketAddress);
|
||||
|
||||
// initialize connection to SOCKS5 proxy
|
||||
if (!establish(socket)) {
|
||||
|
||||
// initialization failed, close socket
|
||||
socket.close();
|
||||
throw new XMPPException("establishing connection to SOCKS5 proxy failed");
|
||||
|
||||
}
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
});
|
||||
Thread executor = new Thread(futureTask);
|
||||
executor.start();
|
||||
|
||||
// get connection to initiator with timeout
|
||||
try {
|
||||
return futureTask.get(timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch (ExecutionException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause != null) {
|
||||
// case exceptions to comply with method signature
|
||||
if (cause instanceof IOException) {
|
||||
throw (IOException) cause;
|
||||
}
|
||||
if (cause instanceof XMPPException) {
|
||||
throw (XMPPException) cause;
|
||||
}
|
||||
}
|
||||
|
||||
// throw generic IO exception if unexpected exception was thrown
|
||||
throw new IOException("Error while connection to SOCKS5 proxy");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the connection to the SOCKS5 proxy by negotiating authentication method and
|
||||
* requesting a stream for the given digest. Currently only the no-authentication method is
|
||||
* supported by the Socks5Client.
|
||||
* <p>
|
||||
* Returns <code>true</code> if a stream could be established, otherwise <code>false</code>. If
|
||||
* <code>false</code> is returned the given Socket should be closed.
|
||||
*
|
||||
* @param socket connected to a SOCKS5 proxy
|
||||
* @return <code>true</code> if if a stream could be established, otherwise <code>false</code>.
|
||||
* If <code>false</code> is returned the given Socket should be closed.
|
||||
* @throws IOException if a network error occurred
|
||||
*/
|
||||
protected boolean establish(Socket socket) throws IOException {
|
||||
|
||||
/*
|
||||
* use DataInputStream/DataOutpuStream to assure read and write is completed in a single
|
||||
* statement
|
||||
*/
|
||||
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
||||
|
||||
// authentication negotiation
|
||||
byte[] cmd = new byte[3];
|
||||
|
||||
cmd[0] = (byte) 0x05; // protocol version 5
|
||||
cmd[1] = (byte) 0x01; // number of authentication methods supported
|
||||
cmd[2] = (byte) 0x00; // authentication method: no-authentication required
|
||||
|
||||
out.write(cmd);
|
||||
out.flush();
|
||||
|
||||
byte[] response = new byte[2];
|
||||
in.readFully(response);
|
||||
|
||||
// check if server responded with correct version and no-authentication method
|
||||
if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// request SOCKS5 connection with given address/digest
|
||||
byte[] connectionRequest = createSocks5ConnectRequest();
|
||||
out.write(connectionRequest);
|
||||
out.flush();
|
||||
|
||||
// receive response
|
||||
byte[] connectionResponse;
|
||||
try {
|
||||
connectionResponse = Socks5Utils.receiveSocks5Message(in);
|
||||
}
|
||||
catch (XMPPException e) {
|
||||
return false; // server answered in an unsupported way
|
||||
}
|
||||
|
||||
// verify response
|
||||
connectionRequest[1] = (byte) 0x00; // set expected return status to 0
|
||||
return Arrays.equals(connectionRequest, connectionResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SOCKS5 connection request message. It contains the command "connect", the address
|
||||
* type "domain" and the digest as address.
|
||||
* <p>
|
||||
* (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
|
||||
*
|
||||
* @return SOCKS5 connection request message
|
||||
*/
|
||||
private byte[] createSocks5ConnectRequest() {
|
||||
byte addr[] = this.digest.getBytes();
|
||||
|
||||
byte[] data = new byte[7 + addr.length];
|
||||
data[0] = (byte) 0x05; // version (SOCKS5)
|
||||
data[1] = (byte) 0x01; // command (1 - connect)
|
||||
data[2] = (byte) 0x00; // reserved byte (always 0)
|
||||
data[3] = (byte) 0x03; // address type (3 - domain name)
|
||||
data[4] = (byte) addr.length; // address length
|
||||
System.arraycopy(addr, 0, data, 5, addr.length); // address
|
||||
data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
|
||||
data[data.length - 1] = (byte) 0;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* All rights reserved. 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.jivesoftware.smack.Connection;
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
|
||||
import org.jivesoftware.smackx.packet.SyncPacketSend;
|
||||
|
||||
/**
|
||||
* Implementation of a SOCKS5 client used on the initiators side. This is needed because connecting
|
||||
* to the local SOCKS5 proxy differs form the regular way to connect to a SOCKS5 proxy. Additionally
|
||||
* a remote SOCKS5 proxy has to be activated by the initiator before data can be transferred between
|
||||
* the peers.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
class Socks5ClientForInitiator extends Socks5Client {
|
||||
|
||||
/* the XMPP connection used to communicate with the SOCKS5 proxy */
|
||||
private Connection connection;
|
||||
|
||||
/* the session ID used to activate SOCKS5 stream */
|
||||
private String sessionID;
|
||||
|
||||
/* the target JID used to activate SOCKS5 stream */
|
||||
private String target;
|
||||
|
||||
/**
|
||||
* Creates a new SOCKS5 client for the initiators side.
|
||||
*
|
||||
* @param streamHost containing network settings of the SOCKS5 proxy
|
||||
* @param digest identifying the SOCKS5 Bytestream
|
||||
* @param connection the XMPP connection
|
||||
* @param sessionID the session ID of the SOCKS5 Bytestream
|
||||
* @param target the target JID of the SOCKS5 Bytestream
|
||||
*/
|
||||
public Socks5ClientForInitiator(StreamHost streamHost, String digest, Connection connection,
|
||||
String sessionID, String target) {
|
||||
super(streamHost, digest);
|
||||
this.connection = connection;
|
||||
this.sessionID = sessionID;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
|
||||
TimeoutException {
|
||||
Socket socket = null;
|
||||
|
||||
// check if stream host is the local SOCKS5 proxy
|
||||
if (this.streamHost.getJID().equals(this.connection.getUser())) {
|
||||
Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
|
||||
socket = socks5Server.getSocket(this.digest);
|
||||
if (socket == null) {
|
||||
throw new XMPPException("target is not connected to SOCKS5 proxy");
|
||||
}
|
||||
}
|
||||
else {
|
||||
socket = super.getSocket(timeout);
|
||||
|
||||
try {
|
||||
activate();
|
||||
}
|
||||
catch (XMPPException e) {
|
||||
socket.close();
|
||||
throw new XMPPException("activating SOCKS5 Bytestream failed", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the SOCKS5 Bytestream by sending a XMPP SOCKS5 Bytestream activation packet to the
|
||||
* SOCKS5 proxy.
|
||||
*/
|
||||
private void activate() throws XMPPException {
|
||||
Bytestream activate = createStreamHostActivation();
|
||||
// if activation fails #getReply throws an exception
|
||||
SyncPacketSend.getReply(this.connection, activate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SOCKS5 Bytestream activation packet.
|
||||
*
|
||||
* @return SOCKS5 Bytestream activation packet
|
||||
*/
|
||||
private Bytestream createStreamHostActivation() {
|
||||
Bytestream activate = new Bytestream(this.sessionID);
|
||||
activate.setMode(null);
|
||||
activate.setType(IQ.Type.SET);
|
||||
activate.setTo(this.streamHost.getJID());
|
||||
|
||||
activate.setToActivate(this.target);
|
||||
|
||||
return activate;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
/**
|
||||
* All rights reserved. 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;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.jivesoftware.smack.SmackConfiguration;
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
|
||||
/**
|
||||
* The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
|
||||
* setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by
|
||||
* invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by
|
||||
* default.
|
||||
* <p>
|
||||
* The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code>
|
||||
* in the <code>smack-config.xml</code> or by invoking
|
||||
* {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the
|
||||
* port to a negative value Smack tries to the absolute value and all following until it finds an
|
||||
* open port.
|
||||
* <p>
|
||||
* If your application is running on a machine with multiple network interfaces or if you want to
|
||||
* provide your public address in case you are behind a NAT router, invoke
|
||||
* {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of
|
||||
* local network addresses used for outgoing SOCKS5 Bytestream requests.
|
||||
* <p>
|
||||
* The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
|
||||
* in the process of establishing a SOCKS5 Bytestream (
|
||||
* {@link Socks5BytestreamManager#establishSession(String)}).
|
||||
* <p>
|
||||
* This Implementation has the following limitations:
|
||||
* <ul>
|
||||
* <li>only supports the no-authentication authentication method</li>
|
||||
* <li>only supports the <code>connect</code> command and will not answer correctly to other
|
||||
* commands</li>
|
||||
* <li>only supports requests with the domain address type and will not correctly answer to requests
|
||||
* with other address types</li>
|
||||
* </ul>
|
||||
* (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
public class Socks5Proxy {
|
||||
|
||||
/* SOCKS5 proxy singleton */
|
||||
private static Socks5Proxy socks5Server;
|
||||
|
||||
/* reusable implementation of a SOCKS5 proxy server process */
|
||||
private Socks5ServerProcess serverProcess;
|
||||
|
||||
/* thread running the SOCKS5 server process */
|
||||
private Thread serverThread;
|
||||
|
||||
/* server socket to accept SOCKS5 connections */
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
/* assigns a connection to a digest */
|
||||
private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
|
||||
|
||||
/* list of digests connections should be stored */
|
||||
private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());
|
||||
|
||||
private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>());
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private Socks5Proxy() {
|
||||
this.serverProcess = new Socks5ServerProcess();
|
||||
|
||||
// add default local address
|
||||
try {
|
||||
this.localAddresses.add(InetAddress.getLocalHost().getHostAddress());
|
||||
}
|
||||
catch (UnknownHostException e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local SOCKS5 proxy server.
|
||||
*
|
||||
* @return the local SOCKS5 proxy server
|
||||
*/
|
||||
public static synchronized Socks5Proxy getSocks5Proxy() {
|
||||
if (socks5Server == null) {
|
||||
socks5Server = new Socks5Proxy();
|
||||
}
|
||||
if (SmackConfiguration.isLocalSocks5ProxyEnabled()) {
|
||||
socks5Server.start();
|
||||
}
|
||||
return socks5Server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
|
||||
*/
|
||||
public synchronized void start() {
|
||||
if (isRunning()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) {
|
||||
int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort());
|
||||
for (int i = 0; i < 65535 - port; i++) {
|
||||
try {
|
||||
this.serverSocket = new ServerSocket(port + i);
|
||||
break;
|
||||
}
|
||||
catch (IOException e) {
|
||||
// port is used, try next one
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort());
|
||||
}
|
||||
|
||||
if (this.serverSocket != null) {
|
||||
this.serverThread = new Thread(this.serverProcess);
|
||||
this.serverThread.start();
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
// couldn't setup server
|
||||
System.err.println("couldn't setup local SOCKS5 proxy on port "
|
||||
+ SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.serverSocket.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if (this.serverThread != null && this.serverThread.isAlive()) {
|
||||
try {
|
||||
this.serverThread.interrupt();
|
||||
this.serverThread.join();
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
this.serverThread = null;
|
||||
this.serverSocket = null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given address to the list of local network addresses.
|
||||
* <p>
|
||||
* Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
|
||||
* This may be necessary if your application is running on a machine with multiple network
|
||||
* interfaces or if you want to provide your public address in case you are behind a NAT router.
|
||||
* <p>
|
||||
* The order of the addresses used is determined by the order you add addresses.
|
||||
* <p>
|
||||
* Note that the list of addresses initially contains the address returned by
|
||||
* <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
|
||||
* addresses by invoking {@link #replaceLocalAddresses(List)}.
|
||||
*
|
||||
* @param address the local network address to add
|
||||
*/
|
||||
public void addLocalAddress(String address) {
|
||||
if (address == null) {
|
||||
throw new IllegalArgumentException("address may not be null");
|
||||
}
|
||||
this.localAddresses.add(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given address from the list of local network addresses. This address will then no
|
||||
* longer be used of outgoing SOCKS5 Bytestream requests.
|
||||
*
|
||||
* @param address the local network address to remove
|
||||
*/
|
||||
public void removeLocalAddress(String address) {
|
||||
this.localAddresses.remove(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable list of the local network addresses that will be used for streamhost
|
||||
* candidates of outgoing SOCKS5 Bytestream requests.
|
||||
*
|
||||
* @return unmodifiable list of the local network addresses
|
||||
*/
|
||||
public List<String> getLocalAddresses() {
|
||||
return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the list of local network addresses.
|
||||
* <p>
|
||||
* Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
|
||||
* want to define their order. This may be necessary if your application is running on a machine
|
||||
* with multiple network interfaces or if you want to provide your public address in case you
|
||||
* are behind a NAT router.
|
||||
*
|
||||
* @param addresses the new list of local network addresses
|
||||
*/
|
||||
public void replaceLocalAddresses(List<String> addresses) {
|
||||
if (addresses == null) {
|
||||
throw new IllegalArgumentException("list must not be null");
|
||||
}
|
||||
this.localAddresses.clear();
|
||||
this.localAddresses.addAll(addresses);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
|
||||
*
|
||||
* @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
|
||||
*/
|
||||
public int getPort() {
|
||||
if (!isRunning()) {
|
||||
return -1;
|
||||
}
|
||||
return this.serverSocket.getLocalPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the socket for the given digest. A socket will be returned if the given digest has
|
||||
* been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
|
||||
* connected to the SOCKS5 proxy.
|
||||
*
|
||||
* @param digest identifying the connection
|
||||
* @return socket or null if there is no socket for the given digest
|
||||
*/
|
||||
protected Socket getSocket(String digest) {
|
||||
return this.connectionMap.get(digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given digest to the list of allowed transfers. Only connections for allowed transfers
|
||||
* are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
|
||||
* the local SOCKS5 proxy that don't contain an allowed digest are discarded.
|
||||
*
|
||||
* @param digest to be added to the list of allowed transfers
|
||||
*/
|
||||
protected void addTransfer(String digest) {
|
||||
this.allowedConnections.add(digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given digest from the list of allowed transfers. After invoking this method
|
||||
* already stored connections with the given digest will be removed.
|
||||
* <p>
|
||||
* The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
|
||||
* occurred while establishing the connection or if the connection is not allowed anymore.
|
||||
*
|
||||
* @param digest to be removed from the list of allowed transfers
|
||||
*/
|
||||
protected void removeTransfer(String digest) {
|
||||
this.allowedConnections.remove(digest);
|
||||
this.connectionMap.remove(digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
|
||||
* <code>false</code>.
|
||||
*
|
||||
* @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
|
||||
* <code>false</code>
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return this.serverSocket != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of a simplified SOCKS5 proxy server.
|
||||
*/
|
||||
private class Socks5ServerProcess implements Runnable {
|
||||
|
||||
public void run() {
|
||||
while (true) {
|
||||
Socket socket = null;
|
||||
|
||||
try {
|
||||
|
||||
if (Socks5Proxy.this.serverSocket.isClosed()
|
||||
|| Thread.currentThread().isInterrupted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// accept connection
|
||||
socket = Socks5Proxy.this.serverSocket.accept();
|
||||
|
||||
// initialize connection
|
||||
establishConnection(socket);
|
||||
|
||||
}
|
||||
catch (SocketException e) {
|
||||
/*
|
||||
* do nothing, if caused by closing the server socket, thread will terminate in
|
||||
* next loop
|
||||
*/
|
||||
}
|
||||
catch (Exception e) {
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
catch (IOException e1) {
|
||||
/* do nothing */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Negotiates a SOCKS5 connection and stores it on success.
|
||||
*
|
||||
* @param socket connection to the client
|
||||
* @throws XMPPException if client requests a connection in an unsupported way
|
||||
* @throws IOException if a network error occurred
|
||||
*/
|
||||
private void establishConnection(Socket socket) throws XMPPException, IOException {
|
||||
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
||||
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||
|
||||
// first byte is version should be 5
|
||||
int b = in.read();
|
||||
if (b != 5) {
|
||||
throw new XMPPException("Only SOCKS5 supported");
|
||||
}
|
||||
|
||||
// second byte number of authentication methods supported
|
||||
b = in.read();
|
||||
|
||||
// read list of supported authentication methods
|
||||
byte[] auth = new byte[b];
|
||||
in.readFully(auth);
|
||||
|
||||
byte[] authMethodSelectionResponse = new byte[2];
|
||||
authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
|
||||
|
||||
// only authentication method 0, no authentication, supported
|
||||
boolean noAuthMethodFound = false;
|
||||
for (int i = 0; i < auth.length; i++) {
|
||||
if (auth[i] == (byte) 0x00) {
|
||||
noAuthMethodFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!noAuthMethodFound) {
|
||||
authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
|
||||
out.write(authMethodSelectionResponse);
|
||||
out.flush();
|
||||
throw new XMPPException("Authentication method not supported");
|
||||
}
|
||||
|
||||
authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
|
||||
out.write(authMethodSelectionResponse);
|
||||
out.flush();
|
||||
|
||||
// receive connection request
|
||||
byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
|
||||
|
||||
// extract digest
|
||||
String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);
|
||||
|
||||
// return error if digest is not allowed
|
||||
if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
|
||||
connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
|
||||
out.write(connectionRequest);
|
||||
out.flush();
|
||||
|
||||
throw new XMPPException("Connection is not allowed");
|
||||
}
|
||||
|
||||
connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
|
||||
out.write(connectionRequest);
|
||||
out.flush();
|
||||
|
||||
// store connection
|
||||
Socks5Proxy.this.connectionMap.put(responseDigest, socket);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* All rights reserved. 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;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A collection of utility methods for SOcKS5 messages.
|
||||
*
|
||||
* @author Henning Staib
|
||||
*/
|
||||
class Socks5Utils {
|
||||
|
||||
/**
|
||||
* Returns a SHA-1 digest of the given parameters as specified in <a
|
||||
* href="http://xmpp.org/extensions/xep-0065.html#impl-socks5">XEP-0065</a>.
|
||||
*
|
||||
* @param sessionID for the SOCKS5 Bytestream
|
||||
* @param initiatorJID JID of the initiator of a SOCKS5 Bytestream
|
||||
* @param targetJID JID of the target of a SOCKS5 Bytestream
|
||||
* @return SHA-1 digest of the given parameters
|
||||
*/
|
||||
public static String createDigest(String sessionID, String initiatorJID, String targetJID) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
b.append(sessionID).append(initiatorJID).append(targetJID);
|
||||
return StringUtils.hash(b.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a SOCKS5 message from the given InputStream. The message can either be a SOCKS5 request
|
||||
* message or a SOCKS5 response message.
|
||||
* <p>
|
||||
* (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
|
||||
*
|
||||
* @param in the DataInputStream to read the message from
|
||||
* @return the SOCKS5 message
|
||||
* @throws IOException if a network error occurred
|
||||
* @throws XMPPException if the SOCKS5 message contains an unsupported address type
|
||||
*/
|
||||
public static byte[] receiveSocks5Message(DataInputStream in) throws IOException, XMPPException {
|
||||
byte[] header = new byte[5];
|
||||
in.readFully(header, 0, 5);
|
||||
|
||||
if (header[3] != (byte) 0x03) {
|
||||
throw new XMPPException("Unsupported SOCKS5 address type");
|
||||
}
|
||||
|
||||
int addressLength = header[4];
|
||||
|
||||
byte[] response = new byte[7 + addressLength];
|
||||
System.arraycopy(header, 0, response, 0, header.length);
|
||||
|
||||
in.readFully(response, header.length, addressLength + 2);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
/**
|
||||
* All rights reserved. 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.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
|
||||
/**
|
||||
* A packet representing part of a SOCKS5 Bytestream negotiation.
|
||||
*
|
||||
* @author Alexander Wenckus
|
||||
*/
|
||||
public class Bytestream extends IQ {
|
||||
|
||||
private String sessionID;
|
||||
|
||||
private Mode mode = Mode.tcp;
|
||||
|
||||
private final List<StreamHost> streamHosts = new ArrayList<StreamHost>();
|
||||
|
||||
private StreamHostUsed usedHost;
|
||||
|
||||
private Activate toActivate;
|
||||
|
||||
/**
|
||||
* The default constructor
|
||||
*/
|
||||
public Bytestream() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
super();
|
||||
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 String 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 String JID, final String address, final int port) {
|
||||
StreamHost host = new StreamHost(JID, address);
|
||||
host.setPort(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 Collection<StreamHost> getStreamHosts() {
|
||||
return Collections.unmodifiableCollection(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 String 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 String 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 packet 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 packet 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 packet 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 String targetID) {
|
||||
this.toActivate = new Activate(targetID);
|
||||
}
|
||||
|
||||
public String getChildElementXML() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
|
||||
buf.append("<query xmlns=\"http://jabber.org/protocol/bytestreams\"");
|
||||
if (this.getType().equals(IQ.Type.SET)) {
|
||||
if (getSessionID() != null) {
|
||||
buf.append(" sid=\"").append(getSessionID()).append("\"");
|
||||
}
|
||||
if (getMode() != null) {
|
||||
buf.append(" mode = \"").append(getMode()).append("\"");
|
||||
}
|
||||
buf.append(">");
|
||||
if (getToActivate() == null) {
|
||||
for (StreamHost streamHost : getStreamHosts()) {
|
||||
buf.append(streamHost.toXML());
|
||||
}
|
||||
}
|
||||
else {
|
||||
buf.append(getToActivate().toXML());
|
||||
}
|
||||
}
|
||||
else if (this.getType().equals(IQ.Type.RESULT)) {
|
||||
buf.append(">");
|
||||
if (getUsedHost() != null) {
|
||||
buf.append(getUsedHost().toXML());
|
||||
}
|
||||
// A result from the server can also contain stream hosts
|
||||
else if (countStreamHosts() > 0) {
|
||||
for (StreamHost host : streamHosts) {
|
||||
buf.append(host.toXML());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.getType().equals(IQ.Type.GET)) {
|
||||
return buf.append("/>").toString();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
buf.append("</query>");
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Packet 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 PacketExtension {
|
||||
|
||||
public static String NAMESPACE = "";
|
||||
|
||||
public static String ELEMENTNAME = "streamhost";
|
||||
|
||||
private final String JID;
|
||||
|
||||
private final String addy;
|
||||
|
||||
private int port = 0;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*
|
||||
* @param JID The JID of the stream host.
|
||||
* @param address The internet address of the stream host.
|
||||
*/
|
||||
public StreamHost(final String JID, final String address) {
|
||||
this.JID = JID;
|
||||
this.addy = address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JID of the stream host.
|
||||
*
|
||||
* @return Returns the JID of the stream host.
|
||||
*/
|
||||
public String 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the port of the stream host.
|
||||
*
|
||||
* @param port The port on which the potential stream host would accept the connection.
|
||||
*/
|
||||
public void setPort(final int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return NAMESPACE;
|
||||
}
|
||||
|
||||
public String getElementName() {
|
||||
return ELEMENTNAME;
|
||||
}
|
||||
|
||||
public String toXML() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
|
||||
buf.append("<").append(getElementName()).append(" ");
|
||||
buf.append("jid=\"").append(getJID()).append("\" ");
|
||||
buf.append("host=\"").append(getAddress()).append("\" ");
|
||||
if (getPort() != 0) {
|
||||
buf.append("port=\"").append(getPort()).append("\"");
|
||||
}
|
||||
else {
|
||||
buf.append("zeroconf=\"_jabber.bytestreams\"");
|
||||
}
|
||||
buf.append("/>");
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After selected a SOCKS5 stream host and successfully connecting, the target of the file
|
||||
* transfer returns a byte stream packet with the stream host used extension.
|
||||
*
|
||||
* @author Alexander Wenckus
|
||||
*/
|
||||
public static class StreamHostUsed implements PacketExtension {
|
||||
|
||||
public String NAMESPACE = "";
|
||||
|
||||
public static String ELEMENTNAME = "streamhost-used";
|
||||
|
||||
private final String JID;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*
|
||||
* @param JID The JID of the selected stream host.
|
||||
*/
|
||||
public StreamHostUsed(final String JID) {
|
||||
this.JID = JID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JID of the selected stream host.
|
||||
*
|
||||
* @return Returns the JID of the selected stream host.
|
||||
*/
|
||||
public String getJID() {
|
||||
return JID;
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return NAMESPACE;
|
||||
}
|
||||
|
||||
public String getElementName() {
|
||||
return ELEMENTNAME;
|
||||
}
|
||||
|
||||
public String toXML() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("<").append(getElementName()).append(" ");
|
||||
buf.append("jid=\"").append(getJID()).append("\" ");
|
||||
buf.append("/>");
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The packet sent by the stream initiator to the stream proxy to activate the connection.
|
||||
*
|
||||
* @author Alexander Wenckus
|
||||
*/
|
||||
public static class Activate implements PacketExtension {
|
||||
|
||||
public String NAMESPACE = "";
|
||||
|
||||
public static String ELEMENTNAME = "activate";
|
||||
|
||||
private final String target;
|
||||
|
||||
/**
|
||||
* Default constructor specifying the target of the stream.
|
||||
*
|
||||
* @param target The target of the stream.
|
||||
*/
|
||||
public Activate(final String target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target of the activation.
|
||||
*
|
||||
* @return Returns the target of the activation.
|
||||
*/
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return NAMESPACE;
|
||||
}
|
||||
|
||||
public String getElementName() {
|
||||
return ELEMENTNAME;
|
||||
}
|
||||
|
||||
public String toXML() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("<").append(getElementName()).append(">");
|
||||
buf.append(getTarget());
|
||||
buf.append("</").append(getElementName()).append(">");
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* All rights reserved. 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.provider;
|
||||
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.provider.IQProvider;
|
||||
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
/**
|
||||
* Parses a bytestream packet.
|
||||
*
|
||||
* @author Alexander Wenckus
|
||||
*/
|
||||
public class BytestreamsProvider implements IQProvider {
|
||||
|
||||
public IQ parseIQ(XmlPullParser parser) throws Exception {
|
||||
boolean done = false;
|
||||
|
||||
Bytestream toReturn = new Bytestream();
|
||||
|
||||
String id = parser.getAttributeValue("", "sid");
|
||||
String mode = parser.getAttributeValue("", "mode");
|
||||
|
||||
// streamhost
|
||||
String JID = null;
|
||||
String host = null;
|
||||
String port = null;
|
||||
|
||||
int eventType;
|
||||
String elementName;
|
||||
while (!done) {
|
||||
eventType = parser.next();
|
||||
elementName = parser.getName();
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
if (elementName.equals(Bytestream.StreamHost.ELEMENTNAME)) {
|
||||
JID = parser.getAttributeValue("", "jid");
|
||||
host = parser.getAttributeValue("", "host");
|
||||
port = parser.getAttributeValue("", "port");
|
||||
}
|
||||
else if (elementName.equals(Bytestream.StreamHostUsed.ELEMENTNAME)) {
|
||||
toReturn.setUsedHost(parser.getAttributeValue("", "jid"));
|
||||
}
|
||||
else if (elementName.equals(Bytestream.Activate.ELEMENTNAME)) {
|
||||
toReturn.setToActivate(parser.getAttributeValue("", "jid"));
|
||||
}
|
||||
}
|
||||
else if (eventType == XmlPullParser.END_TAG) {
|
||||
if (elementName.equals("streamhost")) {
|
||||
if (port == null) {
|
||||
toReturn.addStreamHost(JID, host);
|
||||
}
|
||||
else {
|
||||
toReturn.addStreamHost(JID, host, Integer.parseInt(port));
|
||||
}
|
||||
JID = null;
|
||||
host = null;
|
||||
port = null;
|
||||
}
|
||||
else if (elementName.equals("query")) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toReturn.setMode((Bytestream.Mode.fromName(mode)));
|
||||
toReturn.setSessionID(id);
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue