merge of '3b6c4d6af6fae83cc9b7d42e8515804ae48ec675'

and '799a00a4929a59478c534a56cce350cdb9a042e0'
This commit is contained in:
z3d
2010-11-23 13:54:18 +00:00
51 changed files with 998 additions and 303 deletions

View File

@@ -0,0 +1,14 @@
package org.klomp.snark;
/**
* Callback used to fetch data
* @since 0.8.2
*/
interface DataLoader
{
/**
* This is the callback that PeerConnectionOut calls to get the data from disk
* @return bytes or null for errors
*/
public byte[] loadData(int piece, int begin, int length);
}

View File

@@ -39,23 +39,28 @@ class Message
final static byte REQUEST = 6;
final static byte PIECE = 7;
final static byte CANCEL = 8;
final static byte EXTENSION = 20;
// Not all fields are used for every message.
// KEEP_ALIVE doesn't have a real wire representation
byte type;
// Used for HAVE, REQUEST, PIECE and CANCEL messages.
// low byte used for EXTENSION message
int piece;
// Used for REQUEST, PIECE and CANCEL messages.
int begin;
int length;
// Used for PIECE and BITFIELD messages
// Used for PIECE and BITFIELD and EXTENSION messages
byte[] data;
int off;
int len;
// Used to do deferred fetch of data
DataLoader dataLoader;
SimpleTimer.TimedEvent expireEvent;
/** Utility method for sending a message through a DataStream. */
@@ -68,6 +73,13 @@ class Message
return;
}
// Get deferred data
if (data == null && dataLoader != null) {
data = dataLoader.loadData(piece, begin, length);
if (data == null)
return; // hmm will get retried, but shouldn't happen
}
// Calculate the total length in bytes
// Type is one byte.
@@ -85,8 +97,12 @@ class Message
if (type == REQUEST || type == CANCEL)
datalen += 4;
// length is 1 byte
if (type == EXTENSION)
datalen += 1;
// add length of data for piece or bitfield array.
if (type == BITFIELD || type == PIECE)
if (type == BITFIELD || type == PIECE || type == EXTENSION)
datalen += len;
// Send length
@@ -105,8 +121,11 @@ class Message
if (type == REQUEST || type == CANCEL)
dos.writeInt(length);
if (type == EXTENSION)
dos.writeByte((byte) piece & 0xff);
// Send actual data
if (type == BITFIELD || type == PIECE)
if (type == BITFIELD || type == PIECE || type == EXTENSION)
dos.write(data, off, len);
}
@@ -135,6 +154,8 @@ class Message
return "PIECE(" + piece + "," + begin + "," + length + ")";
case CANCEL:
return "CANCEL(" + piece + "," + begin + "," + length + ")";
case EXTENSION:
return "EXTENSION(" + piece + ',' + data.length + ')';
default:
return "<UNKNOWN>";
}

View File

