package org.limewire.net;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.io.IOUtils;
import org.limewire.io.NetworkInstanceUtils;
import org.limewire.net.ProxySettings.ProxyType;
import org.limewire.nio.channel.NIOMultiplexor;
import org.limewire.nio.observer.ConnectObserver;
import org.limewire.nio.statemachine.BlockingStateMachine;
import org.limewire.nio.statemachine.IOState;
import org.limewire.nio.statemachine.IOStateMachine;
import org.limewire.nio.statemachine.IOStateObserver;
import org.limewire.nio.statemachine.PossibleIOState;
import org.limewire.nio.statemachine.ReadSkipState;
import org.limewire.nio.statemachine.ReadState;
import org.limewire.nio.statemachine.SimpleReadState;
import org.limewire.nio.statemachine.SimpleWriteState;
import org.limewire.util.BufferUtils;
import org.limewire.util.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
* Manages whether or not a connection is proxied, and how it is proxied.
*/
@Singleton
class ProxyManagerImpl implements ProxyManager {
private static final Log LOG = LogFactory.getLog(ProxyManagerImpl.class);
private final ProxySettings proxySettings;
private final NetworkInstanceUtils networkInstanceUtils;
@Inject
public ProxyManagerImpl(ProxySettings proxySettings, NetworkInstanceUtils networkInstanceUtils) {
this.proxySettings = proxySettings;
this.networkInstanceUtils = networkInstanceUtils;
}
/* (non-Javadoc)
* @see org.limewire.net.ProxyManager#getProxyType(java.net.InetAddress)
*/
public ProxyType getProxyType(InetAddress address) {
// if the user specified that he wanted to use a proxy to connect
// to the network, we will use that proxy unless the host we
// want to connect to is a private address
ProxyType connectionType = proxySettings.getCurrentProxyType();
assert connectionType != null;
boolean valid = connectionType != ProxyType.NONE &&
(!networkInstanceUtils.isPrivateAddress(address) ||
proxySettings.isProxyForPrivateEnabled());
if(valid)
return connectionType;
else
return ProxyType.NONE;
}
/* (non-Javadoc)
* @see org.limewire.net.ProxyManager#establishProxy(int, java.net.Socket, java.net.InetSocketAddress, int)
*/
public Socket establishProxy(ProxyType type, Socket proxySocket, InetSocketAddress addr, int timeout) throws IOException {
proxySocket.setSoTimeout(timeout);
List<IOState> states = getProxyStates(type, addr);
InputStream in = proxySocket.getInputStream();
OutputStream out = proxySocket.getOutputStream();
BlockingStateMachine bsm = new BlockingStateMachine(states, in, out);
try {
bsm.process();
} finally {
bsm.shutdown(); // release the buffer.
}
proxySocket.setSoTimeout(0);
return proxySocket;
}
public ProxyConnector getConnectorFor(ProxyType type, ConnectObserver observer, InetSocketAddress host, int timeout) {
return new ProxyConnectorImpl(type, observer, host, timeout);
}
public InetSocketAddress getProxyHost() throws UnknownHostException {
return new InetSocketAddress(proxySettings.getProxyHost(), proxySettings.getProxyPort());
}
/** Returns the correct states for the given proxy. */
private List<IOState> getProxyStates(ProxyType type, InetSocketAddress addr) throws IOException {
switch(type) {
case HTTP: return getHttpStates(addr);
case SOCKS4: return getSocksV4States(addr);
case SOCKS5: return getSocksV5States(addr);
default:
throw new IOException("Unknown proxy type.");
}
}
/** Returns a list of states for processing a SOCKSv4 proxy. */
private List<IOState> getSocksV4States(final InetSocketAddress addr) {
List<IOState> states = new LinkedList<IOState>();
byte[] hostBytes = addr.getAddress().getAddress();
int port = addr.getPort();
byte[] portBytes = new byte[2];
portBytes[0] = ((byte) (port >> 8));
portBytes[1] = ((byte) port);
boolean auth = proxySettings.isProxyAuthenticationRequired();
String authName = proxySettings.getProxyUsername();
// couldn't find specs about username encoding, but username is most likely ascii,
byte[] authData = auth ? StringUtils.toAsciiBytes(authName) : new byte[0];
ByteBuffer outgoing = ByteBuffer.allocate(2 + portBytes.length + hostBytes.length + authData.length + 1);
outgoing.put((byte)0x04);
outgoing.put((byte)0x01);
outgoing.put(portBytes);
outgoing.put(hostBytes);
outgoing.put(authData);
outgoing.put((byte)0x00);
outgoing.flip();
states.add(new SimpleWriteState(outgoing));
// read response
states.add(new SimpleReadState(8) {
@Override
public void validateBuffer(ByteBuffer buffer) throws IOException {
// version should be 0 but some socks proxys answer 4
int version = buffer.get(0);
if (version != 0x00 && version != 0x04)
throw new IOException("Invalid version from socks proxy: " + version + " expected 0 or 4");
// read the status, 0x5A is success
int status = buffer.get(1);
if (status != 0x5A)
throw new IOException("Request rejected with status: " + status);
}
});
return states;
}
/** Returns all states necessary for a SOCKSv5 proxy connection. */
private List<IOState> getSocksV5States(InetSocketAddress addr) {
List<IOState> states = new LinkedList<IOState>();
// If authenticating, write: # of auth methods, support no auth, support usr/password auth
// If not authenticating, write: # of auth methods, support no auth.
byte[] auths = proxySettings.isProxyAuthenticationRequired() ?
new byte[] { 0x02, 0x00, 0x02 } : new byte[] { 0x01, 0x00 };
ByteBuffer outgoing = ByteBuffer.allocate(1 + auths.length);
outgoing.put((byte)0x05);
outgoing.put(auths);
outgoing.flip();
states.add(new SimpleWriteState(outgoing));
final AtomicBoolean authSwitch = new AtomicBoolean(false);
states.add(new SimpleReadState(2) {
@Override
public void validateBuffer(ByteBuffer buffer) throws IOException {
int version = buffer.get(0);
if (version != 0x05)
throw new IOException("Invalid version from socks proxy: " + version + " expected 5");
// Turn on authentication, if told to.
int auth_method = buffer.get(1);
if(auth_method == 0x02)
authSwitch.set(true);
}
});
// username/password
String username = proxySettings.getProxyUsername();
if(username == null)
username = "";
byte[] usernameBytes = StringUtils.toAsciiBytes(username);
String password = proxySettings.getProxyPassword();
if(password == null)
password = "";
byte[] passwordBytes = StringUtils.toAsciiBytes(password);
outgoing = ByteBuffer.allocate(1 + 1 + usernameBytes.length + 1 + passwordBytes.length);
outgoing.put((byte)0x01);
outgoing.put((byte)usernameBytes.length);
outgoing.put(usernameBytes);
outgoing.put((byte)passwordBytes.length);
outgoing.put(passwordBytes);
outgoing.flip();
states.add(new PossibleIOState(authSwitch, new SimpleWriteState(outgoing)));
states.add(new PossibleIOState(authSwitch, new SimpleReadState(2) {
@Override
public void validateBuffer(ByteBuffer buffer) throws IOException {
int version = buffer.get(0);
if (version != 0x01)
throw new IOException("Invalid version for authentication: " + version + " expected 1");
int status = buffer.get(1);
if (status != 0x00)
throw new IOException("Authentication failed with status: " + status);
}
}));
byte[] hostBytes = addr.getAddress().getAddress();
int port = addr.getPort();
byte[] portBytes = new byte[2];
portBytes[0] = ((byte) (port >> 8));
portBytes[1] = ((byte) port);
outgoing = ByteBuffer.allocate(1 + 1 + 1 + 1 + hostBytes.length + portBytes.length);
outgoing.put((byte)0x05); // version again
outgoing.put((byte)0x01); // connect command,
// 0x02 would be bind, 0x03 UDP associate
outgoing.put((byte)0x00); // reserved field, must be 0x00
outgoing.put((byte)0x01); // address type: 0x01 is IPv4, 0x04 would be IPv6
outgoing.put(hostBytes); //host to connect to
outgoing.put(portBytes); //port to connect to
outgoing.flip();
states.add(new SimpleWriteState(outgoing));
final AtomicLong amountToSkip = new AtomicLong(0);
final AtomicBoolean domainLengthSwitch = new AtomicBoolean(false);
states.add(new SimpleReadState(4) {
@Override
public void validateBuffer(ByteBuffer buffer) throws IOException {
int version = buffer.get(0);
if (version != 0x05)
throw new IOException("Invalid version from socks proxy: " + version + " expected 5");
int status = buffer.get(1);
if (status != 0x00)
throw new IOException("Request rejected with status: " + status);
int addrType = buffer.get(3);
switch(addrType) {
case 1: amountToSkip.set(6); break; // IPv4
case 3: domainLengthSwitch.set(true); break; // Domain Name
case 4: amountToSkip.set(18); break; // IPv6
}
}
});
states.add(new PossibleIOState(domainLengthSwitch, new SimpleReadState(1) {
@Override
public void validateBuffer(ByteBuffer buffer) throws IOException {
amountToSkip.set(buffer.get(0) + 2);
}
}));
states.add(new ReadSkipState(amountToSkip));
return states;
}
/** Returns the states associated with an HTTP proxy. */
private List<IOState> getHttpStates(InetSocketAddress addr) {
List<IOState> states = new LinkedList<IOState>();
String connectString = "CONNECT " + addr.getAddress().getHostAddress() + ":" + addr.getPort() + " HTTP/1.0\r\n\r\n";
ByteBuffer outgoing = ByteBuffer.wrap(StringUtils.toAsciiBytes(connectString));
states.add(new SimpleWriteState(outgoing));
// Reads until it encounters \r\n
states.add(new ReadState() {
private StringBuilder sb = new StringBuilder();
private boolean found200 = false;
private ByteBuffer buffer;
@Override
protected boolean processRead(ReadableByteChannel channel, ByteBuffer scratchBuffer) throws IOException {
// LOG.debug("Entered read state");
if(buffer == null) {
buffer = scratchBuffer.slice();
buffer.limit(1); //process 1 byte at a time.
}
int read;
while((read = channel.read(buffer)) > 0) {
buffer.flip();
if(BufferUtils.readLine(buffer, sb)) {
if(!found200) {
// Make sure the first line has a '200' in it.
if(sb.indexOf("200") == -1)
throw new IOException("HTTP connection failed");
found200 = true;
}
// Once we find an empty line, we're done.
if(sb.length() == 0)
return false;
else
sb = new StringBuilder();
}
if(sb.length() > 2048)
throw new IOException("header too big.");
buffer.position(0);
buffer.limit(1);
}
if(read == -1)
throw new IOException("EOF");
return true;
}
public long getAmountProcessed() { return -1; }
});
return states;
}
/**
* ConnectObserver that will establish a proxy prior to delegating the connect back
* to the delegate.
*/
private class ProxyConnectorImpl implements ProxyManager.ProxyConnector, IOStateObserver {
private final ProxyType proxyType;
private final ConnectObserver delegate;
private final InetSocketAddress addr;
private final int timeout;
private volatile Socket socket;
ProxyConnectorImpl(ProxyType type, ConnectObserver observer, InetSocketAddress host, int tout) {
proxyType = type;
delegate = observer;
addr = host;
timeout = tout;
}
public void handleConnect(final Socket s) throws IOException {
this.socket = s;
s.setSoTimeout(timeout);
if(LOG.isDebugEnabled())
LOG.debug("Connected to proxy, beginning proxy handshake for addr: " + addr);
IOStateMachine machine = new IOStateMachine(this, getProxyStates(proxyType, addr));
((NIOMultiplexor)socket).setReadObserver(machine);
((NIOMultiplexor)socket).setWriteObserver(machine);
}
public void shutdown() {
if(LOG.isDebugEnabled())
LOG.debug("Failed to connect with proxy to addr: " + addr);
delegate.shutdown();
}
public void handleIOException(IOException iox) {
if(LOG.isDebugEnabled())
LOG.debug("Failed to connect with proxy to addr: " + addr, iox);
delegate.shutdown();
}
public ConnectObserver getDelegateObserver() {
return delegate;
}
public void handleStatesFinished() {
try {
socket.setSoTimeout(0);
} catch(IOException ignored) {}
if(LOG.isDebugEnabled())
LOG.debug("Finished proxy handshake, notifying connector for address: " + addr);
try {
delegate.handleConnect(socket);
} catch(IOException iox) {
IOUtils.close(socket);
// Do not call shutdown on the delegate, since it already got the handleConnect
}
}
}
}