/** * $RCSfile$ * $Revision: $ * $Date: $ * * Copyright 2003-2006 Jive Software. * * 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.filetransfer; import org.jivesoftware.smack.PacketCollector; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.filter.AndFilter; import org.jivesoftware.smack.filter.FromMatchesFilter; import org.jivesoftware.smack.filter.PacketFilter; import org.jivesoftware.smack.filter.PacketIDFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.ServiceDiscoveryManager; import org.jivesoftware.smackx.packet.Bytestream; import org.jivesoftware.smackx.packet.Bytestream.StreamHost; import org.jivesoftware.smackx.packet.Bytestream.StreamHostUsed; import org.jivesoftware.smackx.packet.DiscoverInfo; import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; import org.jivesoftware.smackx.packet.DiscoverItems; import org.jivesoftware.smackx.packet.DiscoverItems.Item; import org.jivesoftware.smackx.packet.StreamInitiation; import java.io.*; import java.net.*; import java.util.*; /** * A SOCKS5 bytestream is negotiated partly over the XMPP XML stream and partly * over a seperate socket. The actual transfer though takes place over a * seperatly created socket. *
* A SOCKS5 file transfer generally has three parites, the initiator, the * target, and the stream host. The stream host is a specialized SOCKS5 proxy * setup on the server, or, the Initiator can act as the Stream Host if the * proxy is not available. * * The advantage of having a seperate proxy over directly connecting to * eachother is if the Initator and the Target are not on the same LAN and are * operating behind NAT, the proxy allows for a common location for both parties * to connect to and transfer the file. * * Smack will attempt to automatically discover any proxies present on your * server. If any are detected they will be forwarded to any user attempting to * recieve files from you. * * @author Alexander Wenckus * @see JEP-0065: SOCKS5 * Bytestreams */ public class Socks5TransferNegotiator extends StreamNegotiator { protected static final String NAMESPACE = "http://jabber.org/protocol/bytestreams"; public static boolean isAllowLocalProxyHost = true; private final XMPPConnection connection; private List proxies; private List streamHosts; private ProxyProcess proxyProcess; public Socks5TransferNegotiator(final XMPPConnection connection) { this.connection = connection; } /* * (non-Javadoc) * * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateDownload(org.jivesoftware.smackx.packet.StreamInitiation, * java.io.File) */ public InputStream initiateIncomingStream(StreamInitiation initiation) throws XMPPException { StreamInitiation response = super.createInitiationAccept(initiation, NAMESPACE); // establish collector to await response PacketCollector collector = connection .createPacketCollector(new AndFilter(new FromMatchesFilter(initiation.getFrom()), new BytestreamSIDFilter(initiation.getSessionID()))); connection.sendPacket(response); Bytestream streamHostsInfo = (Bytestream) collector .nextResult(SmackConfiguration.getPacketReplyTimeout()); if (streamHostsInfo == null) { throw new XMPPException("No response from file transfer initiator"); } else if (streamHostsInfo.getType().equals(IQ.Type.ERROR)) { throw new XMPPException(streamHostsInfo.getError()); } collector.cancel(); // select appropriate host SelectedHostInfo selectedHost = selectHost(streamHostsInfo); // send used-host confirmation Bytestream streamResponse = createUsedHostConfirmation( selectedHost.selectedHost, streamHostsInfo.getFrom(), streamHostsInfo.getTo(), streamHostsInfo.getPacketID()); connection.sendPacket(streamResponse); try { return selectedHost.establishedSocket.getInputStream(); } catch (IOException e) { throw new XMPPException("Error establishing input stream", e); } } /** * The used host confirmation is sent to the initiator to indicate to them * which of the hosts they provided has been selected and successfully * connected to. * * @param selectedHost The selected stream host. * @param initiator The initiator of the stream. * @param target The target of the stream. * @param packetID The of the packet being responded to. * @return The packet that was created to send to the initiator. */ private Bytestream createUsedHostConfirmation(StreamHost selectedHost, String initiator, String target, String packetID) { Bytestream streamResponse = new Bytestream(); streamResponse.setTo(initiator); streamResponse.setFrom(target); streamResponse.setType(IQ.Type.RESULT); streamResponse.setPacketID(packetID); streamResponse.setUsedHost(selectedHost.getJID()); return streamResponse; } /** * @param streamHostsInfo * @return * @throws XMPPException */ private SelectedHostInfo selectHost(Bytestream streamHostsInfo) throws XMPPException { Iterator it = streamHostsInfo.getStreamHosts().iterator(); StreamHost selectedHost = null; Socket socket = null; while (it.hasNext()) { selectedHost = (StreamHost) it.next(); // establish socket try { socket = new Socket(selectedHost.getAddress(), selectedHost .getPort()); establishSOCKS5ConnectionToProxy(socket, createDigest( streamHostsInfo.getSessionID(), streamHostsInfo .getFrom(), streamHostsInfo.getTo())); break; } catch (IOException e) { e.printStackTrace(); selectedHost = null; socket = null; continue; } } if (selectedHost == null || socket == null) { throw new XMPPException( "Could not establish socket with any provided host"); } return new SelectedHostInfo(selectedHost, socket); } /** * 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 String initiator, final String target) { return StringUtils.hash(sessionID + StringUtils.parseName(initiator) + "@" + StringUtils.parseServer(initiator) + "/" + StringUtils.parseResource(initiator) + StringUtils.parseName(target) + "@" + StringUtils.parseServer(target) + "/" + StringUtils.parseResource(target)); } /* * (non-Javadoc) * * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateUpload(java.lang.String, * org.jivesoftware.smackx.packet.StreamInitiation, java.io.File) */ public OutputStream initiateOutgoingStream(String streamID, String initiator, String target) throws XMPPException { Socket socket; try { socket = initBytestreamSocket(streamID, initiator, target); } catch (Exception e) { throw new XMPPException("Error establishing transfer socket", e); } if (socket != null) { try { return socket.getOutputStream(); } catch (IOException e) { throw new XMPPException("Error establishing output stream", e); } } return null; } private Socket initBytestreamSocket(final String sessionID, String initiator, String target) throws Exception { ProxyProcess process; try { process = establishListeningSocket(); } catch (IOException io) { process = null; } String localIP; try { localIP = discoverLocalIP(); } catch (UnknownHostException e1) { localIP = null; } Bytestream query = createByteStreamInit(initiator, target, sessionID, localIP, (process != null ? process.getPort() : 0)); // if the local host is one of the options we need to wait for the // remote connection. Socket conn = waitForUsedHostResponse(sessionID, process, createDigest( sessionID, initiator, target), query).establishedSocket; cleanupListeningSocket(); return conn; } /** * Waits for the peer to respond with which host they chose to use. * * @param sessionID The session id of the stream. * @param proxy The server socket which will listen locally for remote * connections. * @param digest * @param query * @return * @throws XMPPException * @throws IOException */ private SelectedHostInfo waitForUsedHostResponse(String sessionID, final ProxyProcess proxy, final String digest, final Bytestream query) throws XMPPException, IOException { SelectedHostInfo info = new SelectedHostInfo(); PacketCollector collector = connection .createPacketCollector(new PacketIDFilter(query.getPacketID())); connection.sendPacket(query); Packet packet = collector.nextResult(); collector.cancel(); Bytestream response; if (packet instanceof Bytestream) { response = (Bytestream) packet; } else { throw new XMPPException("Unexpected response from remote user"); } StreamHostUsed used = response.getUsedHost(); StreamHost usedHost = query.getStreamHost(used.getJID()); if (usedHost == null) { throw new XMPPException("Remote user responded with unknown host"); } // The local computer is acting as the proxy if (used.getJID().equals(query.getFrom())) { info.establishedSocket = proxy.getSocket(digest); info.selectedHost = usedHost; return info; } else { info.establishedSocket = new Socket(usedHost.getAddress(), usedHost .getPort()); establishSOCKS5ConnectionToProxy(info.establishedSocket, digest); Bytestream activate = createByteStreamActivate(sessionID, response .getTo(), usedHost.getJID(), response.getFrom()); collector = connection.createPacketCollector(new PacketIDFilter( activate.getPacketID())); connection.sendPacket(activate); IQ serverResponse = (IQ) collector.nextResult(); collector.cancel(); if (!serverResponse.getType().equals(IQ.Type.RESULT)) { info.establishedSocket.close(); return null; } return info; } } private ProxyProcess establishListeningSocket() throws IOException { if (proxyProcess == null) { proxyProcess = new ProxyProcess(new ServerSocket(7777)); proxyProcess.start(); } proxyProcess.addTransfer(); return proxyProcess; } private void cleanupListeningSocket() { if (proxyProcess == null) { return; } proxyProcess.removeTransfer(); } private String discoverLocalIP() throws UnknownHostException { return InetAddress.getLocalHost().getHostAddress(); } /** * The bytestream init looks like this: * ** <iq type='set' * from='initiator@host1/foo' * to='target@host2/bar' * id='initiate'> * <query xmlns='http://jabber.org/protocol/bytestreams' * sid='mySID' * mode='tcp'> * <streamhost * jid='initiator@host1/foo' * host='192.168.4.1' * port='5086'/> * <streamhost * jid='proxy.host3' * host='24.24.24.1' * zeroconf='_jabber.bytestreams'/> * </query> * </iq> ** * @param from initiator@host1/foo - The file transfer initiator. * @param to target@host2/bar - The file transfer target. * @param sid 'mySID' - the unique identifier for this file transfer * @param localIP The IP of the local machine if it is being provided, null otherwise. * @param port The port of the local mahine if it is being provided, null otherwise. * @return Returns the created Bytestream packet */ private Bytestream createByteStreamInit(final String from, final String to, final String sid, final String localIP, final int port) { Bytestream bs = new Bytestream(); bs.setTo(to); bs.setFrom(from); bs.setSessionID(sid); bs.setType(IQ.Type.SET); bs.setMode(Bytestream.Mode.TCP); if (localIP != null && port > 0) { bs.addStreamHost(from, localIP, port); } if (proxies == null) { initProxies(); } if (streamHosts != null) { Iterator it = streamHosts.iterator(); while (it.hasNext()) { bs.addStreamHost((StreamHost) it.next()); } } return bs; } private void initProxies() { proxies = new ArrayList(); ServiceDiscoveryManager manager = ServiceDiscoveryManager .getInstanceFor(connection); DiscoverItems discoItems; try { discoItems = manager.discoverItems(connection.getServiceName()); DiscoverItems.Item item; DiscoverInfo info; DiscoverInfo.Identity identity; Iterator it = discoItems.getItems(); while (it.hasNext()) { item = (Item) it.next(); info = manager.discoverInfo(item.getEntityID()); Iterator itx = info.getIdentities(); while (itx.hasNext()) { identity = (Identity) itx.next(); if (identity.getCategory().equalsIgnoreCase("proxy") && identity.getType().equalsIgnoreCase( "bytestreams")) { proxies.add(info.getFrom()); } } } } catch (XMPPException e) { return; } if (proxies.size() > 0) { initStreamHosts(); } } private void initStreamHosts() { List streamHosts = new ArrayList(); Iterator it = proxies.iterator(); IQ query; PacketCollector collector; Bytestream response; while (it.hasNext()) { String jid = it.next().toString(); query = new IQ() { public String getChildElementXML() { return "