@@ -59,6 +59,11 @@ public class Peer implements Comparable
private long uploaded_old[] = {-1,-1,-1};
private long downloaded_old[] = {-1,-1,-1};
// bytes per bt spec: 0011223344556677
static final long OPTION_EXTENSION = 0x0000000000100000l;
static final long OPTION_FAST = 0x0000000000000004l;
private long options;
/**
* Creates a disconnected peer given a PeerID, your own id and the
* relevant MetaInfo.
@@ -285,9 +290,8 @@ public class Peer implements Comparable
// Handshake write - header
dout.write(19);
dout.write("BitTorrent protocol".getBytes("UTF-8"));
// Handshake write - zeros
byte[] zeros = new byte[8];
dout.write(zeros);
// Handshake write - options
dout.writeLong(OPTION_EXTENSION);
// Handshake write - metainfo hash
byte[] shared_hash = metainfo.getInfoHash();
dout.write(shared_hash);
@@ -312,8 +316,8 @@ public class Peer implements Comparable
+ "'Bittorrent protocol', got '"
+ bittorrentProtocol + "'");
// Handshake read - zeros
din.readFully(zeros);
// Handshake read - options
options = din.readLong();
// Handshake read - metainfo hash
bs = new byte[20];
@@ -325,6 +329,15 @@ public class Peer implements Comparable
din.readFully(bs);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Read the remote side's hash and peerID fully from " + toString());
// if ((options & OPTION_EXTENSION) != 0) {
if (options != 0) {
// send them something
if (_log.shouldLog(Log.DEBUG))
//_log.debug("Peer supports extension message, what should we say? " + toString());
_log.debug("Peer supports options 0x" + Long.toString(options, 16) + ", what should we say? " + toString());
}
return bs;
}

View File

@@ -171,6 +171,13 @@ class PeerConnectionIn implements Runnable
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received cancel(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
break;
case 20: // Extension message
int id = din.readUnsignedByte();
byte[] payload = new byte[i-2];
din.readFully(payload);
ps.extensionMessage(id, payload);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received extension message from " + peer + " on " + peer.metainfo.getName());
default:
byte[] bs = new byte[i-1];
din.readFully(bs);

View File

@@ -430,6 +430,33 @@ class PeerConnectionOut implements Runnable
return total;
}
/** @since 0.8.2 */
void sendPiece(int piece, int begin, int length, DataLoader loader)
{
boolean sendNow = false;
// are there any cases where we should?
if (sendNow) {
// queue the real thing
byte[] bytes = loader.loadData(piece, begin, length);
if (bytes != null)
sendPiece(piece, begin, length, bytes);
return;
}
// queue a fake message... set everything up,
// except save the PeerState instead of the bytes.
Message m = new Message();
m.type = Message.PIECE;
m.piece = piece;
m.begin = begin;
m.length = length;
m.dataLoader = loader;
m.off = 0;
m.len = length;
addMessage(m);
}
void sendPiece(int piece, int begin, int length, byte[] bytes)
{
Message m = new Message();
@@ -488,4 +515,16 @@ class PeerConnectionOut implements Runnable
}
}
}
/** @since 0.8.2 */
void sendExtension(int id, byte[] bytes) {
Message m = new Message();
m.type = Message.EXTENSION;
m.piece = id;
m.data = bytes;
m.begin = 0;
m.length = bytes.length;
addMessage(m);
}
}

View File

