diff --git a/README.md b/README.md index e014586bb..fd1028b48 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,12 @@ Client client = new Client( new File("/path/to/your.torrent"), new File("/path/to/output/directory"))); +// You can optionally set download/upload rate limits +// in kB/second. Setting a limit to 0.0 disables rate +// limits. +client.setMaxDownloadRate(50.0); +client.setMaxUploadRate(50.0); + // At this point, can you either call download() to download the torrent and // stop immediately after... client.download(); @@ -134,7 +140,7 @@ License version 2.0. See COPYING file for more details. Authors and contributors ------------------------ -* Maxime Petazzoni <> (Platform Engineer at Turn, Inc) +* Maxime Petazzoni <> (Software Engineer at SignalFuse, Inc) Original author, main developer and maintainer * David Giffin <> Contributed parallel hashing and multi-file torrent support. diff --git a/bin/tracker b/bin/tracker deleted file mode 100755 index 95aca596b..000000000 --- a/bin/tracker +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2012 Turn, Inc. -# -# 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. - -base=$(dirname $(readlink -f $0)) -exec java -cp $(find ${base}/../build -name "ttorrent-*.jar" | tail -n 1) com.turn.ttorrent.tracker.Tracker $* diff --git a/bin/ttorrent b/bin/ttorrent new file mode 100755 index 000000000..fc21deaf6 --- /dev/null +++ b/bin/ttorrent @@ -0,0 +1,73 @@ +#!/bin/sh + +# Copyright (C) 2012 Turn, Inc. +# +# 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. + +EXECJAR="ttorrent-*-shaded.jar" + +real_path() { + case $1 in + /*) + SCRIPT="$1" + ;; + *) + PWD=`pwd` + SCRIPT="$PWD/$1" + ;; + esac + CHANGED=true + while [ "X$CHANGED" != "X" ] ; do + # Change spaces to ":" so the tokens can be parsed. + SAFESCRIPT=`echo $SCRIPT | sed -e 's; ;:;g'` + # Get the real path to this script, resolving any symbolic links + TOKENS=`echo $SAFESCRIPT | sed -e 's;/; ;g'` + REALPATH= + for C in $TOKENS; do + # Change any ":" in the token back to a space. + C=`echo $C | sed -e 's;:; ;g'` + REALPATH="$REALPATH/$C" + # If REALPATH is a sym link, resolve it. Loop for nested links. + while [ -h "$REALPATH" ] ; do + LS="`ls -ld "$REALPATH"`" + LINK="`expr "$LS" : '.*-> \(.*\)$'`" + if expr "$LINK" : '/.*' > /dev/null; then + # LINK is absolute. + REALPATH="$LINK" + else + # LINK is relative. + REALPATH="`dirname "$REALPATH"`""/$LINK" + fi + done + done + if [ "$REALPATH" = "$SCRIPT" ] ; then + CHANGED="" + else + SCRIPT="$REALPATH" + fi + done + echo "$REALPATH" +} + +base=$(dirname $(real_path $0)) +CPARG=$(find ${base}/../build -name "$EXECJAR" | tail -n 1) +if [ -z "$CPARG" ] ; then + echo "Unable to find $EXECJAR" + exit 1 +fi +if [ -z "$MAINCLASS" ] ; then + CPARG="-jar $CPARG" +else + CPARG="-cp $CPARG $MAINCLASS" +fi +exec java $CPARG "$@" diff --git a/bin/client b/bin/ttorrent-torrent similarity index 82% rename from bin/client rename to bin/ttorrent-torrent index cd77b761a..4193c4e2c 100755 --- a/bin/client +++ b/bin/ttorrent-torrent @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Copyright (C) 2012 Turn, Inc. # @@ -14,5 +14,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -base=$(dirname $(readlink -f $0)) -exec java -jar $(find ${base}/../build -name "ttorrent-*.jar" | tail -n 1) $* +EXEFILE="${0%-torrent}" +MAINCLASS="com.turn.ttorrent.cli.TorrentMain" "${EXEFILE}" "$@" diff --git a/bin/torrent b/bin/ttorrent-tracker similarity index 78% rename from bin/torrent rename to bin/ttorrent-tracker index 99d46d258..e6bacd5d9 100755 --- a/bin/torrent +++ b/bin/ttorrent-tracker @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Copyright (C) 2012 Turn, Inc. # @@ -14,5 +14,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -base=$(dirname $(readlink -f $0)) -exec java -cp $(find ${base}/../build -name "ttorrent-*.jar" | tail -n 1) com.turn.ttorrent.common.Torrent $* +EXEFILE="${0%-tracker}" +MAINCLASS="com.turn.ttorrent.cli.TrackerMain" "${EXEFILE}" "$@" diff --git a/pom.xml b/pom.xml index 33d6c98ca..389d5b930 100644 --- a/pom.xml +++ b/pom.xml @@ -6,6 +6,7 @@ org.sonatype.oss oss-parent 7 + Java BitTorrent library @@ -17,7 +18,7 @@ http://turn.github.com/ttorrent/ com.turn ttorrent - 1.2 + 1.3-SNAPSHOT jar @@ -27,7 +28,9 @@ scm:git:git://github.com/turn/ttorrent.git + scm:git:ssh://git@github.com/turn/ttorrent.git http://github.com/turn/ttorrent + HEAD @@ -46,10 +49,10 @@ mpetazzoni Maxime Petazzoni - mpetazzoni@turn.com + maxime.petazzoni@bulix.org http://www.bulix.org - Turn, Inc - http://www.turn.com + SignalFuse, Inc + http://www.signalfuse.com maintainer architect @@ -75,10 +78,16 @@ + + commons-codec + commons-codec + 1.8 + + commons-io commons-io - 2.1 + 2.4 @@ -127,6 +136,11 @@ maven-jar-plugin 2.4 + + + true + + ** @@ -144,28 +158,24 @@ - maven-assembly-plugin - - - jar-with-dependencies - - false - - - false - true - com.turn.ttorrent.client.Client - - - + maven-shade-plugin + 2.1 - make-my-jar-with-dependencies package - - assembly + shade + + ${project.build.directory}/${project.artifactId}-${project.version}-shaded.jar + + + + com.turn.ttorrent.cli.ClientMain + + + + diff --git a/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java b/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java index 305c56170..29a941dc9 100644 --- a/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java +++ b/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java @@ -206,7 +206,7 @@ public BEValue bdecodeNumber() throws IOException { c = this.read(); if (c == '0') throw new InvalidBEncodingException("Negative zero not allowed"); - chars[off] = (char)c; + chars[off] = '-'; off++; } diff --git a/src/main/java/com/turn/ttorrent/cli/ClientMain.java b/src/main/java/com/turn/ttorrent/cli/ClientMain.java new file mode 100644 index 000000000..ae56e5f75 --- /dev/null +++ b/src/main/java/com/turn/ttorrent/cli/ClientMain.java @@ -0,0 +1,176 @@ +/** + * Copyright (C) 2011-2013 Turn, Inc. + * + * 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 com.turn.ttorrent.cli; + +import com.turn.ttorrent.client.Client; +import com.turn.ttorrent.client.SharedTorrent; + +import java.io.File; +import java.io.PrintStream; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.channels.UnsupportedAddressTypeException; +import java.util.Enumeration; + +import jargs.gnu.CmdLineParser; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.PatternLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Command-line entry-point for starting a {@link Client} + */ +public class ClientMain { + + private static final Logger logger = + LoggerFactory.getLogger(ClientMain.class); + + /** + * Default data output directory. + */ + private static final String DEFAULT_OUTPUT_DIRECTORY = "/tmp"; + + /** + * Returns a usable {@link Inet4Address} for the given interface name. + * + *

+ * If an interface name is given, return the first usable IPv4 address for + * that interface. If no interface name is given or if that interface + * doesn't have an IPv4 address, return's localhost address (if IPv4). + *

+ * + *

+ * It is understood this makes the client IPv4 only, but it is important to + * remember that most BitTorrent extensions (like compact peer lists from + * trackers and UDP tracker support) are IPv4-only anyway. + *

