package edu.washington.cs.oneswarm.f2f;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.bouncycastle.util.encoders.Base64;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.global.GlobalManagerStats;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.DirectByteBuffer;
import org.gudy.azureus2.core3.util.DirectByteBufferPool;
import com.aelitis.azureus.core.networkmanager.IncomingMessageQueue.MessageQueueListener;
import com.aelitis.azureus.core.networkmanager.NetworkConnection;
import com.aelitis.azureus.core.networkmanager.NetworkConnection.ConnectionListener;
import com.aelitis.azureus.core.networkmanager.NetworkManager;
import com.aelitis.azureus.core.networkmanager.NetworkManager.RoutingListener;
import com.aelitis.azureus.core.networkmanager.impl.TransportHelper;
import com.aelitis.azureus.core.peermanager.messaging.Message;
import com.aelitis.azureus.core.peermanager.messaging.MessageStreamDecoder;
import com.aelitis.azureus.core.peermanager.messaging.MessageStreamEncoder;
import com.aelitis.azureus.core.peermanager.messaging.MessageStreamFactory;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FChannelDataMsg;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FChannelMsg;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FChannelReset;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FHandshake;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessage;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessageDecoder;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessageEncoder;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessageFactory;
public class OSF2FSpeedChecker {
private final static byte[] legacy_handshake_header = new byte[OSF2FMessage.SPD_HANDSHAKE
.getBytes().length + 1];
private final static Logger logger = Logger.getLogger(OSF2FSpeedChecker.class.getName());
public final static long SPEED_CHECK_TIME = 15 * 1000;
static {
ByteBuffer b = ByteBuffer.wrap(legacy_handshake_header);
b.put((byte) OSF2FMessage.SPD_HANDSHAKE.length());
b.put(OSF2FMessage.SPD_HANDSHAKE.getBytes());
}
private Timer checkTimer;
private int currentId = 0;
private final LinkedList<IncomingSpeedCheckConnection> incomingConnections = new LinkedList<IncomingSpeedCheckConnection>();
private boolean running = false;
private BufferedWriter speedCheckLog;
private final Map<Integer, OutgoingSpeedCheck> speedChecks = new HashMap<Integer, OutgoingSpeedCheck>();
private NetworkManager.ByteMatcher speedMatcher;
private final GlobalManagerStats stats;
public OSF2FSpeedChecker(GlobalManagerStats stats) {
this.stats = stats;
COConfigurationManager.addAndFireParameterListener("Allow.Incoming.Speed.Check",
new ParameterListener() {
@Override
public void parameterChanged(String parameterName) {
boolean enabled = COConfigurationManager
.getBooleanParameter("Allow.Incoming.Speed.Check");
if (enabled) {
installSpeedCheckMatcher();
} else {
uninstallSpeedCheckMatcher();
}
}
});
}
public OutgoingSpeedCheck getSpeedCheck(int testId) {
return speedChecks.get(testId);
}
private void installSpeedCheckMatcher() {
synchronized (this) {
if (!running) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
synchronized (OSF2FSpeedChecker.this) {
running = true;
logger.fine("installing speed check routing");
speedMatcher = new OsSpeedMatcher();
NetworkManager.getSingleton().requestIncomingConnectionRouting(
speedMatcher, new RoutingListener() {
@Override
public boolean autoCryptoFallback() {
// TODO Auto-generated method stub
return false;
}
@Override
public void connectionRouted(NetworkConnection connection,
Object routing_data) {
logger.fine("connection routed to speed checker, isTcp="
+ connection.getTransport().isTCP());
synchronized (incomingConnections) {
incomingConnections
.add(new IncomingSpeedCheckConnection(
stats, connection));
}
}
}, new MessageStreamFactory() {
@Override
public MessageStreamDecoder createDecoder() {
return new OSF2FMessageDecoder();
}
@Override
public MessageStreamEncoder createEncoder() {
return new OSF2FMessageEncoder();
}
});
}
}
});
t.setDaemon(true);
t.setName("OSF2F speed matcher loader");
t.start();
try {
speedCheckLog = new BufferedWriter(new FileWriter(new File("speed_check.log"),
true));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
checkTimer = new Timer("speed check connection checker", true);
checkTimer.schedule(new TimerTask() {
@Override
public void run() {
StringBuilder toLog = new StringBuilder();
synchronized (incomingConnections) {
for (Iterator<IncomingSpeedCheckConnection> iterator = incomingConnections
.iterator(); iterator.hasNext();) {
IncomingSpeedCheckConnection c = iterator.next();
if (c.isTimedOut()) {
c.close();
}
if (c.isClosed()) {
toLog.append(System.currentTimeMillis()
+ " "
+ c.getRemoteIp()
+ " "
+ new String(Base64.encode(c.incomingHandshake
.getFlags())) + " " + c.remoteLocalEstimate
+ " " + c.remoteRemoteEstimate + " "
+ c.getAverageSpeed() + " " + c.getSecondHalfSpeed()
+ " " + c.getTimeStamps() + "\n");
iterator.remove();
}
}
logger.finest("speed check connection cleanup done, currently active="
+ incomingConnections.size());
}
try {
speedCheckLog.append(toLog.toString());
speedCheckLog.flush();
logger.finest(toLog.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, 30 * 1000, 10 * 1000);
}
}
}
public int performSpeedCheck() {
currentId++;
speedChecks.put(currentId, new OutgoingSpeedCheck(stats));
return currentId;
}
private void uninstallSpeedCheckMatcher() {
synchronized (this) {
if (running && speedMatcher != null) {
logger.fine("removing speed check routing");
NetworkManager.getSingleton().cancelIncomingConnectionRouting(speedMatcher);
running = false;
if (checkTimer != null) {
checkTimer.cancel();
}
if (speedCheckLog != null) {
try {
speedCheckLog.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
static class IncomingSpeedCheckConnection implements MessageQueueListener {
private final static long TIMEOUT_MS = 60 * 1000;
private boolean closed = false;
final NetworkConnection connection;
private OSF2FHandshake incomingHandshake;
private long lastMessageTime;
private final LinkedList<Integer> messageTimes = new LinkedList<Integer>();
private int packetSize = 0;
private int remoteLocalEstimate = -1;
private int remoteRemoteEstimate = -1;
private long startTime;
private final GlobalManagerStats stats;
public IncomingSpeedCheckConnection(GlobalManagerStats stats, NetworkConnection conn) {
this.startTime = System.currentTimeMillis();
this.connection = conn;
this.stats = stats;
/*
* register for notifications about incoming messages
*/
connection.getIncomingMessageQueue().registerQueueListener(this);
connection.connect(true, new ConnectionListener() {
@Override
public void connectFailure(Throwable failureMsg) {
connection.close();
}
@Override
public void connectStarted() {
}
@Override
public void connectSuccess(ByteBuffer remainingInitialData) {
NetworkManager.getSingleton().upgradeTransferProcessing(connection, null);
logger.finest("incoming speed check connected: " + getRemoteIp());
}
@Override
public void exceptionThrown(Throwable error) {
logger.warning("got exception in incoming speed test: " + error.getMessage());
error.printStackTrace();
connection.close();
}
@Override
public String getDescription() {
return "speed connection listener";
}
});
NetworkManager.getSingleton().startTransferProcessing(connection);
}
public void close() {
if (!closed) {
closed = true;
logger.fine("closing incoming connection");
connection.close();
}
}
@Override
public void dataBytesReceived(int byteCount) {
if (stats != null) {
stats.protocolBytesReceived(byteCount, false);
}
}
public long getAverageSpeed() {
long bytesTransfered = messageTimes.size() * packetSize;
if (bytesTransfered == 0) {
return 0;
}
long timeTaken = messageTimes.getLast();
return (1000 * bytesTransfered) / timeTaken;
}
public String getRemoteIp() {
return connection.getEndpoint().getNotionalAddress().getAddress().getHostAddress();
}
public long getSecondHalfSpeed() {
long bytesTransfered = (messageTimes.size() * packetSize) / 2;
if (bytesTransfered == 0) {
return 0;
}
long timeTaken = messageTimes.getLast() - messageTimes.get(messageTimes.size() / 2);
return (1000 * bytesTransfered) / timeTaken;
}
public String getTimeStamps() {
StringBuilder b = new StringBuilder();
for (Integer l : messageTimes) {
b.append(l + " ");
}
return b.toString();
}
public boolean isClosed() {
return closed;
}
public boolean isTimedOut() {
return System.currentTimeMillis() - lastMessageTime > TIMEOUT_MS;
}
@Override
public boolean messageReceived(Message message) {
logger.finest("incoming speed packet: " + message.getDescription());
lastMessageTime = System.currentTimeMillis();
if (incomingHandshake == null) {
if (message instanceof OSF2FHandshake) {
incomingHandshake = (OSF2FHandshake) message;
} else {
logger.warning("incoming connection not started with handshake");
close();
}
} else if (message instanceof OSF2FChannelReset) {
OSF2FChannelReset m = (OSF2FChannelReset) message;
if (remoteLocalEstimate == -1) {
remoteLocalEstimate = m.getChannelId();
} else {
remoteRemoteEstimate = m.getChannelId();
logger.finer("estimate of " + getRemoteIp() + " remote_local_estimate="
+ remoteLocalEstimate + " remote_remote=" + remoteRemoteEstimate);
close();
}
} else if ((message instanceof OSF2FChannelMsg)) {
OSF2FChannelMsg m = (OSF2FChannelMsg) message;
if (packetSize == 0) {
packetSize = m.getMessageSize();
}
if (m.getMessageSize() != packetSize) {
logger.warning("got different size payload packet");
close();
return true;
}
/*
* ok all is good, send back a timestamp to the other side
*/
int relTime = (int) (System.currentTimeMillis() - startTime);
messageTimes.add(relTime);
/*
* we only need to send an int over, channel reset is the
* smallest message (5+4 bytes), a bit of a hack but wth...
*/
connection.getOutgoingMessageQueue().addMessage(
new OSF2FChannelReset(OSF2FMessage.CURRENT_VERSION, relTime), false);
} else {
logger.warning("incoming speed check connection unknown packet");
close();
return true;
}
return true;
}
@Override
public void protocolBytesReceived(int byteCount) {
if (stats != null) {
stats.protocolBytesReceived(byteCount, false);
}
};
}
static class OsSpeedMatcher implements NetworkManager.ByteMatcher {
private final int size;
public OsSpeedMatcher() {
this.size = legacy_handshake_header.length;
}
@Override
public byte[][] getSharedSecrets() {
return null;
}
@Override
public int getSpecificPort() {
return (-1);
}
@Override
public Object matches(TransportHelper transport, ByteBuffer to_compare, int port) {
// logger.finest("looking at: " + new String(to_compare.array()));
int old_limit = to_compare.limit();
to_compare.limit(to_compare.position() + maxSize());
boolean matches = to_compare.equals(ByteBuffer.wrap(legacy_handshake_header));
to_compare.limit(old_limit); // restore buffer structure
return matches ? "" : null;
}
@Override
public int matchThisSizeOrBigger() {
return (maxSize());
}
@Override
public int maxSize() {
return size;
}
@Override
public Object minMatches(TransportHelper transport, ByteBuffer to_compare, int port) {
return (matches(transport, to_compare, port));
}
@Override
public int minSize() {
return maxSize();
}
}
static class OutgoingSpeedCheck {
private static final String SPEEDTEST_URL = "http://" + Constants.VERSION_SERVER_V4
+ "/speedcheck";
private final List<OutgoingSpeedCheckConnection> connections = new LinkedList<OutgoingSpeedCheckConnection>();
private int localEstimate = -1;
private int remoteEstimate = -1;
private final long startTime;
private final StringBuffer log = new StringBuffer();
private String lastLine = "";
private void log(String str) {
log.append(str + "\n");
lastLine = str;
logger.finer(str);
}
public void close() {
for (OutgoingSpeedCheckConnection c : connections) {
try {
c.closeSocket();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public OutgoingSpeedCheck(GlobalManagerStats stats) {
this.startTime = System.currentTimeMillis();
log("starting speed test");
InetAddress addr;
try {
log("connecting to " + SPEEDTEST_URL);
HttpURLConnection conn = (HttpURLConnection) new URL(SPEEDTEST_URL)
.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
List<String> hosts = new LinkedList<String>();
while ((line = in.readLine()) != null && !line.equals("")) {
hosts.add(line);
}
for (String s : hosts) {
String[] split = s.split(" ");
String host = split[0];
int port = Integer.parseInt(split[1]);
OutgoingSpeedCheckConnection c = new OutgoingSpeedCheckConnection(stats, host,
port);
connections.add(c);
Thread t = new Thread(c);
t.setName("speed check connection: " + s);
t.setDaemon(true);
t.start();
}
log("Speed test running to " + hosts.size() + " servers");
final Timer killTimer = new Timer("speed check killer", true);
// kill it 5 sec after we are done
killTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
localEstimate = getLocalEstimate();
remoteEstimate = getRemoteEstimate();
for (int i = 0; i < 10; i++) {
boolean allDone = true;
for (OutgoingSpeedCheckConnection c : connections) {
if (!c.isCompleted()) {
allDone = false;
}
}
if (allDone) {
break;
} else {
Thread.sleep(1000);
}
}
for (OutgoingSpeedCheckConnection c : connections) {
try {
c.sendReport(localEstimate);
c.sendReport(remoteEstimate);
// remote side should close conn after
// getting
// second report
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
connections.clear();
log("speed test completed");
logger.fine("done clearing connections, local=" + localEstimate
+ " remote=" + remoteEstimate);
killTimer.cancel();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, 1000 + SPEED_CHECK_TIME);
} catch (UnknownHostException e1) {
log("unable to resolv host: " + SPEEDTEST_URL);
} catch (IOException e) {
log("unable to get server list");
}
}
public double getProgress() {
long elapsed = System.currentTimeMillis() - startTime;
return Math.min(1.0, (1.0 * elapsed) / (SPEED_CHECK_TIME));
}
public int getLocalEstimate() {
if (localEstimate != -1) {
return localEstimate;
}
int totalSpeed = 0;
synchronized (connections) {
for (OutgoingSpeedCheckConnection c : connections) {
totalSpeed += c.getLocalEstimate();
}
}
return totalSpeed;
}
public int getRemoteEstimate() {
if (remoteEstimate != -1) {
return remoteEstimate;
}
int totalSpeed = 0;
synchronized (connections) {
for (OutgoingSpeedCheckConnection c : connections) {
totalSpeed += c.getRemoteEstimate();
}
}
return totalSpeed;
}
public int getGoodServers() {
int num = 0;
for (OutgoingSpeedCheckConnection c : connections) {
if (c.getRemoteEstimate() > 0) {
num++;
}
}
return num;
}
public int getServerCount() {
return connections.size();
}
public boolean isClosed() {
for (OutgoingSpeedCheckConnection c : connections) {
if (!c.isClosed()) {
return false;
}
}
return true;
}
public boolean isCompleted() {
for (OutgoingSpeedCheckConnection c : connections) {
if (!c.isCompleted()) {
return false;
}
}
return true;
}
public static void main(String[] args) throws SecurityException, FileNotFoundException,
IOException, InterruptedException {
LogManager.getLogManager().readConfiguration(
new FileInputStream("./logging.properties"));
// List<String> speedTestHosts = new LinkedList<String>();
// // speedTestHosts.add("127.0.0.1:11338");
// speedTestHosts.add("swede:46549");
// speedTestHosts.add("jermaine:48401");
OutgoingSpeedCheck check = new OutgoingSpeedCheck(null);// ,
// speedTestHosts);
while (!check.isClosed()) {
System.out.println(("" + check.getProgress()).substring(0, 3) + ": local="
+ check.getLocalEstimate() + " remote=" + check.getRemoteEstimate());
Thread.sleep(1000);
}
System.out.println("finished: completed=" + check.isCompleted() + " closed="
+ check.isClosed());
System.out.println("local=" + check.getLocalEstimate() + " remote="
+ check.getRemoteEstimate());
}
}
static class OutgoingSpeedCheckConnection implements Runnable {
private boolean closed = false;
private boolean completed = false;
private final String host;
private final List<Integer> localTimeStamps = new Vector<Integer>();
private OutputStream out;
private final int port;
private final List<Integer> remoteTimeStamps = new Vector<Integer>();
private Socket s;
private boolean sendCompleted = false;
private final GlobalManagerStats stats;
private OutgoingSpeedCheckConnection(GlobalManagerStats stats, String host, int port) {
super();
this.stats = stats;
this.port = port;
this.host = host;
}
public void closeSocket() throws IOException {
if (s != null) {
s.close();
}
}
public long getLocalEstimate() {
return getSpeed(localTimeStamps);
}
public long getRemoteEstimate() {
return getSpeed(remoteTimeStamps);
}
public boolean isClosed() {
return closed;
}
public boolean isCompleted() {
return completed;
}
@Override
public void run() {
long startTime = System.currentTimeMillis();
try {
s = new Socket(InetAddress.getByName(host), port);
out = s.getOutputStream();
final DataInputStream in = new DataInputStream(s.getInputStream());
Thread readThread = new Thread(new Runnable() {
@Override
public void run() {
logger.fine("running speed check to " + host + ":" + port);
try {
/*
* read the time stamps
*/
while (!sendCompleted
|| localTimeStamps.size() > remoteTimeStamps.size()) {
logger.finest("reading again: complete=" + sendCompleted
+ " sizediff="
+ (localTimeStamps.size() - remoteTimeStamps.size()));
// read message len
int len = in.readInt();
logger.finest("message len: " + len);
byte id = in.readByte();
logger.finest("message id: " + id);
int timestamp = in.readInt();
logger.finest("message timestamp: " + timestamp);
remoteTimeStamps.add(timestamp);
if (stats != null) {
stats.protocolBytesReceived(9, false);
}
}
logger.fine("incoming reader complete");
completed = true;
} catch (IOException e) {
logger.fine("speed check connection closed: " + e.getClass().getName() + "::"
+ e.getMessage());
closed = true;
}
}
});
readThread.setDaemon(true);
readThread.setName("speed check outgoing: inputreader " + host + ":" + port);
readThread.start();
/*
* first, send the handshake
*/
byte[] rand = new byte[8];
new Random().nextBytes(rand);
out.write(getHsBytes(rand));
/*
* second, send the dummy data
*/
byte[] dummyPacket = getDummyPacket();
long elapsed;
while ((elapsed = System.currentTimeMillis() - startTime) < SPEED_CHECK_TIME) {
logger.finest("sending " + dummyPacket.length + " bytes");
localTimeStamps.add((int) elapsed);
out.write(dummyPacket);
if (stats != null) {
stats.protocolBytesSent(dummyPacket.length, false);
}
}
sendCompleted = true;
logger.fine("send completed: " + host + ":" + port);
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
logger.fine("speed check connection closed: " + e.getClass().getName() + "::"
+ e.getMessage());
closed = true;
}
}
public void sendReport(int speed) throws IOException {
if (!closed && out != null) {
out.write(getReportBytes(speed));
}
}
private static byte[] getDummyPacket() throws IOException {
DirectByteBuffer dbuffer = DirectByteBufferPool.getBuffer(DirectByteBuffer.AL_MSG,
OSF2FMessage.MAX_PAYLOAD_SIZE);
while (dbuffer.hasRemaining(DirectByteBuffer.SS_NET)) {
dbuffer.put(DirectByteBuffer.SS_NET, (byte) 0);
}
dbuffer.flip(DirectByteBuffer.SS_NET);
DirectByteBuffer[] data = OSF2FMessageFactory.createOSF2FRawMessage(
new OSF2FChannelDataMsg(OSF2FMessage.CURRENT_VERSION, 0, dbuffer)).getRawData();
ByteBuffer buf = ByteBuffer.allocate(OSF2FMessage.MAX_MESSAGE_SIZE + 20);
for (int i = 0; i < data.length; i++) {
buf.put(data[i].getBuffer(DirectByteBuffer.SS_MSG));
}
buf.flip();
byte[] b = new byte[buf.remaining()];
buf.get(b);
return b;
}
private static byte[] getHsBytes(byte[] rand) {
ByteBuffer buf = ByteBuffer.allocate(2000);
buf.put((byte) OSF2FMessage.SPD_HANDSHAKE.getBytes().length);
buf.put(OSF2FMessage.SPD_HANDSHAKE.getBytes());
buf.put(rand);
buf.flip();
byte[] data = new byte[buf.remaining()];
buf.get(data);
return data;
}
private static byte[] getReportBytes(int speed) throws IOException {
DirectByteBuffer[] data = OSF2FMessageFactory.createOSF2FRawMessage(
new OSF2FChannelReset(OSF2FMessage.CURRENT_VERSION, speed)).getRawData();
ByteBuffer buf = ByteBuffer.allocate(20);
for (int i = 0; i < data.length; i++) {
buf.put(data[i].getBuffer(DirectByteBuffer.SS_MSG));
}
buf.flip();
byte[] b = new byte[buf.remaining()];
buf.get(b);
return b;
}
private static long getSpeed(List<Integer> timeStamps) {
long bytesTransfered = timeStamps.size() * OSF2FMessage.MAX_MESSAGE_SIZE;
if (bytesTransfered == 0) {
return 0;
}
if (timeStamps.size() < 2) {
return 0;
}
long timeTaken = timeStamps.get(timeStamps.size() - 1);
if (timeTaken <= 0) {
return 0;
}
return (1000 * bytesTransfered) / timeTaken;
}
}
}