@@ -20,14 +20,20 @@
package org.klomp.snark;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.i2p.I2PAppContext;
import net.i2p.util.Log;
class PeerState
import org.klomp.snark.bencode.BDecoder;
import org.klomp.snark.bencode.BEValue;
class PeerState implements DataLoader
{
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerState.class);
final Peer peer;
@@ -201,13 +207,28 @@ class PeerState
return;
}
if (_log.shouldLog(Log.INFO))
_log.info("Queueing (" + piece + ", " + begin + ", "
+ length + ")" + " to " + peer);
// don't load the data into mem now, let PeerConnectionOut do it
out.sendPiece(piece, begin, length, this);
}
/**
* This is the callback that PeerConnectionOut calls
*
* @return bytes or null for errors
* @since 0.8.2
*/
public byte[] loadData(int piece, int begin, int length) {
byte[] pieceBytes = listener.gotRequest(peer, piece, begin, length);
if (pieceBytes == null)
{
// XXX - Protocol error-> diconnect?
if (_log.shouldLog(Log.WARN))
_log.warn("Got request for unknown piece: " + piece);
return;
return null;
}
// More sanity checks
@@ -219,13 +240,13 @@ class PeerState
+ ", " + begin
+ ", " + length
+ "' message from " + peer);
return;
return null;
}
if (_log.shouldLog(Log.INFO))
_log.info("Sending (" + piece + ", " + begin + ", "
+ length + ")" + " to " + peer);
out.sendPiece(piece, begin, length, pieceBytes);
return pieceBytes;
}
/**
@@ -413,6 +434,24 @@ class PeerState
out.cancelRequest(piece, begin, length);
}
/** @since 0.8.2 */
void extensionMessage(int id, byte[] bs)
{
if (id == 0) {
InputStream is = new ByteArrayInputStream(bs);
try {
BDecoder dec = new BDecoder(is);
BEValue bev = dec.bdecodeMap();
Map map = bev.getMap();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Got extension handshake message " + bev.toString());
} catch (Exception e) {}
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Got extended message type: " + id + " length: " + bs.length);
}
}
void unknownMessage(int type, byte[] bs)
{
if (_log.shouldLog(Log.WARN))

View File

@@ -242,6 +242,13 @@ public class I2PSnarkServlet extends Default {
List snarks = getSortedSnarks(req);
String uri = req.getRequestURI();
boolean isForm = _manager.util().connected() || !snarks.isEmpty();
if (isForm) {
out.write("<form action=\"");
out.write(uri);
out.write("\" method=\"POST\">\n");
out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n");
}
out.write(TABLE_HEADER);
out.write("<img border=\"0\" src=\"/themes/snark/ubergine/images/status.png\"");
out.write(" title=\"");
@@ -301,25 +308,17 @@ public class I2PSnarkServlet extends Default {
out.write("</th>\n");
out.write("<th align=\"center\">");
if (_manager.util().connected()) {
out.write("<a href=\"" + uri + "?action=StopAll&nonce=" + _nonce +
"\" title=\"");
out.write("<input type=\"image\" name=\"action\" value=\"StopAll\" title=\"");
out.write(_("Stop all torrents and the I2P tunnel"));
out.write("\">");
out.write("<img src=\"/themes/snark/ubergine/images/stop_all.png\" title=\"");
out.write(_("Stop all torrents and the I2P tunnel"));
out.write("\" alt=\"");
out.write("\" src=\"/themes/snark/ubergine/images/stop_all.png\" alt=\"");
out.write(_("Stop All"));
out.write("\">");
out.write("</a>");
} else if (!snarks.isEmpty()) {
out.write("<a href=\"" + uri + "?action=StartAll&nonce=" + _nonce +
"\" title=\"");
out.write("<input type=\"image\" name=\"action\" value=\"StartAll\" title=\"");
out.write(_("Start all torrents and the I2P tunnel"));
out.write("\" src=\"/themes/snark/ubergine/images/start_all.png\" alt=\"");
out.write(_("Start All"));
out.write("\">");
out.write("<img src=\"/themes/snark/ubergine/images/start_all.png\" title=\"");
out.write(_("Start all torrents and the I2P tunnel"));
out.write("\" alt=\"Start All\">");
out.write("</a>");
} else {
out.write("&nbsp;");
}
@@ -357,6 +356,8 @@ public class I2PSnarkServlet extends Default {
}
out.write("</table>");
if (isForm)
out.write("</form>\n");
}
/**
@@ -366,7 +367,11 @@ public class I2PSnarkServlet extends Default {
String action = req.getParameter("action");
if (action == null) {
// noop
} else if ("Add".equals(action)) {
return;
}
if (!"POST".equals(req.getMethod()))
return;
if ("Add".equals(action)) {
String newFile = req.getParameter("newFile");
String newURL = req.getParameter("newURL");
// NOTE - newFile currently disabled in HTML form - see below
@@ -410,8 +415,8 @@ public class I2PSnarkServlet extends Default {
} else {
// no file or URL specified
}
} else if ("Stop".equals(action)) {
String torrent = req.getParameter("torrent");
} else if (action.startsWith("Stop_")) {
String torrent = action.substring(5);
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
@@ -425,8 +430,8 @@ public class I2PSnarkServlet extends Default {
}
}
}
} else if ("Start".equals(action)) {
String torrent = req.getParameter("torrent");
} else if (action.startsWith("Start_")) {
String torrent = action.substring(6);
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
@@ -441,8 +446,8 @@ public class I2PSnarkServlet extends Default {
}
}
}
} else if ("Remove".equals(action)) {
String torrent = req.getParameter("torrent");
} else if (action.startsWith("Remove_")) {
String torrent = action.substring(7);
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
@@ -461,8 +466,8 @@ public class I2PSnarkServlet extends Default {
}
}
}
} else if ("Delete".equals(action)) {
String torrent = req.getParameter("torrent");
} else if (action.startsWith("Delete_")) {
String torrent = action.substring(7);
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
@@ -618,10 +623,10 @@ public class I2PSnarkServlet extends Default {
if (r.startsWith(skip))
r = r.substring(skip.length());
String llc = l.toLowerCase();
if (llc.startsWith("the ") || llc.startsWith("the."))
if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_"))
l = l.substring(4);
String rlc = r.toLowerCase();
if (rlc.startsWith("the ") || rlc.startsWith("the."))
if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_"))
r = r.substring(4);
return collator.compare(l, r);
}
@@ -828,62 +833,55 @@ public class I2PSnarkServlet extends Default {
out.write("</td>\n\t");
out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">");
String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.meta.getInfoHash());
String b64 = Base64.encode(snark.meta.getInfoHash());
if (showPeers)
parameters = parameters + "&p=1";
if (isRunning) {
out.write("<a href=\"" + uri + "?action=Stop" + parameters
+ "\" title=\"");
out.write("<input type=\"image\" name=\"action\" value=\"Stop_");
out.write(b64);
out.write("\" title=\"");
out.write(_("Stop the torrent"));
out.write("\">");
out.write("<img src=\"/themes/snark/ubergine/images/stop.png\" title=\"");
out.write(_("Stop the torrent"));
out.write("\" alt=\"");
out.write("\" src=\"/themes/snark/ubergine/images/stop.png\" alt=\"");
out.write(_("Stop"));
out.write("\">");
out.write("</a>");
} else {
if (isValid) {
out.write("<a href=\"" + uri + "?action=Start" + parameters
+ "\" title=\"");
out.write("<input type=\"image\" name=\"action\" value=\"Start_");
out.write(b64);
out.write("\" title=\"");
out.write(_("Start the torrent"));
out.write("\">");
out.write("<img src=\"/themes/snark/ubergine/images/start.png\" title=\"");
out.write(_("Start the torrent"));
out.write("\" alt=\"");
out.write("\" src=\"/themes/snark/ubergine/images/start.png\" alt=\"");
out.write(_("Start"));
out.write("\">");
out.write("</a>");
}
out.write("<a href=\"" + uri + "?action=Remove" + parameters
+ "\" title=\"");
out.write("<input type=\"image\" name=\"action\" value=\"Remove_");
out.write(b64);
out.write("\" title=\"");
out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
out.write("\" onclick=\"if (!confirm('");
// Can't figure out how to escape double quotes inside the onclick string.
// Single quotes in translate strings with parameters must be doubled.
// Then the remaining single quite must be escaped
out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
out.write("')) { return false; }\">");
out.write("<img src=\"/themes/snark/ubergine/images/remove.png\" title=\"");
out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
out.write("\" alt=\"");
out.write("')) { return false; }\"");
out.write(" src=\"/themes/snark/ubergine/images/remove.png\" alt=\"");
out.write(_("Remove"));
out.write("\">");
out.write("</a>");
out.write("<a href=\"" + uri + "?action=Delete" + parameters
+ "\" title=\"");
out.write("<input type=\"image\" name=\"action\" value=\"Delete_");
out.write(b64);
out.write("\" title=\"");
out.write(_("Delete the .torrent file and the associated data file(s)"));
out.write("\" onclick=\"if (!confirm('");
// Can't figure out how to escape double quotes inside the onclick string.
// Single quotes in translate strings with parameters must be doubled.
// Then the remaining single quite must be escaped
out.write(_("Are you sure you want to delete the torrent \\''{0}\\'' and all downloaded data?", fullFilename));
out.write("')) { return false; }\">");
out.write("<img src=\"/themes/snark/ubergine/images/delete.png\" title=\"");
out.write(_("Delete the .torrent file and the associated data file(s)"));
out.write("\" alt=\"");
out.write("')) { return false; }\"");
out.write(" src=\"/themes/snark/ubergine/images/delete.png\" alt=\"");
out.write(_("Delete"));
out.write("\">");
out.write("</a>");
}
out.write("</td>\n</tr>\n");