+ * + * @param iface The network interface name. + * @return A usable IPv4 address as a {@link Inet4Address}. + * @throws UnsupportedAddressTypeException If no IPv4 address was available + * to bind on. + */ + private static Inet4Address getIPv4Address(String iface) + throws SocketException, UnsupportedAddressTypeException, + UnknownHostException { + if (iface != null) { + Enumeration addresses = + NetworkInterface.getByName(iface).getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr instanceof Inet4Address) { + return (Inet4Address)addr; + } + } + } + + InetAddress localhost = InetAddress.getLocalHost(); + if (localhost instanceof Inet4Address) { + return (Inet4Address)localhost; + } + + throw new UnsupportedAddressTypeException(); + } + + /** + * Display program usage on the given {@link PrintStream}. + */ + private static void usage(PrintStream s) { + s.println("usage: Client [options] "); + s.println(); + s.println("Available options:"); + s.println(" -h,--help Show this help and exit."); + s.println(" -o,--output DIR Read/write data to directory DIR."); + s.println(" -i,--iface IFACE Bind to interface IFACE."); + s.println(" -s,--seed SECONDS Time to seed after downloading (default: infinitely)."); + s.println(" -d,--max-download KB/SEC Max download rate (default: unlimited)."); + s.println(" -u,--max-upload KB/SEC Max upload rate (default: unlimited)."); + s.println(); + } + + /** + * Main client entry point for stand-alone operation. + */ + public static void main(String[] args) { + BasicConfigurator.configure(new ConsoleAppender( + new PatternLayout("%d [%-25t] %-5p: %m%n"))); + + CmdLineParser parser = new CmdLineParser(); + CmdLineParser.Option help = parser.addBooleanOption('h', "help"); + CmdLineParser.Option output = parser.addStringOption('o', "output"); + CmdLineParser.Option iface = parser.addStringOption('i', "iface"); + CmdLineParser.Option seedTime = parser.addIntegerOption('s', "seed"); + CmdLineParser.Option maxUpload = parser.addDoubleOption('u', "max-upload"); + CmdLineParser.Option maxDownload = parser.addDoubleOption('d', "max-download"); + + try { + parser.parse(args); + } catch (CmdLineParser.OptionException oe) { + System.err.println(oe.getMessage()); + usage(System.err); + System.exit(1); + } + + // Display help and exit if requested + if (Boolean.TRUE.equals((Boolean)parser.getOptionValue(help))) { + usage(System.out); + System.exit(0); + } + + String outputValue = (String)parser.getOptionValue(output, + DEFAULT_OUTPUT_DIRECTORY); + String ifaceValue = (String)parser.getOptionValue(iface); + int seedTimeValue = (Integer)parser.getOptionValue(seedTime, -1); + + double maxDownloadRate = (Double)parser.getOptionValue(maxDownload, 0.0); + double maxUploadRate = (Double)parser.getOptionValue(maxUpload, 0.0); + + String[] otherArgs = parser.getRemainingArgs(); + if (otherArgs.length != 1) { + usage(System.err); + System.exit(1); + } + + try { + Client c = new Client( + getIPv4Address(ifaceValue), + SharedTorrent.fromFile( + new File(otherArgs[0]), + new File(outputValue))); + + c.setMaxDownloadRate(maxDownloadRate); + c.setMaxUploadRate(maxUploadRate); + + // Set a shutdown hook that will stop the sharing/seeding and send + // a STOPPED announce request. + Runtime.getRuntime().addShutdownHook( + new Thread(new Client.ClientShutdown(c, null))); + + c.share(seedTimeValue); + if (Client.ClientState.ERROR.equals(c.getState())) { + System.exit(1); + } + } catch (Exception e) { + logger.error("Fatal error: {}", e.getMessage(), e); + System.exit(2); + } + } +} diff --git a/src/main/java/com/turn/ttorrent/cli/TorrentMain.java b/src/main/java/com/turn/ttorrent/cli/TorrentMain.java new file mode 100644 index 000000000..62b87bbc1 --- /dev/null +++ b/src/main/java/com/turn/ttorrent/cli/TorrentMain.java @@ -0,0 +1,166 @@ +/** + * Copyright (C) 2011-2013 Turn, Inc. + * + * 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 com.turn.ttorrent.cli; + +import com.turn.ttorrent.common.Torrent; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URI; +import java.util.Arrays; + +import jargs.gnu.CmdLineParser; +import org.apache.commons.io.IOUtils; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.PatternLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Command-line entry-point for reading and writing {@link Torrent} files. + */ +public class TorrentMain { + + private static final Logger logger = + LoggerFactory.getLogger(TorrentMain.class); + + /** + * Display program usage on the given {@link PrintStream}. + */ + private static void usage(PrintStream s) { + usage(s, null); + } + + /** + * Display a message and program usage on the given {@link PrintStream}. + */ + private static void usage(PrintStream s, String msg) { + if (msg != null) { + s.println(msg); + s.println(); + } + + s.println("usage: Torrent [options] [file|directory]"); + s.println(); + s.println("Available options:"); + s.println(" -h,--help Show this help and exit."); + s.println(" -t,--torrent FILE Use FILE to read/write torrent file."); + s.println(); + s.println(" -c,--create Create a new torrent file using " + + "the given announce URL and data."); + s.println(" -a,--announce Tracker URL (can be repeated)."); + s.println(); + } + + /** + * Torrent reader and creator. + * + *

+ * You can use the {@code main()} function of this class to read or create + * torrent files. See usage for details. + *

+ * + * TODO: support multiple announce URLs. + */ + public static void main(String[] args) { + BasicConfigurator.configure(new ConsoleAppender( + new PatternLayout("%-5p: %m%n"))); + + CmdLineParser parser = new CmdLineParser(); + CmdLineParser.Option help = parser.addBooleanOption('h', "help"); + CmdLineParser.Option filename = parser.addStringOption('t', "torrent"); + CmdLineParser.Option create = parser.addBooleanOption('c', "create"); + CmdLineParser.Option announce = parser.addStringOption('a', "announce"); + + try { + parser.parse(args); + } catch (CmdLineParser.OptionException oe) { + System.err.println(oe.getMessage()); + usage(System.err); + System.exit(1); + } + + // Display help and exit if requested + if (Boolean.TRUE.equals((Boolean)parser.getOptionValue(help))) { + usage(System.out); + System.exit(0); + } + + String filenameValue = (String)parser.getOptionValue(filename); + if (filenameValue == null) { + usage(System.err, "Torrent file must be provided!"); + System.exit(1); + } + + Boolean createFlag = (Boolean)parser.getOptionValue(create); + String announceURL = (String)parser.getOptionValue(announce); + + String[] otherArgs = parser.getRemainingArgs(); + + if (Boolean.TRUE.equals(createFlag) && + (otherArgs.length != 1 || announceURL == null)) { + usage(System.err, "Announce URL and a file or directory must be " + + "provided to create a torrent file!"); + System.exit(1); + } + + OutputStream fos = null; + try { + if (Boolean.TRUE.equals(createFlag)) { + if (filenameValue != null) { + fos = new FileOutputStream(filenameValue); + } else { + fos = System.out; + } + + URI announceURI = new URI(announceURL); + File source = new File(otherArgs[0]); + if (!source.exists() || !source.canRead()) { + throw new IllegalArgumentException( + "Cannot access source file or directory " + + source.getName()); + } + + String creator = String.format("%s (ttorrent)", + System.getProperty("user.name")); + + Torrent torrent = null; + if (source.isDirectory()) { + File[] files = source.listFiles(); + Arrays.sort(files); + torrent = Torrent.create(source, Arrays.asList(files), + announceURI, creator); + } else { + torrent = Torrent.create(source, announceURI, creator); + } + + torrent.save(fos); + } else { + Torrent.load(new File(filenameValue), true); + } + } catch (Exception e) { + logger.error("{}", e.getMessage(), e); + System.exit(2); + } finally { + if (fos != System.out) { + IOUtils.closeQuietly(fos); + } + } + } +} diff --git a/src/main/java/com/turn/ttorrent/cli/TrackerMain.java b/src/main/java/com/turn/ttorrent/cli/TrackerMain.java new file mode 100644 index 000000000..bd3ffa473 --- /dev/null +++ b/src/main/java/com/turn/ttorrent/cli/TrackerMain.java @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2011-2013 Turn, Inc. + * + * 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 com.turn.ttorrent.cli; + +import com.turn.ttorrent.tracker.TrackedTorrent; +import com.turn.ttorrent.tracker.Tracker; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.PrintStream; +import java.net.InetSocketAddress; + +import jargs.gnu.CmdLineParser; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.PatternLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Command-line entry-point for starting a {@link Tracker} + */ +public class TrackerMain { + + private static final Logger logger = + LoggerFactory.getLogger(TrackerMain.class); + + /** + * Display program usage on the given {@link PrintStream}. + */ + private static void usage(PrintStream s) { + s.println("usage: Tracker [options] [directory]"); + s.println(); + s.println("Available options:"); + s.println(" -h,--help Show this help and exit."); + s.println(" -p,--port PORT Bind to port PORT."); + s.println(); + } + + /** + * Main function to start a tracker. + */ + public static void main(String[] args) { + BasicConfigurator.configure(new ConsoleAppender( + new PatternLayout("%d [%-25t] %-5p: %m%n"))); + + CmdLineParser parser = new CmdLineParser(); + CmdLineParser.Option help = parser.addBooleanOption('h', "help"); + CmdLineParser.Option port = parser.addIntegerOption('p', "port"); + + try { + parser.parse(args); + } catch (CmdLineParser.OptionException oe) { + System.err.println(oe.getMessage()); + usage(System.err); + System.exit(1); + } + + // Display help and exit if requested + if (Boolean.TRUE.equals((Boolean)parser.getOptionValue(help))) { + usage(System.out); + System.exit(0); + } + + Integer portValue = (Integer)parser.getOptionValue(port, + Integer.valueOf(Tracker.DEFAULT_TRACKER_PORT)); + + String[] otherArgs = parser.getRemainingArgs(); + + if (otherArgs.length > 1) { + usage(System.err); + System.exit(1); + } + + // Get directory from command-line argument or default to current + // directory + String directory = otherArgs.length > 0 + ? otherArgs[0] + : "."; + + FilenameFilter filter = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".torrent"); + } + }; + + try { + Tracker t = new Tracker(new InetSocketAddress(portValue.intValue())); + + File parent = new File(directory); + for (File f : parent.listFiles(filter)) { + logger.info("Loading torrent from " + f.getName()); + t.announce(TrackedTorrent.load(f)); + } + + logger.info("Starting tracker with {} announced torrents...", + t.getTrackedTorrents().size()); + t.start(); + } catch (Exception e) { + logger.error("{}", e.getMessage(), e); + System.exit(2); + } + } +} diff --git a/src/main/java/com/turn/ttorrent/client/Client.java b/src/main/java/com/turn/ttorrent/client/Client.java index 480657dbf..359ade64b 100644 --- a/src/main/java/com/turn/ttorrent/client/Client.java +++ b/src/main/java/com/turn/ttorrent/client/Client.java @@ -19,26 +19,19 @@ import com.turn.ttorrent.client.announce.AnnounceException; import com.turn.ttorrent.client.announce.AnnounceResponseListener; import com.turn.ttorrent.client.peer.PeerActivityListener; +import com.turn.ttorrent.client.peer.SharingPeer; import com.turn.ttorrent.common.Peer; import com.turn.ttorrent.common.Torrent; import com.turn.ttorrent.common.protocol.PeerMessage; import com.turn.ttorrent.common.protocol.TrackerMessage; -import com.turn.ttorrent.client.peer.SharingPeer; -import java.io.File; import java.io.IOException; -import java.io.PrintStream; -import java.net.Inet4Address; import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; -import java.nio.channels.UnsupportedAddressTypeException; import java.util.BitSet; import java.util.Comparator; -import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Observable; @@ -51,11 +44,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import jargs.gnu.CmdLineParser; - -import org.apache.log4j.BasicConfigurator; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.PatternLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -99,9 +87,6 @@ public class Client extends Observable implements Runnable, private static final int RATE_COMPUTATION_ITERATIONS = 2; private static final int MAX_DOWNLOADERS_UNCHOKE = 4; - /** Default data output directory. */ - private static final String DEFAULT_OUTPUT_DIRECTORY = "/tmp"; - public enum ClientState { WAITING, VALIDATING, @@ -172,6 +157,26 @@ public Client(InetAddress address, SharedTorrent torrent) this.random = new Random(System.currentTimeMillis()); } + /** + * Set the maximum download rate (in kB/second) for this + * torrent. A setting of <= 0.0 disables rate limiting. + * + * @param rate The maximum download rate + */ + public void setMaxDownloadRate(double rate) { + this.torrent.setMaxDownloadRate(rate); + } + + /** + * Set the maximum upload rate (in kB/second) for this + * torrent. A setting of <= 0.0 disables rate limiting. + * + * @param rate The maximum upload rate + */ + public void setMaxUploadRate(double rate) { + this.torrent.setMaxUploadRate(rate); + } + /** * Get this client's peer specification. */ @@ -316,7 +321,8 @@ public void run() { // First, analyze the torrent's local data. try { this.setState(ClientState.VALIDATING); - this.torrent.init(); + if (!this.torrent.isInitialized()) + this.torrent.init(); } catch (IOException ioe) { logger.warn("Error while initializing torrent data: {}!", ioe.getMessage(), ioe); @@ -921,11 +927,11 @@ public void handleIOException(SharingPeer peer, IOException ioe) { *

* When the download is complete, the client switches to seeding mode for * as long as requested in the share() call, if seeding was - * requested. If not, the StopSeedingTask will execute immediately to stop - * the client's main loop. + * requested. If not, the {@link ClientShutdown} will execute + * immediately to stop the client's main loop. *

* - * @see StopSeedingTask + * @see ClientShutdown */ private synchronized void seed() { // Silently ignore if we're already seeding. @@ -965,12 +971,12 @@ private synchronized void seed() { * * @author mpetazzoni */ - private static class ClientShutdown extends TimerTask { + public static class ClientShutdown extends TimerTask { private final Client client; private final Timer timer; - ClientShutdown(Client client, Timer timer) { + public ClientShutdown(Client client, Timer timer) { this.client = client; this.timer = timer; } @@ -983,120 +989,4 @@ public void run() { } } }; - - /** - * Display program usage on the given {@link PrintStream}. - */ - private static void usage(PrintStream s) { - s.println("usage: Client [options] "); - s.println(); - s.println("Available options:"); - s.println(" -h,--help Show this help and exit."); - s.println(" -o,--output DIR Read/write data to directory DIR."); - s.println(" -i,--iface IFACE Bind to interface IFACE."); - s.println(" -s,--seed SECONDS Time to seed after downloading (default: infinitely)."); - s.println(); - } - - /** - * Returns a usable {@link Inet4Address} for the given interface name. - * - *

- * If an interface name is given, return the first usable IPv4 address for - * that interface. If no interface name is given or if that interface - * doesn't have an IPv4 address, return's localhost address (if IPv4). - *

- * - *

- * It is understood this makes the client IPv4 only, but it is important to - * remember that most BitTorrent extensions (like compact peer lists from - * trackers and UDP tracker support) are IPv4-only anyway. - *

- * - * @param iface The network interface name. - * @return A usable IPv4 address as a {@link Inet4Address}. - * @throws UnsupportedAddressTypeException If no IPv4 address was available - * to bind on. - */ - private static Inet4Address getIPv4Address(String iface) - throws SocketException, UnsupportedAddressTypeException, - UnknownHostException { - if (iface != null) { - Enumeration addresses = - NetworkInterface.getByName(iface).getInetAddresses(); - while (addresses.hasMoreElements()) { - InetAddress addr = addresses.nextElement(); - if (addr instanceof Inet4Address) { - return (Inet4Address)addr; - } - } - } - - InetAddress localhost = InetAddress.getLocalHost(); - if (localhost instanceof Inet4Address) { - return (Inet4Address)localhost; - } - - throw new UnsupportedAddressTypeException(); - } - - /** - * Main client entry point for stand-alone operation. - */ - public static void main(String[] args) { - BasicConfigurator.configure(new ConsoleAppender( - new PatternLayout("%d [%-25t] %-5p: %m%n"))); - - CmdLineParser parser = new CmdLineParser(); - CmdLineParser.Option help = parser.addBooleanOption('h', "help"); - CmdLineParser.Option output = parser.addStringOption('o', "output"); - CmdLineParser.Option iface = parser.addStringOption('i', "iface"); - CmdLineParser.Option seedTime = parser.addIntegerOption('s', "seed"); - - try { - parser.parse(args); - } catch (CmdLineParser.OptionException oe) { - System.err.println(oe.getMessage()); - usage(System.err); - System.exit(1); - } - - // Display help and exit if requested - if (Boolean.TRUE.equals((Boolean)parser.getOptionValue(help))) { - usage(System.out); - System.exit(0); - } - - String outputValue = (String)parser.getOptionValue(output, - DEFAULT_OUTPUT_DIRECTORY); - String ifaceValue = (String)parser.getOptionValue(iface); - int seedTimeValue = (Integer)parser.getOptionValue(seedTime, -1); - - String[] otherArgs = parser.getRemainingArgs(); - if (otherArgs.length != 1) { - usage(System.err); - System.exit(1); - } - - try { - Client c = new Client( - getIPv4Address(ifaceValue), - SharedTorrent.fromFile( - new File(otherArgs[0]), - new File(outputValue))); - - // Set a shutdown hook that will stop the sharing/seeding and send - // a STOPPED announce request. - Runtime.getRuntime().addShutdownHook( - new Thread(new ClientShutdown(c, null))); - - c.share(seedTimeValue); - if (ClientState.ERROR.equals(c.getState())) { - System.exit(1); - } - } catch (Exception e) { - logger.error("Fatal error: {}", e.getMessage(), e); - System.exit(2); - } - } } diff --git a/src/main/java/com/turn/ttorrent/client/ConnectionHandler.java b/src/main/java/com/turn/ttorrent/client/ConnectionHandler.java index 8b56336c9..4653f9466 100644 --- a/src/main/java/com/turn/ttorrent/client/ConnectionHandler.java +++ b/src/main/java/com/turn/ttorrent/client/ConnectionHandler.java @@ -36,6 +36,7 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -309,16 +310,12 @@ private void accept(SocketChannel client) } catch (ParseException pe) { logger.info("Invalid handshake from {}: {}", this.socketRepr(client), pe.getMessage()); - try { client.close(); } catch (IOException e) { } + IOUtils.closeQuietly(client); } catch (IOException ioe) { logger.warn("An error occured while reading an incoming " + "handshake: {}", ioe.getMessage()); - try { - if (client.isConnected()) { - client.close(); - } - } catch (IOException e) { - // Ignore + if (client.isConnected()) { + IOUtils.closeQuietly(client); } } } @@ -456,7 +453,7 @@ public Thread newThread(Runnable r) { t.setName("bt-connect-" + ++this.number); return t; } - }; + } /** @@ -510,15 +507,11 @@ public void run() { channel.configureBlocking(false); this.handler.fireNewPeerConnection(channel, hs.getPeerId()); } catch (Exception e) { - try { - if (channel != null && channel.isConnected()) { - channel.close(); - } - } catch (IOException ioe) { - // Ignore + if (channel != null && channel.isConnected()) { + IOUtils.closeQuietly(channel); } this.handler.fireFailedConnection(this.peer, e); } } - }; + } } diff --git a/src/main/java/com/turn/ttorrent/client/Piece.java b/src/main/java/com/turn/ttorrent/client/Piece.java index 77d60640b..5cd8534ac 100644 --- a/src/main/java/com/turn/ttorrent/client/Piece.java +++ b/src/main/java/com/turn/ttorrent/client/Piece.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.nio.ByteBuffer; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.concurrent.Callable; @@ -159,16 +158,10 @@ public synchronized boolean validate() throws IOException { logger.trace("Validating {}...", this); this.valid = false; - try { - // TODO: remove cast to int when large ByteBuffer support is - // implemented in Java. - ByteBuffer buffer = this._read(0, this.length); - byte[] data = new byte[(int)this.length]; - buffer.get(data); - this.valid = Arrays.equals(Torrent.hash(data), this.hash); - } catch (NoSuchAlgorithmException nsae) { - logger.error("{}", nsae); - } + ByteBuffer buffer = this._read(0, this.length); + byte[] data = new byte[(int)this.length]; + buffer.get(data); + this.valid = Arrays.equals(Torrent.hash(data), this.hash); return this.isValid(); } @@ -283,15 +276,11 @@ public String toString() { * @param other The piece to compare with, should not be null. */ public int compareTo(Piece other) { - if (this == other) { - return 0; - } - - if (this.seen < other.seen) { - return -1; - } else { - return 1; + if (this.seen != other.seen) { + return this.seen < other.seen ? -1 : 1; } + return this.index == other.index ? 0 : + (this.index < other.index ? -1 : 1); } /** diff --git a/src/main/java/com/turn/ttorrent/client/SharedTorrent.java b/src/main/java/com/turn/ttorrent/client/SharedTorrent.java index 85f4ac310..82280586b 100644 --- a/src/main/java/com/turn/ttorrent/client/SharedTorrent.java +++ b/src/main/java/com/turn/ttorrent/client/SharedTorrent.java @@ -18,18 +18,17 @@ import com.turn.ttorrent.bcodec.InvalidBEncodingException; import com.turn.ttorrent.common.Torrent; import com.turn.ttorrent.client.peer.PeerActivityListener; +import com.turn.ttorrent.client.peer.Rate; import com.turn.ttorrent.client.peer.SharingPeer; import com.turn.ttorrent.client.storage.TorrentByteStorage; import com.turn.ttorrent.client.storage.FileStorage; import com.turn.ttorrent.client.storage.FileCollectionStorage; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; @@ -43,6 +42,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,11 +95,14 @@ public class SharedTorrent extends Torrent implements PeerActivityListener { private final ByteBuffer piecesHashes; private boolean initialized; + private boolean peerOnly; private Piece[] pieces; private SortedSet rarest; private BitSet completedPieces; private BitSet requestedPieces; - + + private double maxUploadRate = 0.0; + private double maxDownloadRate = 0.0; /** * Create a new shared torrent from a base Torrent object. * @@ -114,10 +117,9 @@ public class SharedTorrent extends Torrent implements PeerActivityListener { * @throws FileNotFoundException If the torrent file location or * destination directory does not exist and can't be created. * @throws IOException If the torrent file cannot be read or decoded. - * @throws NoSuchAlgorithmException */ public SharedTorrent(Torrent torrent, File destDir) - throws FileNotFoundException, IOException, NoSuchAlgorithmException { + throws FileNotFoundException, IOException { this(torrent, destDir, false); } @@ -137,10 +139,9 @@ public SharedTorrent(Torrent torrent, File destDir) * @throws FileNotFoundException If the torrent file location or * destination directory does not exist and can't be created. * @throws IOException If the torrent file cannot be read or decoded. - * @throws NoSuchAlgorithmException */ public SharedTorrent(Torrent torrent, File destDir, boolean seeder) - throws FileNotFoundException, IOException, NoSuchAlgorithmException { + throws FileNotFoundException, IOException { this(torrent.getEncoded(), destDir, seeder); } @@ -155,7 +156,7 @@ public SharedTorrent(Torrent torrent, File destDir, boolean seeder) * @throws IOException If the torrent file cannot be read or decoded. */ public SharedTorrent(byte[] torrent, File destDir) - throws FileNotFoundException, IOException, NoSuchAlgorithmException { + throws FileNotFoundException, IOException { this(torrent, destDir, false); } @@ -169,20 +170,22 @@ public SharedTorrent(byte[] torrent, File destDir) * @throws FileNotFoundException If the torrent file location or * destination directory does not exist and can't be created. * @throws IOException If the torrent file cannot be read or decoded. - * @throws NoSuchAlgorithmException - * @throws URISyntaxException When one of the defined tracker addresses is - * invalid. */ public SharedTorrent(byte[] torrent, File parent, boolean seeder) - throws FileNotFoundException, IOException, NoSuchAlgorithmException { + throws FileNotFoundException, IOException { super(torrent, seeder); - if (parent == null || !parent.isDirectory()) { - throw new IllegalArgumentException("Invalid parent directory!"); + this.peerOnly = false; + String parentPath = null; + + if (parent == null) { + this.peerOnly = true; + } else if (!parent.isDirectory()) { + throw new IllegalArgumentException("Invalid parent directory!"); + } else { + parentPath = parent.getCanonicalPath(); } - String parentPath = parent.getCanonicalPath(); - try { this.pieceLength = this.decoded_info.get("piece length").getInt(); this.piecesHashes = ByteBuffer.wrap(this.decoded_info.get("pieces") @@ -198,21 +201,25 @@ public SharedTorrent(byte[] torrent, File parent, boolean seeder) "Error reading torrent meta-info fields!"); } - List files = new LinkedList(); - long offset = 0L; - for (Torrent.TorrentFile file : this.files) { - File actual = new File(parent, file.file.getPath()); + if (this.peerOnly) { + this.bucket = null; + } else { + List files = new LinkedList(); + long offset = 0L; + for (Torrent.TorrentFile file : this.files) { + File actual = new File(parent, file.file.getPath()); + + if (!actual.getCanonicalPath().startsWith(parentPath)) { + throw new SecurityException("Torrent file path attempted " + + "to break directory jail!"); + } - if (!actual.getCanonicalPath().startsWith(parentPath)) { - throw new SecurityException("Torrent file path attempted " + - "to break directory jail!"); + actual.getParentFile().mkdirs(); + files.add(new FileStorage(actual, offset, file.size)); + offset += file.size; } - - actual.getParentFile().mkdirs(); - files.add(new FileStorage(actual, offset, file.size)); - offset += file.size; + this.bucket = new FileCollectionStorage(files, this.getSize()); } - this.bucket = new FileCollectionStorage(files, this.getSize()); this.random = new Random(System.currentTimeMillis()); this.stop = false; @@ -221,8 +228,21 @@ public SharedTorrent(byte[] torrent, File parent, boolean seeder) this.downloaded = 0; this.left = this.getSize(); - this.initialized = false; - this.pieces = new Piece[0]; + if (this.peerOnly) { + this.initialized = true; + int nPieces = (int) (Math.ceil( + (double)this.getSize() / this.pieceLength)); + this.pieces = new Piece[nPieces]; + + for (int idx=0;idx()); this.completedPieces = new BitSet(); this.requestedPieces = new BitSet(); @@ -235,17 +255,41 @@ public SharedTorrent(byte[] torrent, File parent, boolean seeder) * meta-info from. * @param parent The parent directory or location of the torrent files. * @throws IOException When the torrent file cannot be read or decoded. - * @throws NoSuchAlgorithmException */ public static SharedTorrent fromFile(File source, File parent) - throws IOException, NoSuchAlgorithmException { - FileInputStream fis = new FileInputStream(source); - byte[] data = new byte[(int)source.length()]; - fis.read(data); - fis.close(); + throws IOException { + byte[] data = FileUtils.readFileToByteArray(source); return new SharedTorrent(data, parent); } + public double getMaxUploadRate() { + return this.maxUploadRate; + } + + /** + * Set the maximum upload rate (in kB/second) for this + * torrent. A setting of <= 0.0 disables rate limiting. + * + * @param rate The maximum upload rate + */ + public void setMaxUploadRate(double rate) { + this.maxUploadRate = rate; + } + + public double getMaxDownloadRate() { + return this.maxDownloadRate; + } + + /** + * Set the maximum download rate (in kB/second) for this + * torrent. A setting of <= 0.0 disables rate limiting. + * + * @param rate The maximum download rate + */ + public void setMaxDownloadRate(double rate) { + this.maxDownloadRate = rate; + } + /** * Get the number of bytes uploaded for this torrent. */ @@ -453,7 +497,7 @@ public BitSet getAvailablePieces() { synchronized (this.pieces) { for (Piece piece : this.pieces) { - if (piece.available()) { + if (piece != null && piece.available()) { availablePieces.set(piece.getIndex()); } } @@ -727,9 +771,11 @@ public synchronized void handleBitfieldAvailability(SharingPeer peer, // Record that the peer has all the pieces it told us it had. for (int i = availablePieces.nextSetBit(0); i >= 0; i = availablePieces.nextSetBit(i+1)) { - this.rarest.remove(this.pieces[i]); - this.pieces[i].seenAt(peer); - this.rarest.add(this.pieces[i]); + if (this.pieces[i] != null) { + this.rarest.remove(this.pieces[i]); + this.pieces[i].seenAt(peer); + this.rarest.add(this.pieces[i]); + } } logger.trace("Peer {} contributes {} piece(s) ({} interesting) " + diff --git a/src/main/java/com/turn/ttorrent/client/announce/Announce.java b/src/main/java/com/turn/ttorrent/client/announce/Announce.java index 4c0a1b223..0146b011f 100644 --- a/src/main/java/com/turn/ttorrent/client/announce/Announce.java +++ b/src/main/java/com/turn/ttorrent/client/announce/Announce.java @@ -75,8 +75,6 @@ public class Announce implements Runnable { * * @param torrent The torrent we're announcing about. * @param peer Our peer specification. - * @param type A string representing the announce type (used in the thread - * name). */ public Announce(SharedTorrent torrent, Peer peer) { this.peer = peer; @@ -226,7 +224,12 @@ public void run() { event = AnnounceRequestMessage.RequestEvent.NONE; } catch (AnnounceException ae) { logger.warn(ae.getMessage()); - this.moveToNextTrackerClient(); + + try { + this.moveToNextTrackerClient(); + } catch (AnnounceException e) { + logger.error("Unable to move to the next tracker client: {}", e.getMessage()); + } } try { @@ -281,8 +284,15 @@ private TrackerClient createTrackerClient(SharedTorrent torrent, Peer peer, /** * Returns the current tracker client used for announces. + * @throws AnnounceException When the current announce tier isn't defined + * in the torrent. */ - public TrackerClient getCurrentTrackerClient() { + public TrackerClient getCurrentTrackerClient() throws AnnounceException { + if ((this.currentTier >= this.clients.size()) || + (this.currentClient >= this.clients.get(this.currentTier).size())) { + throw new AnnounceException("Current tier or client isn't available"); + } + return this.clients .get(this.currentTier) .get(this.currentClient); @@ -300,8 +310,10 @@ public TrackerClient getCurrentTrackerClient() { * The index of the currently used {@link TrackerClient} is reset to 0 to * reflect this change. *

+ * + * @throws AnnounceException */ - private void promoteCurrentTrackerClient() { + private void promoteCurrentTrackerClient() throws AnnounceException { logger.trace("Promoting current tracker client for {} " + "(tier {}, position {} -> 0).", new Object[] { @@ -327,8 +339,10 @@ private void promoteCurrentTrackerClient() { * By design no empty tier can be in the tracker list structure so we don't * need to check for empty tiers here. *

+ * + * @throws AnnounceException */ - private void moveToNextTrackerClient() { + private void moveToNextTrackerClient() throws AnnounceException { int tier = this.currentTier; int client = this.currentClient + 1; diff --git a/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java b/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java index b757e0e17..7cfa0ece4 100644 --- a/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java +++ b/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java @@ -83,7 +83,7 @@ public abstract void announce(AnnounceRequestMessage.RequestEvent event, * Close any opened announce connection. * *

- * This method is called by {@link #stop()} to make sure all connections + * This method is called by {@link Announce#stop()} to make sure all connections * are correctly closed when the announce thread is asked to stop. *

*/ diff --git a/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java b/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java index 01b1c0420..5df3229b0 100644 --- a/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java +++ b/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java @@ -239,7 +239,7 @@ public void announce(AnnounceRequestMessage.RequestEvent event, * *

* Verifies the transaction ID of the message before passing it over to - * {@link Announce#handleTrackerAnnounceResponse()}. + * any registered {@link AnnounceResponseListener}. *

* * @param message The message received from the tracker in response to the @@ -352,7 +352,7 @@ private void send(ByteBuffer data) { * * @param attempt The attempt number, used to calculate the timeout for the * receive operation. - * @retun Returns a {@link ByteBuffer} containing the packet data. + * @return Returns a {@link ByteBuffer} containing the packet data. */ private ByteBuffer recv(int attempt) throws IOException, SocketException, SocketTimeoutException { diff --git a/src/main/java/com/turn/ttorrent/client/peer/PeerExchange.java b/src/main/java/com/turn/ttorrent/client/peer/PeerExchange.java index a7448d00c..b76012e25 100644 --- a/src/main/java/com/turn/ttorrent/client/peer/PeerExchange.java +++ b/src/main/java/com/turn/ttorrent/client/peer/PeerExchange.java @@ -17,6 +17,7 @@ import com.turn.ttorrent.client.SharedTorrent; import com.turn.ttorrent.common.protocol.PeerMessage; +import com.turn.ttorrent.common.protocol.PeerMessage.Type; import java.io.EOFException; import java.io.IOException; @@ -32,6 +33,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -177,16 +179,73 @@ public void close() { this.stop = true; if (this.channel.isConnected()) { - try { - this.channel.close(); - } catch (IOException ioe) { - // Ignore - } + IOUtils.closeQuietly(this.channel); } logger.debug("Peer exchange with {} closed.", this.peer); } + /** + * Abstract Thread subclass that allows conditional rate limiting + * for PIECE messages. + * + *

+ * To impose rate limits, we only want to throttle when processing PIECE + * messages. All other peer messages should be exchanged as quickly as + * possible. + *

+ * + * @author ptgoetz + */ + private abstract class RateLimitThread extends Thread { + + protected final Rate rate = new Rate(); + protected long sleep = 1000; + + /** + * Dynamically determines an amount of time to sleep, based on the + * average read/write throughput. + * + *

+ * The algorithm is functional, but could certainly be improved upon. + * One obvious drawback is that with large changes in + * maxRate, it will take a while for the sleep time to + * adjust and the throttled rate to "smooth out." + *

+ * + *

+ * Ideally, it would calculate the optimal sleep time necessary to hit + * a desired throughput rather than continuously adjust toward a goal. + *

+ * + * @param maxRate the target rate in kB/second. + * @param messageSize the size, in bytes, of the last message read/written. + * @param message the last PeerMessage read/written. + */ + protected void rateLimit(double maxRate, long messageSize, PeerMessage message) { + if (message.getType() != Type.PIECE || maxRate <= 0) { + return; + } + + try { + this.rate.add(messageSize); + + // Continuously adjust the sleep time to try to hit our target + // rate limit. + if (rate.get() > (maxRate * 1024)) { + Thread.sleep(this.sleep); + this.sleep += 50; + } else { + this.sleep = this.sleep > 50 + ? this.sleep - 50 + : 0; + } + } catch (InterruptedException e) { + // Not critical, eat it. + } + } + } + /** * Incoming messages thread. * @@ -199,7 +258,7 @@ public void close() { * * @author mpetazzoni */ - private class IncomingThread extends Thread { + private class IncomingThread extends RateLimitThread { @Override public void run() { @@ -210,28 +269,28 @@ public void run() { buffer.rewind(); buffer.limit(PeerMessage.MESSAGE_LENGTH_FIELD_SIZE); - if (channel.read(buffer) < 0) { - throw new EOFException( - "Reached end-of-stream while reading size header"); - } - // Keep reading bytes until the length field has been read // entirely. - if (buffer.hasRemaining()) { + while (!stop && buffer.hasRemaining()) { + if (channel.read(buffer) < 0) { + throw new EOFException( + "Reached end-of-stream while reading size header"); + } try { Thread.sleep(1); } catch (InterruptedException ie) { // Ignore and move along. } - - continue; } int pstrlen = buffer.getInt(0); buffer.limit(PeerMessage.MESSAGE_LENGTH_FIELD_SIZE + pstrlen); + long size = 0; while (!stop && buffer.hasRemaining()) { - if (channel.read(buffer) < 0) { + int read = channel.read(buffer); + size += read; + if (read < 0) { throw new EOFException( "Reached end-of-stream while reading message"); } @@ -243,6 +302,10 @@ public void run() { PeerMessage message = PeerMessage.parse(buffer, torrent); logger.trace("Received {} from {}", message, peer); + // Wait if needed to reach configured download rate. + this.rateLimit(PeerExchange.this.torrent.getMaxDownloadRate(), + size, message); + for (MessageListener listener : listeners) { listener.handleMessage(message); } @@ -277,7 +340,7 @@ public void run() { * * @author mpetazzoni */ - private class OutgoingThread extends Thread { + private class OutgoingThread extends RateLimitThread { @Override public void run() { @@ -302,12 +365,19 @@ public void run() { logger.trace("Sending {} to {}", message, peer); ByteBuffer data = message.getData(); + long size = 0; while (!stop && data.hasRemaining()) { - if (channel.write(data) < 0) { + int written = channel.write(data); + size += written; + if (written < 0) { throw new EOFException( "Reached end of stream while writing"); } } + + // Wait if needed to reach configured upload rate. + this.rateLimit(PeerExchange.this.torrent.getMaxUploadRate(), + size, message); } catch (InterruptedException ie) { // Ignore and potentially terminate } diff --git a/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java b/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java index c753132a6..6bb50d3b6 100644 --- a/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java +++ b/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java @@ -182,7 +182,7 @@ private List select(long offset, long length) { long bytes = 0; for (FileStorage file : this.files) { - if (file.offset() > offset + length) { + if (file.offset() >= offset + length) { break; } diff --git a/src/main/java/com/turn/ttorrent/common/Torrent.java b/src/main/java/com/turn/ttorrent/common/Torrent.java index e468dc560..5ee4f471c 100644 --- a/src/main/java/com/turn/ttorrent/common/Torrent.java +++ b/src/main/java/com/turn/ttorrent/common/Torrent.java @@ -23,18 +23,14 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.io.PrintStream; import java.io.UnsupportedEncodingException; -import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -51,12 +47,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; -import jargs.gnu.CmdLineParser; - -import org.apache.log4j.BasicConfigurator; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.PatternLayout; - +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,7 +97,7 @@ public TorrentFile(File file, long size) { this.file = file; this.size = size; } - }; + } protected final byte[] encoded; @@ -133,15 +126,11 @@ public TorrentFile(File file, long size) { * BitTorrent specification) and create a Torrent object from it. * * @param torrent The meta-info byte data. - * @param parent The parent directory or location of the torrent files. * @param seeder Whether we'll be seeding for this torrent or not. * @throws IOException When the info dictionary can't be read or * encoded and hashed back to create the torrent's SHA-1 hash. - * @throws NoSuchAlgorithmException If the SHA-1 algorithm is not - * available. */ - public Torrent(byte[] torrent, boolean seeder) - throws IOException, NoSuchAlgorithmException { + public Torrent(byte[] torrent, boolean seeder) throws IOException { this.encoded = torrent; this.seeder = seeder; @@ -412,10 +401,8 @@ public void save(OutputStream output) throws IOException { output.write(this.getEncoded()); } - public static byte[] hash(byte[] data) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - md.update(data); - return md.digest(); + public static byte[] hash(byte[] data) { + return DigestUtils.sha1(data); } /** @@ -425,8 +412,7 @@ public static byte[] hash(byte[] data) throws NoSuchAlgorithmException { * @param bytes The byte array to convert. */ public static String byteArrayToHexString(byte[] bytes) { - BigInteger bi = new BigInteger(1, bytes); - return String.format("%0" + (bytes.length << 1) + "X", bi); + return new String(Hex.encodeHex(bytes, false)); } /** @@ -485,10 +471,8 @@ protected static int getHashingThreadsCount() { * @param torrent The abstract {@link File} object representing the * .torrent file to load. * @throws IOException When the torrent file cannot be read. - * @throws NoSuchAlgorithmException */ - public static Torrent load(File torrent) - throws IOException, NoSuchAlgorithmException { + public static Torrent load(File torrent) throws IOException { return Torrent.load(torrent, false); } @@ -500,21 +484,11 @@ public static Torrent load(File torrent) * @param seeder Whether we are a seeder for this torrent or not (disables * local data validation). * @throws IOException When the torrent file cannot be read. - * @throws NoSuchAlgorithmException */ public static Torrent load(File torrent, boolean seeder) - throws IOException, NoSuchAlgorithmException { - FileInputStream fis = null; - try { - fis = new FileInputStream(torrent); - byte[] data = new byte[(int)torrent.length()]; - fis.read(data); - return new Torrent(data, seeder); - } finally { - if (fis != null) { - fis.close(); - } - } + throws IOException { + byte[] data = FileUtils.readFileToByteArray(torrent); + return new Torrent(data, seeder); } /** Torrent creation --------------------------------------------------- */ @@ -534,7 +508,7 @@ public static Torrent load(File torrent, boolean seeder) * torrent's creator. */ public static Torrent create(File source, URI announce, String createdBy) - throws NoSuchAlgorithmException, InterruptedException, IOException { + throws InterruptedException, IOException { return Torrent.create(source, null, announce, null, createdBy); } @@ -556,8 +530,7 @@ public static Torrent create(File source, URI announce, String createdBy) * torrent's creator. */ public static Torrent create(File parent, List files, URI announce, - String createdBy) throws NoSuchAlgorithmException, - InterruptedException, IOException { + String createdBy) throws InterruptedException, IOException { return Torrent.create(parent, files, announce, null, createdBy); } @@ -577,8 +550,7 @@ public static Torrent create(File parent, List files, URI announce, * torrent's creator. */ public static Torrent create(File source, List> announceList, - String createdBy) throws NoSuchAlgorithmException, - InterruptedException, IOException { + String createdBy) throws InterruptedException, IOException { return Torrent.create(source, null, null, announceList, createdBy); } @@ -592,7 +564,7 @@ public static Torrent create(File source, List> announceList, * considering we'll be a full initial seeder for it. *

* - * @param parent The parent directory or location of the torrent files, + * @param source The parent directory or location of the torrent files, * also used as the torrent's name. * @param files The files to add into this torrent. * @param announceList The announce URIs organized as tiers that will @@ -602,7 +574,7 @@ public static Torrent create(File source, List> announceList, */ public static Torrent create(File source, List files, List> announceList, String createdBy) - throws NoSuchAlgorithmException, InterruptedException, IOException { + throws InterruptedException, IOException { return Torrent.create(source, files, null, announceList, createdBy); } @@ -627,7 +599,7 @@ public static Torrent create(File source, List files, */ private static Torrent create(File parent, List files, URI announce, List> announceList, String createdBy) - throws NoSuchAlgorithmException, InterruptedException, IOException { + throws InterruptedException, IOException { if (files == null || files.isEmpty()) { logger.info("Creating single-file torrent for {}...", parent.getName()); @@ -704,9 +676,8 @@ private static class CallableChunkHasher implements Callable { private final MessageDigest md; private final ByteBuffer data; - CallableChunkHasher(ByteBuffer buffer) - throws NoSuchAlgorithmException { - this.md = MessageDigest.getInstance("SHA-1"); + CallableChunkHasher(ByteBuffer buffer) { + this.md = DigestUtils.getSha1Digest(); this.data = ByteBuffer.allocate(buffer.remaining()); buffer.mark(); @@ -739,12 +710,12 @@ public String call() throws UnsupportedEncodingException { * @param file The file to hash. */ private static String hashFile(File file) - throws NoSuchAlgorithmException, InterruptedException, IOException { + throws InterruptedException, IOException { return Torrent.hashFiles(Arrays.asList(new File[] { file })); } private static String hashFiles(List files) - throws NoSuchAlgorithmException, InterruptedException, IOException { + throws InterruptedException, IOException { int threads = getHashingThreadsCount(); ExecutorService executor = Executors.newFixedThreadPool(threads); ByteBuffer buffer = ByteBuffer.allocate(Torrent.PIECE_LENGTH); @@ -843,131 +814,4 @@ private static int accumulateHashes(StringBuilder hashes, throw new IOException("Error while hashing the torrent data!", ee); } } - - /** - * Display program usage on the given {@link PrintStream}. - */ - private static void usage(PrintStream s) { - usage(s, null); - } - - /** - * Display a message and program usage on the given {@link PrintStream}. - */ - private static void usage(PrintStream s, String msg) { - if (msg != null) { - s.println(msg); - s.println(); - } - - s.println("usage: Torrent [options] [file|directory]"); - s.println(); - s.println("Available options:"); - s.println(" -h,--help Show this help and exit."); - s.println(" -t,--torrent FILE Use FILE to read/write torrent file."); - s.println(); - s.println(" -c,--create Create a new torrent file using " + - "the given announce URL and data."); - s.println(" -a,--announce Tracker URL (can be repeated)."); - s.println(); - } - - /** - * Torrent reader and creator. - * - *

- * You can use the {@code main()} function of this {@link Torrent} class to - * read or create torrent files. See usage for details. - *

- * - * TODO: support multiple announce URLs. - */ - public static void main(String[] args) { - BasicConfigurator.configure(new ConsoleAppender( - new PatternLayout("%-5p: %m%n"))); - - CmdLineParser parser = new CmdLineParser(); - CmdLineParser.Option help = parser.addBooleanOption('h', "help"); - CmdLineParser.Option filename = parser.addStringOption('t', "torrent"); - CmdLineParser.Option create = parser.addBooleanOption('c', "create"); - CmdLineParser.Option announce = parser.addStringOption('a', "announce"); - - try { - parser.parse(args); - } catch (CmdLineParser.OptionException oe) { - System.err.println(oe.getMessage()); - usage(System.err); - System.exit(1); - } - - // Display help and exit if requested - if (Boolean.TRUE.equals((Boolean)parser.getOptionValue(help))) { - usage(System.out); - System.exit(0); - } - - String filenameValue = (String)parser.getOptionValue(filename); - if (filenameValue == null) { - usage(System.err, "Torrent file must be provided!"); - System.exit(1); - } - - Boolean createFlag = (Boolean)parser.getOptionValue(create); - String announceURL = (String)parser.getOptionValue(announce); - - String[] otherArgs = parser.getRemainingArgs(); - - if (Boolean.TRUE.equals(createFlag) && - (otherArgs.length != 1 || announceURL == null)) { - usage(System.err, "Announce URL and a file or directory must be " + - "provided to create a torrent file!"); - System.exit(1); - } - - OutputStream fos = null; - try { - if (Boolean.TRUE.equals(createFlag)) { - if (filenameValue != null) { - fos = new FileOutputStream(filenameValue); - } else { - fos = System.out; - } - - URI announceURI = new URI(announceURL); - File source = new File(otherArgs[0]); - if (!source.exists() || !source.canRead()) { - throw new IllegalArgumentException( - "Cannot access source file or directory " + - source.getName()); - } - - String creator = String.format("%s (ttorrent)", - System.getProperty("user.name")); - - Torrent torrent = null; - if (source.isDirectory()) { - File[] files = source.listFiles(); - Arrays.sort(files); - torrent = Torrent.create(source, Arrays.asList(files), - announceURI, creator); - } else { - torrent = Torrent.create(source, announceURI, creator); - } - - torrent.save(fos); - } else { - Torrent.load(new File(filenameValue), true); - } - } catch (Exception e) { - logger.error("{}", e.getMessage(), e); - System.exit(2); - } finally { - if (fos != null && fos != System.out) { - try { - fos.close(); - } catch (IOException ioe) { - } - } - } - } } diff --git a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java index 0362ba72f..3645573d0 100644 --- a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java +++ b/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java @@ -254,7 +254,7 @@ public static HTTPAnnounceRequestMessage parse(ByteBuffer data) return new HTTPAnnounceRequestMessage(data, infoHash, new Peer(ip, port, ByteBuffer.wrap(peerId)), - downloaded, uploaded, left, compact, noPeerId, + uploaded, downloaded, left, compact, noPeerId, event, numWant); } catch (InvalidBEncodingException ibee) { throw new MessageValidationException( diff --git a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java index efb75fce3..01d27cd7f 100644 --- a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java +++ b/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java @@ -87,6 +87,11 @@ public static HTTPAnnounceResponseMessage parse(ByteBuffer data) Map params = decoded.getMap(); + if (params.get("interval") == null) { + throw new MessageValidationException( + "Tracker message missing mandatory field 'interval'!"); + } + try { List peers; @@ -102,8 +107,8 @@ public static HTTPAnnounceResponseMessage parse(ByteBuffer data) return new HTTPAnnounceResponseMessage(data, params.get("interval").getInt(), - params.get("complete").getInt(), - params.get("incomplete").getInt(), + params.get("complete") != null ? params.get("complete").getInt() : 0, + params.get("incomplete") != null ? params.get("incomplete").getInt() : 0, peers); } catch (InvalidBEncodingException ibee) { throw new MessageValidationException("Invalid response " + diff --git a/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java b/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java index 98a2734b5..3e8e500d6 100644 --- a/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java +++ b/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java @@ -20,12 +20,10 @@ import com.turn.ttorrent.common.protocol.TrackerMessage.AnnounceRequestMessage.RequestEvent; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; -import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -33,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,11 +76,8 @@ public class TrackedTorrent extends Torrent { * @param torrent The meta-info byte data. * @throws IOException When the info dictionary can't be * encoded and hashed back to create the torrent's SHA-1 hash. - * @throws NoSuchAlgorithmException If the SHA-1 algorithm is not - * available. */ - public TrackedTorrent(byte[] torrent) - throws IOException, NoSuchAlgorithmException { + public TrackedTorrent(byte[] torrent) throws IOException { super(torrent, false); this.peers = new ConcurrentHashMap(); @@ -89,8 +85,7 @@ public TrackedTorrent(byte[] torrent) this.announceInterval = TrackedTorrent.DEFAULT_ANNOUNCE_INTERVAL_SECONDS; } - public TrackedTorrent(Torrent torrent) - throws IOException, NoSuchAlgorithmException { + public TrackedTorrent(Torrent torrent) throws IOException { this(torrent.getEncoded()); } @@ -293,20 +288,9 @@ public List getSomePeers(TrackedPeer peer) { * @param torrent The abstract {@link File} object representing the * .torrent file to load. * @throws IOException When the torrent file cannot be read. - * @throws NoSuchAlgorithmException */ - public static TrackedTorrent load(File torrent) throws IOException, - NoSuchAlgorithmException { - FileInputStream fis = null; - try { - fis = new FileInputStream(torrent); - byte[] data = new byte[(int)torrent.length()]; - fis.read(data); - return new TrackedTorrent(data); - } finally { - if (fis != null) { - fis.close(); - } - } + public static TrackedTorrent load(File torrent) throws IOException { + byte[] data = FileUtils.readFileToByteArray(torrent); + return new TrackedTorrent(data); } } diff --git a/src/main/java/com/turn/ttorrent/tracker/Tracker.java b/src/main/java/com/turn/ttorrent/tracker/Tracker.java index 6ba05d8b1..638fae259 100644 --- a/src/main/java/com/turn/ttorrent/tracker/Tracker.java +++ b/src/main/java/com/turn/ttorrent/tracker/Tracker.java @@ -17,30 +17,21 @@ import com.turn.ttorrent.common.Torrent; -import java.io.File; -import java.io.FilenameFilter; import java.io.IOException; -import java.io.PrintStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.URL; +import java.util.Collection; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import jargs.gnu.CmdLineParser; - -import org.apache.log4j.BasicConfigurator; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.PatternLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.simpleframework.transport.connect.Connection; import org.simpleframework.transport.connect.SocketConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * BitTorrent tracker. @@ -180,6 +171,13 @@ public void stop() { } } + /** + * Returns the list of tracker's torrents + */ + public Collection getTrackedTorrents() { + return torrents.values(); + } + /** * Announce a new torrent on this tracker. * @@ -318,82 +316,4 @@ public void run() { } } } - - /** - * Display program usage on the given {@link PrintStream}. - */ - private static void usage(PrintStream s) { - s.println("usage: Tracker [options] [directory]"); - s.println(); - s.println("Available options:"); - s.println(" -h,--help Show this help and exit."); - s.println(" -p,--port PORT Bind to port PORT."); - s.println(); - } - - /** - * Main function to start a tracker. - */ - public static void main(String[] args) { - BasicConfigurator.configure(new ConsoleAppender( - new PatternLayout("%d [%-25t] %-5p: %m%n"))); - - CmdLineParser parser = new CmdLineParser(); - CmdLineParser.Option help = parser.addBooleanOption('h', "help"); - CmdLineParser.Option port = parser.addIntegerOption('p', "port"); - - try { - parser.parse(args); - } catch (CmdLineParser.OptionException oe) { - System.err.println(oe.getMessage()); - usage(System.err); - System.exit(1); - } - - // Display help and exit if requested - if (Boolean.TRUE.equals((Boolean)parser.getOptionValue(help))) { - usage(System.out); - System.exit(0); - } - - Integer portValue = (Integer)parser.getOptionValue(port, - Integer.valueOf(DEFAULT_TRACKER_PORT)); - - String[] otherArgs = parser.getRemainingArgs(); - - if (otherArgs.length > 1) { - usage(System.err); - System.exit(1); - } - - // Get directory from command-line argument or default to current - // directory - String directory = otherArgs.length > 0 - ? otherArgs[0] - : "."; - - FilenameFilter filter = new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.endsWith(".torrent"); - } - }; - - try { - Tracker t = new Tracker(new InetSocketAddress(portValue.intValue())); - - File parent = new File(directory); - for (File f : parent.listFiles(filter)) { - logger.info("Loading torrent from " + f.getName()); - t.announce(TrackedTorrent.load(f)); - } - - logger.info("Starting tracker with {} announced torrents...", - t.torrents.size()); - t.start(); - } catch (Exception e) { - logger.error("{}", e.getMessage(), e); - System.exit(2); - } - } } diff --git a/src/main/java/com/turn/ttorrent/tracker/TrackerService.java b/src/main/java/com/turn/ttorrent/tracker/TrackerService.java index 014ec5da6..ae24cabd3 100644 --- a/src/main/java/com/turn/ttorrent/tracker/TrackerService.java +++ b/src/main/java/com/turn/ttorrent/tracker/TrackerService.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentMap; +import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -117,13 +118,7 @@ public void handle(Request request, Response response) { } catch (IOException ioe) { logger.warn("Error while writing response: {}!", ioe.getMessage()); } finally { - if (body != null) { - try { - body.close(); - } catch (IOException ioe) { - // Ignore - } - } + IOUtils.closeQuietly(body); } } @@ -252,7 +247,7 @@ private void process(Request request, Response response, * Tracker HTTP protocol. *

* - * @param uri The request's full URI, including query parameters. + * @param request The request's full URI, including query parameters. * @return The {@link AnnounceRequestMessage} representing the client's * announce request. */ @@ -350,7 +345,7 @@ private void serveError(Response response, OutputStream body, * @param response The HTTP response object. * @param body The response output stream to write to. * @param status The HTTP status code to return. - * @param error The failure reason reported by the tracker. + * @param reason The failure reason reported by the tracker. */ private void serveError(Response response, OutputStream body, Status status, ErrorMessage.FailureReason reason) throws IOException {