org.jivesoftware.openfire.filetransfer.proxy.ProxyConnectionManager Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 1999-2008 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.openfire.filetransfer.proxy;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.filetransfer.FileTransferManager;
import org.jivesoftware.openfire.filetransfer.FileTransferRejectedException;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.stats.StatisticsManager;
import org.jivesoftware.openfire.stats.i18nStatistic;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
/**
* 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 static final Logger Log = LoggerFactory.getLogger(ProxyConnectionManager.class);
private static final String proxyTransferRate = "proxyTransferRate";
private Cache connectionMap;
private final Object connectionLock = new Object();
private ExecutorService executor = Executors.newCachedThreadPool();
private Future> socketProcess;
private ServerSocket serverSocket;
private int proxyPort;
private FileTransferManager transferManager;
private String className;
public ProxyConnectionManager(FileTransferManager manager) {
String cacheName = "File Transfer";
connectionMap = CacheFactory.createCache(cacheName);
className = JiveGlobals.getProperty("provider.transfer.proxy",
"org.jivesoftware.openfire.filetransfer.proxy.DefaultProxyTransfer");
transferManager = manager;
StatisticsManager.getInstance().addStatistic(proxyTransferRate, new ProxyTracker());
}
/*
* 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.
*/
synchronized void processConnections(final InetAddress bindInterface, final int port) {
if (socketProcess != null) {
if (proxyPort == port) {
return;
}
}
reset();
socketProcess = executor.submit(new Runnable() {
@Override
public void run() {
try {
serverSocket = new ServerSocket(port, -1, bindInterface);
}
catch (IOException e) {
Log.error("Error creating server socket", e);
return;
}
while (serverSocket.isBound()) {
final Socket socket;
try {
socket = serverSocket.accept();
}
catch (IOException e) {
if (!serverSocket.isClosed()) {
Log.error("Error accepting proxy connection", e);
continue;
}
else {
break;
}
}
executor.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(socket);
}
catch (IOException ie) {
Log.error("Error processing file transfer proxy connection",
ie);
try {
socket.close();
}
catch (IOException e) {
/* Do Nothing */
}
}
}
});
}
}
});
proxyPort = port;
}
public int getProxyPort() {
return proxyPort;
}
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);
try {
synchronized (connectionLock) {
ProxyTransfer transfer = connectionMap.get(responseDigest);
if (transfer == null) {
transfer = createProxyTransfer(responseDigest, connection);
transferManager.registerProxyTransfer(responseDigest, transfer);
connectionMap.put(responseDigest, transfer);
}
else {
transfer.setInputStream(connection.getInputStream());
}
}
cmd = createOutgoingSocks5Message(0, responseDigest);
out.write(cmd);
}
catch (UnauthorizedException eu) {
cmd = createOutgoingSocks5Message(2, responseDigest);
out.write(cmd);
throw new IOException("Illegal proxy transfer");
}
}
private ProxyTransfer createProxyTransfer(String transferDigest, Socket targetSocket)
throws IOException {
ProxyTransfer provider;
try {
Class c = ClassUtils.forName(className);
provider = (ProxyTransfer) c.newInstance();
}
catch (Exception e) {
Log.error("Error loading proxy transfer provider: " + className, e);
provider = new DefaultProxyTransfer();
}
provider.setTransferDigest(transferDigest);
provider.setOutputStream(targetSocket.getOutputStream());
return provider;
}
@SuppressWarnings({"ResultOfMethodCallIgnored"})
private static String processIncomingSocks5Message(InputStream in)
throws IOException {
// read the version and command
byte[] cmd = new byte[5];
int read = in.read(cmd, 0, 5);
if (read != 5) {
throw new IOException("Error reading Socks5 version and command");
}
// read the digest
byte[] addr = new byte[cmd[4]];
read = in.read(addr, 0, addr.length);
if (read != addr.length) {
throw new IOException("Error reading provided address");
}
String digest = new String(addr);
in.read();
in.read();
return digest;
}
private static 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;
}
synchronized void shutdown() {
disable();
executor.shutdown();
StatisticsManager.getInstance().removeStatistic(proxyTransferRate);
}
/**
* 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 receiver of the file transfer.
* @param sid The session id that 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 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.setInitiator(initiator.toString());
transfer.setTarget(target.toString());
transfer.setSessionID(sid);
transfer.setTransferFuture(executor.submit(new Runnable() {
@Override
public void run() {
try {
transferManager.fireFileTransferStart( transfer.getSessionID(), true );
}
catch (FileTransferRejectedException e) {
notifyFailure(transfer, e);
return;
}
try {
transfer.doTransfer();
transferManager.fireFileTransferCompleted( transfer.getSessionID(), true );
}
catch (IOException e) {
Log.error("Error during file transfer", e);
transferManager.fireFileTransferCompleted( transfer.getSessionID(), false );
}
finally {
connectionMap.remove(digest);
}
}
}));
}
private void notifyFailure(ProxyTransfer transfer, FileTransferRejectedException e) {
}
/**
* 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 initiator of the stream negotiation
* @param target The target of the stream negotiation
* @return SHA-1 hash of the three parameters
*/
public static String createDigest(final String sessionID, final JID initiator,
final JID target) {
return StringUtils.hash(sessionID + initiator.getNode()
+ "@" + initiator.getDomain() + "/"
+ initiator.getResource()
+ target.getNode() + "@"
+ target.getDomain() + "/"
+ target.getResource(), "SHA-1");
}
public boolean isRunning() {
return socketProcess != null && !socketProcess.isDone();
}
public void disable() {
reset();
}
private void reset() {
if (socketProcess != null) {
socketProcess.cancel(true);
socketProcess = null;
}
if (serverSocket != null) {
try {
serverSocket.close();
}
catch (IOException e) {
Log.warn("Error closing proxy listening socket", e);
}
}
}
private static class ProxyTracker extends i18nStatistic {
public ProxyTracker() {
super("filetransferproxy.transfered", Statistic.Type.rate);
}
@Override
public double sample() {
return (ProxyOutputStream.amountTransferred.getAndSet(0) / 1000d);
}
@Override
public boolean isPartialSample() {
return true;
}
}
}