Commit e053d44d authored by Alex Wenckus's avatar Alex Wenckus Committed by alex

File transfer proxy. JM-108

git-svn-id: http://svn.igniterealtime.org/svn/repos/wildfire/trunk@3447 b35dd754-fafc-0310-a699-88a17e54d16e
parent a558c3a1
/**
* $Revision$
* $Date$
* <p/>
* Copyright (C) 1999-2005 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.wildfire.filetransfer;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.jivesoftware.wildfire.*;
import org.jivesoftware.wildfire.auth.UnauthorizedException;
import org.jivesoftware.wildfire.container.BasicModule;
import org.jivesoftware.wildfire.disco.DiscoInfoProvider;
import org.jivesoftware.wildfire.disco.DiscoItemsProvider;
import org.jivesoftware.wildfire.disco.DiscoServerItem;
import org.jivesoftware.wildfire.disco.ServerItemsProvider;
import org.jivesoftware.wildfire.forms.spi.XDataFormImpl;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* Manages the transfering of files between two remote entities on the jabber network.
* This class acts independtly as a Jabber component from the rest of the server, according to
* the Jabber <a href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 bytestreams protocol</a>.
*
* @author Alexander Wenckus
*/
public class FileTransferProxy extends BasicModule
implements ServerItemsProvider, DiscoInfoProvider, DiscoItemsProvider,
RoutableChannelHandler {
private static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
private String proxyServiceName;
private IQHandlerInfo info;
private RoutingTable routingTable;
private PacketRouter router;
private int proxyPort;
private String proxyIP;
private ProxyConnectionManager connectionManager;
public FileTransferProxy() {
super("SOCKS5 file transfer proxy");
info = new IQHandlerInfo("query", NAMESPACE);
}
public boolean handleIQ(IQ packet) throws UnauthorizedException {
Element childElement = packet.getChildElement();
String namespace = null;
// ignore errors
if (packet.getType() == IQ.Type.error) {
return true;
}
if (childElement != null) {
namespace = childElement.getNamespaceURI();
}
if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
try {
IQ reply = XMPPServer.getInstance().getIQDiscoInfoHandler().handleIQ(packet);
router.route(reply);
return true;
}
catch (UnauthorizedException e) {
// Do nothing. This error should never happen
}
}
else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
try {
// a component
IQ reply = XMPPServer.getInstance().getIQDiscoItemsHandler().handleIQ(packet);
router.route(reply);
return true;
}
catch (UnauthorizedException e) {
// Do nothing. This error should never happen
}
}
else if (NAMESPACE.equals(namespace)) {
if (packet.getType() == IQ.Type.get) {
IQ reply = IQ.createResultIQ(packet);
reply.setChildElement("query", NAMESPACE);
Element response = DocumentHelper.createElement(QName.get("streamhost"));
response.addAttribute("jid", getServiceDomain());
response.addAttribute("host", proxyIP);
response.addAttribute("port", String.valueOf(proxyPort));
reply.getChildElement().add(response);
router.route(reply);
return true;
}
else if (packet.getType() == IQ.Type.set && childElement != null) {
String sid = childElement.attributeValue("sid");
JID from = packet.getFrom();
JID to = new JID(childElement.elementTextTrim("activate"));
IQ reply = IQ.createResultIQ(packet);
try {
connectionManager.activate(from, to, sid);
}
catch (IllegalArgumentException ie) {
Log.error("Error activating connection", ie);
reply.setType(IQ.Type.error);
reply.setError(new PacketError(PacketError.Condition.not_allowed));
}
router.route(reply);
return true;
}
}
return false;
}
public IQHandlerInfo getInfo() {
return info;
}
public void initialize(XMPPServer server) {
super.initialize(server);
proxyServiceName = JiveGlobals.getProperty("xmpp.proxy.service", "proxy");
routingTable = server.getRoutingTable();
router = server.getPacketRouter();
// Load the external IP and port information
try {
proxyIP = JiveGlobals.getProperty("xmpp.proxy.externalip",
InetAddress.getLocalHost().getHostAddress());
}
catch (UnknownHostException e) {
Log.error("Couldn't discover local host", e);
}
proxyPort = JiveGlobals.getIntProperty("xmpp.proxy.port", 7777);
connectionManager = new ProxyConnectionManager(proxyPort);
}
public void start() {
super.start();
routingTable.addRoute(getAddress(), this);
connectionManager.processConnections();
}
public void stop() {
super.stop();
routingTable.removeRoute(getAddress());
}
/**
* Returns the fully-qualifed domain name of this chat service.
* The domain is composed by the service name and the
* name of the XMPP server where the service is running.
*
* @return the file transfer server domain (service name + host name).
*/
public String getServiceDomain() {
return proxyServiceName + "." + XMPPServer.getInstance().getServerInfo().getName();
}
public JID getAddress() {
return new JID(null, getServiceDomain(), null);
}
public Iterator getItems() {
ArrayList<DiscoServerItem> items = new ArrayList<DiscoServerItem>();
items.add(new DiscoServerItem() {
public String getJID() {
return getServiceDomain();
}
public String getName() {
return "Socks 5 Bytestreams Proxy";
}
public String getAction() {
return null;
}
public String getNode() {
return null;
}
public DiscoInfoProvider getDiscoInfoProvider() {
return FileTransferProxy.this;
}
public DiscoItemsProvider getDiscoItemsProvider() {
return FileTransferProxy.this;
}
});
return items.iterator();
}
public Iterator<Element> getIdentities(String name, String node, JID senderJID) {
List<Element> identities = new ArrayList<Element>();
// Answer the identity of the proxy
Element identity = DocumentHelper.createElement("identity");
identity.addAttribute("category", "proxy");
identity.addAttribute("name", "SOCKS5 Bytestreams Service");
identity.addAttribute("type", "bytestreams");
identities.add(identity);
return identities.iterator();
}
public Iterator<String> getFeatures(String name, String node, JID senderJID) {
return Arrays.asList(new String[]{NAMESPACE, "http://jabber.org/protocol/disco#info"})
.iterator();
}
public XDataFormImpl getExtendedInfo(String name, String node, JID senderJID) {
return null;
}
public boolean hasInfo(String name, String node, JID senderJID) {
Log.info("Name Info: " + name);
return true;
}
public Iterator<Element> getItems(String name, String node, JID senderJID) {
// A proxy server has no items
return null;
}
public void process(Packet packet) throws UnauthorizedException, PacketException {
// Check if the packet is a disco request or a packet with namespace iq:register
if (packet instanceof IQ) {
if (handleIQ((IQ) packet)) {
return;
}
else {
IQ reply = IQ.createResultIQ((IQ) packet);
reply.setChildElement(((IQ) packet).getChildElement().createCopy());
reply.setError(PacketError.Condition.feature_not_implemented);
router.route(reply);
}
}
}
}
/**
* $Revision$
* $Date$
* Copyright (C) 1999-2005 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.wildfire.filetransfer;
import org.jivesoftware.util.Cache;
import org.jivesoftware.util.Log;
import org.xmpp.packet.JID;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Manages the connections to the proxy server. The connections go through two stages before
* file transfer begins. The first stage is when the file transfer target initiates a connection
* to this manager. Stage two is when the initiator connects, the manager will then match the two
* connections using the unique SHA-1 hash defined in the SOCKS5 protocol.
*
* @author Alexander Wenckus
*/
public class ProxyConnectionManager {
private int port;
private Map<String, ProxyTransfer> connectionMap =
new Cache("File Transfer Cache", -1, 1000 * 60 * 10);
private final Object connectionLock = new Object();
private ExecutorService executor = Executors.newCachedThreadPool();
public ProxyConnectionManager(int port) {
this.port = port;
}
/*
* Processes the clients connecting to the proxy matching the initiator and target together.
* This is the main loop of the manager which will run until the process is canceled.
*/
public void processConnections() {
executor.submit(new Runnable() {
public void run() {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
}
catch (IOException e) {
Log.error("Error creating server socket", e);
return;
}
while (serverSocket != null) {
final Socket socket;
try {
socket = serverSocket.accept();
}
catch (IOException e) {
Log.error("Error accepting procy connection", e);
continue;
}
executor.submit(new Runnable() {
public void run() {
try {
processConnection(socket);
}
catch (IOException ie) {
Log.error("Error processing file transfer proxy connection",
ie);
}
}
});
}
}
});
}
private void processConnection(Socket connection) throws IOException {
OutputStream out = new DataOutputStream(connection.getOutputStream());
InputStream in = new DataInputStream(connection.getInputStream());
// first byte is version should be 5
int b = in.read();
if (b != 5) {
throw new IOException("Only SOCKS5 supported");
}
// second byte number of authentication methods supported
b = in.read();
int[] auth = new int[b];
for (int i = 0; i < b; i++) {
auth[i] = in.read();
}
int authMethod = -1;
for (int anAuth : auth) {
authMethod = (anAuth == 0 ? 0 : -1); // only auth method
// 0, no
// authentication,
// supported
if (authMethod == 0) {
break;
}
}
if (authMethod != 0) {
throw new IOException("Authentication method not supported");
}
// No auth method so respond with success
byte[] cmd = new byte[2];
cmd[0] = (byte) 0x05;
cmd[1] = (byte) 0x00;
out.write(cmd);
String responseDigest = processIncomingSocks5Message(in);
cmd = createOutgoingSocks5Message(0, responseDigest);
synchronized (connectionLock) {
ProxyTransfer transfer = connectionMap.get(responseDigest);
if (transfer == null) {
connectionMap.put(responseDigest, new ProxyTransfer(responseDigest, connection));
}
else {
transfer.setInitiatorSocket(connection);
}
}
if (!connection.isConnected()) {
throw new IOException("Socket closed by remote user");
}
out.write(cmd);
}
private String processIncomingSocks5Message(InputStream in)
throws IOException {
// read the version and command
byte[] cmd = new byte[5];
in.read(cmd, 0, 5);
// read the digest
byte[] addr = new byte[cmd[4]];
in.read(addr, 0, addr.length);
String digest = new String(addr);
in.read();
in.read();
return digest;
}
private byte[] createOutgoingSocks5Message(int cmd, String digest) {
byte addr[] = digest.getBytes();
byte[] data = new byte[7 + addr.length];
data[0] = (byte) 5;
data[1] = (byte) cmd;
data[2] = (byte) 0;
data[3] = (byte) 0x3;
data[4] = (byte) addr.length;
System.arraycopy(addr, 0, data, 5, addr.length);
data[data.length - 2] = (byte) 0;
data[data.length - 1] = (byte) 0;
return data;
}
/**
* Activates the stream, this method should be called when the initiator sends the activate
* packet after both parties have connected to the proxy.
*
* @param initiator The initiator or sender of the file transfer.
* @param target The target or reciever of the file transfer.
* @param sid The sessionid the uniquely identifies the transfer between
* the two participants.
* @throws IllegalArgumentException This exception is thrown when the activated transfer does
* not exist or is missing one or both of the realted sockets.
*/
void activate(JID initiator, JID target, String sid) {
final String digest = createDigest(sid, initiator, target);
ProxyTransfer temp;
synchronized (connectionLock) {
temp = connectionMap.get(digest);
}
final ProxyTransfer transfer = temp;
// check to make sure we have all the required
// information to start the transfer
if (transfer == null || !transfer.isActivatable()) {
throw new IllegalArgumentException("Transfer doesn't exist or is missing parameters");
}
transfer.setInitiatorJID(initiator.toString());
transfer.setTargetJID(target.toString());
transfer.setTransferSession(sid);
transfer.setTransferFuture(executor.submit(new Runnable() {
public void run() {
try {
transfer(transfer);
}
catch (IOException e) {
Log.error("Error during file transfer", e);
}
finally {
connectionMap.remove(digest);
}
}
}));
}
private void transfer(ProxyTransfer transfer) throws IOException {
InputStream in = transfer.getInitiatorSocket().getInputStream();
OutputStream out = transfer.getTargetSocket().getOutputStream();
final byte[] b = new byte[1000];
int count = 0;
int amountWritten = 0;
count = in.read(b);
while (count != -1) {
// write to the output stream
out.write(b, 0, count);
amountWritten += count;
// read more bytes from the input stream
count = in.read(b);
}
}
/**
* Creates the digest needed for a byte stream. It is the SHA1(sessionID +
* initiator + target).
*
* @param sessionID The sessionID of the stream negotiation
* @param initiator The inititator of the stream negotiation
* @param target The target of the stream negotiation
* @return SHA-1 hash of the three parameters
*/
private String createDigest(final String sessionID, final JID initiator,
final JID target) {
return hash(sessionID + initiator.getNode()
+ "@" + initiator.getDomain() + "/"
+ initiator.getResource()
+ target.getNode() + "@"
+ target.getDomain() + "/"
+ target.getResource());
}
private static MessageDigest digest = null;
private synchronized static String hash(String data) {
if (digest == null) {
try {
digest = MessageDigest.getInstance("SHA-1");
}
catch (NoSuchAlgorithmException nsae) {
Log.error("Failed to load the SHA-1 MessageDigest. " +
"Jive will be unable to function normally.", nsae);
}
}
// Now, compute hash.
try {
digest.update(data.getBytes("UTF-8"));
}
catch (UnsupportedEncodingException e) {
Log.error(e);
}
return encodeHex(digest.digest());
}
private static String encodeHex(byte[] bytes) {
StringBuilder hex = new StringBuilder(bytes.length * 2);
for (int i = 0; i < bytes.length; i++) {
if (((int) bytes[i] & 0xff) < 0x10) {
hex.append("0");
}
hex.append(Integer.toString((int) bytes[i] & 0xff, 16));
}
return hex.toString();
}
}
/**
* $Revision$
* $Date$
*
* Copyright (C) 1999-2005 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.wildfire.filetransfer;
import java.net.Socket;
import java.util.concurrent.Future;
/**
* Tracks the different connections related to a file transfer. There are two connections, the
* initiator and the target and when both connections are completed the transfer can begin.
*/
public class ProxyTransfer {
private String initiatorJID;
private Socket initiatorSocket;
private Socket targetSocket;
private String targetJID;
private String transferDigest;
private String transferSession;
private Future<?> future;
public ProxyTransfer(String transferDigest, Socket targetSocket) {
this.transferDigest = transferDigest;
this.targetSocket = targetSocket;
}
public String getInitiatorJID() {
return initiatorJID;
}
public void setInitiatorJID(String initiatorJID) {
this.initiatorJID = initiatorJID;
}
public Socket getInitiatorSocket() {
return initiatorSocket;
}
public void setInitiatorSocket(Socket initiatorSocket) {
this.initiatorSocket = initiatorSocket;
}
public Socket getTargetSocket() {
return targetSocket;
}
public void setTargetSocket(Socket targetSocket) {
this.targetSocket = targetSocket;
}
public String getTargetJID() {
return targetJID;
}
public void setTargetJID(String targetJID) {
this.targetJID = targetJID;
}
public String getTransferDigest() {
return transferDigest;
}
public void setTransferDigest(String transferDigest) {
this.transferDigest = transferDigest;
}
public String getTransferSession() {
return transferSession;
}
public void setTransferSession(String transferSession) {
this.transferSession = transferSession;
}
/**
* Returns true if the Bytestream is ready to be activated and the transfer can begin.
*
* @return Returns true if the Bytestream is ready to be activated.
*/
public boolean isActivatable() {
return ((initiatorSocket != null) && (targetSocket != null));
}
public void setTransferFuture(Future<?> future) {
this.future = future;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment