/** * $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.packet.XMPPError; import org.jivesoftware.smack.util.StringUtils; 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.StreamInitiation; import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.Collection; import java.util.Iterator; /** * 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"; /** * The number of connection failures it takes to a streamhost for that particular streamhost * to be blacklisted. When a host is blacklisted no more connection attempts will be made to * it for a period of 2 hours. */ private static final int CONNECT_FAILURE_THRESHOLD = 2; public static boolean isAllowLocalProxyHost = true; private final XMPPConnection connection; private Socks5TransferNegotiatorManager transferNegotiatorManager; public Socks5TransferNegotiator(Socks5TransferNegotiatorManager transferNegotiatorManager, final XMPPConnection connection) { this.connection = connection; this.transferNegotiatorManager = transferNegotiatorManager; } public PacketFilter getInitiationPacketFilter(String from, String sessionID) { return new AndFilter(new FromMatchesFilter(from), new BytestreamSIDFilter(sessionID)); } /* * (non-Javadoc) * * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateDownload( * org.jivesoftware.smackx.packet.StreamInitiation, java.io.File) */ InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException { Bytestream streamHostsInfo = (Bytestream) streamInitiation; if (streamHostsInfo.getType().equals(IQ.Type.ERROR)) { throw new XMPPException(streamHostsInfo.getError()); } SelectedHostInfo selectedHost; try { // select appropriate host selectedHost = selectHost(streamHostsInfo); } catch (XMPPException ex) { if (ex.getXMPPError() != null) { IQ errorPacket = super.createError(streamHostsInfo.getTo(), streamHostsInfo.getFrom(), streamHostsInfo.getPacketID(), ex.getXMPPError()); connection.sendPacket(errorPacket); } throw (ex); } // send used-host confirmation Bytestream streamResponse = createUsedHostConfirmation( selectedHost.selectedHost, streamHostsInfo.getFrom(), streamHostsInfo.getTo(), streamHostsInfo.getPacketID()); connection.sendPacket(streamResponse); try { PushbackInputStream stream = new PushbackInputStream( selectedHost.establishedSocket.getInputStream()); int firstByte = stream.read(); stream.unread(firstByte); return stream; } catch (IOException e) { throw new XMPPException("Error establishing input stream", e); } } public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException { Packet streamInitiation = initiateIncomingStream(connection, initiation); return negotiateIncomingStream(streamInitiation); } /** * 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; } /** * Selects a host to connect to over which the file will be transmitted. * * @param streamHostsInfo the packet recieved from the initiator containing the available hosts * to transfer the file * @return the selected host and socket that were created. * @throws XMPPException when there is no appropriate host. */ 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(); String address = selectedHost.getAddress(); // Check to see if this address has been blacklisted int failures = getConnectionFailures(address); if (failures >= CONNECT_FAILURE_THRESHOLD) { continue; } // establish socket try { socket = new Socket(address, selectedHost .getPort()); establishSOCKS5ConnectionToProxy(socket, createDigest( streamHostsInfo.getSessionID(), streamHostsInfo .getFrom(), streamHostsInfo.getTo())); break; } catch (IOException e) { e.printStackTrace(); incrementConnectionFailures(address); selectedHost = null; socket = null; } } if (selectedHost == null || socket == null || !socket.isConnected()) { String errorMessage = "Could not establish socket with any provided host"; throw new XMPPException(errorMessage, new XMPPError( XMPPError.Condition.no_acceptable, errorMessage)); } return new SelectedHostInfo(selectedHost, socket); } private void incrementConnectionFailures(String address) { transferNegotiatorManager.incrementConnectionFailures(address); } private int getConnectionFailures(String address) { return transferNegotiatorManager.getConnectionFailures(address); } /** * 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 createOutgoingStream(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 new BufferedOutputStream(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 { Socks5TransferNegotiatorManager.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 the digest of the userids and the session id * @param query the query which the response is being awaited * @return the selected host * @throws XMPPException when the response from the peer is an error or doesn't occur * @throws IOException when there is an error establishing the local socket */ private SelectedHostInfo waitForUsedHostResponse(String sessionID, final Socks5TransferNegotiatorManager.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(SmackConfiguration.getPacketReplyTimeout()); collector.cancel(); Bytestream response; if (packet != null && packet instanceof Bytestream) { response = (Bytestream) packet; } else { throw new XMPPException("Unexpected response from remote user"); } // check for an error if (response.getType().equals(IQ.Type.ERROR)) { throw new XMPPException("Remote client returned error, stream hosts expected", response.getError()); } 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(SmackConfiguration .getPacketReplyTimeout()); collector.cancel(); if (!serverResponse.getType().equals(IQ.Type.RESULT)) { info.establishedSocket.close(); return null; } return info; } } private Socks5TransferNegotiatorManager.ProxyProcess establishListeningSocket() throws IOException { return transferNegotiatorManager.addTransfer(); } private void cleanupListeningSocket() { transferNegotiatorManager.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 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); } // make sure the proxies have been initialized completely Collection