diff --git a/.gitignore b/.gitignore index 9fc5403e2..9dc641b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ # Git ignore patterns # Author: Maxime Petazzoni +# Author: Shevek # Ignore build output -/build/* - -# Ignore Javadoc output -/doc/* +build # Ignore any eventual Eclipse project files, these don't belong in the # repository. @@ -18,3 +16,7 @@ *.bak *~ *~ + +/.gradle +/.nb-gradle + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..dff5f3a5d --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: java diff --git a/README.md b/README.md index e014586bb..88d38225f 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,23 @@ metainfo files creation and parsing capabilities. It is designed to be embedded into larger applications, but its components can also be used as standalone programs. +This fork of ttorrent is a complete structural overhaul for +correctness, improved performance using [Netty](http://netty.io/), +vastly reduced memory consumption, monitoring and metrics, and +optional integration with [Spring Framework](http://spring.io/) and +[Jetty](http://www.eclipse.org/jetty/). + Ttorrent supports the following BEPs (BitTorrent enhancement proposals): -* `BEP#0003`: The BitTorrent protocol specification +* `BEP#0003`: The BitTorrent protocol specification (complete) This is the base official protocol specification, which Ttorrent implements fully. +* `BEP#0007`: IPv6 Tracker Extension +* `BEP#0010`: Extension Protocol * `BEP#0012`: Multi-tracker metadata extension Full support for the `announce-list` meta-info key providing a tiered tracker list. -* `BEP#0015`: UDP Tracker Protocol for BitTorrent +* `BEP#0015`: UDP Tracker Protocol for BitTorrent (partial) The UDP tracker protocol is fully supported in the BitTorrent client to make announce requests to UDP trackers. UDP tracker support itself is planned. * `BEP#0020`: Peer ID conventions @@ -28,6 +36,12 @@ Ttorrent supports the following BEPs (BitTorrent enhancement proposals): Compact peer lists are supported in both the client and the tracker. Currently the tracker only supports sending back compact peer lists to an announce request. +* `BEP#0024`: Tracker Returns External IP + +In addition, the following extensions are supported: + +* Peer Exchange (ut\_pex) with PEX seeding for fast trackerless operation. +* Linux epoll() (See ClientEnvironment#setEventLoopType()). History ------- @@ -44,15 +58,9 @@ to re-integrate into another application; * Snark's, which is old, and unfortunately unstable; * bitext, which was also unfortunately unstable, and extremely slow. -This implementation aims at providing a down-to-earth, simple to use library. -No fancy protocol extensions are implemented here: just the basics that allows -for the exchange and distribution of files through the BitTorrent protocol. - -Although the write performance of the BitTorrent client is currently quite poor -(~10MB/sec/connected peer), it has been measured that the distribution of a -150MB file to thousands of machines across several datacenters took no more -than 30 seconds, with very little network overhead for the initial seeder (only -125% of the original file size uploaded by the initial seeder). +The APIs and implementation were thoroughly overhauled in this fork, +with the objective of sharing a multi-gigabyte file to a thousand +hosts in a matter of seconds. How to use @@ -65,65 +73,67 @@ usage message on the console when invoked with the ``-h`` command-line flag. ### As a library -*Thanks to Anatoli Vladev for the code examples in #16.* +To use ``ttorrent`` is a library in your project, all you need is to +declare the dependency on the latest version of ``ttorrent``. For +example, if you use Maven, add the following in your POM's dependencies +section: + +```xml + + ... + + org.anarres.ttorrent + ttorrent-client + 1.8.0-SNAPSHOT + + +``` #### Client code ```java // First, instantiate the Client object. -Client client = new Client( - // This is the interface the client will listen on (you might need something - // else than localhost here). - InetAddress.getLocalHost(), - - // Load the torrent from the torrent file and use the given - // output directory. Partials downloads are automatically recovered. - SharedTorrent.fromFile( - new File("/path/to/your.torrent"), - new File("/path/to/output/directory"))); - -// At this point, can you either call download() to download the torrent and -// stop immediately after... -client.download(); - -// Or call client.share(...) with a seed time in seconds: -// client.share(3600); -// Which would seed the torrent for an hour after the download is complete. - -// Downloading and seeding is done in background threads. -// To wait for this process to finish, call: -client.waitForCompletion(); - -// At any time you can call client.stop() to interrupt the download. +Client client = new Client(); + +// Configure the client as desired. +client.getEnvironment.set*(); + +// Add one or more torrents. +Torrent torrent = new Torrent(new File("/path/to/your.torrent")) +client.addTorrent(torrent, new File("/path/to/output/directory")); + +client.start(); + ``` +At any time you can call `client.stop()` to interrupt the download. +You can also add a +[ClientListener](ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientListener.java) +to wait for a download to complete. #### Tracker code +For a tracker, there are a number of options: + +Standalone: + ```java -// First, instantiate a Tracker object with the port you want it to listen on. -// The default tracker port recommended by the BitTorrent protocol is 6969. -Tracker tracker = new Tracker(new InetSocketAddress(6969)); - -// Then, for each torrent you wish to announce on this tracker, simply created -// a TrackedTorrent object and pass it to the tracker.announce() method: -FilenameFilter filter = new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.endsWith(".torrent"); - } -}; - -for (File f : new File("/path/to/torrent/files").listFiles(filter)) { - tracker.announce(TrackedTorrent.load(f)); -} - -// Once done, you just have to start the tracker's main operation loop: +SimpleTracker tracker = new SimpleTracker(new InetSocketAddress(6969)); +tracker.addTorrent(new Torrent("/path/to/file")); tracker.start(); // You can stop the tracker when you're done with: tracker.stop(); ``` +Servlets/Jetty: See [TrackerServlet](ttorrent-tracker-servlet/src/main/java/com/turn/ttorrent/tracker/servlet/TrackerServlet.java). + +Spring Framework: See [TrackerController](ttorrent-tracker-spring/src/main/java/com/turn/ttorrent/tracker/spring/TrackerController.java). + +#### JavaDoc API + +The [JavaDoc API](http://shevek.github.io/ttorrent/docs/javadoc/) +is available. + License ------- @@ -134,14 +144,14 @@ 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. * Thomas Zink <> Fixed a piece length computation issue when the total torrent size is an exact multiple of the piece size. -* Johan Parent <> +* Johan Parent <> Fixed a bug in unfresh peer collection and issues on download completion on Windows platforms. * Dmitriy Dumanskiy @@ -153,8 +163,5 @@ Authors and contributors Caveats ------- -* Client write performance is a bit poor, mainly due to a (too?) simple piece - caching algorithm. - Contributions are welcome in all areas, even more so for these few points above! diff --git a/TODO b/TODO new file mode 100644 index 000000000..c0bd9eeb5 --- /dev/null +++ b/TODO @@ -0,0 +1,9 @@ +Code required: + +1) Protocol extension to specify maximum block size - we want 1Mb. +2) Keepalives. + +Tests required: + +Connect to peers which never offer anything. Make sure rarestPiece is null. + diff --git a/bin/client b/bin/client deleted file mode 100755 index cd77b761a..000000000 --- a/bin/client +++ /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 -jar $(find ${base}/../build -name "ttorrent-*.jar" | tail -n 1) $* diff --git a/bin/torrent b/bin/torrent deleted file mode 100755 index 99d46d258..000000000 --- a/bin/torrent +++ /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.common.Torrent $* 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/build.gradle b/build.gradle new file mode 100644 index 000000000..c758518e9 --- /dev/null +++ b/build.gradle @@ -0,0 +1,147 @@ +buildscript { + // Executed in context of buildscript + repositories { + // mavenLocal() + mavenCentral() + // jcenter() + // maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } + gradlePluginPortal() + } + + dependencies { + classpath 'org.anarres.gradle:gradle-stdproject-plugin:1.0.10' + } +} + +apply plugin: 'org.anarres.stdproject' +stdproject { + javadocLink "http://netty.io/4.0/api/" + + javadocGroup "BitTorrent Protocols", "com.turn.ttorrent.protocol*" + javadocGroup "BitTorrent Client Implementation", "com.turn.ttorrent.client*" + javadocGroup "BitTorrent Tracker Implementations", "com.turn.ttorrent.tracker*" +} + +subprojects { + group = "org.anarres.ttorrent" + + apply plugin: 'org.anarres.stdmodule' + stdmodule { + description "An embeddable high performance bittorrent library, client and tracker." + author id: 'shevek', name: 'Shevek', email: 'github@anarres.org' + license 'Apache-2.0' + } + + apply plugin: 'eclipse' + eclipse { + classpath { + downloadSources = true + } + jdt { + sourceCompatibility = 1.7 + targetCompatibility = 1.7 + } + } + + configurations { + compile { + exclude group: 'commons-logging', module: 'commons-logging' + // exclude group: 'log4j', module: 'log4j' + // exclude group: 'org.slf4j', module: 'slf4j-log4j12' + } + } + + dependencies { + testCompile 'org.easymock:easymock:3.2' + + testRuntime 'org.slf4j:jcl-over-slf4j:1.7.12' + } + + sourceCompatibility = 1.7 +} + +project(':ttorrent-protocol') { + dependencies { + compile 'com.google.code.findbugs:annotations:2.0.3' + compile 'org.slf4j:slf4j-api:1.7.7' + compile 'com.google.guava:guava:18.0' + compile 'io.dropwizard.metrics:metrics-core:3.1.0' + compile 'io.netty:netty-all:4.0.23.Final' + + testCompile project(':ttorrent-tracker-simple') + testCompile 'commons-io:commons-io:2.4' + } +} + +project(':ttorrent-tracker-client') { + dependencies { + compile project(':ttorrent-protocol') + compile 'org.apache.httpcomponents:httpasyncclient:4.0.2' + + testCompile project(':ttorrent-tracker-simple') + } +} + +project(':ttorrent-tracker') { + dependencies { + compile project(':ttorrent-protocol') + + testCompile project(':ttorrent-tracker-simple') + } +} + +project(':ttorrent-tracker-simple') { + apply plugin: 'application' + + mainClassName = 'com.turn.ttorrent.tracker.simple.SimpleTrackerMain' + + dependencies { + compile project(':ttorrent-tracker') + + compile 'org.simpleframework:simple:5.1.6' + compile 'net.sf.jopt-simple:jopt-simple:4.7' + + testCompile project(':ttorrent-tracker-client') + } +} + +project(':ttorrent-tracker-servlet') { + dependencies { + compile project(':ttorrent-tracker') + + compile 'javax.servlet:javax.servlet-api:3.1.0' + } +} + +project(':ttorrent-tracker-spring') { + dependencies { + compile project(':ttorrent-tracker-servlet') + + def springVersion = "4.1.1.RELEASE" + compile "org.springframework:spring-webmvc:$springVersion" + + // compile 'javax.servlet:servlet-api:2.5' + compile 'javax.servlet:javax.servlet-api:3.1.0' + } +} + +project(':ttorrent-client') { + apply plugin: 'application' + + mainClassName = 'com.turn.ttorrent.client.main.ClientMain' + + dependencies { + compile project(':ttorrent-tracker-client') + + compile 'commons-io:commons-io:2.4' + compile 'net.sf.jopt-simple:jopt-simple:4.7' + + testCompile project(':ttorrent-tracker-simple') + testCompile project(':ttorrent-protocol').sourceSets.test.output + testCompile project(':ttorrent-tracker-client').sourceSets.test.output + } + + test { + maxHeapSize = "4096m" + } +} diff --git a/codequality/HEADER b/codequality/HEADER new file mode 100644 index 000000000..d1d3f9802 --- /dev/null +++ b/codequality/HEADER @@ -0,0 +1,14 @@ +Copyright 2011-2012 Turn, Inc +Copyright ${year} Shevek + +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. diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml new file mode 100644 index 000000000..47c01a2ea --- /dev/null +++ b/codequality/checkstyle.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..a04c8df8c --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version=1.8.0-SNAPSHOT diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..5c2d1cf01 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ef9a9e05e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..83f2acfdc --- /dev/null +++ b/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..24467a141 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 33d6c98ca..000000000 --- a/pom.xml +++ /dev/null @@ -1,174 +0,0 @@ - - 4.0.0 - - - org.sonatype.oss - oss-parent - 7 - - - Java BitTorrent library - - ttorrent is a pure-Java implementation of the BitTorrent protocol, - including support for several BEPs. It also provides a standalone client, - a tracker and a torrent manipulation utility. - - http://turn.github.com/ttorrent/ - com.turn - ttorrent - 1.2 - jar - - - Turn, Inc. - http://www.turn.com - - - - scm:git:git://github.com/turn/ttorrent.git - http://github.com/turn/ttorrent - - - - - Apache Software License version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - GitHub - https://github.com/turn/ttorrent/issues - - - - - mpetazzoni - Maxime Petazzoni - mpetazzoni@turn.com - http://www.bulix.org - Turn, Inc - http://www.turn.com - - maintainer - architect - developer - - -8 - - https://secure.gravatar.com/avatar/6f705e0c299bca294444de3a6a3308b3 - - - - - - UTF-8 - - - - - jboss-thirdparty-releases - JBoss Thirdparty Releases - https://repository.jboss.org/nexus/content/repositories/thirdparty-releases/ - - - - - - commons-io - commons-io - 2.1 - - - - org.simpleframework - simple - 4.1.21 - - - - org.slf4j - slf4j-log4j12 - 1.6.4 - - - - org.testng - testng - 6.1.1 - test - - - - net.sf - jargs - 1.0 - - - - - package - ${basedir}/build - - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - ** - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.8.1 - - ${basedir} - doc - - - - - maven-assembly-plugin - - - jar-with-dependencies - - false - - - false - true - com.turn.ttorrent.client.Client - - - - - - make-my-jar-with-dependencies - package - - - assembly - - - - - - - diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..d55b81fb0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,9 @@ +rootProject.name='ttorrent' +include \ + 'ttorrent-protocol', + 'ttorrent-tracker', + 'ttorrent-tracker-simple', + 'ttorrent-tracker-servlet', + 'ttorrent-tracker-spring', + 'ttorrent-tracker-client', + 'ttorrent-client' diff --git a/src/main/ghpages/index.html b/src/main/ghpages/index.html new file mode 100644 index 000000000..32292c370 --- /dev/null +++ b/src/main/ghpages/index.html @@ -0,0 +1,6 @@ + + +Javadoc +Coverage + + diff --git a/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java b/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java deleted file mode 100644 index 305c56170..000000000 --- a/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java +++ /dev/null @@ -1,324 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.bcodec; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.EOFException; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.HashMap; -import java.util.Map; - -import org.apache.commons.io.input.AutoCloseInputStream; - - -/** - * B-encoding decoder. - * - *

- * A b-encoded byte stream can represent byte arrays, numbers, lists and maps - * (dictionaries). This class implements a decoder of such streams into - * {@link BEValue}s. - *

- * - *

- * Inspired by Snark's implementation. - *

- * - * @author mpetazzoni - * @see B-encoding specification - */ -public class BDecoder { - - // The InputStream to BDecode. - private final InputStream in; - - // The last indicator read. - // Zero if unknown. - // '0'..'9' indicates a byte[]. - // 'i' indicates an Number. - // 'l' indicates a List. - // 'd' indicates a Map. - // 'e' indicates end of Number, List or Map (only used internally). - // -1 indicates end of stream. - // Call getNextIndicator to get the current value (will never return zero). - private int indicator = 0; - - /** - * Initializes a new BDecoder. - * - *

- * Nothing is read from the given InputStream yet. - *

- * - * @param in The input stream to read from. - */ - public BDecoder(InputStream in) { - this.in = in; - } - - /** - * Decode a B-encoded stream. - * - *

- * Automatically instantiates a new BDecoder for the provided input stream - * and decodes its root member. - *

- * - * @param in The input stream to read from. - */ - public static BEValue bdecode(InputStream in) throws IOException { - return new BDecoder(in).bdecode(); - } - - /** - * Decode a B-encoded byte buffer. - * - *

- * Automatically instantiates a new BDecoder for the provided buffer and - * decodes its root member. - *

- * - * @param data The {@link ByteBuffer} to read from. - */ - public static BEValue bdecode(ByteBuffer data) throws IOException { - return BDecoder.bdecode(new AutoCloseInputStream( - new ByteArrayInputStream(data.array()))); - } - - /** - * Returns what the next b-encoded object will be on the stream or -1 - * when the end of stream has been reached. - * - *

- * Can return something unexpected (not '0' .. '9', 'i', 'l' or 'd') when - * the stream isn't b-encoded. - *

- * - * This might or might not read one extra byte from the stream. - */ - private int getNextIndicator() throws IOException { - if (this.indicator == 0) { - this.indicator = in.read(); - } - return this.indicator; - } - - /** - * Gets the next indicator and returns either null when the stream - * has ended or b-decodes the rest of the stream and returns the - * appropriate BEValue encoded object. - */ - public BEValue bdecode() throws IOException { - if (this.getNextIndicator() == -1) - return null; - - if (this.indicator >= '0' && this.indicator <= '9') - return this.bdecodeBytes(); - else if (this.indicator == 'i') - return this.bdecodeNumber(); - else if (this.indicator == 'l') - return this.bdecodeList(); - else if (this.indicator == 'd') - return this.bdecodeMap(); - else - throw new InvalidBEncodingException - ("Unknown indicator '" + this.indicator + "'"); - } - - /** - * Returns the next b-encoded value on the stream and makes sure it is a - * byte array. - * - * @throws InvalidBEncodingException If it is not a b-encoded byte array. - */ - public BEValue bdecodeBytes() throws IOException { - int c = this.getNextIndicator(); - int num = c - '0'; - if (num < 0 || num > 9) - throw new InvalidBEncodingException("Number expected, not '" - + (char)c + "'"); - this.indicator = 0; - - c = this.read(); - int i = c - '0'; - while (i >= 0 && i <= 9) { - // This can overflow! - num = num*10 + i; - c = this.read(); - i = c - '0'; - } - - if (c != ':') { - throw new InvalidBEncodingException("Colon expected, not '" + - (char)c + "'"); - } - - return new BEValue(read(num)); - } - - /** - * Returns the next b-encoded value on the stream and makes sure it is a - * number. - * - * @throws InvalidBEncodingException If it is not a number. - */ - public BEValue bdecodeNumber() throws IOException { - int c = this.getNextIndicator(); - if (c != 'i') { - throw new InvalidBEncodingException("Expected 'i', not '" + - (char)c + "'"); - } - this.indicator = 0; - - c = this.read(); - if (c == '0') { - c = this.read(); - if (c == 'e') - return new BEValue(BigInteger.ZERO); - else - throw new InvalidBEncodingException("'e' expected after zero," + - " not '" + (char)c + "'"); - } - - // We don't support more the 255 char big integers - char[] chars = new char[256]; - int off = 0; - - if (c == '-') { - c = this.read(); - if (c == '0') - throw new InvalidBEncodingException("Negative zero not allowed"); - chars[off] = (char)c; - off++; - } - - if (c < '1' || c > '9') - throw new InvalidBEncodingException("Invalid Integer start '" - + (char)c + "'"); - chars[off] = (char)c; - off++; - - c = this.read(); - int i = c - '0'; - while (i >= 0 && i <= 9) { - chars[off] = (char)c; - off++; - c = read(); - i = c - '0'; - } - - if (c != 'e') - throw new InvalidBEncodingException("Integer should end with 'e'"); - - String s = new String(chars, 0, off); - return new BEValue(new BigInteger(s)); - } - - /** - * Returns the next b-encoded value on the stream and makes sure it is a - * list. - * - * @throws InvalidBEncodingException If it is not a list. - */ - public BEValue bdecodeList() throws IOException { - int c = this.getNextIndicator(); - if (c != 'l') { - throw new InvalidBEncodingException("Expected 'l', not '" + - (char)c + "'"); - } - this.indicator = 0; - - List result = new ArrayList(); - c = this.getNextIndicator(); - while (c != 'e') { - result.add(this.bdecode()); - c = this.getNextIndicator(); - } - this.indicator = 0; - - return new BEValue(result); - } - - /** - * Returns the next b-encoded value on the stream and makes sure it is a - * map (dictionary). - * - * @throws InvalidBEncodingException If it is not a map. - */ - public BEValue bdecodeMap() throws IOException { - int c = this.getNextIndicator(); - if (c != 'd') { - throw new InvalidBEncodingException("Expected 'd', not '" + - (char)c + "'"); - } - this.indicator = 0; - - Map result = new HashMap(); - c = this.getNextIndicator(); - while (c != 'e') { - // Dictionary keys are always strings. - String key = this.bdecode().getString(); - - BEValue value = this.bdecode(); - result.put(key, value); - - c = this.getNextIndicator(); - } - this.indicator = 0; - - return new BEValue(result); - } - - /** - * Returns the next byte read from the InputStream (as int). - * - * @throws EOFException If InputStream.read() returned -1. - */ - private int read() throws IOException { - int c = this.in.read(); - if (c == -1) - throw new EOFException(); - return c; - } - - /** - * Returns a byte[] containing length valid bytes starting at offset zero. - * - * @throws EOFException If InputStream.read() returned -1 before all - * requested bytes could be read. Note that the byte[] returned might be - * bigger then requested but will only contain length valid bytes. The - * returned byte[] will be reused when this method is called again. - */ - private byte[] read(int length) throws IOException { - byte[] result = new byte[length]; - - int read = 0; - while (read < length) - { - int i = this.in.read(result, read, length - read); - if (i == -1) - throw new EOFException(); - read += i; - } - - return result; - } -} diff --git a/src/main/java/com/turn/ttorrent/bcodec/BEValue.java b/src/main/java/com/turn/ttorrent/bcodec/BEValue.java deleted file mode 100644 index fba151759..000000000 --- a/src/main/java/com/turn/ttorrent/bcodec/BEValue.java +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.bcodec; - -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - - -/** - * A type-agnostic container for B-encoded values. - * - * @author mpetazzoni - */ -public class BEValue { - - /** - * The B-encoded value can be a byte array, a Number, a List or a Map. - * Lists and Maps contains BEValues too. - */ - private final Object value; - - public BEValue(byte[] value) { - this.value = value; - } - - public BEValue(String value) throws UnsupportedEncodingException { - this.value = value.getBytes("UTF-8"); - } - - public BEValue(String value, String enc) - throws UnsupportedEncodingException { - this.value = value.getBytes(enc); - } - - public BEValue(int value) { - this.value = new Integer(value); - } - - public BEValue(long value) { - this.value = new Long(value); - } - - public BEValue(Number value) { - this.value = value; - } - - public BEValue(List value) { - this.value = value; - } - - public BEValue(Map value) { - this.value = value; - } - - public Object getValue() { - return this.value; - } - - /** - * Returns this BEValue as a String, interpreted as UTF-8. - * @throws InvalidBEncodingException If the value is not a byte[]. - */ - public String getString() throws InvalidBEncodingException { - return this.getString("UTF-8"); - } - - /** - * Returns this BEValue as a String, interpreted with the specified - * encoding. - * - * @param encoding The encoding to interpret the bytes as when converting - * them into a {@link String}. - * @throws InvalidBEncodingException If the value is not a byte[]. - */ - public String getString(String encoding) throws InvalidBEncodingException { - try { - return new String(this.getBytes(), encoding); - } catch (ClassCastException cce) { - throw new InvalidBEncodingException(cce.toString()); - } catch (UnsupportedEncodingException uee) { - throw new InternalError(uee.toString()); - } - } - - /** - * Returns this BEValue as a byte[]. - * - * @throws InvalidBEncodingException If the value is not a byte[]. - */ - public byte[] getBytes() throws InvalidBEncodingException { - try { - return (byte[])this.value; - } catch (ClassCastException cce) { - throw new InvalidBEncodingException(cce.toString()); - } - } - - /** - * Returns this BEValue as a Number. - * - * @throws InvalidBEncodingException If the value is not a {@link Number}. - */ - public Number getNumber() throws InvalidBEncodingException { - try { - return (Number)this.value; - } catch (ClassCastException cce) { - throw new InvalidBEncodingException(cce.toString()); - } - } - - /** - * Returns this BEValue as short. - * - * @throws InvalidBEncodingException If the value is not a {@link Number}. - */ - public short getShort() throws InvalidBEncodingException { - return this.getNumber().shortValue(); - } - - /** - * Returns this BEValue as int. - * - * @throws InvalidBEncodingException If the value is not a {@link Number}. - */ - public int getInt() throws InvalidBEncodingException { - return this.getNumber().intValue(); - } - - /** - * Returns this BEValue as long. - * - * @throws InvalidBEncodingException If the value is not a {@link Number}. - */ - public long getLong() throws InvalidBEncodingException { - return this.getNumber().longValue(); - } - - /** - * Returns this BEValue as a List of BEValues. - * - * @throws InvalidBEncodingException If the value is not an - * {@link ArrayList}. - */ - @SuppressWarnings("unchecked") - public List getList() throws InvalidBEncodingException { - if (this.value instanceof ArrayList) { - return (ArrayList)this.value; - } else { - throw new InvalidBEncodingException("Excepted List !"); - } - } - - /** - * Returns this BEValue as a Map of String keys and BEValue values. - * - * @throws InvalidBEncodingException If the value is not a {@link HashMap}. - */ - @SuppressWarnings("unchecked") - public Map getMap() throws InvalidBEncodingException { - if (this.value instanceof HashMap) { - return (Map)this.value; - } else { - throw new InvalidBEncodingException("Expected Map !"); - } - } -} diff --git a/src/main/java/com/turn/ttorrent/bcodec/BEncoder.java b/src/main/java/com/turn/ttorrent/bcodec/BEncoder.java deleted file mode 100644 index 264d98044..000000000 --- a/src/main/java/com/turn/ttorrent/bcodec/BEncoder.java +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.bcodec; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * B-encoding encoder. - * - *

- * This class provides utility methods to encode objects and - * {@link BEValue}s to B-encoding into a provided output stream. - *

- * - *

- * Inspired by Snark's implementation. - *

- * - * @author mpetazzoni - * @see B-encoding specification - */ -public class BEncoder { - - @SuppressWarnings("unchecked") - public static void bencode(Object o, OutputStream out) - throws IOException, IllegalArgumentException { - if (o instanceof BEValue) { - o = ((BEValue)o).getValue(); - } - - if (o instanceof String) { - bencode((String)o, out); - } else if (o instanceof byte[]) { - bencode((byte[])o, out); - } else if (o instanceof Number) { - bencode((Number)o, out); - } else if (o instanceof List) { - bencode((List)o, out); - } else if (o instanceof Map) { - bencode((Map)o, out); - } else { - throw new IllegalArgumentException("Cannot bencode: " + - o.getClass()); - } - } - - public static void bencode(String s, OutputStream out) throws IOException { - byte[] bs = s.getBytes("UTF-8"); - bencode(bs, out); - } - - public static void bencode(Number n, OutputStream out) throws IOException { - out.write('i'); - String s = n.toString(); - out.write(s.getBytes("UTF-8")); - out.write('e'); - } - - public static void bencode(List l, OutputStream out) - throws IOException { - out.write('l'); - for (BEValue value : l) { - bencode(value, out); - } - out.write('e'); - } - - public static void bencode(byte[] bs, OutputStream out) throws IOException { - String l = Integer.toString(bs.length); - out.write(l.getBytes("UTF-8")); - out.write(':'); - out.write(bs); - } - - public static void bencode(Map m, OutputStream out) - throws IOException { - out.write('d'); - - // Keys must be sorted. - Set s = m.keySet(); - List l = new ArrayList(s); - Collections.sort(l); - - for (String key : l) { - Object value = m.get(key); - bencode(key, out); - bencode(value, out); - } - - out.write('e'); - } - - public static ByteBuffer bencode(Map m) - throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - BEncoder.bencode(m, baos); - baos.close(); - return ByteBuffer.wrap(baos.toByteArray()); - } -} diff --git a/src/main/java/com/turn/ttorrent/client/Client.java b/src/main/java/com/turn/ttorrent/client/Client.java deleted file mode 100644 index 480657dbf..000000000 --- a/src/main/java/com/turn/ttorrent/client/Client.java +++ /dev/null @@ -1,1102 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client; - -import com.turn.ttorrent.client.announce.Announce; -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.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; -import java.util.Random; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; -import java.util.TreeSet; -import java.util.UUID; -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; - -/** - * A pure-java BitTorrent client. - * - *

- * A BitTorrent client in its bare essence shares a given torrent. If the - * torrent is not complete locally, it will continue to download it. If or - * after the torrent is complete, the client may eventually continue to seed it - * for other clients. - *

- * - *

- * This BitTorrent client implementation is made to be simple to embed and - * simple to use. First, initialize a ShareTorrent object from a torrent - * meta-info source (either a file or a byte array, see - * com.turn.ttorrent.SharedTorrent for how to create a SharedTorrent object). - * Then, instantiate your Client object with this SharedTorrent and call one of - * {@link #download} to simply download the torrent, or {@link #share} to - * download and continue seeding for the given amount of time after the - * download completes. - *

- * - * @author mpetazzoni - */ -public class Client extends Observable implements Runnable, - AnnounceResponseListener, IncomingConnectionListener, PeerActivityListener { - - private static final Logger logger = - LoggerFactory.getLogger(Client.class); - - /** Peers unchoking frequency, in seconds. Current BitTorrent specification - * recommends 10 seconds to avoid choking fibrilation. */ - private static final int UNCHOKING_FREQUENCY = 3; - - /** Optimistic unchokes are done every 2 loop iterations, i.e. every - * 2*UNCHOKING_FREQUENCY seconds. */ - private static final int OPTIMISTIC_UNCHOKE_ITERATIONS = 3; - - 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, - SHARING, - SEEDING, - ERROR, - DONE; - }; - - private static final String BITTORRENT_ID_PREFIX = "-TO0042-"; - - private SharedTorrent torrent; - private ClientState state; - private Peer self; - - private Thread thread; - private boolean stop; - private long seed; - - private ConnectionHandler service; - private Announce announce; - private ConcurrentMap peers; - private ConcurrentMap connected; - - private Random random; - - /** - * Initialize the BitTorrent client. - * - * @param address The address to bind to. - * @param torrent The torrent to download and share. - */ - public Client(InetAddress address, SharedTorrent torrent) - throws UnknownHostException, IOException { - this.torrent = torrent; - this.state = ClientState.WAITING; - - String id = Client.BITTORRENT_ID_PREFIX + UUID.randomUUID() - .toString().split("-")[4]; - - // Initialize the incoming connection handler and register ourselves to - // it. - this.service = new ConnectionHandler(this.torrent, id, address); - this.service.register(this); - - this.self = new Peer( - this.service.getSocketAddress() - .getAddress().getHostAddress(), - (short)this.service.getSocketAddress().getPort(), - ByteBuffer.wrap(id.getBytes(Torrent.BYTE_ENCODING))); - - // Initialize the announce request thread, and register ourselves to it - // as well. - this.announce = new Announce(this.torrent, this.self); - this.announce.register(this); - - logger.info("BitTorrent client [{}] for {} started and " + - "listening at {}:{}...", - new Object[] { - this.self.getShortHexPeerId(), - this.torrent.getName(), - this.self.getIp(), - this.self.getPort() - }); - - this.peers = new ConcurrentHashMap(); - this.connected = new ConcurrentHashMap(); - this.random = new Random(System.currentTimeMillis()); - } - - /** - * Get this client's peer specification. - */ - public Peer getPeerSpec() { - return this.self; - } - - /** - * Return the torrent this client is exchanging on. - */ - public SharedTorrent getTorrent() { - return this.torrent; - } - - /** - * Returns the set of known peers. - */ - public Set getPeers() { - return new HashSet(this.peers.values()); - } - - /** - * Change this client's state and notify its observers. - * - *

- * If the state has changed, this client's observers will be notified. - *

- * - * @param state The new client state. - */ - private synchronized void setState(ClientState state) { - if (this.state != state) { - this.setChanged(); - } - this.state = state; - this.notifyObservers(this.state); - } - - /** - * Return the current state of this BitTorrent client. - */ - public ClientState getState() { - return this.state; - } - - /** - * Download the torrent without seeding after completion. - */ - public void download() { - this.share(0); - } - - /** - * Download and share this client's torrent until interrupted. - */ - public void share() { - this.share(-1); - } - - /** - * Download and share this client's torrent. - * - * @param seed Seed time in seconds after the download is complete. Pass - * 0 to immediately stop after downloading. - */ - public synchronized void share(int seed) { - this.seed = seed; - this.stop = false; - - if (this.thread == null || !this.thread.isAlive()) { - this.thread = new Thread(this); - this.thread.setName("bt-client(" + - this.self.getShortHexPeerId() + ")"); - this.thread.start(); - } - } - - /** - * Immediately but gracefully stop this client. - */ - public void stop() { - this.stop(true); - } - - /** - * Immediately but gracefully stop this client. - * - * @param wait Whether to wait for the client execution thread to complete - * or not. This allows for the client's state to be settled down in one of - * the DONE or ERROR states when this method returns. - */ - public void stop(boolean wait) { - this.stop = true; - - if (this.thread != null && this.thread.isAlive()) { - this.thread.interrupt(); - if (wait) { - this.waitForCompletion(); - } - } - - this.thread = null; - } - - /** - * Wait for downloading (and seeding, if requested) to complete. - */ - public void waitForCompletion() { - if (this.thread != null && this.thread.isAlive()) { - try { - this.thread.join(); - } catch (InterruptedException ie) { - logger.error(ie.getMessage(), ie); - } - } - } - - /** - * Tells whether we are a seed for the torrent we're sharing. - */ - public boolean isSeed() { - return this.torrent.isComplete(); - } - - /** - * Main client loop. - * - *

- * The main client download loop is very simple: it starts the announce - * request thread, the incoming connection handler service, and loops - * unchoking peers every UNCHOKING_FREQUENCY seconds until told to stop. - * Every OPTIMISTIC_UNCHOKE_ITERATIONS, an optimistic unchoke will be - * attempted to try out other peers. - *

- * - *

- * Once done, it stops the announce and connection services, and returns. - *

- */ - @Override - public void run() { - // First, analyze the torrent's local data. - try { - this.setState(ClientState.VALIDATING); - this.torrent.init(); - } catch (IOException ioe) { - logger.warn("Error while initializing torrent data: {}!", - ioe.getMessage(), ioe); - } catch (InterruptedException ie) { - logger.warn("Client was interrupted during initialization. " + - "Aborting right away."); - } finally { - if (!this.torrent.isInitialized()) { - try { - this.service.close(); - } catch (IOException ioe) { - logger.warn("Error while releasing bound channel: {}!", - ioe.getMessage(), ioe); - } - - this.setState(ClientState.ERROR); - this.torrent.close(); - return; - } - } - - // Initial completion test - if (this.torrent.isComplete()) { - this.seed(); - } else { - this.setState(ClientState.SHARING); - } - - // Detect early stop - if (this.stop) { - logger.info("Download is complete and no seeding was requested."); - this.finish(); - return; - } - - this.announce.start(); - this.service.start(); - - int optimisticIterations = 0; - int rateComputationIterations = 0; - - while (!this.stop) { - optimisticIterations = - (optimisticIterations == 0 ? - Client.OPTIMISTIC_UNCHOKE_ITERATIONS : - optimisticIterations - 1); - - rateComputationIterations = - (rateComputationIterations == 0 ? - Client.RATE_COMPUTATION_ITERATIONS : - rateComputationIterations - 1); - - try { - this.unchokePeers(optimisticIterations == 0); - this.info(); - if (rateComputationIterations == 0) { - this.resetPeerRates(); - } - } catch (Exception e) { - logger.error("An exception occurred during the BitTorrent " + - "client main loop execution!", e); - } - - try { - Thread.sleep(Client.UNCHOKING_FREQUENCY*1000); - } catch (InterruptedException ie) { - logger.trace("BitTorrent main loop interrupted."); - } - } - - logger.debug("Stopping BitTorrent client connection service " + - "and announce threads..."); - - this.service.stop(); - try { - this.service.close(); - } catch (IOException ioe) { - logger.warn("Error while releasing bound channel: {}!", - ioe.getMessage(), ioe); - } - - this.announce.stop(); - - // Close all peer connections - logger.debug("Closing all remaining peer connections..."); - for (SharingPeer peer : this.connected.values()) { - peer.unbind(true); - } - - this.finish(); - } - - /** - * Close torrent and set final client state before signing off. - */ - private void finish() { - this.torrent.close(); - - // Determine final state - if (this.torrent.isFinished()) { - this.setState(ClientState.DONE); - } else { - this.setState(ClientState.ERROR); - } - - logger.info("BitTorrent client signing off."); - } - - /** - * Display information about the BitTorrent client state. - * - *

- * This emits an information line in the log about this client's state. It - * includes the number of choked peers, number of connected peers, number - * of known peers, information about the torrent availability and - * completion and current transmission rates. - *

- */ - public synchronized void info() { - float dl = 0; - float ul = 0; - for (SharingPeer peer : this.connected.values()) { - dl += peer.getDLRate().get(); - ul += peer.getULRate().get(); - } - - logger.info("{} {}/{} pieces ({}%) [{}/{}] with {}/{} peers at {}/{} kB/s.", - new Object[] { - this.getState().name(), - this.torrent.getCompletedPieces().cardinality(), - this.torrent.getPieceCount(), - String.format("%.2f", this.torrent.getCompletion()), - this.torrent.getAvailablePieces().cardinality(), - this.torrent.getRequestedPieces().cardinality(), - this.connected.size(), - this.peers.size(), - String.format("%.2f", dl/1024.0), - String.format("%.2f", ul/1024.0), - }); - for (SharingPeer peer : this.connected.values()) { - Piece piece = peer.getRequestedPiece(); - logger.debug(" | {} {}", - peer, - piece != null - ? "(downloading " + piece + ")" - : "" - ); - } - } - - /** - * Reset peers download and upload rates. - * - *

- * This method is called every RATE_COMPUTATION_ITERATIONS to reset the - * download and upload rates of all peers. This contributes to making the - * download and upload rate computations rolling averages every - * UNCHOKING_FREQUENCY * RATE_COMPUTATION_ITERATIONS seconds (usually 20 - * seconds). - *

- */ - private synchronized void resetPeerRates() { - for (SharingPeer peer : this.connected.values()) { - peer.getDLRate().reset(); - peer.getULRate().reset(); - } - } - - /** - * Retrieve a SharingPeer object from the given peer specification. - * - *

- * This function tries to retrieve an existing peer object based on the - * provided peer specification or otherwise instantiates a new one and adds - * it to our peer repository. - *

- * - * @param search The {@link Peer} specification. - */ - private SharingPeer getOrCreatePeer(Peer search) { - SharingPeer peer; - - synchronized (this.peers) { - logger.trace("Searching for {}...", search); - if (search.hasPeerId()) { - peer = this.peers.get(search.getHexPeerId()); - if (peer != null) { - logger.trace("Found peer (by peer ID): {}.", peer); - this.peers.put(peer.getHostIdentifier(), peer); - this.peers.put(search.getHostIdentifier(), peer); - return peer; - } - } - - peer = this.peers.get(search.getHostIdentifier()); - if (peer != null) { - if (search.hasPeerId()) { - logger.trace("Recording peer ID {} for {}.", - search.getHexPeerId(), peer); - peer.setPeerId(search.getPeerId()); - this.peers.put(search.getHexPeerId(), peer); - } - - logger.debug("Found peer (by host ID): {}.", peer); - return peer; - } - - peer = new SharingPeer(search.getIp(), search.getPort(), - search.getPeerId(), this.torrent); - logger.trace("Created new peer: {}.", peer); - - this.peers.put(peer.getHostIdentifier(), peer); - if (peer.hasPeerId()) { - this.peers.put(peer.getHexPeerId(), peer); - } - - return peer; - } - } - - /** - * Retrieve a peer comparator. - * - *

- * Returns a peer comparator based on either the download rate or the - * upload rate of each peer depending on our state. While sharing, we rely - * on the download rate we get from each peer. When our download is - * complete and we're only seeding, we use the upload rate instead. - *

- * - * @return A SharingPeer comparator that can be used to sort peers based on - * the download or upload rate we get from them. - */ - private Comparator getPeerRateComparator() { - if (ClientState.SHARING.equals(this.state)) { - return new SharingPeer.DLRateComparator(); - } else if (ClientState.SEEDING.equals(this.state)) { - return new SharingPeer.ULRateComparator(); - } else { - throw new IllegalStateException("Client is neither sharing nor " + - "seeding, we shouldn't be comparing peers at this point."); - } - } - - /** - * Unchoke connected peers. - * - *

- * This is one of the "clever" places of the BitTorrent client. Every - * OPTIMISTIC_UNCHOKING_FREQUENCY seconds, we decide which peers should be - * unchocked and authorized to grab pieces from us. - *

- * - *

- * Reciprocation (tit-for-tat) and upload capping is implemented here by - * carefully choosing which peers we unchoke, and which peers we choke. - *

- * - *

- * The four peers with the best download rate and are interested in us get - * unchoked. This maximizes our download rate as we'll be able to get data - * from there four "best" peers quickly, while allowing these peers to - * download from us and thus reciprocate their generosity. - *

- * - *

- * Peers that have a better download rate than these four downloaders but - * are not interested get unchoked too, we want to be able to download from - * them to get more data more quickly. If one becomes interested, it takes - * a downloader's place as one of the four top downloaders (i.e. we choke - * the downloader with the worst upload rate). - *

- * - * @param optimistic Whether to perform an optimistic unchoke as well. - */ - private synchronized void unchokePeers(boolean optimistic) { - // Build a set of all connected peers, we don't care about peers we're - // not connected to. - TreeSet bound = new TreeSet( - this.getPeerRateComparator()); - bound.addAll(this.connected.values()); - - if (bound.size() == 0) { - logger.trace("No connected peers, skipping unchoking."); - return; - } else { - logger.trace("Running unchokePeers() on {} connected peers.", - bound.size()); - } - - int downloaders = 0; - Set choked = new HashSet(); - - // We're interested in the top downloaders first, so use a descending - // set. - for (SharingPeer peer : bound.descendingSet()) { - if (downloaders < Client.MAX_DOWNLOADERS_UNCHOKE) { - // Unchoke up to MAX_DOWNLOADERS_UNCHOKE interested peers - if (peer.isChoking()) { - if (peer.isInterested()) { - downloaders++; - } - - peer.unchoke(); - } - } else { - // Choke everybody else - choked.add(peer); - } - } - - // Actually choke all chosen peers (if any), except the eventual - // optimistic unchoke. - if (choked.size() > 0) { - SharingPeer randomPeer = choked.toArray( - new SharingPeer[0])[this.random.nextInt(choked.size())]; - - for (SharingPeer peer : choked) { - if (optimistic && peer == randomPeer) { - logger.debug("Optimistic unchoke of {}.", peer); - continue; - } - - peer.choke(); - } - } - } - - - /** AnnounceResponseListener handler(s). **********************************/ - - /** - * Handle an announce response event. - * - * @param interval The announce interval requested by the tracker. - * @param complete The number of seeders on this torrent. - * @param incomplete The number of leechers on this torrent. - */ - @Override - public void handleAnnounceResponse(int interval, int complete, - int incomplete) { - this.announce.setInterval(interval); - } - - /** - * Handle the discovery of new peers. - * - * @param peers The list of peers discovered (from the announce response or - * any other means like DHT/PEX, etc.). - */ - @Override - public void handleDiscoveredPeers(List peers) { - if (peers == null || peers.isEmpty()) { - // No peers returned by the tracker. Apparently we're alone on - // this one for now. - return; - } - - logger.info("Got {} peer(s) in tracker response.", peers.size()); - - if (!this.service.isAlive()) { - logger.warn("Connection handler service is not available."); - return; - } - - for (Peer peer : peers) { - // Attempt to connect to the peer if and only if: - // - We're not already connected or connecting to it; - // - We're not a seeder (we leave the responsibility - // of connecting to peers that need to download - // something). - SharingPeer match = this.getOrCreatePeer(peer); - if (this.isSeed()) { - continue; - } - - synchronized (match) { - if (!match.isConnected()) { - this.service.connect(match); - } - } - } - } - - - /** IncomingConnectionListener handler(s). ********************************/ - - /** - * Handle a new peer connection. - * - *

- * This handler is called once the connection has been successfully - * established and the handshake exchange made. This generally simply means - * binding the peer to the socket, which will put in place the communication - * thread and logic with this peer. - *

- * - * @param channel The connected socket channel to the remote peer. Note - * that if the peer somehow rejected our handshake reply, this socket might - * very soon get closed, but this is handled down the road. - * @param peerId The byte-encoded peerId extracted from the peer's - * handshake, after validation. - * @see com.turn.ttorrent.client.peer.SharingPeer - */ - @Override - public void handleNewPeerConnection(SocketChannel channel, byte[] peerId) { - Peer search = new Peer( - channel.socket().getInetAddress().getHostAddress(), - channel.socket().getPort(), - (peerId != null - ? ByteBuffer.wrap(peerId) - : (ByteBuffer)null)); - - logger.info("Handling new peer connection with {}...", search); - SharingPeer peer = this.getOrCreatePeer(search); - - try { - synchronized (peer) { - if (peer.isConnected()) { - logger.info("Already connected with {}, closing link.", - peer); - channel.close(); - return; - } - - peer.register(this); - peer.bind(channel); - } - - this.connected.put(peer.getHexPeerId(), peer); - peer.register(this.torrent); - logger.debug("New peer connection with {} [{}/{}].", - new Object[] { - peer, - this.connected.size(), - this.peers.size() - }); - } catch (Exception e) { - this.connected.remove(peer.getHexPeerId()); - logger.warn("Could not handle new peer connection " + - "with {}: {}", peer, e.getMessage()); - } - } - - /** - * Handle a failed peer connection. - * - *

- * If an outbound connection failed (could not connect, invalid handshake, - * etc.), remove the peer from our known peers. - *

- * - * @param peer The peer we were trying to connect with. - * @param cause The exception encountered when connecting with the peer. - */ - @Override - public void handleFailedConnection(SharingPeer peer, Throwable cause) { - logger.warn("Could not connect to {}: {}.", peer, cause.getMessage()); - this.peers.remove(peer.getHostIdentifier()); - if (peer.hasPeerId()) { - this.peers.remove(peer.getHexPeerId()); - } - } - - /** PeerActivityListener handler(s). **************************************/ - - @Override - public void handlePeerChoked(SharingPeer peer) { /* Do nothing */ } - - @Override - public void handlePeerReady(SharingPeer peer) { /* Do nothing */ } - - @Override - public void handlePieceAvailability(SharingPeer peer, - Piece piece) { /* Do nothing */ } - - @Override - public void handleBitfieldAvailability(SharingPeer peer, - BitSet availablePieces) { /* Do nothing */ } - - @Override - public void handlePieceSent(SharingPeer peer, - Piece piece) { /* Do nothing */ } - - /** - * Piece download completion handler. - * - *

- * When a piece is completed, and valid, we announce to all connected peers - * that we now have this piece. - *

- * - *

- * We use this handler to identify when all of the pieces have been - * downloaded. When that's the case, we can start the seeding period, if - * any. - *

- * - * @param peer The peer we got the piece from. - * @param piece The piece in question. - */ - @Override - public void handlePieceCompleted(SharingPeer peer, Piece piece) - throws IOException { - synchronized (this.torrent) { - if (piece.isValid()) { - // Make sure the piece is marked as completed in the torrent - // Note: this is required because the order the - // PeerActivityListeners are called is not defined, and we - // might be called before the torrent's piece completion - // handler is. - this.torrent.markCompleted(piece); - logger.debug("Completed download of {} from {}. " + - "We now have {}/{} pieces", - new Object[] { - piece, - peer, - this.torrent.getCompletedPieces().cardinality(), - this.torrent.getPieceCount() - }); - - // Send a HAVE message to all connected peers - PeerMessage have = PeerMessage.HaveMessage.craft(piece.getIndex()); - for (SharingPeer remote : this.connected.values()) { - remote.send(have); - } - - // Force notify after each piece is completed to propagate download - // completion information (or new seeding state) - this.setChanged(); - this.notifyObservers(this.state); - } else { - logger.warn("Downloaded piece#{} from {} was not valid ;-(", - piece.getIndex(), peer); - } - - if (this.torrent.isComplete()) { - logger.info("Last piece validated and completed, finishing download..."); - - // Cancel all remaining outstanding requests - for (SharingPeer remote : this.connected.values()) { - if (remote.isDownloading()) { - int requests = remote.cancelPendingRequests().size(); - logger.info("Cancelled {} remaining pending requests on {}.", - requests, remote); - } - } - - this.torrent.finish(); - - try { - this.announce.getCurrentTrackerClient() - .announce(TrackerMessage - .AnnounceRequestMessage - .RequestEvent.COMPLETED, true); - } catch (AnnounceException ae) { - logger.warn("Error announcing completion event to " + - "tracker: {}", ae.getMessage()); - } - - logger.info("Download is complete and finalized."); - this.seed(); - } - } - } - - @Override - public void handlePeerDisconnected(SharingPeer peer) { - if (this.connected.remove(peer.hasPeerId() - ? peer.getHexPeerId() - : peer.getHostIdentifier()) != null) { - logger.debug("Peer {} disconnected, [{}/{}].", - new Object[] { - peer, - this.connected.size(), - this.peers.size() - }); - } - - peer.reset(); - } - - @Override - public void handleIOException(SharingPeer peer, IOException ioe) { - logger.warn("I/O error while exchanging data with {}, " + - "closing connection with it!", peer, ioe.getMessage()); - peer.unbind(true); - } - - - /** Post download seeding. ************************************************/ - - /** - * Start the seeding period, if any. - * - *

- * This method is called when all the pieces of our torrent have been - * retrieved. This may happen immediately after the client starts if the - * torrent was already fully download or we are the initial seeder client. - *

- * - *

- * 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. - *

- * - * @see StopSeedingTask - */ - private synchronized void seed() { - // Silently ignore if we're already seeding. - if (ClientState.SEEDING.equals(this.getState())) { - return; - } - - logger.info("Download of {} pieces completed.", - this.torrent.getPieceCount()); - - this.setState(ClientState.SEEDING); - if (this.seed < 0) { - logger.info("Seeding indefinetely..."); - return; - } - - // In case seeding for 0 seconds we still need to schedule the task in - // order to call stop() from different thread to avoid deadlock - logger.info("Seeding for {} seconds...", this.seed); - Timer timer = new Timer(); - timer.schedule(new ClientShutdown(this, timer), this.seed*1000); - } - - /** - * Timer task to stop seeding. - * - *

- * This TimerTask will be called by a timer set after the download is - * complete to stop seeding from this client after a certain amount of - * requested seed time (might be 0 for immediate termination). - *

- * - *

- * This task simply contains a reference to this client instance and calls - * its stop() method to interrupt the client's main loop. - *

- * - * @author mpetazzoni - */ - private static class ClientShutdown extends TimerTask { - - private final Client client; - private final Timer timer; - - ClientShutdown(Client client, Timer timer) { - this.client = client; - this.timer = timer; - } - - @Override - public void run() { - this.client.stop(); - if (this.timer != null) { - this.timer.cancel(); - } - } - }; - - /** - * 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 deleted file mode 100644 index 8b56336c9..000000000 --- a/src/main/java/com/turn/ttorrent/client/ConnectionHandler.java +++ /dev/null @@ -1,524 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client; - -import com.turn.ttorrent.common.Torrent; -import com.turn.ttorrent.client.peer.SharingPeer; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketTimeoutException; -import java.nio.ByteBuffer; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.text.ParseException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * Incoming peer connections service. - * - *

- * Every BitTorrent client, BitTorrent being a peer-to-peer protocol, listens - * on a port for incoming connections from other peers sharing the same - * torrent. - *

- * - *

- * This ConnectionHandler implements this service and starts a listening socket - * in the first available port in the default BitTorrent client port range - * 6881-6889. When a peer connects to it, it expects the BitTorrent handshake - * message, parses it and replies with our own handshake. - *

- * - *

- * Outgoing connections to other peers are also made through this service, - * which handles the handshake procedure with the remote peer. Regardless of - * the direction of the connection, once this handshake is successful, all - * {@link IncomingConnectionListener}s are notified and passed the connected - * socket and the remote peer ID. - *

- * - *

- * This class does nothing more. All further peer-to-peer communication happens - * in the {@link com.turn.ttorrent.client.peer.PeerExchange PeerExchange} - * class. - *

- * - * @author mpetazzoni - * @see BitTorrent handshake specification - */ -public class ConnectionHandler implements Runnable { - - private static final Logger logger = - LoggerFactory.getLogger(ConnectionHandler.class); - - public static final int PORT_RANGE_START = 6881; - public static final int PORT_RANGE_END = 6889; - - private static final int OUTBOUND_CONNECTIONS_POOL_SIZE = 20; - private static final int OUTBOUND_CONNECTIONS_THREAD_KEEP_ALIVE_SECS = 10; - - private static final int CLIENT_KEEP_ALIVE_MINUTES = 3; - - private SharedTorrent torrent; - private String id; - private ServerSocketChannel channel; - private InetSocketAddress address; - - private Set listeners; - private ExecutorService executor; - private Thread thread; - private boolean stop; - - /** - * Create and start a new listening service for out torrent, reporting - * with our peer ID on the given address. - * - *

- * This binds to the first available port in the client port range - * PORT_RANGE_START to PORT_RANGE_END. - *

- * - * @param torrent The torrent shared by this client. - * @param id This client's peer ID. - * @param address The address to bind to. - * @throws IOException When the service can't be started because no port in - * the defined range is available or usable. - */ - ConnectionHandler(SharedTorrent torrent, String id, InetAddress address) - throws IOException { - this.torrent = torrent; - this.id = id; - - // Bind to the first available port in the range - // [PORT_RANGE_START; PORT_RANGE_END]. - for (int port = ConnectionHandler.PORT_RANGE_START; - port <= ConnectionHandler.PORT_RANGE_END; - port++) { - InetSocketAddress tryAddress = - new InetSocketAddress(address, port); - - try { - this.channel = ServerSocketChannel.open(); - this.channel.socket().bind(tryAddress); - this.channel.configureBlocking(false); - this.address = tryAddress; - break; - } catch (IOException ioe) { - // Ignore, try next port - logger.warn("Could not bind to {}, trying next port...", tryAddress); - } - } - - if (this.channel == null || !this.channel.socket().isBound()) { - throw new IOException("No available port for the BitTorrent client!"); - } - - logger.info("Listening for incoming connections on {}.", this.address); - - this.listeners = new HashSet(); - this.executor = null; - this.thread = null; - } - - /** - * Return the full socket address this service is bound to. - */ - public InetSocketAddress getSocketAddress() { - return this.address; - } - - /** - * Register a new incoming connection listener. - * - * @param listener The listener who wants to receive connection - * notifications. - */ - public void register(IncomingConnectionListener listener) { - this.listeners.add(listener); - } - - /** - * Start accepting new connections in a background thread. - */ - public void start() { - if (this.channel == null) { - throw new IllegalStateException( - "Connection handler cannot be recycled!"); - } - - this.stop = false; - - if (this.executor == null || this.executor.isShutdown()) { - this.executor = new ThreadPoolExecutor( - OUTBOUND_CONNECTIONS_POOL_SIZE, - OUTBOUND_CONNECTIONS_POOL_SIZE, - OUTBOUND_CONNECTIONS_THREAD_KEEP_ALIVE_SECS, - TimeUnit.SECONDS, - new LinkedBlockingQueue(), - new ConnectorThreadFactory()); - } - - if (this.thread == null || !this.thread.isAlive()) { - this.thread = new Thread(this); - this.thread.setName("bt-serve"); - this.thread.start(); - } - } - - /** - * Stop accepting connections. - * - *

- * Note: the underlying socket remains open and bound. - *

- */ - public void stop() { - this.stop = true; - - if (this.thread != null && this.thread.isAlive()) { - try { - this.thread.join(); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } - } - - if (this.executor != null && !this.executor.isShutdown()) { - this.executor.shutdownNow(); - } - - this.executor = null; - this.thread = null; - } - - /** - * Close this connection handler to release the port it is bound to. - * - * @throws IOException If the channel could not be closed. - */ - public void close() throws IOException { - if (this.channel != null) { - this.channel.close(); - this.channel = null; - } - } - - /** - * The main service loop. - * - *

- * The service waits for new connections for 250ms, then waits 100ms so it - * can be interrupted. - *

- */ - @Override - public void run() { - while (!this.stop) { - try { - SocketChannel client = this.channel.accept(); - if (client != null) { - this.accept(client); - } - } catch (SocketTimeoutException ste) { - // Ignore and go back to sleep - } catch (IOException ioe) { - logger.warn("Unrecoverable error in connection handler", ioe); - this.stop(); - } - - try { - Thread.sleep(100); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } - } - } - - /** - * Return a human-readable representation of a connected socket channel. - * - * @param channel The socket channel to represent. - * @return A textual representation (host:port) of the given - * socket. - */ - private String socketRepr(SocketChannel channel) { - Socket s = channel.socket(); - return String.format("%s:%d%s", - s.getInetAddress().getHostName(), - s.getPort(), - channel.isConnected() ? "+" : "-"); - } - - /** - * Accept the next incoming connection. - * - *

- * When a new peer connects to this service, wait for it to send its - * handshake. We then parse and check that the handshake advertises the - * torrent hash we expect, then reply with our own handshake. - *

- * - *

- * If everything goes according to plan, notify the - * IncomingConnectionListeners with the connected socket and - * the parsed peer ID. - *

- * - * @param client The accepted client's socket channel. - */ - private void accept(SocketChannel client) - throws IOException, SocketTimeoutException { - try { - logger.debug("New incoming connection, waiting for handshake..."); - Handshake hs = this.validateHandshake(client, null); - int sent = this.sendHandshake(client); - logger.trace("Replied to {} with handshake ({} bytes).", - this.socketRepr(client), sent); - - // Go to non-blocking mode for peer interaction - client.configureBlocking(false); - client.socket().setSoTimeout(CLIENT_KEEP_ALIVE_MINUTES*60*1000); - this.fireNewPeerConnection(client, hs.getPeerId()); - } catch (ParseException pe) { - logger.info("Invalid handshake from {}: {}", - this.socketRepr(client), pe.getMessage()); - try { client.close(); } catch (IOException e) { } - } 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 - } - } - } - - /** - * Tells whether the connection handler is running and can be used to - * handle new peer connections. - */ - public boolean isAlive() { - return this.executor != null && - !this.executor.isShutdown() && - !this.executor.isTerminated(); - } - - /** - * Connect to the given peer and perform the BitTorrent handshake. - * - *

- * Submits an asynchronous connection task to the outbound connections - * executor to connect to the given peer. - *

- * - * @param peer The peer to connect to. - */ - public void connect(SharingPeer peer) { - if (!this.isAlive()) { - throw new IllegalStateException( - "Connection handler is not accepting new peers at this time!"); - } - - this.executor.submit(new ConnectorTask(this, peer)); - } - - /** - * Validate an expected handshake on a connection. - * - *

- * Reads an expected handshake message from the given connected socket, - * parses it and validates that the torrent hash_info corresponds to the - * torrent we're sharing, and that the peerId matches the peer ID we expect - * to see coming from the remote peer. - *

- * - * @param channel The connected socket channel to the remote peer. - * @param peerId The peer ID we expect in the handshake. If null, - * any peer ID is accepted (this is the case for incoming connections). - * @return The validated handshake message object. - */ - private Handshake validateHandshake(SocketChannel channel, byte[] peerId) - throws IOException, ParseException { - ByteBuffer len = ByteBuffer.allocate(1); - ByteBuffer data; - - // Read the handshake from the wire - logger.trace("Reading handshake size (1 byte) from {}...", this.socketRepr(channel)); - if (channel.read(len) < len.capacity()) { - throw new IOException("Handshake size read underrrun"); - } - - len.rewind(); - int pstrlen = len.get(); - - data = ByteBuffer.allocate(Handshake.BASE_HANDSHAKE_LENGTH + pstrlen); - data.put((byte)pstrlen); - int expected = data.remaining(); - int read = channel.read(data); - if (read < expected) { - throw new IOException("Handshake data read underrun (" + - read + " < " + expected + " bytes)"); - } - - // Parse and check the handshake - data.rewind(); - Handshake hs = Handshake.parse(data); - if (!Arrays.equals(hs.getInfoHash(), this.torrent.getInfoHash())) { - throw new ParseException("Handshake for unknow torrent " + - Torrent.byteArrayToHexString(hs.getInfoHash()) + - " from " + this.socketRepr(channel) + ".", pstrlen + 9); - } - - if (peerId != null && !Arrays.equals(hs.getPeerId(), peerId)) { - throw new ParseException("Announced peer ID " + - Torrent.byteArrayToHexString(hs.getPeerId()) + - " did not match expected peer ID " + - Torrent.byteArrayToHexString(peerId) + ".", pstrlen + 29); - } - - return hs; - } - - /** - * Send our handshake message to the socket. - * - * @param channel The socket channel to the remote peer. - */ - private int sendHandshake(SocketChannel channel) throws IOException { - return channel.write( - Handshake.craft( - this.torrent.getInfoHash(), - this.id.getBytes(Torrent.BYTE_ENCODING)).getData()); - } - - /** - * Trigger the new peer connection event on all registered listeners. - * - * @param channel The socket channel to the newly connected peer. - * @param peerId The peer ID of the connected peer. - */ - private void fireNewPeerConnection(SocketChannel channel, byte[] peerId) { - for (IncomingConnectionListener listener : this.listeners) { - listener.handleNewPeerConnection(channel, peerId); - } - } - - private void fireFailedConnection(SharingPeer peer, Throwable cause) { - for (IncomingConnectionListener listener : this.listeners) { - listener.handleFailedConnection(peer, cause); - } - } - - - /** - * A simple thread factory that returns appropriately named threads for - * outbound connector threads. - * - * @author mpetazzoni - */ - private static class ConnectorThreadFactory implements ThreadFactory { - - private int number = 0; - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setName("bt-connect-" + ++this.number); - return t; - } - }; - - - /** - * An outbound connection task. - * - *

- * These tasks are fed to the thread executor in charge of processing - * outbound connection requests. It attempts to connect to the given peer - * and proceeds with the BitTorrent handshake. If the handshake is - * successful, the new peer connection event is fired to all incoming - * connection listeners. Otherwise, the failed connection event is fired. - *

- * - * @author mpetazzoni - */ - private static class ConnectorTask implements Runnable { - - private final ConnectionHandler handler; - private final SharingPeer peer; - - private ConnectorTask(ConnectionHandler handler, SharingPeer peer) { - this.handler = handler; - this.peer = peer; - } - - @Override - public void run() { - InetSocketAddress address = - new InetSocketAddress(this.peer.getIp(), this.peer.getPort()); - SocketChannel channel = null; - - try { - logger.info("Connecting to {}...", this.peer); - channel = SocketChannel.open(address); - while (!channel.isConnected()) { - Thread.sleep(10); - } - - logger.debug("Connected. Sending handshake to {}...", this.peer); - channel.configureBlocking(true); - int sent = this.handler.sendHandshake(channel); - logger.debug("Sent handshake ({} bytes), waiting for response...", sent); - Handshake hs = this.handler.validateHandshake(channel, - (this.peer.hasPeerId() - ? this.peer.getPeerId().array() - : null)); - logger.info("Handshaked with {}, peer ID is {}.", - this.peer, Torrent.byteArrayToHexString(hs.getPeerId())); - - // Go to non-blocking mode for peer interaction - channel.configureBlocking(false); - this.handler.fireNewPeerConnection(channel, hs.getPeerId()); - } catch (Exception e) { - try { - if (channel != null && channel.isConnected()) { - channel.close(); - } - } catch (IOException ioe) { - // Ignore - } - this.handler.fireFailedConnection(this.peer, e); - } - } - }; -} diff --git a/src/main/java/com/turn/ttorrent/client/Handshake.java b/src/main/java/com/turn/ttorrent/client/Handshake.java deleted file mode 100644 index d6505d7a8..000000000 --- a/src/main/java/com/turn/ttorrent/client/Handshake.java +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client; - -import com.turn.ttorrent.common.Torrent; - -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.text.ParseException; - - -/** - * Peer handshake handler. - * - * @author mpetazzoni - */ -public class Handshake { - - public static final String BITTORRENT_PROTOCOL_IDENTIFIER = "BitTorrent protocol"; - public static final int BASE_HANDSHAKE_LENGTH = 49; - - ByteBuffer data; - ByteBuffer infoHash; - ByteBuffer peerId; - - private Handshake(ByteBuffer data, ByteBuffer infoHash, - ByteBuffer peerId) { - this.data = data; - this.data.rewind(); - - this.infoHash = infoHash; - this.peerId = peerId; - } - - public ByteBuffer getData() { - return this.data; - } - - public byte[] getInfoHash() { - return this.infoHash.array(); - } - - public byte[] getPeerId() { - return this.peerId.array(); - } - - public static Handshake parse(ByteBuffer buffer) - throws ParseException, UnsupportedEncodingException { - int pstrlen = Byte.valueOf(buffer.get()).intValue(); - if (pstrlen < 0 || - buffer.remaining() != BASE_HANDSHAKE_LENGTH + pstrlen - 1) { - throw new ParseException("Incorrect handshake message length " + - "(pstrlen=" + pstrlen + ") !", 0); - } - - // Check the protocol identification string - byte[] pstr = new byte[pstrlen]; - buffer.get(pstr); - - if (!Handshake.BITTORRENT_PROTOCOL_IDENTIFIER.equals( - new String(pstr, Torrent.BYTE_ENCODING))) { - throw new ParseException("Invalid protocol identifier!", 1); - } - - // Ignore reserved bytes - byte[] reserved = new byte[8]; - buffer.get(reserved); - - byte[] infoHash = new byte[20]; - buffer.get(infoHash); - byte[] peerId = new byte[20]; - buffer.get(peerId); - return new Handshake(buffer, ByteBuffer.wrap(infoHash), - ByteBuffer.wrap(peerId)); - } - - public static Handshake craft(byte[] torrentInfoHash, - byte[] clientPeerId) { - try { - ByteBuffer buffer = ByteBuffer.allocate( - Handshake.BASE_HANDSHAKE_LENGTH + - Handshake.BITTORRENT_PROTOCOL_IDENTIFIER.length()); - - byte[] reserved = new byte[8]; - ByteBuffer infoHash = ByteBuffer.wrap(torrentInfoHash); - ByteBuffer peerId = ByteBuffer.wrap(clientPeerId); - - buffer.put((byte)Handshake - .BITTORRENT_PROTOCOL_IDENTIFIER.length()); - buffer.put(Handshake - .BITTORRENT_PROTOCOL_IDENTIFIER.getBytes(Torrent.BYTE_ENCODING)); - buffer.put(reserved); - buffer.put(infoHash); - buffer.put(peerId); - - return new Handshake(buffer, infoHash, peerId); - } catch (UnsupportedEncodingException uee) { - return null; - } - } -} diff --git a/src/main/java/com/turn/ttorrent/client/IncomingConnectionListener.java b/src/main/java/com/turn/ttorrent/client/IncomingConnectionListener.java deleted file mode 100644 index 92b814253..000000000 --- a/src/main/java/com/turn/ttorrent/client/IncomingConnectionListener.java +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client; - -import com.turn.ttorrent.client.peer.SharingPeer; - -import java.nio.channels.SocketChannel; -import java.util.EventListener; - -/** - * EventListener interface for objects that want to handle incoming peer - * connections. - * - * @author mpetazzoni - */ -public interface IncomingConnectionListener extends EventListener { - - public void handleNewPeerConnection(SocketChannel channel, byte[] peerId); - - public void handleFailedConnection(SharingPeer peer, Throwable cause); -} diff --git a/src/main/java/com/turn/ttorrent/client/Piece.java b/src/main/java/com/turn/ttorrent/client/Piece.java deleted file mode 100644 index 77d60640b..000000000 --- a/src/main/java/com/turn/ttorrent/client/Piece.java +++ /dev/null @@ -1,324 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client; - -import com.turn.ttorrent.common.Torrent; -import com.turn.ttorrent.client.peer.SharingPeer; -import com.turn.ttorrent.client.storage.TorrentByteStorage; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.concurrent.Callable; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * A torrent piece. - * - *

- * This class represents a torrent piece. Torrents are made of pieces, which - * are in turn made of blocks that are exchanged using the peer protocol. - * The piece length is defined at the torrent level, but the last piece that - * makes the torrent might be smaller. - *

- * - *

- * If the torrent has multiple files, pieces can spread across file boundaries. - * The TorrentByteStorage abstracts this problem to give Piece objects the - * impression of a contiguous, linear byte storage. - *

- * - * @author mpetazzoni - */ -public class Piece implements Comparable { - - private static final Logger logger = - LoggerFactory.getLogger(Piece.class); - - private final TorrentByteStorage bucket; - private final int index; - private final long offset; - private final long length; - private final byte[] hash; - private final boolean seeder; - - private volatile boolean valid; - private int seen; - private ByteBuffer data; - - /** - * Initialize a new piece in the byte bucket. - * - * @param bucket The underlying byte storage bucket. - * @param index This piece index in the torrent. - * @param offset This piece offset, in bytes, in the storage. - * @param length This piece length, in bytes. - * @param hash This piece 20-byte SHA1 hash sum. - * @param seeder Whether we're seeding this torrent or not (disables piece - * validation). - */ - public Piece(TorrentByteStorage bucket, int index, long offset, - long length, byte[] hash, boolean seeder) { - this.bucket = bucket; - this.index = index; - this.offset = offset; - this.length = length; - this.hash = hash; - this.seeder = seeder; - - // Piece is considered invalid until first check. - this.valid = false; - - // Piece start unseen - this.seen = 0; - - this.data = null; - } - - /** - * Tells whether this piece's data is valid or not. - */ - public boolean isValid() { - return this.valid; - } - - /** - * Returns the index of this piece in the torrent. - */ - public int getIndex() { - return this.index; - } - - /** - * Returns the size, in bytes, of this piece. - * - *

- * All pieces, except the last one, are expected to have the same size. - *

- */ - public long size() { - return this.length; - } - - /** - * Tells whether this piece is available in the current connected peer swarm. - */ - public boolean available() { - return this.seen > 0; - } - - /** - * Mark this piece as being seen at the given peer. - * - * @param peer The sharing peer this piece has been seen available at. - */ - public void seenAt(SharingPeer peer) { - this.seen++; - } - - /** - * Mark this piece as no longer being available at the given peer. - * - * @param peer The sharing peer from which the piece is no longer available. - */ - public void noLongerAt(SharingPeer peer) { - this.seen--; - } - - /** - * Validates this piece. - * - * @return Returns true if this piece, as stored in the underlying byte - * storage, is valid, i.e. its SHA1 sum matches the one from the torrent - * meta-info. - */ - public synchronized boolean validate() throws IOException { - if (this.seeder) { - logger.trace("Skipping validation of {} (seeder mode).", this); - this.valid = true; - return true; - } - - 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); - } - - return this.isValid(); - } - - /** - * Internal piece data read function. - * - *

- * This function will read the piece data without checking if the piece has - * been validated. It is simply meant at factoring-in the common read code - * from the validate and read functions. - *

- * - * @param offset Offset inside this piece where to start reading. - * @param length Number of bytes to read from the piece. - * @return A byte buffer containing the piece data. - * @throws IllegalArgumentException If offset + length goes over - * the piece boundary. - * @throws IOException If the read can't be completed (I/O error, or EOF - * reached, which can happen if the piece is not complete). - */ - private ByteBuffer _read(long offset, long length) throws IOException { - if (offset + length > this.length) { - throw new IllegalArgumentException("Piece#" + this.index + - " overrun (" + offset + " + " + length + " > " + - this.length + ") !"); - } - - // TODO: remove cast to int when large ByteBuffer support is - // implemented in Java. - ByteBuffer buffer = ByteBuffer.allocate((int)length); - int bytes = this.bucket.read(buffer, this.offset + offset); - buffer.rewind(); - buffer.limit(bytes >= 0 ? bytes : 0); - return buffer; - } - - /** - * Read a piece block from the underlying byte storage. - * - *

- * This is the public method for reading this piece's data, and it will - * only succeed if the piece is complete and valid on disk, thus ensuring - * any data that comes out of this function is valid piece data we can send - * to other peers. - *

- * - * @param offset Offset inside this piece where to start reading. - * @param length Number of bytes to read from the piece. - * @return A byte buffer containing the piece data. - * @throws IllegalArgumentException If offset + length goes over - * the piece boundary. - * @throws IllegalStateException If the piece is not valid when attempting - * to read it. - * @throws IOException If the read can't be completed (I/O error, or EOF - * reached, which can happen if the piece is not complete). - */ - public ByteBuffer read(long offset, int length) - throws IllegalArgumentException, IllegalStateException, IOException { - if (!this.valid) { - throw new IllegalStateException("Attempting to read an " + - "known-to-be invalid piece!"); - } - - return this._read(offset, length); - } - - /** - * Record the given block at the given offset in this piece. - * - *

- * Note: this has synchronized access to the underlying byte storage. - *

- * - * @param block The ByteBuffer containing the block data. - * @param offset The block offset in this piece. - */ - public synchronized void record(ByteBuffer block, int offset) - throws IOException { - if (this.data == null || offset == 0) { - // TODO: remove cast to int when large ByteBuffer support is - // implemented in Java. - this.data = ByteBuffer.allocate((int)this.length); - } - - int pos = block.position(); - this.data.position(offset); - this.data.put(block); - block.position(pos); - - if (block.remaining() + offset == this.length) { - this.data.rewind(); - logger.trace("Recording {}...", this); - this.bucket.write(this.data, this.offset); - this.data = null; - } - } - - /** - * Return a human-readable representation of this piece. - */ - public String toString() { - return String.format("piece#%4d%s", - this.index, - this.isValid() ? "+" : "-"); - } - - /** - * Piece comparison function for ordering pieces based on their - * availability. - * - * @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; - } - } - - /** - * A {@link Callable} to call the piece validation function. - * - *

- * This {@link Callable} implementation allows for the calling of the piece - * validation function in a controlled context like a thread or an - * executor. It returns the piece it was created for. Results of the - * validation can easily be extracted from the {@link Piece} object after - * it is returned. - *

- * - * @author mpetazzoni - */ - public static class CallableHasher implements Callable { - - private final Piece piece; - - public CallableHasher(Piece piece) { - this.piece = piece; - } - - @Override - public Piece call() throws IOException { - this.piece.validate(); - return this.piece; - } - } -} diff --git a/src/main/java/com/turn/ttorrent/client/SharedTorrent.java b/src/main/java/com/turn/ttorrent/client/SharedTorrent.java deleted file mode 100644 index 85f4ac310..000000000 --- a/src/main/java/com/turn/ttorrent/client/SharedTorrent.java +++ /dev/null @@ -1,838 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client; - -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.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; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * A torrent shared by the BitTorrent client. - * - *

- * The {@link SharedTorrent} class extends the Torrent class with all the data - * and logic required by the BitTorrent client implementation. - *

- * - *

- * Note: this implementation currently only supports single-file - * torrents. - *

- * - * @author mpetazzoni - */ -public class SharedTorrent extends Torrent implements PeerActivityListener { - - private static final Logger logger = - LoggerFactory.getLogger(SharedTorrent.class); - - /** Randomly select the next piece to download from a peer from the - * RAREST_PIECE_JITTER available from it. */ - private static final int RAREST_PIECE_JITTER = 42; - - /** End-game trigger ratio. - * - *

- * Eng-game behavior (requesting already requested pieces from available - * and ready peers to try to speed-up the end of the transfer) will only be - * enabled when the ratio of completed pieces over total pieces in the - * torrent is over this value. - *

- */ - private static final float ENG_GAME_COMPLETION_RATIO = 0.95f; - - private Random random; - private boolean stop; - - private long uploaded; - private long downloaded; - private long left; - - private final TorrentByteStorage bucket; - - private final int pieceLength; - private final ByteBuffer piecesHashes; - - private boolean initialized; - private Piece[] pieces; - private SortedSet rarest; - private BitSet completedPieces; - private BitSet requestedPieces; - - /** - * Create a new shared torrent from a base Torrent object. - * - *

- * This will recreate a SharedTorrent object from the provided Torrent - * object's encoded meta-info data. - *

- * - * @param torrent The Torrent object. - * @param destDir The destination directory or location of the torrent - * files. - * @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 { - this(torrent, destDir, false); - } - - /** - * Create a new shared torrent from a base Torrent object. - * - *

- * This will recreate a SharedTorrent object from the provided Torrent - * object's encoded meta-info data. - *

- * - * @param torrent The Torrent object. - * @param destDir The destination directory or location of the torrent - * files. - * @param seeder Whether we're a seeder for this torrent or not (disables - * validation). - * @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 { - this(torrent.getEncoded(), destDir, seeder); - } - - /** - * Create a new shared torrent from meta-info binary data. - * - * @param torrent The meta-info byte data. - * @param destDir The destination directory or location of the torrent - * files. - * @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. - */ - public SharedTorrent(byte[] torrent, File destDir) - throws FileNotFoundException, IOException, NoSuchAlgorithmException { - this(torrent, destDir, false); - } - - /** - * Create a new shared torrent from meta-info binary data. - * - * @param torrent The meta-info byte data. - * @param parent The parent directory or location the torrent files. - * @param seeder Whether we're a seeder for this torrent or not (disables - * validation). - * @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 { - super(torrent, seeder); - - if (parent == null || !parent.isDirectory()) { - throw new IllegalArgumentException("Invalid parent directory!"); - } - - String parentPath = parent.getCanonicalPath(); - - try { - this.pieceLength = this.decoded_info.get("piece length").getInt(); - this.piecesHashes = ByteBuffer.wrap(this.decoded_info.get("pieces") - .getBytes()); - - if (this.piecesHashes.capacity() / Torrent.PIECE_HASH_SIZE * - (long)this.pieceLength < this.getSize()) { - throw new IllegalArgumentException("Torrent size does not " + - "match the number of pieces and the piece size!"); - } - } catch (InvalidBEncodingException ibee) { - throw new IllegalArgumentException( - "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 (!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; - } - this.bucket = new FileCollectionStorage(files, this.getSize()); - - this.random = new Random(System.currentTimeMillis()); - this.stop = false; - - this.uploaded = 0; - this.downloaded = 0; - this.left = this.getSize(); - - this.initialized = false; - this.pieces = new Piece[0]; - this.rarest = Collections.synchronizedSortedSet(new TreeSet()); - this.completedPieces = new BitSet(); - this.requestedPieces = new BitSet(); - } - - /** - * Create a new shared torrent from the given torrent file. - * - * @param source The .torrent file to read the torrent - * 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(); - return new SharedTorrent(data, parent); - } - - /** - * Get the number of bytes uploaded for this torrent. - */ - public long getUploaded() { - return this.uploaded; - } - - /** - * Get the number of bytes downloaded for this torrent. - * - *

- * Note: this could be more than the torrent's length, and should - * not be used to determine a completion percentage. - *

- */ - public long getDownloaded() { - return this.downloaded; - } - - /** - * Get the number of bytes left to download for this torrent. - */ - public long getLeft() { - return this.left; - } - - /** - * Tells whether this torrent has been fully initialized yet. - */ - public boolean isInitialized() { - return this.initialized; - } - - /** - * Stop the torrent initialization as soon as possible. - */ - public void stop() { - this.stop = true; - } - - /** - * Build this torrent's pieces array. - * - *

- * Hash and verify any potentially present local data and create this - * torrent's pieces array from their respective hash provided in the - * torrent meta-info. - *

- * - *

- * This function should be called soon after the constructor to initialize - * the pieces array. - *

- */ - public synchronized void init() throws InterruptedException, IOException { - if (this.isInitialized()) { - throw new IllegalStateException("Torrent was already initialized!"); - } - - int threads = getHashingThreadsCount(); - int nPieces = (int) (Math.ceil( - (double)this.getSize() / this.pieceLength)); - int step = 10; - - this.pieces = new Piece[nPieces]; - this.completedPieces = new BitSet(nPieces); - this.piecesHashes.clear(); - - ExecutorService executor = Executors.newFixedThreadPool(threads); - List> results = new LinkedList>(); - - try { - logger.info("Analyzing local data for {} with {} threads ({} pieces)...", - new Object[] { this.getName(), threads, nPieces }); - for (int idx=0; idx hasher = new Piece.CallableHasher(this.pieces[idx]); - results.add(executor.submit(hasher)); - - if (results.size() >= threads) { - this.validatePieces(results); - } - - if (idx / (float)nPieces * 100f > step) { - logger.info(" ... {}% complete", step); - step += 10; - } - } - - this.validatePieces(results); - } finally { - // Request orderly executor shutdown and wait for hashing tasks to - // complete. - executor.shutdown(); - while (!executor.isTerminated()) { - if (this.stop) { - throw new InterruptedException("Torrent data analysis " + - "interrupted."); - } - - Thread.sleep(10); - } - } - - logger.debug("{}: we have {}/{} bytes ({}%) [{}/{} pieces].", - new Object[] { - this.getName(), - (this.getSize() - this.left), - this.getSize(), - String.format("%.1f", (100f * (1f - this.left / (float)this.getSize()))), - this.completedPieces.cardinality(), - this.pieces.length - }); - this.initialized = true; - } - - /** - * Process the pieces enqueued for hash validation so far. - * - * @param results The list of {@link Future}s of pieces to process. - */ - private void validatePieces(List> results) - throws IOException { - try { - for (Future task : results) { - Piece piece = task.get(); - if (this.pieces[piece.getIndex()].isValid()) { - this.completedPieces.set(piece.getIndex()); - this.left -= piece.size(); - } - } - - results.clear(); - } catch (Exception e) { - throw new IOException("Error while hashing a torrent piece!", e); - } - } - - - public synchronized void close() { - try { - this.bucket.close(); - } catch (IOException ioe) { - logger.error("Error closing torrent byte storage: {}", - ioe.getMessage()); - } - } - - /** - * Retrieve a piece object by index. - * - * @param index The index of the piece in this torrent. - */ - public Piece getPiece(int index) { - if (this.pieces == null) { - throw new IllegalStateException("Torrent not initialized yet."); - } - - if (index >= this.pieces.length) { - throw new IllegalArgumentException("Invalid piece index!"); - } - - return this.pieces[index]; - } - - /** - * Get the number of pieces in this torrent. - */ - public int getPieceCount() { - if (this.pieces == null) { - throw new IllegalStateException("Torrent not initialized yet."); - } - - return this.pieces.length; - } - - - /** - * Return a copy of the bit field of available pieces for this torrent. - * - *

- * Available pieces are pieces available in the swarm, and it does not - * include our own pieces. - *

- */ - public BitSet getAvailablePieces() { - if (!this.isInitialized()) { - throw new IllegalStateException("Torrent not yet initialized!"); - } - - BitSet availablePieces = new BitSet(this.pieces.length); - - synchronized (this.pieces) { - for (Piece piece : this.pieces) { - if (piece.available()) { - availablePieces.set(piece.getIndex()); - } - } - } - - return availablePieces; - } - - /** - * Return a copy of the completed pieces bitset. - */ - public BitSet getCompletedPieces() { - if (!this.isInitialized()) { - throw new IllegalStateException("Torrent not yet initialized!"); - } - - synchronized (this.completedPieces) { - return (BitSet)this.completedPieces.clone(); - } - } - - /** - * Return a copy of the requested pieces bitset. - */ - public BitSet getRequestedPieces() { - if (!this.isInitialized()) { - throw new IllegalStateException("Torrent not yet initialized!"); - } - - synchronized (this.requestedPieces) { - return (BitSet)this.requestedPieces.clone(); - } - } - - /** - * Tells whether this torrent has been fully downloaded, or is fully - * available locally. - */ - public synchronized boolean isComplete() { - return this.pieces.length > 0 && - this.completedPieces.cardinality() == this.pieces.length; - } - - /** - * Finalize the download of this torrent. - * - *

- * This realizes the final, pre-seeding phase actions on this torrent, - * which usually consists in putting the torrent data in their final form - * and at their target location. - *

- * - * @see TorrentByteStorage#finish - */ - public synchronized void finish() throws IOException { - if (!this.isInitialized()) { - throw new IllegalStateException("Torrent not yet initialized!"); - } - - if (!this.isComplete()) { - throw new IllegalStateException("Torrent download is not complete!"); - } - - this.bucket.finish(); - } - - public synchronized boolean isFinished() { - return this.isComplete() && this.bucket.isFinished(); - } - - /** - * Return the completion percentage of this torrent. - * - *

- * This is computed from the number of completed pieces divided by the - * number of pieces in this torrent, times 100. - *

- */ - public float getCompletion() { - return this.isInitialized() - ? (float)this.completedPieces.cardinality() / - (float)this.pieces.length * 100.0f - : 0.0f; - } - - /** - * Mark a piece as completed, decrementing the piece size in bytes from our - * left bytes to download counter. - */ - public synchronized void markCompleted(Piece piece) { - if (this.completedPieces.get(piece.getIndex())) { - return; - } - - // A completed piece means that's that much data left to download for - // this torrent. - this.left -= piece.size(); - this.completedPieces.set(piece.getIndex()); - } - - /** PeerActivityListener handler(s). *************************************/ - - /** - * Peer choked handler. - * - *

- * When a peer chokes, the requests made to it are canceled and we need to - * mark the eventually piece we requested from it as available again for - * download tentative from another peer. - *

- * - * @param peer The peer that choked. - */ - @Override - public synchronized void handlePeerChoked(SharingPeer peer) { - Piece piece = peer.getRequestedPiece(); - - if (piece != null) { - this.requestedPieces.set(piece.getIndex(), false); - } - - logger.trace("Peer {} choked, we now have {} outstanding " + - "request(s): {}", - new Object[] { - peer, - this.requestedPieces.cardinality(), - this.requestedPieces - }); - } - - /** - * Peer ready handler. - * - *

- * When a peer becomes ready to accept piece block requests, select a piece - * to download and go for it. - *

- * - * @param peer The peer that became ready. - */ - @Override - public synchronized void handlePeerReady(SharingPeer peer) { - BitSet interesting = peer.getAvailablePieces(); - interesting.andNot(this.completedPieces); - interesting.andNot(this.requestedPieces); - - logger.trace("Peer {} is ready and has {} interesting piece(s).", - peer, interesting.cardinality()); - - // If we didn't find interesting pieces, we need to check if we're in - // an end-game situation. If yes, we request an already requested piece - // to try to speed up the end. - if (interesting.cardinality() == 0) { - interesting = peer.getAvailablePieces(); - interesting.andNot(this.completedPieces); - if (interesting.cardinality() == 0) { - logger.trace("No interesting piece from {}!", peer); - return; - } - - if (this.completedPieces.cardinality() < - ENG_GAME_COMPLETION_RATIO * this.pieces.length) { - logger.trace("Not far along enough to warrant end-game mode."); - return; - } - - logger.trace("Possible end-game, we're about to request a piece " + - "that was already requested from another peer."); - } - - // Extract the RAREST_PIECE_JITTER rarest pieces from the interesting - // pieces of this peer. - ArrayList choice = new ArrayList(RAREST_PIECE_JITTER); - synchronized (this.rarest) { - for (Piece piece : this.rarest) { - if (interesting.get(piece.getIndex())) { - choice.add(piece); - if (choice.size() >= RAREST_PIECE_JITTER) { - break; - } - } - } - } - - Piece chosen = choice.get( - this.random.nextInt( - Math.min(choice.size(), - RAREST_PIECE_JITTER))); - this.requestedPieces.set(chosen.getIndex()); - - logger.trace("Requesting {} from {}, we now have {} " + - "outstanding request(s): {}", - new Object[] { - chosen, - peer, - this.requestedPieces.cardinality(), - this.requestedPieces - }); - - peer.downloadPiece(chosen); - } - - /** - * Piece availability handler. - * - *

- * Handle updates in piece availability from a peer's HAVE message. When - * this happens, we need to mark that piece as available from the peer. - *

- * - * @param peer The peer we got the update from. - * @param piece The piece that became available. - */ - @Override - public synchronized void handlePieceAvailability(SharingPeer peer, - Piece piece) { - // If we don't have this piece, tell the peer we're interested in - // getting it from him. - if (!this.completedPieces.get(piece.getIndex()) && - !this.requestedPieces.get(piece.getIndex())) { - peer.interesting(); - } - - this.rarest.remove(piece); - piece.seenAt(peer); - this.rarest.add(piece); - - logger.trace("Peer {} contributes {} piece(s) [{}/{}/{}].", - new Object[] { - peer, - peer.getAvailablePieces().cardinality(), - this.completedPieces.cardinality(), - this.getAvailablePieces().cardinality(), - this.pieces.length - }); - - if (!peer.isChoked() && - peer.isInteresting() && - !peer.isDownloading()) { - this.handlePeerReady(peer); - } - } - - /** - * Bit field availability handler. - * - *

- * Handle updates in piece availability from a peer's BITFIELD message. - * When this happens, we need to mark in all the pieces the peer has that - * they can be reached through this peer, thus augmenting the global - * availability of pieces. - *

- * - * @param peer The peer we got the update from. - * @param availablePieces The pieces availability bit field of the peer. - */ - @Override - public synchronized void handleBitfieldAvailability(SharingPeer peer, - BitSet availablePieces) { - // Determine if the peer is interesting for us or not, and notify it. - BitSet interesting = (BitSet)availablePieces.clone(); - interesting.andNot(this.completedPieces); - interesting.andNot(this.requestedPieces); - - if (interesting.cardinality() == 0) { - peer.notInteresting(); - } else { - peer.interesting(); - } - - // 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]); - } - - logger.trace("Peer {} contributes {} piece(s) ({} interesting) " + - "[completed={}; available={}/{}].", - new Object[] { - peer, - availablePieces.cardinality(), - interesting.cardinality(), - this.completedPieces.cardinality(), - this.getAvailablePieces().cardinality(), - this.pieces.length - }); - } - - /** - * Piece upload completion handler. - * - *

- * When a piece has been sent to a peer, we just record that we sent that - * many bytes. If the piece is valid on the peer's side, it will send us a - * HAVE message and we'll record that the piece is available on the peer at - * that moment (see handlePieceAvailability()). - *

- * - * @param peer The peer we got this piece from. - * @param piece The piece in question. - */ - @Override - public synchronized void handlePieceSent(SharingPeer peer, Piece piece) { - logger.trace("Completed upload of {} to {}.", piece, peer); - this.uploaded += piece.size(); - } - - /** - * Piece download completion handler. - * - *

- * If the complete piece downloaded is valid, we can record in the torrent - * completedPieces bit field that we know have this piece. - *

- * - * @param peer The peer we got this piece from. - * @param piece The piece in question. - */ - @Override - public synchronized void handlePieceCompleted(SharingPeer peer, - Piece piece) throws IOException { - // Regardless of validity, record the number of bytes downloaded and - // mark the piece as not requested anymore - this.downloaded += piece.size(); - this.requestedPieces.set(piece.getIndex(), false); - - logger.trace("We now have {} piece(s) and {} outstanding request(s): {}", - new Object[] { - this.completedPieces.cardinality(), - this.requestedPieces.cardinality(), - this.requestedPieces - }); - } - - /** - * Peer disconnection handler. - * - *

- * When a peer disconnects, we need to mark in all of the pieces it had - * available that they can't be reached through this peer anymore. - *

- * - * @param peer The peer we got this piece from. - */ - @Override - public synchronized void handlePeerDisconnected(SharingPeer peer) { - BitSet availablePieces = peer.getAvailablePieces(); - - for (int i = availablePieces.nextSetBit(0); i >= 0; - i = availablePieces.nextSetBit(i+1)) { - this.rarest.remove(this.pieces[i]); - this.pieces[i].noLongerAt(peer); - this.rarest.add(this.pieces[i]); - } - - Piece requested = peer.getRequestedPiece(); - if (requested != null) { - this.requestedPieces.set(requested.getIndex(), false); - } - - logger.debug("Peer {} went away with {} piece(s) [completed={}; available={}/{}]", - new Object[] { - peer, - availablePieces.cardinality(), - this.completedPieces.cardinality(), - this.getAvailablePieces().cardinality(), - this.pieces.length - }); - logger.trace("We now have {} piece(s) and {} outstanding request(s): {}", - new Object[] { - this.completedPieces.cardinality(), - this.requestedPieces.cardinality(), - this.requestedPieces - }); - } - - @Override - public synchronized void handleIOException(SharingPeer peer, - IOException ioe) { /* Do nothing */ } -} diff --git a/src/main/java/com/turn/ttorrent/client/announce/Announce.java b/src/main/java/com/turn/ttorrent/client/announce/Announce.java deleted file mode 100644 index 4c0a1b223..000000000 --- a/src/main/java/com/turn/ttorrent/client/announce/Announce.java +++ /dev/null @@ -1,370 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client.announce; - -import com.turn.ttorrent.client.SharedTorrent; -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.protocol.TrackerMessage.*; - -import java.net.URI; -import java.net.UnknownHostException; -import java.net.UnknownServiceException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * BitTorrent announce sub-system. - * - *

- * A BitTorrent client must check-in to the torrent's tracker(s) to get peers - * and to report certain events. - *

- * - *

- * This Announce class implements a periodic announce request thread that will - * notify announce request event listeners for each tracker response. - *

- * - * @author mpetazzoni - * @see com.turn.ttorrent.common.protocol.TrackerMessage - */ -public class Announce implements Runnable { - - protected static final Logger logger = - LoggerFactory.getLogger(Announce.class); - - private final Peer peer; - - /** The tiers of tracker clients matching the tracker URIs defined in the - * torrent. */ - private final List> clients; - private final Set allClients; - - /** Announce thread and control. */ - private Thread thread; - private boolean stop; - private boolean forceStop; - - /** Announce interval. */ - private int interval; - - private int currentTier; - private int currentClient; - - /** - * Initialize the base announce class members for the announcer. - * - * @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; - this.clients = new ArrayList>(); - this.allClients = new HashSet(); - - /** - * Build the tiered structure of tracker clients mapping to the - * trackers of the torrent. - */ - for (List tier : torrent.getAnnounceList()) { - ArrayList tierClients = new ArrayList(); - for (URI tracker : tier) { - try { - TrackerClient client = this.createTrackerClient(torrent, - peer, tracker); - - tierClients.add(client); - this.allClients.add(client); - } catch (Exception e) { - logger.warn("Will not announce on {}: {}!", - tracker, - e.getMessage() != null - ? e.getMessage() - : e.getClass().getSimpleName()); - } - } - - // Shuffle the list of tracker clients once on creation. - Collections.shuffle(tierClients); - - // Tier is guaranteed to be non-empty by - // Torrent#parseAnnounceInformation(), so we can add it safely. - clients.add(tierClients); - } - - this.thread = null; - this.currentTier = 0; - this.currentClient = 0; - - logger.info("Initialized announce sub-system with {} trackers on {}.", - new Object[] { torrent.getTrackerCount(), torrent }); - } - - /** - * Register a new announce response listener. - * - * @param listener The listener to register on this announcer events. - */ - public void register(AnnounceResponseListener listener) { - for (TrackerClient client : this.allClients) { - client.register(listener); - } - } - - /** - * Start the announce request thread. - */ - public void start() { - this.stop = false; - this.forceStop = false; - - if (this.clients.size() > 0 && (this.thread == null || !this.thread.isAlive())) { - this.thread = new Thread(this); - this.thread.setName("bt-announce(" + - this.peer.getShortHexPeerId() + ")"); - this.thread.start(); - } - } - - /** - * Set the announce interval. - */ - public void setInterval(int interval) { - if (interval <= 0) { - this.stop(true); - return; - } - - if (this.interval == interval) { - return; - } - - logger.info("Setting announce interval to {}s per tracker request.", - interval); - this.interval = interval; - } - - /** - * Stop the announce thread. - * - *

- * One last 'stopped' announce event might be sent to the tracker to - * announce we're going away, depending on the implementation. - *

- */ - public void stop() { - this.stop = true; - - if (this.thread != null && this.thread.isAlive()) { - this.thread.interrupt(); - - for (TrackerClient client : this.allClients) { - client.close(); - } - - try { - this.thread.join(); - } catch (InterruptedException ie) { - // Ignore - } - } - - this.thread = null; - } - - /** - * Main announce loop. - * - *

- * The announce thread starts by making the initial 'started' announce - * request to register on the tracker and get the announce interval value. - * Subsequent announce requests are ordinary, event-less, periodic requests - * for peers. - *

- * - *

- * Unless forcefully stopped, the announce thread will terminate by sending - * a 'stopped' announce request before stopping. - *

- */ - @Override - public void run() { - logger.info("Starting announce loop..."); - - // Set an initial announce interval to 5 seconds. This will be updated - // in real-time by the tracker's responses to our announce requests. - this.interval = 5; - - AnnounceRequestMessage.RequestEvent event = - AnnounceRequestMessage.RequestEvent.STARTED; - - while (!this.stop) { - try { - this.getCurrentTrackerClient().announce(event, false); - this.promoteCurrentTrackerClient(); - event = AnnounceRequestMessage.RequestEvent.NONE; - } catch (AnnounceException ae) { - logger.warn(ae.getMessage()); - this.moveToNextTrackerClient(); - } - - try { - Thread.sleep(this.interval * 1000); - } catch (InterruptedException ie) { - // Ignore - } - } - - logger.info("Exited announce loop."); - - if (!this.forceStop) { - // Send the final 'stopped' event to the tracker after a little - // while. - event = AnnounceRequestMessage.RequestEvent.STOPPED; - try { - Thread.sleep(500); - } catch (InterruptedException ie) { - // Ignore - } - - try { - this.getCurrentTrackerClient().announce(event, true); - } catch (AnnounceException ae) { - logger.warn(ae.getMessage()); - } - } - } - - /** - * Create a {@link TrackerClient} annoucing to the given tracker address. - * - * @param torrent The torrent the tracker client will be announcing for. - * @param peer The peer the tracker client will announce on behalf of. - * @param tracker The tracker address as a {@link URI}. - * @throws UnknownHostException If the tracker address is invalid. - * @throws UnknownServiceException If the tracker protocol is not supported. - */ - private TrackerClient createTrackerClient(SharedTorrent torrent, Peer peer, - URI tracker) throws UnknownHostException, UnknownServiceException { - String scheme = tracker.getScheme(); - - if ("http".equals(scheme) || "https".equals(scheme)) { - return new HTTPTrackerClient(torrent, peer, tracker); - } else if ("udp".equals(scheme)) { - return new UDPTrackerClient(torrent, peer, tracker); - } - - throw new UnknownServiceException( - "Unsupported announce scheme: " + scheme + "!"); - } - - /** - * Returns the current tracker client used for announces. - */ - public TrackerClient getCurrentTrackerClient() { - return this.clients - .get(this.currentTier) - .get(this.currentClient); - } - - /** - * Promote the current tracker client to the top of its tier. - * - *

- * As defined by BEP#0012, when communication with a tracker is successful, - * it should be moved to the front of its tier. - *

- * - *

- * The index of the currently used {@link TrackerClient} is reset to 0 to - * reflect this change. - *

- */ - private void promoteCurrentTrackerClient() { - logger.trace("Promoting current tracker client for {} " + - "(tier {}, position {} -> 0).", - new Object[] { - this.getCurrentTrackerClient().getTrackerURI(), - this.currentTier, - this.currentClient - }); - - Collections.swap(this.clients.get(this.currentTier), - this.currentClient, 0); - this.currentClient = 0; - } - - /** - * Move to the next tracker client. - * - *

- * If no more trackers are available in the current tier, move to the next - * tier. If we were on the last tier, restart from the first tier. - *

- * - *

- * By design no empty tier can be in the tracker list structure so we don't - * need to check for empty tiers here. - *

- */ - private void moveToNextTrackerClient() { - int tier = this.currentTier; - int client = this.currentClient + 1; - - if (client >= this.clients.get(tier).size()) { - client = 0; - - tier++; - - if (tier >= this.clients.size()) { - tier = 0; - } - } - - if (tier != this.currentTier || - client != this.currentClient) { - this.currentTier = tier; - this.currentClient = client; - - logger.debug("Switched to tracker client for {} " + - "(tier {}, position {}).", - new Object[] { - this.getCurrentTrackerClient().getTrackerURI(), - this.currentTier, - this.currentClient - }); - } - } - - /** - * Stop the announce thread. - * - * @param hard Whether to force stop the announce thread or not, i.e. not - * send the final 'stopped' announce request or not. - */ - private void stop(boolean hard) { - this.forceStop = hard; - this.stop(); - } -} diff --git a/src/main/java/com/turn/ttorrent/client/announce/HTTPTrackerClient.java b/src/main/java/com/turn/ttorrent/client/announce/HTTPTrackerClient.java deleted file mode 100644 index ae338afdf..000000000 --- a/src/main/java/com/turn/ttorrent/client/announce/HTTPTrackerClient.java +++ /dev/null @@ -1,183 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.client.announce; - -import com.turn.ttorrent.client.SharedTorrent; -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.protocol.TrackerMessage.*; -import com.turn.ttorrent.common.protocol.http.*; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.nio.ByteBuffer; - -import org.apache.commons.io.output.ByteArrayOutputStream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Announcer for HTTP trackers. - * - * @author mpetazzoni - * @see BitTorrent tracker request specification - */ -public class HTTPTrackerClient extends TrackerClient { - - protected static final Logger logger = - LoggerFactory.getLogger(HTTPTrackerClient.class); - - /** - * Create a new HTTP announcer for the given torrent. - * - * @param torrent The torrent we're announcing about. - * @param peer Our own peer specification. - */ - protected HTTPTrackerClient(SharedTorrent torrent, Peer peer, - URI tracker) { - super(torrent, peer, tracker); - } - - /** - * Build, send and process a tracker announce request. - * - *

- * This function first builds an announce request for the specified event - * with all the required parameters. Then, the request is made to the - * tracker and the response analyzed. - *

- * - *

- * All registered {@link AnnounceResponseListener} objects are then fired - * with the decoded payload. - *

- * - * @param event The announce event type (can be AnnounceEvent.NONE for - * periodic updates). - * @param inhibitEvents Prevent event listeners from being notified. - */ - @Override - public void announce(AnnounceRequestMessage.RequestEvent event, - boolean inhibitEvents) throws AnnounceException { - logger.info("Announcing{} to tracker with {}U/{}D/{}L bytes...", - new Object[] { - this.formatAnnounceEvent(event), - this.torrent.getUploaded(), - this.torrent.getDownloaded(), - this.torrent.getLeft() - }); - - URL target = null; - try { - HTTPAnnounceRequestMessage request = - this.buildAnnounceRequest(event); - target = request.buildAnnounceURL(this.tracker.toURL()); - } catch (MalformedURLException mue) { - throw new AnnounceException("Invalid announce URL (" + - mue.getMessage() + ")", mue); - } catch (MessageValidationException mve) { - throw new AnnounceException("Announce request creation violated " + - "expected protocol (" + mve.getMessage() + ")", mve); - } catch (IOException ioe) { - throw new AnnounceException("Error building announce request (" + - ioe.getMessage() + ")", ioe); - } - - HttpURLConnection conn = null; - InputStream in = null; - try { - conn = (HttpURLConnection)target.openConnection(); - in = conn.getInputStream(); - } catch (IOException ioe) { - if (conn != null) { - in = conn.getErrorStream(); - } - } - - // At this point if the input stream is null it means we have neither a - // response body nor an error stream from the server. No point in going - // any further. - if (in == null) { - throw new AnnounceException("No response or unreachable tracker!"); - } - - try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - baos.write(in); - - // Parse and handle the response - HTTPTrackerMessage message = - HTTPTrackerMessage.parse(ByteBuffer.wrap(baos.toByteArray())); - this.handleTrackerAnnounceResponse(message, inhibitEvents); - } catch (IOException ioe) { - throw new AnnounceException("Error reading tracker response!", ioe); - } catch (MessageValidationException mve) { - throw new AnnounceException("Tracker message violates expected " + - "protocol (" + mve.getMessage() + ")", mve); - } finally { - // Make sure we close everything down at the end to avoid resource - // leaks. - try { - in.close(); - } catch (IOException ioe) { - logger.warn("Problem ensuring error stream closed!", ioe); - } - - // This means trying to close the error stream as well. - InputStream err = conn.getErrorStream(); - if (err != null) { - try { - err.close(); - } catch (IOException ioe) { - logger.warn("Problem ensuring error stream closed!", ioe); - } - } - } - } - - /** - * Build the announce request tracker message. - * - * @param event The announce event (can be NONE or null) - * @return Returns an instance of a {@link HTTPAnnounceRequestMessage} - * that can be used to generate the fully qualified announce URL, with - * parameters, to make the announce request. - * @throws UnsupportedEncodingException - * @throws IOException - * @throws MessageValidationException - */ - private HTTPAnnounceRequestMessage buildAnnounceRequest( - AnnounceRequestMessage.RequestEvent event) - throws UnsupportedEncodingException, IOException, - MessageValidationException { - // Build announce request message - return HTTPAnnounceRequestMessage.craft( - this.torrent.getInfoHash(), - this.peer.getPeerId().array(), - this.peer.getPort(), - this.torrent.getUploaded(), - this.torrent.getDownloaded(), - this.torrent.getLeft(), - true, false, event, - this.peer.getIp(), - AnnounceRequestMessage.DEFAULT_NUM_WANT); - } -} diff --git a/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java b/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java deleted file mode 100644 index b757e0e17..000000000 --- a/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client.announce; - -import com.turn.ttorrent.client.SharedTorrent; -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.protocol.TrackerMessage; -import com.turn.ttorrent.common.protocol.TrackerMessage.*; - -import java.net.URI; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public abstract class TrackerClient { - - /** The set of listeners to announce request answers. */ - private final Set listeners; - - protected final SharedTorrent torrent; - protected final Peer peer; - protected final URI tracker; - - public TrackerClient(SharedTorrent torrent, Peer peer, URI tracker) { - this.listeners = new HashSet(); - this.torrent = torrent; - this.peer = peer; - this.tracker = tracker; - } - - /** - * Register a new announce response listener. - * - * @param listener The listener to register on this announcer events. - */ - public void register(AnnounceResponseListener listener) { - this.listeners.add(listener); - } - - /** - * Returns the URI this tracker clients connects to. - */ - public URI getTrackerURI() { - return this.tracker; - } - - /** - * Build, send and process a tracker announce request. - * - *

- * This function first builds an announce request for the specified event - * with all the required parameters. Then, the request is made to the - * tracker and the response analyzed. - *

- * - *

- * All registered {@link AnnounceResponseListener} objects are then fired - * with the decoded payload. - *

- * - * @param event The announce event type (can be AnnounceEvent.NONE for - * periodic updates). - * @param inhibitEvent Prevent event listeners from being notified. - */ - public abstract void announce(AnnounceRequestMessage.RequestEvent event, - boolean inhibitEvent) throws AnnounceException; - - /** - * Close any opened announce connection. - * - *

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

- */ - protected void close() { - // Do nothing by default, but can be overloaded. - } - - /** - * Formats an announce event into a usable string. - */ - protected String formatAnnounceEvent( - AnnounceRequestMessage.RequestEvent event) { - return AnnounceRequestMessage.RequestEvent.NONE.equals(event) - ? "" - : String.format(" %s", event.name()); - } - - /** - * Handle the announce response from the tracker. - * - *

- * Analyzes the response from the tracker and acts on it. If the response - * is an error, it is logged. Otherwise, the announce response is used - * to fire the corresponding announce and peer events to all announce - * listeners. - *

- * - * @param message The incoming {@link TrackerMessage}. - * @param inhibitEvents Whether or not to prevent events from being fired. - */ - protected void handleTrackerAnnounceResponse(TrackerMessage message, - boolean inhibitEvents) throws AnnounceException { - if (message instanceof ErrorMessage) { - ErrorMessage error = (ErrorMessage)message; - throw new AnnounceException(error.getReason()); - } - - if (! (message instanceof AnnounceResponseMessage)) { - throw new AnnounceException("Unexpected tracker message type " + - message.getType().name() + "!"); - } - - if (inhibitEvents) { - return; - } - - AnnounceResponseMessage response = - (AnnounceResponseMessage)message; - this.fireAnnounceResponseEvent( - response.getComplete(), - response.getIncomplete(), - response.getInterval()); - this.fireDiscoveredPeersEvent( - response.getPeers()); - } - - /** - * Fire the announce response event to all listeners. - * - * @param complete The number of seeders on this torrent. - * @param incomplete The number of leechers on this torrent. - * @param interval The announce interval requested by the tracker. - */ - protected void fireAnnounceResponseEvent(int complete, int incomplete, - int interval) { - for (AnnounceResponseListener listener : this.listeners) { - listener.handleAnnounceResponse(interval, complete, incomplete); - } - } - - /** - * Fire the new peer discovery event to all listeners. - * - * @param peers The list of peers discovered. - */ - protected void fireDiscoveredPeersEvent(List peers) { - for (AnnounceResponseListener listener : this.listeners) { - listener.handleDiscoveredPeers(peers); - } - } -} diff --git a/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java b/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java deleted file mode 100644 index 01b1c0420..000000000 --- a/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java +++ /dev/null @@ -1,374 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.client.announce; - -import com.turn.ttorrent.client.SharedTorrent; -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.protocol.TrackerMessage; -import com.turn.ttorrent.common.protocol.TrackerMessage.*; -import com.turn.ttorrent.common.protocol.udp.*; - -import java.io.IOException; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; -import java.net.URI; -import java.nio.ByteBuffer; -import java.nio.channels.UnsupportedAddressTypeException; -import java.util.Calendar; -import java.util.Date; -import java.util.Random; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Announcer for UDP trackers. - * - *

- * The UDP tracker protocol requires a two-step announce request/response - * exchange where the peer is first required to establish a "connection" - * with the tracker by sending a connection request message and retreiving - * a connection ID from the tracker to use in the following announce - * request messages (valid for 2 minutes). - *

- * - *

- * It also contains a backing-off retry mechanism (on a 15*2^n seconds - * scheme), in which if the announce request times-out for more than the - * connection ID validity period, another connection request/response - * exchange must be made before attempting to retransmit the announce - * request. - *

- * - * @author mpetazzoni - */ -public class UDPTrackerClient extends TrackerClient { - - protected static final Logger logger = - LoggerFactory.getLogger(UDPTrackerClient.class); - - /** - * Back-off timeout uses 15 * 2 ^ n formula. - */ - private static final int UDP_BASE_TIMEOUT_SECONDS = 15; - - /** - * We don't try more than 8 times (3840 seconds, as per the formula defined - * for the backing-off timeout. - * - * @see #UDP_BASE_TIMEOUT_SECONDS - */ - private static final int UDP_MAX_TRIES = 8; - - /** - * For STOPPED announce event, we don't want to be bothered with waiting - * that long. We'll try once and bail-out early. - */ - private static final int UDP_MAX_TRIES_ON_STOPPED = 1; - - /** - * Maximum UDP packet size expected, in bytes. - * - * The biggest packet in the exchange is the announce response, which in 20 - * bytes + 6 bytes per peer. Common numWant is 50, so 20 + 6 * 50 = 320. - * With headroom, we'll ask for 512 bytes. - */ - private static final int UDP_PACKET_LENGTH = 512; - - private final InetSocketAddress address; - private final Random random; - - private DatagramSocket socket; - private Date connectionExpiration; - private long connectionId; - private int transactionId; - private boolean stop; - - private enum State { - CONNECT_REQUEST, - ANNOUNCE_REQUEST; - }; - - /** - * - * @param torrent - */ - protected UDPTrackerClient(SharedTorrent torrent, Peer peer, URI tracker) - throws UnknownHostException { - super(torrent, peer, tracker); - - /** - * The UDP announce request protocol only supports IPv4 - * - * @see http://bittorrent.org/beps/bep_0015.html#ipv6 - */ - if (! (InetAddress.getByName(peer.getIp()) instanceof Inet4Address)) { - throw new UnsupportedAddressTypeException(); - } - - this.address = new InetSocketAddress( - tracker.getHost(), - tracker.getPort()); - - this.socket = null; - this.random = new Random(); - this.connectionExpiration = null; - this.stop = false; - } - - @Override - public void announce(AnnounceRequestMessage.RequestEvent event, - boolean inhibitEvents) throws AnnounceException { - logger.info("Announcing{} to tracker with {}U/{}D/{}L bytes...", - new Object[] { - this.formatAnnounceEvent(event), - this.torrent.getUploaded(), - this.torrent.getDownloaded(), - this.torrent.getLeft() - }); - - State state = State.CONNECT_REQUEST; - int maxAttempts = AnnounceRequestMessage.RequestEvent - .STOPPED.equals(event) - ? UDP_MAX_TRIES_ON_STOPPED - : UDP_MAX_TRIES; - int attempts = -1; - - try { - this.socket = new DatagramSocket(); - this.socket.connect(this.address); - - while (++attempts <= maxAttempts) { - // Transaction ID is randomized for each exchange. - this.transactionId = this.random.nextInt(); - - // Immediately decide if we can send the announce request - // directly or not. For this, we need a valid, non-expired - // connection ID. - if (this.connectionExpiration != null) { - if (new Date().before(this.connectionExpiration)) { - state = State.ANNOUNCE_REQUEST; - } else { - logger.debug("Announce connection ID expired, " + - "reconnecting with tracker..."); - } - } - - switch (state) { - case CONNECT_REQUEST: - this.send(UDPConnectRequestMessage - .craft(this.transactionId).getData()); - - try { - this.handleTrackerConnectResponse( - UDPTrackerMessage.UDPTrackerResponseMessage - .parse(this.recv(attempts))); - attempts = -1; - } catch (SocketTimeoutException ste) { - // Silently ignore the timeout and retry with a - // longer timeout, unless announce stop was - // requested in which case we need to exit right - // away. - if (stop) { - return; - } - } - break; - - case ANNOUNCE_REQUEST: - this.send(this.buildAnnounceRequest(event).getData()); - - try { - this.handleTrackerAnnounceResponse( - UDPTrackerMessage.UDPTrackerResponseMessage - .parse(this.recv(attempts)), inhibitEvents); - // If we got here, we succesfully completed this - // announce exchange and can simply return to exit the - // loop. - return; - } catch (SocketTimeoutException ste) { - // Silently ignore the timeout and retry with a - // longer timeout, unless announce stop was - // requested in which case we need to exit right - // away. - if (stop) { - return; - } - } - break; - default: - throw new IllegalStateException("Invalid announce state!"); - } - } - - // When the maximum number of attempts was reached, the announce - // really timed-out. We'll try again in the next announce loop. - throw new AnnounceException("Timeout while announcing" + - this.formatAnnounceEvent(event) + " to tracker!"); - } catch (IOException ioe) { - throw new AnnounceException("Error while announcing" + - this.formatAnnounceEvent(event) + - " to tracker: " + ioe.getMessage(), ioe); - } catch (MessageValidationException mve) { - throw new AnnounceException("Tracker message violates expected " + - "protocol (" + mve.getMessage() + ")", mve); - } - } - - /** - * Handles the tracker announce response message. - * - *

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

- * - * @param message The message received from the tracker in response to the - * announce request. - */ - @Override - protected void handleTrackerAnnounceResponse(TrackerMessage message, - boolean inhibitEvents) throws AnnounceException { - this.validateTrackerResponse(message); - super.handleTrackerAnnounceResponse(message, inhibitEvents); - } - - /** - * Close this announce connection. - */ - @Override - protected void close() { - this.stop = true; - - // Close the socket to force blocking operations to return. - if (this.socket != null && !this.socket.isClosed()) { - this.socket.close(); - } - } - - private UDPAnnounceRequestMessage buildAnnounceRequest( - AnnounceRequestMessage.RequestEvent event) { - return UDPAnnounceRequestMessage.craft( - this.connectionId, - transactionId, - this.torrent.getInfoHash(), - this.peer.getPeerId().array(), - this.torrent.getDownloaded(), - this.torrent.getUploaded(), - this.torrent.getLeft(), - event, - this.peer.getAddress(), - 0, - TrackerMessage.AnnounceRequestMessage.DEFAULT_NUM_WANT, - this.peer.getPort()); - } - - /** - * Validates an incoming tracker message. - * - *

- * Verifies that the message is not an error message (throws an exception - * with the error message if it is) and that the transaction ID matches the - * current one. - *

- * - * @param message The incoming tracker message. - */ - private void validateTrackerResponse(TrackerMessage message) - throws AnnounceException { - if (message instanceof ErrorMessage) { - throw new AnnounceException(((ErrorMessage)message).getReason()); - } - - if (message instanceof UDPTrackerMessage && - (((UDPTrackerMessage)message).getTransactionId() != this.transactionId)) { - throw new AnnounceException("Invalid transaction ID!"); - } - } - - /** - * Handles the tracker connect response message. - * - * @param message The message received from the tracker in response to the - * connection request. - */ - private void handleTrackerConnectResponse(TrackerMessage message) - throws AnnounceException { - this.validateTrackerResponse(message); - - if (! (message instanceof ConnectionResponseMessage)) { - throw new AnnounceException("Unexpected tracker message type " + - message.getType().name() + "!"); - } - - UDPConnectResponseMessage connectResponse = - (UDPConnectResponseMessage)message; - - this.connectionId = connectResponse.getConnectionId(); - Calendar now = Calendar.getInstance(); - now.add(Calendar.MINUTE, 1); - this.connectionExpiration = now.getTime(); - } - - /** - * Send a UDP packet to the tracker. - * - * @param data The {@link ByteBuffer} to send in a datagram packet to the - * tracker. - */ - private void send(ByteBuffer data) { - try { - this.socket.send(new DatagramPacket( - data.array(), - data.capacity(), - this.address)); - } catch (IOException ioe) { - logger.warn("Error sending datagram packet to tracker at {}: {}.", - this.address, ioe.getMessage()); - } - } - - /** - * Receive a UDP packet from the tracker. - * - * @param attempt The attempt number, used to calculate the timeout for the - * receive operation. - * @retun Returns a {@link ByteBuffer} containing the packet data. - */ - private ByteBuffer recv(int attempt) - throws IOException, SocketException, SocketTimeoutException { - int timeout = UDP_BASE_TIMEOUT_SECONDS * (int)Math.pow(2, attempt); - logger.trace("Setting receive timeout to {}s for attempt {}...", - timeout, attempt); - this.socket.setSoTimeout(timeout * 1000); - - try { - DatagramPacket p = new DatagramPacket( - new byte[UDP_PACKET_LENGTH], - UDP_PACKET_LENGTH); - this.socket.receive(p); - return ByteBuffer.wrap(p.getData(), 0, p.getLength()); - } catch (SocketTimeoutException ste) { - throw ste; - } - } -} diff --git a/src/main/java/com/turn/ttorrent/client/peer/PeerActivityListener.java b/src/main/java/com/turn/ttorrent/client/peer/PeerActivityListener.java deleted file mode 100644 index a9e5b85df..000000000 --- a/src/main/java/com/turn/ttorrent/client/peer/PeerActivityListener.java +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client.peer; - -import com.turn.ttorrent.client.Piece; - -import java.io.IOException; - -import java.util.BitSet; -import java.util.EventListener; - - -/** - * EventListener interface for objects that want to handle peer activity - * events like piece availability, or piece completion events, and more. - * - * @author mpetazzoni - */ -public interface PeerActivityListener extends EventListener { - - /** - * Peer choked handler. - * - *

- * This handler is fired when a peer choked and now refuses to send data to - * us. This means we should not try to request or expect anything from it - * until it becomes ready again. - *

- * - * @param peer The peer that choked. - */ - public void handlePeerChoked(SharingPeer peer); - - /** - * Peer ready handler. - * - *

- * This handler is fired when a peer notified that it is no longer choked. - * This means we can send piece block requests to it and start downloading. - *

- * - * @param peer The peer that became ready. - */ - public void handlePeerReady(SharingPeer peer); - - /** - * Piece availability handler. - * - *

- * This handler is fired when an update in piece availability is received - * from a peer's HAVE message. - *

- * - * @param peer The peer we got the update from. - * @param piece The piece that became available from this peer. - */ - public void handlePieceAvailability(SharingPeer peer, Piece piece); - - /** - * Bit field availability handler. - * - *

- * This handler is fired when an update in piece availability is received - * from a peer's BITFIELD message. - *

- * - * @param peer The peer we got the update from. - * @param availablePieces The pieces availability bit field of the peer. - */ - public void handleBitfieldAvailability(SharingPeer peer, - BitSet availablePieces); - - /** - * Piece upload completion handler. - * - *

- * This handler is fired when a piece has been uploaded entirely to a peer. - *

- * - * @param peer The peer the piece was sent to. - * @param piece The piece in question. - */ - public void handlePieceSent(SharingPeer peer, Piece piece); - - /** - * Piece download completion handler. - * - *

- * This handler is fired when a piece has been downloaded entirely and the - * piece data has been revalidated. - *

- * - *

- * Note: the piece may not be valid after it has been - * downloaded, in which case appropriate action should be taken to - * redownload the piece. - *

- * - * @param peer The peer we got this piece from. - * @param piece The piece in question. - */ - public void handlePieceCompleted(SharingPeer peer, Piece piece) - throws IOException; - - /** - * Peer disconnection handler. - * - *

- * This handler is fired when a peer disconnects, or is disconnected due to - * protocol violation. - *

- * - * @param peer The peer we got this piece from. - */ - public void handlePeerDisconnected(SharingPeer peer); - - /** - * Handler for IOException during peer operation. - * - * @param peer The peer whose activity trigger the exception. - * @param ioe The IOException object, for reporting. - */ - public void handleIOException(SharingPeer peer, IOException ioe); -} diff --git a/src/main/java/com/turn/ttorrent/client/peer/PeerExchange.java b/src/main/java/com/turn/ttorrent/client/peer/PeerExchange.java deleted file mode 100644 index a7448d00c..000000000 --- a/src/main/java/com/turn/ttorrent/client/peer/PeerExchange.java +++ /dev/null @@ -1,325 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client.peer; - -import com.turn.ttorrent.client.SharedTorrent; -import com.turn.ttorrent.common.protocol.PeerMessage; - -import java.io.EOFException; -import java.io.IOException; -import java.lang.InterruptedException; -import java.net.SocketException; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; -import java.text.ParseException; -import java.util.BitSet; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * Incoming and outgoing peer communication system. - * - *

- * The peer exchange is a wrapper around peer communication. It provides both - * incoming and outgoing communication channels to a connected peer after a - * successful handshake. - *

- * - *

- * When a socket is bound to a sharing peer, a PeerExchange is automatically - * created to wrap this socket into a more usable system for communication with - * the remote peer. - *

- * - *

- * For incoming messages, the peer exchange provides message parsing and calls - * the handleMessage() method of the peer for each successfully - * parsed message. - *

- * - *

- * For outgoing message, the peer exchange offers a send() message - * that queues messages, and takes care of automatically sending a keep-alive - * message to the remote peer every two minutes when other message have been - * sent in that period of time, as recommended by the BitTorrent protocol - * specification. - *

- * - * @author mpetazzoni - */ -class PeerExchange { - - private static final Logger logger = - LoggerFactory.getLogger(PeerExchange.class); - - private static final int KEEP_ALIVE_IDLE_MINUTES = 2; - - private SharingPeer peer; - private SharedTorrent torrent; - private SocketChannel channel; - - private Set listeners; - - private IncomingThread in; - private OutgoingThread out; - private BlockingQueue sendQueue; - private volatile boolean stop; - - /** - * Initialize and start a new peer exchange. - * - * @param peer The remote peer to communicate with. - * @param torrent The torrent we're exchanging on with the peer. - * @param channel A channel on the connected socket to the peer. - */ - public PeerExchange(SharingPeer peer, SharedTorrent torrent, - SocketChannel channel) throws SocketException { - this.peer = peer; - this.torrent = torrent; - this.channel = channel; - - this.listeners = new HashSet(); - this.sendQueue = new LinkedBlockingQueue(); - - if (!this.peer.hasPeerId()) { - throw new IllegalStateException("Peer does not have a " + - "peer ID. Was the handshake made properly?"); - } - - this.in = new IncomingThread(); - this.in.setName("bt-peer(" + - this.peer.getShortHexPeerId() + ")-recv"); - - this.out = new OutgoingThread(); - this.out.setName("bt-peer(" + - this.peer.getShortHexPeerId() + ")-send"); - this.out.setDaemon(true); - - // Automatically start the exchange activity loops - this.stop = false; - this.in.start(); - this.out.start(); - - logger.debug("Started peer exchange with {} for {}.", - this.peer, this.torrent); - - // If we have pieces, start by sending a BITFIELD message to the peer. - BitSet pieces = this.torrent.getCompletedPieces(); - if (pieces.cardinality() > 0) { - this.send(PeerMessage.BitfieldMessage.craft(pieces)); - } - } - - /** - * Register a new message listener to receive messages. - * - * @param listener The message listener object. - */ - public void register(MessageListener listener) { - this.listeners.add(listener); - } - - /** - * Tells if the peer exchange is active. - */ - public boolean isConnected() { - return this.channel.isConnected(); - } - - /** - * Send a message to the connected peer. - * - *

- * The message is queued in the outgoing message queue and will be - * processed as soon as possible. - *

- * - * @param message The message object to send. - */ - public void send(PeerMessage message) { - try { - this.sendQueue.put(message); - } catch (InterruptedException ie) { - // Ignore, our send queue will only block if it contains - // MAX_INTEGER messages, in which case we're already in big - // trouble, and we'd have to be interrupted, too. - } - } - - /** - * Close and stop the peer exchange. - * - *

- * Closes the socket channel and stops both incoming and outgoing threads. - *

- */ - public void close() { - this.stop = true; - - if (this.channel.isConnected()) { - try { - this.channel.close(); - } catch (IOException ioe) { - // Ignore - } - } - - logger.debug("Peer exchange with {} closed.", this.peer); - } - - /** - * Incoming messages thread. - * - *

- * The incoming messages thread reads from the socket's input stream and - * waits for incoming messages. When a message is fully retrieve, it is - * parsed and passed to the peer's handleMessage() method that - * will act based on the message type. - *

- * - * @author mpetazzoni - */ - private class IncomingThread extends Thread { - - @Override - public void run() { - ByteBuffer buffer = ByteBuffer.allocateDirect(1*1024*1024); - - try { - while (!stop) { - 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()) { - 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); - - while (!stop && buffer.hasRemaining()) { - if (channel.read(buffer) < 0) { - throw new EOFException( - "Reached end-of-stream while reading message"); - } - } - - buffer.rewind(); - - try { - PeerMessage message = PeerMessage.parse(buffer, torrent); - logger.trace("Received {} from {}", message, peer); - - for (MessageListener listener : listeners) { - listener.handleMessage(message); - } - } catch (ParseException pe) { - logger.warn("{}", pe.getMessage()); - } - } - } catch (IOException ioe) { - logger.debug("Could not read message from {}: {}", - peer, - ioe.getMessage() != null - ? ioe.getMessage() - : ioe.getClass().getName()); - peer.unbind(true); - } - } - } - - /** - * Outgoing messages thread. - * - *

- * The outgoing messages thread waits for messages to appear in the send - * queue and processes them, in order, as soon as they arrive. - *

- * - *

- * If no message is available for KEEP_ALIVE_IDLE_MINUTES minutes, it will - * automatically send a keep-alive message to the remote peer to keep the - * connection active. - *

- * - * @author mpetazzoni - */ - private class OutgoingThread extends Thread { - - @Override - public void run() { - try { - // Loop until told to stop. When stop was requested, loop until - // the queue is served. - while (!stop || (stop && sendQueue.size() > 0)) { - try { - // Wait for two minutes for a message to send - PeerMessage message = sendQueue.poll( - PeerExchange.KEEP_ALIVE_IDLE_MINUTES, - TimeUnit.MINUTES); - - if (message == null) { - if (stop) { - return; - } - - message = PeerMessage.KeepAliveMessage.craft(); - } - - logger.trace("Sending {} to {}", message, peer); - - ByteBuffer data = message.getData(); - while (!stop && data.hasRemaining()) { - if (channel.write(data) < 0) { - throw new EOFException( - "Reached end of stream while writing"); - } - } - } catch (InterruptedException ie) { - // Ignore and potentially terminate - } - } - } catch (IOException ioe) { - logger.debug("Could not send message to {}: {}", - peer, - ioe.getMessage() != null - ? ioe.getMessage() - : ioe.getClass().getName()); - peer.unbind(true); - } - } - } -} diff --git a/src/main/java/com/turn/ttorrent/client/peer/Rate.java b/src/main/java/com/turn/ttorrent/client/peer/Rate.java deleted file mode 100644 index 8a7911bca..000000000 --- a/src/main/java/com/turn/ttorrent/client/peer/Rate.java +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ - -package com.turn.ttorrent.client.peer; - -import java.io.Serializable; -import java.util.Comparator; - - -/** - * A data exchange rate representation. - * - *

- * This is a utility class to keep track, and compare, of the data exchange - * rate (either download or upload) with a peer. - *

- * - * @author mpetazzoni - */ -public class Rate implements Comparable { - - public static final Comparator RATE_COMPARATOR = - new RateComparator(); - - private long bytes = 0; - private long reset = 0; - private long last = 0; - - /** - * Add a byte count to the current measurement. - * - * @param count The number of bytes exchanged since the last reset. - */ - public synchronized void add(long count) { - this.bytes += count; - if (this.reset == 0) { - this.reset = System.currentTimeMillis(); - } - this.last = System.currentTimeMillis(); - } - - /** - * Get the current rate. - * - *

- * The exchange rate is the number of bytes exchanged since the last - * reset and the last input. - *

- */ - public synchronized float get() { - if (this.last - this.reset == 0) { - return 0; - } - - return this.bytes / ((this.last - this.reset) / 1000.0f); - } - - /** - * Reset the measurement. - */ - public synchronized void reset() { - this.bytes = 0; - this.reset = System.currentTimeMillis(); - this.last = this.reset; - } - - @Override - public int compareTo(Rate other) { - return RATE_COMPARATOR.compare(this, other); - } - - /** - * A rate comparator. - * - *

- * This class provides a comparator to sort peers by an exchange rate, - * comparing two rates and returning an ascending ordering. - *

- * - *

- * Note: we need to make sure here that we don't return 0, which - * would provide an ordering that is inconsistent with - * equals()'s behavior, and result in unpredictable behavior - * for sorted collections using this comparator. - *

- * - * @author mpetazzoni - */ - private static class RateComparator - implements Comparator, Serializable { - - private static final long serialVersionUID = 72460233003600L; - - /** - * Compare two rates together. - * - *

- * This method compares float, but we don't care too much about - * rounding errors. It's just to order peers so super-strict rate based - * order is not required. - *

- * - * @param a - * @param b - */ - @Override - public int compare(Rate a, Rate b) { - if (a.get() > b.get()) { - return 1; - } - - return -1; - } - } -} diff --git a/src/main/java/com/turn/ttorrent/client/peer/SharingPeer.java b/src/main/java/com/turn/ttorrent/client/peer/SharingPeer.java deleted file mode 100644 index c24244ec6..000000000 --- a/src/main/java/com/turn/ttorrent/client/peer/SharingPeer.java +++ /dev/null @@ -1,797 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client.peer; - -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.protocol.PeerMessage; -import com.turn.ttorrent.client.Piece; -import com.turn.ttorrent.client.SharedTorrent; - -import java.io.IOException; -import java.io.Serializable; -import java.net.SocketException; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; -import java.util.BitSet; -import java.util.Comparator; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * A peer exchanging on a torrent with the BitTorrent client. - * - *

- * A SharingPeer extends the base Peer class with all the data and logic needed - * by the BitTorrent client to interact with a peer exchanging on the same - * torrent. - *

- * - *

- * Peers are defined by their peer ID, IP address and port number, just like - * base peers. Peers we exchange with also contain four crucial attributes: - *

- * - *
    - *
  • choking, which means we are choking this peer and we're - * not willing to send him anything for now;
  • - *
  • interesting, which means we are interested in a piece - * this peer has;
  • - *
  • choked, if this peer is choking and won't send us - * anything right now;
  • - *
  • interested, if this peer is interested in something we - * have.
  • - *
- * - *

- * Peers start choked and uninterested. - *

- * - * @author mpetazzoni - */ -public class SharingPeer extends Peer implements MessageListener { - - private static final Logger logger = - LoggerFactory.getLogger(SharingPeer.class); - - private static final int MAX_PIPELINED_REQUESTS = 5; - - private boolean choking; - private boolean interesting; - - private boolean choked; - private boolean interested; - - private SharedTorrent torrent; - private BitSet availablePieces; - - private Piece requestedPiece; - private int lastRequestedOffset; - - private BlockingQueue requests; - private volatile boolean downloading; - - private PeerExchange exchange; - private Rate download; - private Rate upload; - - private Set listeners; - - private Object requestsLock, exchangeLock; - - /** - * Create a new sharing peer on a given torrent. - * - * @param ip The peer's IP address. - * @param port The peer's port. - * @param peerId The byte-encoded peer ID. - * @param torrent The torrent this peer exchanges with us on. - */ - public SharingPeer(String ip, int port, ByteBuffer peerId, - SharedTorrent torrent) { - super(ip, port, peerId); - - this.torrent = torrent; - this.listeners = new HashSet(); - this.availablePieces = new BitSet(this.torrent.getPieceCount()); - - this.requestsLock = new Object(); - this.exchangeLock = new Object(); - - this.reset(); - this.requestedPiece = null; - } - - /** - * Register a new peer activity listener. - * - * @param listener The activity listener that wants to receive events from - * this peer's activity. - */ - public void register(PeerActivityListener listener) { - this.listeners.add(listener); - } - - public Rate getDLRate() { - return this.download; - } - - public Rate getULRate() { - return this.upload; - } - - /** - * Reset the peer state. - * - *

- * Initially, peers are considered choked, choking, and neither interested - * nor interesting. - *

- */ - public synchronized void reset() { - this.choking = true; - this.interesting = false; - this.choked = true; - this.interested = false; - - this.exchange = null; - - this.requests = null; - this.lastRequestedOffset = 0; - this.downloading = false; - } - - /** - * Choke this peer. - * - *

- * We don't want to upload to this peer anymore, so mark that we're choking - * from this peer. - *

- */ - public void choke() { - if (!this.choking) { - logger.trace("Choking {}", this); - this.send(PeerMessage.ChokeMessage.craft()); - this.choking = true; - } - } - - /** - * Unchoke this peer. - * - *

- * Mark that we are no longer choking from this peer and can resume - * uploading to it. - *

- */ - public void unchoke() { - if (this.choking) { - logger.trace("Unchoking {}", this); - this.send(PeerMessage.UnchokeMessage.craft()); - this.choking = false; - } - } - - public boolean isChoking() { - return this.choking; - } - - - public void interesting() { - if (!this.interesting) { - logger.trace("Telling {} we're interested.", this); - this.send(PeerMessage.InterestedMessage.craft()); - this.interesting = true; - } - } - - public void notInteresting() { - if (this.interesting) { - logger.trace("Telling {} we're no longer interested.", this); - this.send(PeerMessage.NotInterestedMessage.craft()); - this.interesting = false; - } - } - - public boolean isInteresting() { - return this.interesting; - } - - - public boolean isChoked() { - return this.choked; - } - - public boolean isInterested() { - return this.interested; - } - - /** - * Returns the available pieces from this peer. - * - * @return A clone of the available pieces bit field from this peer. - */ - public BitSet getAvailablePieces() { - synchronized (this.availablePieces) { - return (BitSet)this.availablePieces.clone(); - } - } - - /** - * Returns the currently requested piece, if any. - */ - public Piece getRequestedPiece() { - return this.requestedPiece; - } - - /** - * Tells whether this peer is a seed. - * - * @return Returns true if the peer has all of the torrent's pieces - * available. - */ - public synchronized boolean isSeed() { - return this.torrent.getPieceCount() > 0 && - this.getAvailablePieces().cardinality() == - this.torrent.getPieceCount(); - } - - /** - * Bind a connected socket to this peer. - * - *

- * This will create a new peer exchange with this peer using the given - * socket, and register the peer as a message listener. - *

- * - * @param channel The connected socket channel for this peer. - */ - public synchronized void bind(SocketChannel channel) throws SocketException { - this.unbind(true); - - this.exchange = new PeerExchange(this, this.torrent, channel); - this.exchange.register(this); - - this.download = new Rate(); - this.download.reset(); - - this.upload = new Rate(); - this.upload.reset(); - } - - /** - * Tells whether this peer as an active connection through a peer exchange. - */ - public boolean isConnected() { - synchronized (this.exchangeLock) { - return this.exchange != null && this.exchange.isConnected(); - } - } - - /** - * Unbind and disconnect this peer. - * - *

- * This terminates the eventually present and/or connected peer exchange - * with the peer and fires the peer disconnected event to any peer activity - * listeners registered on this peer. - *

- * - * @param force Force unbind without sending cancel requests. - */ - public void unbind(boolean force) { - if (!force) { - // Cancel all outgoing requests, and send a NOT_INTERESTED message to - // the peer. - this.cancelPendingRequests(); - this.send(PeerMessage.NotInterestedMessage.craft()); - } - - synchronized (this.exchangeLock) { - if (this.exchange != null) { - this.exchange.close(); - this.exchange = null; - } - } - - this.firePeerDisconnected(); - this.requestedPiece = null; - } - - /** - * Send a message to the peer. - * - *

- * Delivery of the message can only happen if the peer is connected. - *

- * - * @param message The message to send to the remote peer through our peer - * exchange. - */ - public void send(PeerMessage message) throws IllegalStateException { - if (this.isConnected()) { - this.exchange.send(message); - } else { - logger.warn("Attempting to send a message to non-connected peer {}!", this); - } - } - - /** - * Download the given piece from this peer. - * - *

- * Starts a block request queue and pre-fill it with MAX_PIPELINED_REQUESTS - * block requests. - *

- * - *

- * Further requests will be added, one by one, every time a block is - * returned. - *

- * - * @param piece The piece chosen to be downloaded from this peer. - */ - public synchronized void downloadPiece(Piece piece) - throws IllegalStateException { - if (this.isDownloading()) { - IllegalStateException up = new IllegalStateException( - "Trying to download a piece while previous " + - "download not completed!"); - logger.warn("What's going on? {}", up.getMessage(), up); - throw up; // ah ah. - } - - this.requests = new LinkedBlockingQueue( - SharingPeer.MAX_PIPELINED_REQUESTS); - this.requestedPiece = piece; - this.lastRequestedOffset = 0; - this.requestNextBlocks(); - } - - public boolean isDownloading() { - return this.downloading; - } - - /** - * Request some more blocks from this peer. - * - *

- * Re-fill the pipeline to get download the next blocks from the peer. - *

- */ - private void requestNextBlocks() { - synchronized (this.requestsLock) { - if (this.requests == null || this.requestedPiece == null) { - // If we've been taken out of a piece download context it means our - // outgoing requests have been cancelled. Don't enqueue new - // requests until a proper piece download context is - // re-established. - return; - } - - while (this.requests.remainingCapacity() > 0 && - this.lastRequestedOffset < this.requestedPiece.size()) { - PeerMessage.RequestMessage request = PeerMessage.RequestMessage - .craft( - this.requestedPiece.getIndex(), - this.lastRequestedOffset, - Math.min( - (int)(this.requestedPiece.size() - - this.lastRequestedOffset), - PeerMessage.RequestMessage.DEFAULT_REQUEST_SIZE)); - this.requests.add(request); - this.send(request); - this.lastRequestedOffset += request.getLength(); - } - - this.downloading = this.requests.size() > 0; - } - } - - /** - * Remove the REQUEST message from the request pipeline matching this - * PIECE message. - * - *

- * Upon reception of a piece block with a PIECE message, remove the - * corresponding request from the pipeline to make room for the next block - * requests. - *

- * - * @param message The PIECE message received. - */ - private void removeBlockRequest(PeerMessage.PieceMessage message) { - synchronized (this.requestsLock) { - if (this.requests == null) { - return; - } - - for (PeerMessage.RequestMessage request : this.requests) { - if (request.getPiece() == message.getPiece() && - request.getOffset() == message.getOffset()) { - this.requests.remove(request); - break; - } - } - - this.downloading = this.requests.size() > 0; - } - } - - /** - * Cancel all pending requests. - * - *

- * This queues CANCEL messages for all the requests in the queue, and - * returns the list of requests that were in the queue. - *

- * - *

- * If no request queue existed, or if it was empty, an empty set of request - * messages is returned. - *

- */ - public Set cancelPendingRequests() { - synchronized (this.requestsLock) { - Set requests = - new HashSet(); - - if (this.requests != null) { - for (PeerMessage.RequestMessage request : this.requests) { - this.send(PeerMessage.CancelMessage.craft(request.getPiece(), - request.getOffset(), request.getLength())); - requests.add(request); - } - } - - this.requests = null; - this.downloading = false; - return requests; - } - } - - /** - * Handle an incoming message from this peer. - * - * @param msg The incoming, parsed message. - */ - @Override - public synchronized void handleMessage(PeerMessage msg) { - switch (msg.getType()) { - case KEEP_ALIVE: - // Nothing to do, we're keeping the connection open anyways. - break; - case CHOKE: - this.choked = true; - this.firePeerChoked(); - this.cancelPendingRequests(); - break; - case UNCHOKE: - this.choked = false; - logger.trace("Peer {} is now accepting requests.", this); - this.firePeerReady(); - break; - case INTERESTED: - this.interested = true; - break; - case NOT_INTERESTED: - this.interested = false; - break; - case HAVE: - // Record this peer has the given piece - PeerMessage.HaveMessage have = (PeerMessage.HaveMessage)msg; - Piece havePiece = this.torrent.getPiece(have.getPieceIndex()); - - synchronized (this.availablePieces) { - this.availablePieces.set(havePiece.getIndex()); - logger.trace("Peer {} now has {} [{}/{}].", - new Object[] { - this, - havePiece, - this.availablePieces.cardinality(), - this.torrent.getPieceCount() - }); - } - - this.firePieceAvailabity(havePiece); - break; - case BITFIELD: - // Augment the hasPiece bit field from this BITFIELD message - PeerMessage.BitfieldMessage bitfield = - (PeerMessage.BitfieldMessage)msg; - - synchronized (this.availablePieces) { - this.availablePieces.or(bitfield.getBitfield()); - logger.trace("Recorded bitfield from {} with {} " + - "pieces(s) [{}/{}].", - new Object[] { - this, - bitfield.getBitfield().cardinality(), - this.availablePieces.cardinality(), - this.torrent.getPieceCount() - }); - } - - this.fireBitfieldAvailabity(); - break; - case REQUEST: - PeerMessage.RequestMessage request = - (PeerMessage.RequestMessage)msg; - Piece rp = this.torrent.getPiece(request.getPiece()); - - // If we are choking from this peer and it still sends us - // requests, it is a violation of the BitTorrent protocol. - // Similarly, if the peer requests a piece we don't have, it - // is a violation of the BitTorrent protocol. In these - // situation, terminate the connection. - if (this.isChoking() || !rp.isValid()) { - logger.warn("Peer {} violated protocol, " + - "terminating exchange.", this); - this.unbind(true); - break; - } - - if (request.getLength() > - PeerMessage.RequestMessage.MAX_REQUEST_SIZE) { - logger.warn("Peer {} requested a block too big, " + - "terminating exchange.", this); - this.unbind(true); - break; - } - - // At this point we agree to send the requested piece block to - // the remote peer, so let's queue a message with that block - try { - ByteBuffer block = rp.read(request.getOffset(), - request.getLength()); - this.send(PeerMessage.PieceMessage.craft(request.getPiece(), - request.getOffset(), block)); - this.upload.add(block.capacity()); - - if (request.getOffset() + request.getLength() == rp.size()) { - this.firePieceSent(rp); - } - } catch (IOException ioe) { - this.fireIOException(new IOException( - "Error while sending piece block request!", ioe)); - } - - break; - case PIECE: - // Record the incoming piece block. - - // Should we keep track of the requested pieces and act when we - // get a piece we didn't ask for, or should we just stay - // greedy? - PeerMessage.PieceMessage piece = (PeerMessage.PieceMessage)msg; - Piece p = this.torrent.getPiece(piece.getPiece()); - - // Remove the corresponding request from the request queue to - // make room for next block requests. - this.removeBlockRequest(piece); - this.download.add(piece.getBlock().capacity()); - - try { - synchronized (p) { - if (p.isValid()) { - this.requestedPiece = null; - this.cancelPendingRequests(); - this.firePeerReady(); - logger.debug("Discarding block for already completed " + p); - break; - } - - p.record(piece.getBlock(), piece.getOffset()); - - // If the block offset equals the piece size and the block - // length is 0, it means the piece has been entirely - // downloaded. In this case, we have nothing to save, but - // we should validate the piece. - if (piece.getOffset() + piece.getBlock().capacity() - == p.size()) { - p.validate(); - this.firePieceCompleted(p); - this.requestedPiece = null; - this.firePeerReady(); - } else { - this.requestNextBlocks(); - } - } - } catch (IOException ioe) { - this.fireIOException(new IOException( - "Error while storing received piece block!", ioe)); - break; - } - break; - case CANCEL: - // No need to support - break; - } - } - - /** - * Fire the peer choked event to all registered listeners. - * - *

- * The event contains the peer that chocked. - *

- */ - private void firePeerChoked() { - for (PeerActivityListener listener : this.listeners) { - listener.handlePeerChoked(this); - } - } - - /** - * Fire the peer ready event to all registered listeners. - * - *

- * The event contains the peer that unchoked or became ready. - *

- */ - private void firePeerReady() { - for (PeerActivityListener listener : this.listeners) { - listener.handlePeerReady(this); - } - } - - /** - * Fire the piece availability event to all registered listeners. - * - *

- * The event contains the peer (this), and the piece that became available. - *

- */ - private void firePieceAvailabity(Piece piece) { - for (PeerActivityListener listener : this.listeners) { - listener.handlePieceAvailability(this, piece); - } - } - - /** - * Fire the bit field availability event to all registered listeners. - * - * The event contains the peer (this), and the bit field of available pieces - * from this peer. - */ - private void fireBitfieldAvailabity() { - for (PeerActivityListener listener : this.listeners) { - listener.handleBitfieldAvailability(this, - this.getAvailablePieces()); - } - } - - /** - * Fire the piece sent event to all registered listeners. - * - *

- * The event contains the peer (this), and the piece number that was - * sent to the peer. - *

- * - * @param piece The completed piece. - */ - private void firePieceSent(Piece piece) { - for (PeerActivityListener listener : this.listeners) { - listener.handlePieceSent(this, piece); - } - } - - /** - * Fire the piece completion event to all registered listeners. - * - *

- * The event contains the peer (this), and the piece number that was - * completed. - *

- * - * @param piece The completed piece. - */ - private void firePieceCompleted(Piece piece) throws IOException { - for (PeerActivityListener listener : this.listeners) { - listener.handlePieceCompleted(this, piece); - } - } - - /** - * Fire the peer disconnected event to all registered listeners. - * - *

- * The event contains the peer that disconnected (this). - *

- */ - private void firePeerDisconnected() { - for (PeerActivityListener listener : this.listeners) { - listener.handlePeerDisconnected(this); - } - } - - /** - * Fire the IOException event to all registered listeners. - * - *

- * The event contains the peer that triggered the problem, and the - * exception object. - *

- */ - private void fireIOException(IOException ioe) { - for (PeerActivityListener listener : this.listeners) { - listener.handleIOException(this, ioe); - } - } - - /** - * Download rate comparator. - * - *

- * Compares sharing peers based on their current download rate. - *

- * - * @author mpetazzoni - * @see Rate.RateComparator - */ - public static class DLRateComparator - implements Comparator, Serializable { - - private static final long serialVersionUID = 96307229964730L; - - @Override - public int compare(SharingPeer a, SharingPeer b) { - return Rate.RATE_COMPARATOR.compare(a.getDLRate(), b.getDLRate()); - } - } - - /** - * Upload rate comparator. - * - *

- * Compares sharing peers based on their current upload rate. - *

- * - * @author mpetazzoni - * @see Rate.RateComparator - */ - public static class ULRateComparator - implements Comparator, Serializable { - - private static final long serialVersionUID = 38794949747717L; - - @Override - public int compare(SharingPeer a, SharingPeer b) { - return Rate.RATE_COMPARATOR.compare(a.getULRate(), b.getULRate()); - } - } - - public String toString() { - return new StringBuilder(super.toString()) - .append(" [") - .append((this.choked ? "C" : "c")) - .append((this.interested ? "I" : "i")) - .append("|") - .append((this.choking ? "C" : "c")) - .append((this.interesting ? "I" : "i")) - .append("|") - .append(this.availablePieces.cardinality()) - .append("]") - .toString(); - } -} diff --git a/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java b/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java deleted file mode 100644 index c753132a6..000000000 --- a/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java +++ /dev/null @@ -1,209 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client.storage; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * Multi-file torrent byte storage. - * - *

- * This implementation of the torrent byte storage provides support for - * multi-file torrents and completely abstracts the read/write operations from - * the notion of different files. The byte storage is represented as one - * continuous byte storage, directly accessible by offset regardless of which - * file this offset lands. - *

- * - * @author mpetazzoni - * @author dgiffin - */ -public class FileCollectionStorage implements TorrentByteStorage { - - private static final Logger logger = - LoggerFactory.getLogger(FileCollectionStorage.class); - - private final List files; - private final long size; - - /** - * Initialize a new multi-file torrent byte storage. - * - * @param files The list of individual {@link FileStorage} - * objects making up the torrent. - * @param size The total size of the torrent data, in bytes. - */ - public FileCollectionStorage(List files, - long size) { - this.files = files; - this.size = size; - - logger.info("Initialized torrent byte storage on {} file(s) " + - "({} total byte(s)).", files.size(), size); - } - - @Override - public long size() { - return this.size; - } - - @Override - public int read(ByteBuffer buffer, long offset) throws IOException { - int requested = buffer.remaining(); - int bytes = 0; - - for (FileOffset fo : this.select(offset, requested)) { - // TODO: remove cast to int when large ByteBuffer support is - // implemented in Java. - buffer.limit((int)(bytes + fo.length)); - bytes += fo.file.read(buffer, fo.offset); - } - - if (bytes < requested) { - throw new IOException("Storage collection read underrun!"); - } - - return bytes; - } - - @Override - public int write(ByteBuffer buffer, long offset) throws IOException { - int requested = buffer.remaining(); - - int bytes = 0; - - for (FileOffset fo : this.select(offset, requested)) { - buffer.limit(bytes + (int)fo.length); - bytes += fo.file.write(buffer, fo.offset); - } - - if (bytes < requested) { - throw new IOException("Storage collection write underrun!"); - } - - return bytes; - } - - @Override - public void close() throws IOException { - for (FileStorage file : this.files) { - file.close(); - } - } - - @Override - public void finish() throws IOException { - for (FileStorage file : this.files) { - file.finish(); - } - } - - @Override - public boolean isFinished() { - for (FileStorage file : this.files) { - if (!file.isFinished()) { - return false; - } - } - - return true; - } - - /** - * File operation details holder. - * - *

- * This simple inner class holds the details for a read or write operation - * on one of the underlying {@link FileStorage}s. - *

- * - * @author dgiffin - * @author mpetazzoni - */ - private static class FileOffset { - - public final FileStorage file; - public final long offset; - public final long length; - - FileOffset(FileStorage file, long offset, long length) { - this.file = file; - this.offset = offset; - this.length = length; - } - }; - - /** - * Select the group of files impacted by an operation. - * - *

- * This function selects which files are impacted by a read or write - * operation, with their respective relative offset and chunk length. - *

- * - * @param offset The offset of the operation, in bytes, relative to the - * complete byte storage. - * @param length The number of bytes to read or write. - * @return A list of {@link FileOffset} objects representing the {@link - * FileStorage}s impacted by the operation, bundled with their - * respective relative offset and number of bytes to read or write. - * @throws IllegalArgumentException If the offset and length go over the - * byte storage size. - * @throws IllegalStateException If the files registered with this byte - * storage can't accommodate the request (should not happen, really). - */ - private List select(long offset, long length) { - if (offset + length > this.size) { - throw new IllegalArgumentException("Buffer overrun (" + - offset + " + " + length + " > " + this.size + ") !"); - } - - List selected = new LinkedList(); - long bytes = 0; - - for (FileStorage file : this.files) { - if (file.offset() > offset + length) { - break; - } - - if (file.offset() + file.size() < offset) { - continue; - } - - long position = offset - file.offset(); - position = position > 0 ? position : 0; - long size = Math.min( - file.size() - position, - length - bytes); - selected.add(new FileOffset(file, position, size)); - bytes += size; - } - - if (selected.size() == 0 || bytes < length) { - throw new IllegalStateException("Buffer underrun (only got " + - bytes + " out of " + length + " byte(s) requested)!"); - } - - return selected; - } -} diff --git a/src/main/java/com/turn/ttorrent/client/storage/FileStorage.java b/src/main/java/com/turn/ttorrent/client/storage/FileStorage.java deleted file mode 100644 index a47f053cb..000000000 --- a/src/main/java/com/turn/ttorrent/client/storage/FileStorage.java +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client.storage; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; - -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * Single-file torrent byte data storage. - * - *

- * This implementation of TorrentByteStorageFile provides a torrent byte data - * storage relying on a single underlying file and uses a RandomAccessFile - * FileChannel to expose thread-safe read/write methods. - *

- * - * @author mpetazzoni - */ -public class FileStorage implements TorrentByteStorage { - - private static final Logger logger = - LoggerFactory.getLogger(FileStorage.class); - - private final File target; - private final File partial; - private final long offset; - private final long size; - - private RandomAccessFile raf; - private FileChannel channel; - private File current; - - public FileStorage(File file, long size) throws IOException { - this(file, 0, size); - } - - public FileStorage(File file, long offset, long size) - throws IOException { - this.target = file; - this.offset = offset; - this.size = size; - - this.partial = new File(this.target.getAbsolutePath() + - TorrentByteStorage.PARTIAL_FILE_NAME_SUFFIX); - - if (this.partial.exists()) { - logger.debug("Partial download found at {}. Continuing...", - this.partial.getAbsolutePath()); - this.current = this.partial; - } else if (!this.target.exists()) { - logger.debug("Downloading new file to {}...", - this.partial.getAbsolutePath()); - this.current = this.partial; - } else { - logger.debug("Using existing file {}.", - this.target.getAbsolutePath()); - this.current = this.target; - } - - this.raf = new RandomAccessFile(this.current, "rw"); - - // Set the file length to the appropriate size, eventually truncating - // or extending the file if it already exists with a different size. - this.raf.setLength(this.size); - - this.channel = raf.getChannel(); - logger.info("Initialized byte storage file at {} " + - "({}+{} byte(s)).", - new Object[] { - this.current.getAbsolutePath(), - this.offset, - this.size, - }); - } - - protected long offset() { - return this.offset; - } - - @Override - public long size() { - return this.size; - } - - @Override - public int read(ByteBuffer buffer, long offset) throws IOException { - int requested = buffer.remaining(); - - if (offset + requested > this.size) { - throw new IllegalArgumentException("Invalid storage read request!"); - } - - int bytes = this.channel.read(buffer, offset); - if (bytes < requested) { - throw new IOException("Storage underrun!"); - } - - return bytes; - } - - @Override - public int write(ByteBuffer buffer, long offset) throws IOException { - int requested = buffer.remaining(); - - if (offset + requested > this.size) { - throw new IllegalArgumentException("Invalid storage write request!"); - } - - return this.channel.write(buffer, offset); - } - - @Override - public synchronized void close() throws IOException { - logger.debug("Closing file channel to " + this.current.getName() + "..."); - if (this.channel.isOpen()) { - this.channel.force(true); - } - this.raf.close(); - } - - /** Move the partial file to its final location. - */ - @Override - public synchronized void finish() throws IOException { - logger.debug("Closing file channel to " + this.current.getName() + - " (download complete)."); - if (this.channel.isOpen()) { - this.channel.force(true); - } - - // Nothing more to do if we're already on the target file. - if (this.isFinished()) { - return; - } - - this.raf.close(); - FileUtils.deleteQuietly(this.target); - FileUtils.moveFile(this.current, this.target); - - logger.debug("Re-opening torrent byte storage at {}.", - this.target.getAbsolutePath()); - - this.raf = new RandomAccessFile(this.target, "rw"); - this.raf.setLength(this.size); - this.channel = this.raf.getChannel(); - this.current = this.target; - - FileUtils.deleteQuietly(this.partial); - logger.info("Moved torrent data from {} to {}.", - this.partial.getName(), - this.target.getName()); - } - - @Override - public boolean isFinished() { - return this.current.equals(this.target); - } -} diff --git a/src/main/java/com/turn/ttorrent/client/storage/TorrentByteStorage.java b/src/main/java/com/turn/ttorrent/client/storage/TorrentByteStorage.java deleted file mode 100644 index ae057a6c2..000000000 --- a/src/main/java/com/turn/ttorrent/client/storage/TorrentByteStorage.java +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.client.storage; - -import java.io.IOException; -import java.nio.ByteBuffer; - - -/** - * Abstract torrent byte storage. - * - *

- * This interface defines the methods for accessing an abstracted torrent byte - * storage. A torrent, especially when it contains multiple files, needs to be - * seen as one single continuous stream of bytes. Torrent pieces will most - * likely span accross file boundaries. This abstracted byte storage aims at - * providing a simple interface for read/write access to the torrent data, - * regardless of how it is composed underneath the piece structure. - *

- * - * @author mpetazzoni - * @author dgiffin - */ -public interface TorrentByteStorage { - - public static final String PARTIAL_FILE_NAME_SUFFIX = ".part"; - - /** - * Returns the total size of the torrent storage. - */ - public long size(); - - /** - * Read from the byte storage. - * - *

- * Read {@code length} bytes at offset {@code offset} from the underlying - * byte storage and return them in a {@link ByteBuffer}. - *

- * - * @param buffer The buffer to read the bytes into. The buffer's limit will - * control how many bytes are read from the storage. - * @param offset The offset, in bytes, to read from. This must be within - * the storage boundary. - * @return The number of bytes read from the storage. - * @throws IOException If an I/O error occurs while reading from the - * byte storage. - */ - public int read(ByteBuffer buffer, long offset) throws IOException; - - /** - * Write bytes to the byte storage. - * - *

- *

- * - * @param block A {@link ByteBuffer} containing the bytes to write to the - * storage. The buffer limit is expected to be set correctly: all bytes - * from the buffer will be used. - * @param offset Offset in the underlying byte storage to write the block - * at. - * @return The number of bytes written to the storage. - * @throws IOException If an I/O error occurs while writing to the byte - * storage. - */ - public int write(ByteBuffer block, long offset) throws IOException; - - /** - * Close this byte storage. - * - * @throws IOException If closing the underlying storage (file(s) ?) - * failed. - */ - public void close() throws IOException; - - /** - * Finalize the byte storage when the download is complete. - * - *

- * This gives the byte storage the opportunity to perform finalization - * operations when the download completes, like moving the files from a - * temporary location to their destination. - *

- * - * @throws IOException If the finalization failed. - */ - public void finish() throws IOException; - - /** - * Tells whether this byte storage has been finalized. - */ - public boolean isFinished(); -} diff --git a/src/main/java/com/turn/ttorrent/common/Peer.java b/src/main/java/com/turn/ttorrent/common/Peer.java deleted file mode 100644 index 86af9fcf0..000000000 --- a/src/main/java/com/turn/ttorrent/common/Peer.java +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.common; - -import com.turn.ttorrent.common.Torrent; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; - - -/** - * A basic BitTorrent peer. - * - *

- * This class is meant to be a common base for the tracker and client, which - * would presumably subclass it to extend its functionality and fields. - *

- * - * @author mpetazzoni - */ -public class Peer { - - private final InetSocketAddress address; - private final String hostId; - - private ByteBuffer peerId; - private String hexPeerId; - - /** - * Instantiate a new peer. - * - * @param address The peer's address, with port. - */ - public Peer(InetSocketAddress address) { - this(address, null); - } - - /** - * Instantiate a new peer. - * - * @param ip The peer's IP address. - * @param port The peer's port. - */ - public Peer(String ip, int port) { - this(new InetSocketAddress(ip, port), null); - } - - /** - * Instantiate a new peer. - * - * @param ip The peer's IP address. - * @param port The peer's port. - * @param peerId The byte-encoded peer ID. - */ - public Peer(String ip, int port, ByteBuffer peerId) { - this(new InetSocketAddress(ip, port), peerId); - } - - /** - * Instantiate a new peer. - * - * @param address The peer's address, with port. - * @param peerId The byte-encoded peer ID. - */ - public Peer(InetSocketAddress address, ByteBuffer peerId) { - this.address = address; - this.hostId = String.format("%s:%d", - this.address.getAddress(), - this.address.getPort()); - - this.setPeerId(peerId); - } - - /** - * Tells whether this peer has a known peer ID yet or not. - */ - public boolean hasPeerId() { - return this.peerId != null; - } - - /** - * Returns the raw peer ID as a {@link ByteBuffer}. - */ - public ByteBuffer getPeerId() { - return this.peerId; - } - - /** - * Set a peer ID for this peer (usually during handshake). - * - * @param peerId The new peer ID for this peer. - */ - public void setPeerId(ByteBuffer peerId) { - if (peerId != null) { - this.peerId = peerId; - this.hexPeerId = Torrent.byteArrayToHexString(peerId.array()); - } else { - this.peerId = null; - this.hexPeerId = null; - } - } - - /** - * Get the hexadecimal-encoded string representation of this peer's ID. - */ - public String getHexPeerId() { - return this.hexPeerId; - } - - /** - * Get the shortened hexadecimal-encoded peer ID. - */ - public String getShortHexPeerId() { - return String.format("..%s", - this.hexPeerId.substring(this.hexPeerId.length()-6).toUpperCase()); - } - - /** - * Returns this peer's IP address. - */ - public String getIp() { - return this.address.getAddress().getHostAddress(); - } - - /** - * Returns this peer's InetAddress. - */ - public InetAddress getAddress() { - return this.address.getAddress(); - } - - /** - * Returns this peer's port number. - */ - public int getPort() { - return this.address.getPort(); - } - - /** - * Returns this peer's host identifier ("host:port"). - */ - public String getHostIdentifier() { - return this.hostId; - } - - /** - * Returns a binary representation of the peer's IP. - */ - public byte[] getRawIp() { - return this.address.getAddress().getAddress(); - } - - /** - * Returns a human-readable representation of this peer. - */ - public String toString() { - StringBuilder s = new StringBuilder("peer://") - .append(this.getIp()).append(":").append(this.getPort()) - .append("/"); - - if (this.hasPeerId()) { - s.append(this.hexPeerId.substring(this.hexPeerId.length()-6)); - } else { - s.append("?"); - } - - if (this.getPort() < 10000) { - s.append(" "); - } - - return s.toString(); - } - - /** - * Tells if two peers seem to look alike (i.e. they have the same IP, port - * and peer ID if they have one). - */ - public boolean looksLike(Peer other) { - if (other == null) { - return false; - } - - return this.hostId.equals(other.hostId) && - (this.hasPeerId() - ? this.hexPeerId.equals(other.hexPeerId) - : true); - } -} diff --git a/src/main/java/com/turn/ttorrent/common/Torrent.java b/src/main/java/com/turn/ttorrent/common/Torrent.java deleted file mode 100644 index e468dc560..000000000 --- a/src/main/java/com/turn/ttorrent/common/Torrent.java +++ /dev/null @@ -1,973 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.common; - -import com.turn.ttorrent.bcodec.BDecoder; -import com.turn.ttorrent.bcodec.BEValue; -import com.turn.ttorrent.bcodec.BEncoder; - -import java.io.ByteArrayInputStream; -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; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -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.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A torrent file tracked by the controller's BitTorrent tracker. - * - *

- * This class represents an active torrent on the tracker. The torrent - * information is kept in-memory, and is created from the byte blob one would - * usually find in a .torrent file. - *

- * - *

- * Each torrent also keeps a repository of the peers seeding and leeching this - * torrent from the tracker. - *

- * - * @author mpetazzoni - * @see Torrent meta-info file structure specification - */ -public class Torrent { - - private static final Logger logger = - LoggerFactory.getLogger(Torrent.class); - - /** Torrent file piece length (in bytes), we use 512 kB. */ - private static final int PIECE_LENGTH = 512 * 1024; - - public static final int PIECE_HASH_SIZE = 20; - - /** The query parameters encoding when parsing byte strings. */ - public static final String BYTE_ENCODING = "ISO-8859-1"; - - /** - * - * @author dgiffin - * @author mpetazzoni - */ - public static class TorrentFile { - - public final File file; - public final long size; - - public TorrentFile(File file, long size) { - this.file = file; - this.size = size; - } - }; - - - protected final byte[] encoded; - protected final byte[] encoded_info; - protected final Map decoded; - protected final Map decoded_info; - - private final byte[] info_hash; - private final String hex_info_hash; - - private final List> trackers; - private final Set allTrackers; - private final Date creationDate; - private final String comment; - private final String createdBy; - private final String name; - private final long size; - protected final List files; - - private final boolean seeder; - - /** - * Create a new torrent from meta-info binary data. - * - * Parses the meta-info data (which should be B-encoded as described in the - * 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 { - this.encoded = torrent; - this.seeder = seeder; - - this.decoded = BDecoder.bdecode( - new ByteArrayInputStream(this.encoded)).getMap(); - - this.decoded_info = this.decoded.get("info").getMap(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - BEncoder.bencode(this.decoded_info, baos); - this.encoded_info = baos.toByteArray(); - this.info_hash = Torrent.hash(this.encoded_info); - this.hex_info_hash = Torrent.byteArrayToHexString(this.info_hash); - - /** - * Parses the announce information from the decoded meta-info - * structure. - * - *

- * If the torrent doesn't define an announce-list, use the mandatory - * announce field value as the single tracker in a single announce - * tier. Otherwise, the announce-list must be parsed and the trackers - * from each tier extracted. - *

- * - * @see BitTorrent BEP#0012 "Multitracker Metadata Extension" - */ - try { - this.trackers = new ArrayList>(); - this.allTrackers = new HashSet(); - - if (this.decoded.containsKey("announce-list")) { - List tiers = this.decoded.get("announce-list").getList(); - for (BEValue tv : tiers) { - List trackers = tv.getList(); - if (trackers.isEmpty()) { - continue; - } - - List tier = new ArrayList(); - for (BEValue tracker : trackers) { - URI uri = new URI(tracker.getString()); - - // Make sure we're not adding duplicate trackers. - if (!this.allTrackers.contains(uri)) { - tier.add(uri); - this.allTrackers.add(uri); - } - } - - // Only add the tier if it's not empty. - if (!tier.isEmpty()) { - this.trackers.add(tier); - } - } - } else if (this.decoded.containsKey("announce")) { - URI tracker = new URI(this.decoded.get("announce").getString()); - this.allTrackers.add(tracker); - - // Build a single-tier announce list. - List tier = new ArrayList(); - tier.add(tracker); - this.trackers.add(tier); - } - } catch (URISyntaxException use) { - throw new IOException(use); - } - - this.creationDate = this.decoded.containsKey("creation date") - ? new Date(this.decoded.get("creation date").getLong() * 1000) - : null; - this.comment = this.decoded.containsKey("comment") - ? this.decoded.get("comment").getString() - : null; - this.createdBy = this.decoded.containsKey("created by") - ? this.decoded.get("created by").getString() - : null; - this.name = this.decoded_info.get("name").getString(); - - this.files = new LinkedList(); - - // Parse multi-file torrent file information structure. - if (this.decoded_info.containsKey("files")) { - for (BEValue file : this.decoded_info.get("files").getList()) { - Map fileInfo = file.getMap(); - StringBuilder path = new StringBuilder(); - for (BEValue pathElement : fileInfo.get("path").getList()) { - path.append(File.separator) - .append(pathElement.getString()); - } - this.files.add(new TorrentFile( - new File(this.name, path.toString()), - fileInfo.get("length").getLong())); - } - } else { - // For single-file torrents, the name of the torrent is - // directly the name of the file. - this.files.add(new TorrentFile( - new File(this.name), - this.decoded_info.get("length").getLong())); - } - - // Calculate the total size of this torrent from its files' sizes. - long size = 0; - for (TorrentFile file : this.files) { - size += file.size; - } - this.size = size; - - logger.info("{}-file torrent information:", - this.isMultifile() ? "Multi" : "Single"); - logger.info(" Torrent name: {}", this.name); - logger.info(" Announced at:" + (this.trackers.size() == 0 ? " Seems to be trackerless" : "")); - for (int i=0; i < this.trackers.size(); i++) { - List tier = this.trackers.get(i); - for (int j=0; j < tier.size(); j++) { - logger.info(" {}{}", - (j == 0 ? String.format("%2d. ", i+1) : " "), - tier.get(j)); - } - } - - if (this.creationDate != null) { - logger.info(" Created on..: {}", this.creationDate); - } - if (this.comment != null) { - logger.info(" Comment.....: {}", this.comment); - } - if (this.createdBy != null) { - logger.info(" Created by..: {}", this.createdBy); - } - - if (this.isMultifile()) { - logger.info(" Found {} file(s) in multi-file torrent structure.", - this.files.size()); - int i = 0; - for (TorrentFile file : this.files) { - logger.debug(" {}. {} ({} byte(s))", - new Object[] { - String.format("%2d", ++i), - file.file.getPath(), - String.format("%,d", file.size) - }); - } - } - - logger.info(" Pieces......: {} piece(s) ({} byte(s)/piece)", - (this.size / this.decoded_info.get("piece length").getInt()) + 1, - this.decoded_info.get("piece length").getInt()); - logger.info(" Total size..: {} byte(s)", - String.format("%,d", this.size)); - } - - /** - * Get this torrent's name. - * - *

- * For a single-file torrent, this is usually the name of the file. For a - * multi-file torrent, this is usually the name of a top-level directory - * containing those files. - *

- */ - public String getName() { - return this.name; - } - - /** - * Get this torrent's comment string. - */ - public String getComment() { - return this.comment; - } - - /** - * Get this torrent's creator (user, software, whatever...). - */ - public String getCreatedBy() { - return this.createdBy; - } - - /** - * Get the total size of this torrent. - */ - public long getSize() { - return this.size; - } - - /** - * Get the file names from this torrent. - * - * @return The list of relative filenames of all the files described in - * this torrent. - */ - public List getFilenames() { - List filenames = new LinkedList(); - for (TorrentFile file : this.files) { - filenames.add(file.file.getPath()); - } - return filenames; - } - - /** - * Tells whether this torrent is multi-file or not. - */ - public boolean isMultifile() { - return this.files.size() > 1; - } - - /** - * Return the hash of the B-encoded meta-info structure of this torrent. - */ - public byte[] getInfoHash() { - return this.info_hash; - } - - /** - * Get this torrent's info hash (as an hexadecimal-coded string). - */ - public String getHexInfoHash() { - return this.hex_info_hash; - } - - /** - * Return a human-readable representation of this torrent object. - * - *

- * The torrent's name is used. - *

- */ - public String toString() { - return this.getName(); - } - - /** - * Return the B-encoded meta-info of this torrent. - */ - public byte[] getEncoded() { - return this.encoded; - } - - /** - * Return the trackers for this torrent. - */ - public List> getAnnounceList() { - return this.trackers; - } - - /** - * Returns the number of trackers for this torrent. - */ - public int getTrackerCount() { - return this.allTrackers.size(); - } - - /** - * Tells whether we were an initial seeder for this torrent. - */ - public boolean isSeeder() { - return this.seeder; - } - - /** - * Save this torrent meta-info structure into a .torrent file. - * - * @param output The stream to write to. - * @throws IOException If an I/O error occurs while writing the file. - */ - 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(); - } - - /** - * Convert a byte string to a string containing an hexadecimal - * representation of the original data. - * - * @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 an hexadecimal representation of the bytes contained in the - * given string, following the default, expected byte encoding. - * - * @param input The input string. - */ - public static String toHexString(String input) { - try { - byte[] bytes = input.getBytes(Torrent.BYTE_ENCODING); - return Torrent.byteArrayToHexString(bytes); - } catch (UnsupportedEncodingException uee) { - return null; - } - } - - /** - * Determine how many threads to use for the piece hashing. - * - *

- * If the environment variable TTORRENT_HASHING_THREADS is set to an - * integer value greater than 0, its value will be used. Otherwise, it - * defaults to the number of processors detected by the Java Runtime. - *

- * - * @return How many threads to use for concurrent piece hashing. - */ - protected static int getHashingThreadsCount() { - String threads = System.getenv("TTORRENT_HASHING_THREADS"); - - if (threads != null) { - try { - int count = Integer.parseInt(threads); - if (count > 0) { - return count; - } - } catch (NumberFormatException nfe) { - // Pass - } - } - - return Runtime.getRuntime().availableProcessors(); - } - - /** Torrent loading ---------------------------------------------------- */ - - /** - * Load a torrent from the given torrent file. - * - *

- * This method assumes we are not a seeder and that local data needs to be - * validated. - *

- * - * @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 { - return Torrent.load(torrent, false); - } - - /** - * Load a torrent from the given torrent file. - * - * @param torrent The abstract {@link File} object representing the - * .torrent file to load. - * @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(); - } - } - } - - /** Torrent creation --------------------------------------------------- */ - - /** - * Create a {@link Torrent} object for a file. - * - *

- * Hash the given file to create the {@link Torrent} object representing - * the Torrent metainfo about this file, needed for announcing and/or - * sharing said file. - *

- * - * @param source The file to use in the torrent. - * @param announce The announce URI that will be used for this torrent. - * @param createdBy The creator's name, or any string identifying the - * torrent's creator. - */ - public static Torrent create(File source, URI announce, String createdBy) - throws NoSuchAlgorithmException, InterruptedException, IOException { - return Torrent.create(source, null, announce, null, createdBy); - } - - /** - * Create a {@link Torrent} object for a set of files. - * - *

- * Hash the given files to create the multi-file {@link Torrent} object - * representing the Torrent meta-info about them, needed for announcing - * and/or sharing these files. Since we created the torrent, we're - * considering we'll be a full initial seeder for it. - *

- * - * @param parent 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 announce The announce URI that will be used for this torrent. - * @param createdBy The creator's name, or any string identifying the - * torrent's creator. - */ - public static Torrent create(File parent, List files, URI announce, - String createdBy) throws NoSuchAlgorithmException, - InterruptedException, IOException { - return Torrent.create(parent, files, announce, null, createdBy); - } - - /** - * Create a {@link Torrent} object for a file. - * - *

- * Hash the given file to create the {@link Torrent} object representing - * the Torrent metainfo about this file, needed for announcing and/or - * sharing said file. - *

- * - * @param source The file to use in the torrent. - * @param announceList The announce URIs organized as tiers that will - * be used for this torrent - * @param createdBy The creator's name, or any string identifying the - * torrent's creator. - */ - public static Torrent create(File source, List> announceList, - String createdBy) throws NoSuchAlgorithmException, - InterruptedException, IOException { - return Torrent.create(source, null, null, announceList, createdBy); - } - - /** - * Create a {@link Torrent} object for a set of files. - * - *

- * Hash the given files to create the multi-file {@link Torrent} object - * representing the Torrent meta-info about them, needed for announcing - * and/or sharing these files. Since we created the torrent, we're - * considering we'll be a full initial seeder for it. - *

- * - * @param parent 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 - * be used for this torrent - * @param createdBy The creator's name, or any string identifying the - * torrent's creator. - */ - public static Torrent create(File source, List files, - List> announceList, String createdBy) - throws NoSuchAlgorithmException, InterruptedException, IOException { - return Torrent.create(source, files, null, announceList, createdBy); - } - - /** - * Helper method to create a {@link Torrent} object for a set of files. - * - *

- * Hash the given files to create the multi-file {@link Torrent} object - * representing the Torrent meta-info about them, needed for announcing - * and/or sharing these files. Since we created the torrent, we're - * considering we'll be a full initial seeder for it. - *

- * - * @param parent 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 announce The announce URI that will be used for this torrent. - * @param announceList The announce URIs organized as tiers that will - * be used for this torrent - * @param createdBy The creator's name, or any string identifying the - * torrent's creator. - */ - private static Torrent create(File parent, List files, URI announce, - List> announceList, String createdBy) - throws NoSuchAlgorithmException, InterruptedException, IOException { - if (files == null || files.isEmpty()) { - logger.info("Creating single-file torrent for {}...", - parent.getName()); - } else { - logger.info("Creating {}-file torrent {}...", - files.size(), parent.getName()); - } - - Map torrent = new HashMap(); - - if (announce != null) { - torrent.put("announce", new BEValue(announce.toString())); - } - if (announceList != null) { - List tiers = new LinkedList(); - for (List trackers : announceList) { - List tierInfo = new LinkedList(); - for (URI trackerURI : trackers) { - tierInfo.add(new BEValue(trackerURI.toString())); - } - tiers.add(new BEValue(tierInfo)); - } - torrent.put("announce-list", new BEValue(tiers)); - } - - torrent.put("creation date", new BEValue(new Date().getTime() / 1000)); - torrent.put("created by", new BEValue(createdBy)); - - Map info = new TreeMap(); - info.put("name", new BEValue(parent.getName())); - info.put("piece length", new BEValue(Torrent.PIECE_LENGTH)); - - if (files == null || files.isEmpty()) { - info.put("length", new BEValue(parent.length())); - info.put("pieces", new BEValue(Torrent.hashFile(parent), - Torrent.BYTE_ENCODING)); - } else { - List fileInfo = new LinkedList(); - for (File file : files) { - Map fileMap = new HashMap(); - fileMap.put("length", new BEValue(file.length())); - - LinkedList filePath = new LinkedList(); - while (file != null) { - if (file.equals(parent)) { - break; - } - - filePath.addFirst(new BEValue(file.getName())); - file = file.getParentFile(); - } - - fileMap.put("path", new BEValue(filePath)); - fileInfo.add(new BEValue(fileMap)); - } - info.put("files", new BEValue(fileInfo)); - info.put("pieces", new BEValue(Torrent.hashFiles(files), - Torrent.BYTE_ENCODING)); - } - torrent.put("info", new BEValue(info)); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - BEncoder.bencode(new BEValue(torrent), baos); - return new Torrent(baos.toByteArray(), true); - } - - /** - * A {@link Callable} to hash a data chunk. - * - * @author mpetazzoni - */ - 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"); - - this.data = ByteBuffer.allocate(buffer.remaining()); - buffer.mark(); - this.data.put(buffer); - this.data.clear(); - buffer.reset(); - } - - @Override - public String call() throws UnsupportedEncodingException { - this.md.reset(); - this.md.update(this.data.array()); - return new String(md.digest(), Torrent.BYTE_ENCODING); - } - } - - /** - * Return the concatenation of the SHA-1 hashes of a file's pieces. - * - *

- * Hashes the given file piece by piece using the default Torrent piece - * length (see {@link #PIECE_LENGTH}) and returns the concatenation of - * these hashes, as a string. - *

- * - *

- * This is used for creating Torrent meta-info structures from a file. - *

- * - * @param file The file to hash. - */ - private static String hashFile(File file) - throws NoSuchAlgorithmException, InterruptedException, IOException { - return Torrent.hashFiles(Arrays.asList(new File[] { file })); - } - - private static String hashFiles(List files) - throws NoSuchAlgorithmException, InterruptedException, IOException { - int threads = getHashingThreadsCount(); - ExecutorService executor = Executors.newFixedThreadPool(threads); - ByteBuffer buffer = ByteBuffer.allocate(Torrent.PIECE_LENGTH); - List> results = new LinkedList>(); - StringBuilder hashes = new StringBuilder(); - - long length = 0L; - int pieces = 0; - - long start = System.nanoTime(); - for (File file : files) { - logger.info("Hashing data from {} with {} threads ({} pieces)...", - new Object[] { - file.getName(), - threads, - (int) (Math.ceil( - (double)file.length() / Torrent.PIECE_LENGTH)) - }); - - length += file.length(); - - FileInputStream fis = new FileInputStream(file); - FileChannel channel = fis.getChannel(); - int step = 10; - - try { - while (channel.read(buffer) > 0) { - if (buffer.remaining() == 0) { - buffer.clear(); - results.add(executor.submit(new CallableChunkHasher(buffer))); - } - - if (results.size() >= threads) { - pieces += accumulateHashes(hashes, results); - } - - if (channel.position() / (double)channel.size() * 100f > step) { - logger.info(" ... {}% complete", step); - step += 10; - } - } - } finally { - channel.close(); - fis.close(); - } - } - - // Hash the last bit, if any - if (buffer.position() > 0) { - buffer.limit(buffer.position()); - buffer.position(0); - results.add(executor.submit(new CallableChunkHasher(buffer))); - } - - pieces += accumulateHashes(hashes, results); - - // Request orderly executor shutdown and wait for hashing tasks to - // complete. - executor.shutdown(); - while (!executor.isTerminated()) { - Thread.sleep(10); - } - long elapsed = System.nanoTime() - start; - - int expectedPieces = (int) (Math.ceil( - (double)length / Torrent.PIECE_LENGTH)); - logger.info("Hashed {} file(s) ({} bytes) in {} pieces ({} expected) in {}ms.", - new Object[] { - files.size(), - length, - pieces, - expectedPieces, - String.format("%.1f", elapsed/1e6), - }); - - return hashes.toString(); - } - - /** - * Accumulate the piece hashes into a given {@link StringBuilder}. - * - * @param hashes The {@link StringBuilder} to append hashes to. - * @param results The list of {@link Future}s that will yield the piece - * hashes. - */ - private static int accumulateHashes(StringBuilder hashes, - List> results) throws InterruptedException, IOException { - try { - int pieces = results.size(); - for (Future chunk : results) { - hashes.append(chunk.get()); - } - results.clear(); - return pieces; - } catch (ExecutionException ee) { - 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/PeerMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/PeerMessage.java deleted file mode 100644 index d9d01d31b..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/PeerMessage.java +++ /dev/null @@ -1,670 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.common.protocol; - -import com.turn.ttorrent.client.SharedTorrent; - -import java.nio.ByteBuffer; -import java.text.ParseException; -import java.util.BitSet; - -/** - * BitTorrent peer protocol messages representations. - * - *

- * This class and its *Messages subclasses provide POJO - * representations of the peer protocol messages, along with easy parsing from - * an input ByteBuffer to quickly get a usable representation of an incoming - * message. - *

- * - * @author mpetazzoni - * @see BitTorrent peer wire protocol - */ -public abstract class PeerMessage { - - /** The size, in bytes, of the length field in a message (one 32-bit - * integer). */ - public static final int MESSAGE_LENGTH_FIELD_SIZE = 4; - - /** - * Message type. - * - *

- * Note that the keep-alive messages don't actually have an type ID defined - * in the protocol as they are of length 0. - *

- */ - public enum Type { - KEEP_ALIVE(-1), - CHOKE(0), - UNCHOKE(1), - INTERESTED(2), - NOT_INTERESTED(3), - HAVE(4), - BITFIELD(5), - REQUEST(6), - PIECE(7), - CANCEL(8); - - private byte id; - Type(int id) { - this.id = (byte)id; - } - - public boolean equals(byte c) { - return this.id == c; - } - - public byte getTypeByte() { - return this.id; - } - - public static Type get(byte c) { - for (Type t : Type.values()) { - if (t.equals(c)) { - return t; - } - } - return null; - } - }; - - private final Type type; - private final ByteBuffer data; - - private PeerMessage(Type type, ByteBuffer data) { - this.type = type; - this.data = data; - this.data.rewind(); - } - - public Type getType() { - return this.type; - } - - /** - * Returns a {@link ByteBuffer} backed by the same data as this message. - * - *

- * This method returns a duplicate of the buffer stored in this {@link - * PeerMessage} object to allow for multiple consumers to read from the - * same message without conflicting access to the buffer's position, mark - * and limit. - *

- */ - public ByteBuffer getData() { - return this.data.duplicate(); - } - - /** - * Validate that this message makes sense for the torrent it's related to. - * - *

- * This method is meant to be overloaded by distinct message types, where - * it makes sense. Otherwise, it defaults to true. - *

- * - * @param torrent The torrent this message is about. - */ - public PeerMessage validate(SharedTorrent torrent) - throws MessageValidationException { - return this; - } - - public String toString() { - return this.getType().name(); - } - - /** - * Parse the given buffer into a peer protocol message. - * - *

- * Parses the provided byte array and builds the corresponding PeerMessage - * subclass object. - *

- * - * @param buffer The byte buffer containing the message data. - * @param torrent The torrent this message is about. - * @return A PeerMessage subclass instance. - * @throws ParseException When the message is invalid, can't be parsed or - * does not match the protocol requirements. - */ - public static PeerMessage parse(ByteBuffer buffer, SharedTorrent torrent) - throws ParseException { - int length = buffer.getInt(); - if (length == 0) { - return KeepAliveMessage.parse(buffer, torrent); - } else if (length != buffer.remaining()) { - throw new ParseException("Message size did not match announced " + - "size!", 0); - } - - Type type = Type.get(buffer.get()); - if (type == null) { - throw new ParseException("Unknown message ID!", - buffer.position()-1); - } - - switch (type) { - case CHOKE: - return ChokeMessage.parse(buffer.slice(), torrent); - case UNCHOKE: - return UnchokeMessage.parse(buffer.slice(), torrent); - case INTERESTED: - return InterestedMessage.parse(buffer.slice(), torrent); - case NOT_INTERESTED: - return NotInterestedMessage.parse(buffer.slice(), torrent); - case HAVE: - return HaveMessage.parse(buffer.slice(), torrent); - case BITFIELD: - return BitfieldMessage.parse(buffer.slice(), torrent); - case REQUEST: - return RequestMessage.parse(buffer.slice(), torrent); - case PIECE: - return PieceMessage.parse(buffer.slice(), torrent); - case CANCEL: - return CancelMessage.parse(buffer.slice(), torrent); - default: - throw new IllegalStateException("Message type should have " + - "been properly defined by now."); - } - } - - public static class MessageValidationException extends ParseException { - - static final long serialVersionUID = -1; - - public MessageValidationException(PeerMessage m) { - super("Message " + m + " is not valid!", 0); - } - - } - - - /** - * Keep alive message. - * - * - */ - public static class KeepAliveMessage extends PeerMessage { - - private static final int BASE_SIZE = 0; - - private KeepAliveMessage(ByteBuffer buffer) { - super(Type.KEEP_ALIVE, buffer); - } - - public static KeepAliveMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - return (KeepAliveMessage)new KeepAliveMessage(buffer) - .validate(torrent); - } - - public static KeepAliveMessage craft() { - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + KeepAliveMessage.BASE_SIZE); - buffer.putInt(KeepAliveMessage.BASE_SIZE); - return new KeepAliveMessage(buffer); - } - } - - /** - * Choke message. - * - * - */ - public static class ChokeMessage extends PeerMessage { - - private static final int BASE_SIZE = 1; - - private ChokeMessage(ByteBuffer buffer) { - super(Type.CHOKE, buffer); - } - - public static ChokeMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - return (ChokeMessage)new ChokeMessage(buffer) - .validate(torrent); - } - - public static ChokeMessage craft() { - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + ChokeMessage.BASE_SIZE); - buffer.putInt(ChokeMessage.BASE_SIZE); - buffer.put(PeerMessage.Type.CHOKE.getTypeByte()); - return new ChokeMessage(buffer); - } - } - - /** - * Unchoke message. - * - * - */ - public static class UnchokeMessage extends PeerMessage { - - private static final int BASE_SIZE = 1; - - private UnchokeMessage(ByteBuffer buffer) { - super(Type.UNCHOKE, buffer); - } - - public static UnchokeMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - return (UnchokeMessage)new UnchokeMessage(buffer) - .validate(torrent); - } - - public static UnchokeMessage craft() { - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + UnchokeMessage.BASE_SIZE); - buffer.putInt(UnchokeMessage.BASE_SIZE); - buffer.put(PeerMessage.Type.UNCHOKE.getTypeByte()); - return new UnchokeMessage(buffer); - } - } - - /** - * Interested message. - * - * - */ - public static class InterestedMessage extends PeerMessage { - - private static final int BASE_SIZE = 1; - - private InterestedMessage(ByteBuffer buffer) { - super(Type.INTERESTED, buffer); - } - - public static InterestedMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - return (InterestedMessage)new InterestedMessage(buffer) - .validate(torrent); - } - - public static InterestedMessage craft() { - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + InterestedMessage.BASE_SIZE); - buffer.putInt(InterestedMessage.BASE_SIZE); - buffer.put(PeerMessage.Type.INTERESTED.getTypeByte()); - return new InterestedMessage(buffer); - } - } - - /** - * Not interested message. - * - * - */ - public static class NotInterestedMessage extends PeerMessage { - - private static final int BASE_SIZE = 1; - - private NotInterestedMessage(ByteBuffer buffer) { - super(Type.NOT_INTERESTED, buffer); - } - - public static NotInterestedMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - return (NotInterestedMessage)new NotInterestedMessage(buffer) - .validate(torrent); - } - - public static NotInterestedMessage craft() { - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + NotInterestedMessage.BASE_SIZE); - buffer.putInt(NotInterestedMessage.BASE_SIZE); - buffer.put(PeerMessage.Type.NOT_INTERESTED.getTypeByte()); - return new NotInterestedMessage(buffer); - } - } - - /** - * Have message. - * - * - */ - public static class HaveMessage extends PeerMessage { - - private static final int BASE_SIZE = 5; - - private int piece; - - private HaveMessage(ByteBuffer buffer, int piece) { - super(Type.HAVE, buffer); - this.piece = piece; - } - - public int getPieceIndex() { - return this.piece; - } - - @Override - public HaveMessage validate(SharedTorrent torrent) - throws MessageValidationException { - if (this.piece >= 0 && this.piece < torrent.getPieceCount()) { - return this; - } - - throw new MessageValidationException(this); - } - - public static HaveMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - return new HaveMessage(buffer, buffer.getInt()) - .validate(torrent); - } - - public static HaveMessage craft(int piece) { - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + HaveMessage.BASE_SIZE); - buffer.putInt(HaveMessage.BASE_SIZE); - buffer.put(PeerMessage.Type.HAVE.getTypeByte()); - buffer.putInt(piece); - return new HaveMessage(buffer, piece); - } - - public String toString() { - return super.toString() + " #" + this.getPieceIndex(); - } - } - - /** - * Bitfield message. - * - * - */ - public static class BitfieldMessage extends PeerMessage { - - private static final int BASE_SIZE = 1; - - private BitSet bitfield; - - private BitfieldMessage(ByteBuffer buffer, BitSet bitfield) { - super(Type.BITFIELD, buffer); - this.bitfield = bitfield; - } - - public BitSet getBitfield() { - return this.bitfield; - } - - @Override - public BitfieldMessage validate(SharedTorrent torrent) - throws MessageValidationException { - if (this.bitfield.length() <= torrent.getPieceCount()) { - return this; - } - - throw new MessageValidationException(this); - } - - public static BitfieldMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - BitSet bitfield = new BitSet(buffer.remaining()*8); - for (int i=0; i < buffer.remaining()*8; i++) { - if ((buffer.get(i/8) & (1 << (7 -(i % 8)))) > 0) { - bitfield.set(i); - } - } - - return new BitfieldMessage(buffer, bitfield) - .validate(torrent); - } - - public static BitfieldMessage craft(BitSet availablePieces) { - byte[] bitfield = new byte[ - (int) Math.ceil((double)availablePieces.length()/8)]; - for (int i=availablePieces.nextSetBit(0); i >= 0; - i=availablePieces.nextSetBit(i+1)) { - bitfield[i/8] |= 1 << (7 -(i % 8)); - } - - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + BitfieldMessage.BASE_SIZE + bitfield.length); - buffer.putInt(BitfieldMessage.BASE_SIZE + bitfield.length); - buffer.put(PeerMessage.Type.BITFIELD.getTypeByte()); - buffer.put(ByteBuffer.wrap(bitfield)); - return new BitfieldMessage(buffer, availablePieces); - } - - public String toString() { - return super.toString() + " " + this.getBitfield().cardinality(); - } - } - - /** - * Request message. - * - * - */ - public static class RequestMessage extends PeerMessage { - - private static final int BASE_SIZE = 13; - - /** Default block size is 2^14 bytes, or 16kB. */ - public static final int DEFAULT_REQUEST_SIZE = 16384; - - /** Max block request size is 2^17 bytes, or 131kB. */ - public static final int MAX_REQUEST_SIZE = 131072; - - private int piece; - private int offset; - private int length; - - private RequestMessage(ByteBuffer buffer, int piece, - int offset, int length) { - super(Type.REQUEST, buffer); - this.piece = piece; - this.offset = offset; - this.length = length; - } - - public int getPiece() { - return this.piece; - } - - public int getOffset() { - return this.offset; - } - - public int getLength() { - return this.length; - } - - @Override - public RequestMessage validate(SharedTorrent torrent) - throws MessageValidationException { - if (this.piece >= 0 && this.piece < torrent.getPieceCount() && - this.offset + this.length <= - torrent.getPiece(this.piece).size()) { - return this; - } - - throw new MessageValidationException(this); - } - - public static RequestMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - int piece = buffer.getInt(); - int offset = buffer.getInt(); - int length = buffer.getInt(); - return new RequestMessage(buffer, piece, - offset, length).validate(torrent); - } - - public static RequestMessage craft(int piece, int offset, int length) { - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + RequestMessage.BASE_SIZE); - buffer.putInt(RequestMessage.BASE_SIZE); - buffer.put(PeerMessage.Type.REQUEST.getTypeByte()); - buffer.putInt(piece); - buffer.putInt(offset); - buffer.putInt(length); - return new RequestMessage(buffer, piece, offset, length); - } - - public String toString() { - return super.toString() + " #" + this.getPiece() + - " (" + this.getLength() + "@" + this.getOffset() + ")"; - } - } - - /** - * Piece message. - * - * - */ - public static class PieceMessage extends PeerMessage { - - private static final int BASE_SIZE = 9; - - private int piece; - private int offset; - private ByteBuffer block; - - private PieceMessage(ByteBuffer buffer, int piece, - int offset, ByteBuffer block) { - super(Type.PIECE, buffer); - this.piece = piece; - this.offset = offset; - this.block = block; - } - - public int getPiece() { - return this.piece; - } - - public int getOffset() { - return this.offset; - } - - public ByteBuffer getBlock() { - return this.block; - } - - @Override - public PieceMessage validate(SharedTorrent torrent) - throws MessageValidationException { - if (this.piece >= 0 && this.piece < torrent.getPieceCount() && - this.offset + this.block.limit() <= - torrent.getPiece(this.piece).size()) { - return this; - } - - throw new MessageValidationException(this); - } - - public static PieceMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - int piece = buffer.getInt(); - int offset = buffer.getInt(); - ByteBuffer block = buffer.slice(); - return new PieceMessage(buffer, piece, offset, block) - .validate(torrent); - } - - public static PieceMessage craft(int piece, int offset, - ByteBuffer block) { - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + PieceMessage.BASE_SIZE + block.capacity()); - buffer.putInt(PieceMessage.BASE_SIZE + block.capacity()); - buffer.put(PeerMessage.Type.PIECE.getTypeByte()); - buffer.putInt(piece); - buffer.putInt(offset); - buffer.put(block); - return new PieceMessage(buffer, piece, offset, block); - } - - public String toString() { - return super.toString() + " #" + this.getPiece() + - " (" + this.getBlock().capacity() + "@" + this.getOffset() + ")"; - } - } - - /** - * Cancel message. - * - * - */ - public static class CancelMessage extends PeerMessage { - - private static final int BASE_SIZE = 13; - - private int piece; - private int offset; - private int length; - - private CancelMessage(ByteBuffer buffer, int piece, - int offset, int length) { - super(Type.CANCEL, buffer); - this.piece = piece; - this.offset = offset; - this.length = length; - } - - public int getPiece() { - return this.piece; - } - - public int getOffset() { - return this.offset; - } - - public int getLength() { - return this.length; - } - - @Override - public CancelMessage validate(SharedTorrent torrent) - throws MessageValidationException { - if (this.piece >= 0 && this.piece < torrent.getPieceCount() && - this.offset + this.length <= - torrent.getPiece(this.piece).size()) { - return this; - } - - throw new MessageValidationException(this); - } - - public static CancelMessage parse(ByteBuffer buffer, - SharedTorrent torrent) throws MessageValidationException { - int piece = buffer.getInt(); - int offset = buffer.getInt(); - int length = buffer.getInt(); - return new CancelMessage(buffer, piece, - offset, length).validate(torrent); - } - - public static CancelMessage craft(int piece, int offset, int length) { - ByteBuffer buffer = ByteBuffer.allocateDirect( - MESSAGE_LENGTH_FIELD_SIZE + CancelMessage.BASE_SIZE); - buffer.putInt(CancelMessage.BASE_SIZE); - buffer.put(PeerMessage.Type.CANCEL.getTypeByte()); - buffer.putInt(piece); - buffer.putInt(offset); - buffer.putInt(length); - return new CancelMessage(buffer, piece, offset, length); - } - - public String toString() { - return super.toString() + " #" + this.getPiece() + - " (" + this.getLength() + "@" + this.getOffset() + ")"; - } - } -} diff --git a/src/main/java/com/turn/ttorrent/common/protocol/TrackerMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/TrackerMessage.java deleted file mode 100644 index 5aba095be..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/TrackerMessage.java +++ /dev/null @@ -1,290 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol; - -import com.turn.ttorrent.common.Peer; - -import java.nio.ByteBuffer; -import java.util.List; - - -/** - * BitTorrent tracker protocol messages representations. - * - *

- * This class and its *TrackerMessage subclasses provide POJO - * representations of the tracker protocol messages, for at least HTTP and UDP - * trackers' protocols, along with easy parsing from an input ByteBuffer to - * quickly get a usable representation of an incoming message. - *

- * - * @author mpetazzoni - */ -public abstract class TrackerMessage { - - /** - * Message type. - */ - public enum Type { - UNKNOWN(-1), - CONNECT_REQUEST(0), - CONNECT_RESPONSE(0), - ANNOUNCE_REQUEST(1), - ANNOUNCE_RESPONSE(1), - SCRAPE_REQUEST(2), - SCRAPE_RESPONSE(2), - ERROR(3); - - private final int id; - - Type(int id) { - this.id = id; - } - - public int getId() { - return this.id; - } - }; - - private final Type type; - private final ByteBuffer data; - - /** - * Constructor for the base tracker message type. - * - * @param type The message type. - * @param data A byte buffer containing the binary data of the message (a - * B-encoded map, a UDP packet data, etc.). - */ - protected TrackerMessage(Type type, ByteBuffer data) { - this.type = type; - this.data = data; - if (this.data != null) { - this.data.rewind(); - } - } - - /** - * Returns the type of this tracker message. - */ - public Type getType() { - return this.type; - } - - /** - * Returns the encoded binary data for this message. - */ - public ByteBuffer getData() { - return this.data; - } - - /** - * Generic exception for message format and message validation exceptions. - */ - public static class MessageValidationException extends Exception { - - static final long serialVersionUID = -1; - - public MessageValidationException(String s) { - super(s); - } - - public MessageValidationException(String s, Throwable cause) { - super(s, cause); - } - - } - - - /** - * Base interface for connection request messages. - * - *

- * This interface must be implemented by all subtypes of connection request - * messages for the various tracker protocols. - *

- * - * @author mpetazzoni - */ - public interface ConnectionRequestMessage { - - }; - - - /** - * Base interface for connection response messages. - * - *

- * This interface must be implemented by all subtypes of connection - * response messages for the various tracker protocols. - *

- * - * @author mpetazzoni - */ - public interface ConnectionResponseMessage { - - }; - - - /** - * Base interface for announce request messages. - * - *

- * This interface must be implemented by all subtypes of announce request - * messages for the various tracker protocols. - *

- * - * @author mpetazzoni - */ - public interface AnnounceRequestMessage { - - public static final int DEFAULT_NUM_WANT = 50; - - /** - * Announce request event types. - * - *

- * When the client starts exchanging on a torrent, it must contact the - * torrent's tracker with a 'started' announce request, which notifies the - * tracker this client now exchanges on this torrent (and thus allows the - * tracker to report the existence of this peer to other clients). - *

- * - *

- * When the client stops exchanging, or when its download completes, it must - * also send a specific announce request. Otherwise, the client must send an - * eventless (NONE), periodic announce request to the tracker at an - * interval specified by the tracker itself, allowing the tracker to - * refresh this peer's status and acknowledge that it is still there. - *

- */ - public enum RequestEvent { - NONE(0), - COMPLETED(1), - STARTED(2), - STOPPED(3); - - private final int id; - RequestEvent(int id) { - this.id = id; - } - - public String getEventName() { - return this.name().toLowerCase(); - } - - public int getId() { - return this.id; - } - - public static RequestEvent getByName(String name) { - for (RequestEvent type : RequestEvent.values()) { - if (type.name().equalsIgnoreCase(name)) { - return type; - } - } - return null; - } - - public static RequestEvent getById(int id) { - for (RequestEvent type : RequestEvent.values()) { - if (type.getId() == id) { - return type; - } - } - return null; - } - }; - - public byte[] getInfoHash(); - public String getHexInfoHash(); - public byte[] getPeerId(); - public String getHexPeerId(); - public int getPort(); - public long getUploaded(); - public long getDownloaded(); - public long getLeft(); - public boolean getCompact(); - public boolean getNoPeerIds(); - public RequestEvent getEvent(); - - public String getIp(); - public int getNumWant(); - }; - - - /** - * Base interface for announce response messages. - * - *

- * This interface must be implemented by all subtypes of announce response - * messages for the various tracker protocols. - *

- * - * @author mpetazzoni - */ - public interface AnnounceResponseMessage { - - public int getInterval(); - public int getComplete(); - public int getIncomplete(); - public List getPeers(); - }; - - - /** - * Base interface for tracker error messages. - * - *

- * This interface must be implemented by all subtypes of tracker error - * messages for the various tracker protocols. - *

- * - * @author mpetazzoni - */ - public interface ErrorMessage { - - /** - * The various tracker error states. - * - *

- * These errors are reported by the tracker to a client when expected - * parameters or conditions are not present while processing an - * announce request from a BitTorrent client. - *

- */ - public enum FailureReason { - UNKNOWN_TORRENT("The requested torrent does not exist on this tracker"), - MISSING_HASH("Missing info hash"), - MISSING_PEER_ID("Missing peer ID"), - MISSING_PORT("Missing port"), - INVALID_EVENT("Unexpected event for peer state"), - NOT_IMPLEMENTED("Feature not implemented"); - - private String message; - - FailureReason(String message) { - this.message = message; - } - - public String getMessage() { - return this.message; - } - }; - - public String getReason(); - }; -} 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 deleted file mode 100644 index 0362ba72f..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java +++ /dev/null @@ -1,300 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.http; - -import com.turn.ttorrent.bcodec.BDecoder; -import com.turn.ttorrent.bcodec.BEValue; -import com.turn.ttorrent.bcodec.BEncoder; -import com.turn.ttorrent.bcodec.InvalidBEncodingException; -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.Torrent; -import com.turn.ttorrent.common.protocol.TrackerMessage.AnnounceRequestMessage; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.Map; - - -/** - * The announce request message for the HTTP tracker protocol. - * - *

- * This class represents the announce request message in the HTTP tracker - * protocol. It doesn't add any specific fields compared to the generic - * announce request message, but it provides the means to parse such - * messages and craft them. - *

- * - * @author mpetazzoni - */ -public class HTTPAnnounceRequestMessage extends HTTPTrackerMessage - implements AnnounceRequestMessage { - - private final byte[] infoHash; - private final Peer peer; - private final long uploaded; - private final long downloaded; - private final long left; - private final boolean compact; - private final boolean noPeerId; - private final RequestEvent event; - private final int numWant; - - private HTTPAnnounceRequestMessage(ByteBuffer data, - byte[] infoHash, Peer peer, long uploaded, long downloaded, - long left, boolean compact, boolean noPeerId, RequestEvent event, - int numWant) { - super(Type.ANNOUNCE_REQUEST, data); - this.infoHash = infoHash; - this.peer = peer; - this.downloaded = downloaded; - this.uploaded = uploaded; - this.left = left; - this.compact = compact; - this.noPeerId = noPeerId; - this.event = event; - this.numWant = numWant; - } - - @Override - public byte[] getInfoHash() { - return this.infoHash; - } - - @Override - public String getHexInfoHash() { - return Torrent.byteArrayToHexString(this.infoHash); - } - - @Override - public byte[] getPeerId() { - return this.peer.getPeerId().array(); - } - - @Override - public String getHexPeerId() { - return this.peer.getHexPeerId(); - } - - @Override - public int getPort() { - return this.peer.getPort(); - } - - @Override - public long getUploaded() { - return this.uploaded; - } - - @Override - public long getDownloaded() { - return this.downloaded; - } - - @Override - public long getLeft() { - return this.left; - } - - @Override - public boolean getCompact() { - return this.compact; - } - - @Override - public boolean getNoPeerIds() { - return this.noPeerId; - } - - @Override - public RequestEvent getEvent() { - return this.event; - } - - @Override - public String getIp() { - return this.peer.getIp(); - } - - @Override - public int getNumWant() { - return this.numWant; - } - - /** - * Build the announce request URL for the given tracker announce URL. - * - * @param trackerAnnounceURL The tracker's announce URL. - * @return The URL object representing the announce request URL. - */ - public URL buildAnnounceURL(URL trackerAnnounceURL) - throws UnsupportedEncodingException, MalformedURLException { - String base = trackerAnnounceURL.toString(); - StringBuilder url = new StringBuilder(base); - url.append(base.contains("?") ? "&" : "?") - .append("info_hash=") - .append(URLEncoder.encode( - new String(this.getInfoHash(), Torrent.BYTE_ENCODING), - Torrent.BYTE_ENCODING)) - .append("&peer_id=") - .append(URLEncoder.encode( - new String(this.getPeerId(), Torrent.BYTE_ENCODING), - Torrent.BYTE_ENCODING)) - .append("&port=").append(this.getPort()) - .append("&uploaded=").append(this.getUploaded()) - .append("&downloaded=").append(this.getDownloaded()) - .append("&left=").append(this.getLeft()) - .append("&compact=").append(this.getCompact() ? 1 : 0) - .append("&no_peer_id=").append(this.getNoPeerIds() ? 1 : 0); - - if (this.getEvent() != null && - !RequestEvent.NONE.equals(this.getEvent())) { - url.append("&event=").append(this.getEvent().getEventName()); - } - - if (this.getIp() != null) { - url.append("&ip=").append(this.getIp()); - } - - return new URL(url.toString()); - } - - public static HTTPAnnounceRequestMessage parse(ByteBuffer data) - throws IOException, MessageValidationException { - BEValue decoded = BDecoder.bdecode(data); - if (decoded == null) { - throw new MessageValidationException( - "Could not decode tracker message (not B-encoded?)!"); - } - - Map params = decoded.getMap(); - - if (!params.containsKey("info_hash")) { - throw new MessageValidationException( - ErrorMessage.FailureReason.MISSING_HASH.getMessage()); - } - - if (!params.containsKey("peer_id")) { - throw new MessageValidationException( - ErrorMessage.FailureReason.MISSING_PEER_ID.getMessage()); - } - - if (!params.containsKey("port")) { - throw new MessageValidationException( - ErrorMessage.FailureReason.MISSING_PORT.getMessage()); - } - - try { - byte[] infoHash = params.get("info_hash").getBytes(); - byte[] peerId = params.get("peer_id").getBytes(); - int port = params.get("port").getInt(); - - // Default 'uploaded' and 'downloaded' to 0 if the client does - // not provide it (although it should, according to the spec). - long uploaded = 0; - if (params.containsKey("uploaded")) { - uploaded = params.get("uploaded").getLong(); - } - - long downloaded = 0; - if (params.containsKey("downloaded")) { - downloaded = params.get("downloaded").getLong(); - } - - // Default 'left' to -1 to avoid peers entering the COMPLETED - // state when they don't provide the 'left' parameter. - long left = -1; - if (params.containsKey("left")) { - left = params.get("left").getLong(); - } - - boolean compact = false; - if (params.containsKey("compact")) { - compact = params.get("compact").getInt() == 1; - } - - boolean noPeerId = false; - if (params.containsKey("no_peer_id")) { - noPeerId = params.get("no_peer_id").getInt() == 1; - } - - int numWant = AnnounceRequestMessage.DEFAULT_NUM_WANT; - if (params.containsKey("numwant")) { - numWant = params.get("numwant").getInt(); - } - - String ip = null; - if (params.containsKey("ip")) { - ip = params.get("ip").getString(Torrent.BYTE_ENCODING); - } - - RequestEvent event = RequestEvent.NONE; - if (params.containsKey("event")) { - event = RequestEvent.getByName(params.get("event") - .getString(Torrent.BYTE_ENCODING)); - } - - return new HTTPAnnounceRequestMessage(data, infoHash, - new Peer(ip, port, ByteBuffer.wrap(peerId)), - downloaded, uploaded, left, compact, noPeerId, - event, numWant); - } catch (InvalidBEncodingException ibee) { - throw new MessageValidationException( - "Invalid HTTP tracker request!", ibee); - } - } - - public static HTTPAnnounceRequestMessage craft(byte[] infoHash, - byte[] peerId, int port, long uploaded, long downloaded, long left, - boolean compact, boolean noPeerId, RequestEvent event, - String ip, int numWant) - throws IOException, MessageValidationException, - UnsupportedEncodingException { - Map params = new HashMap(); - params.put("info_hash", new BEValue(infoHash)); - params.put("peer_id", new BEValue(peerId)); - params.put("port", new BEValue(port)); - params.put("uploaded", new BEValue(uploaded)); - params.put("downloaded", new BEValue(downloaded)); - params.put("left", new BEValue(left)); - params.put("compact", new BEValue(compact ? 1 : 0)); - params.put("no_peer_id", new BEValue(noPeerId ? 1 : 0)); - - if (event != null) { - params.put("event", - new BEValue(event.getEventName(), Torrent.BYTE_ENCODING)); - } - - if (ip != null) { - params.put("ip", - new BEValue(ip, Torrent.BYTE_ENCODING)); - } - - if (numWant != AnnounceRequestMessage.DEFAULT_NUM_WANT) { - params.put("numwant", new BEValue(numWant)); - } - - return new HTTPAnnounceRequestMessage( - BEncoder.bencode(params), - infoHash, new Peer(ip, port, ByteBuffer.wrap(peerId)), - uploaded, downloaded, left, compact, noPeerId, event, numWant); - } -} 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 deleted file mode 100644 index efb75fce3..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java +++ /dev/null @@ -1,205 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.http; - -import com.turn.ttorrent.bcodec.BDecoder; -import com.turn.ttorrent.bcodec.BEValue; -import com.turn.ttorrent.bcodec.BEncoder; -import com.turn.ttorrent.bcodec.InvalidBEncodingException; -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.Torrent; -import com.turn.ttorrent.common.protocol.TrackerMessage.AnnounceResponseMessage; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - - -/** - * The announce response message from an HTTP tracker. - * - * @author mpetazzoni - */ -public class HTTPAnnounceResponseMessage extends HTTPTrackerMessage - implements AnnounceResponseMessage { - - private final int interval; - private final int complete; - private final int incomplete; - private final List peers; - - private HTTPAnnounceResponseMessage(ByteBuffer data, - int interval, int complete, int incomplete, List peers) { - super(Type.ANNOUNCE_RESPONSE, data); - this.interval = interval; - this.complete = complete; - this.incomplete = incomplete; - this.peers = peers; - } - - @Override - public int getInterval() { - return this.interval; - } - - @Override - public int getComplete() { - return this.complete; - } - - @Override - public int getIncomplete() { - return this.incomplete; - } - - @Override - public List getPeers() { - return this.peers; - } - - public static HTTPAnnounceResponseMessage parse(ByteBuffer data) - throws IOException, MessageValidationException { - BEValue decoded = BDecoder.bdecode(data); - if (decoded == null) { - throw new MessageValidationException( - "Could not decode tracker message (not B-encoded?)!"); - } - - Map params = decoded.getMap(); - - try { - List peers; - - try { - // First attempt to decode a compact response, since we asked - // for it. - peers = toPeerList(params.get("peers").getBytes()); - } catch (InvalidBEncodingException ibee) { - // Fall back to peer list, non-compact response, in case the - // tracker did not support compact responses. - peers = toPeerList(params.get("peers").getList()); - } - - return new HTTPAnnounceResponseMessage(data, - params.get("interval").getInt(), - params.get("complete").getInt(), - params.get("incomplete").getInt(), - peers); - } catch (InvalidBEncodingException ibee) { - throw new MessageValidationException("Invalid response " + - "from tracker!", ibee); - } catch (UnknownHostException uhe) { - throw new MessageValidationException("Invalid peer " + - "in tracker response!", uhe); - } - } - - /** - * Build a peer list as a list of {@link Peer}s from the - * announce response's peer list (in non-compact mode). - * - * @param peers The list of {@link BEValue}s dictionaries describing the - * peers from the announce response. - * @return A {@link List} of {@link Peer}s representing the - * peers' addresses. Peer IDs are lost, but they are not crucial. - */ - private static List toPeerList(List peers) - throws InvalidBEncodingException { - List result = new LinkedList(); - - for (BEValue peer : peers) { - Map peerInfo = peer.getMap(); - result.add(new Peer( - peerInfo.get("ip").getString(Torrent.BYTE_ENCODING), - peerInfo.get("port").getInt())); - } - - return result; - } - - /** - * Build a peer list as a list of {@link Peer}s from the - * announce response's binary compact peer list. - * - * @param data The bytes representing the compact peer list from the - * announce response. - * @return A {@link List} of {@link Peer}s representing the - * peers' addresses. Peer IDs are lost, but they are not crucial. - */ - private static List toPeerList(byte[] data) - throws InvalidBEncodingException, UnknownHostException { - if (data.length % 6 != 0) { - throw new InvalidBEncodingException("Invalid peers " + - "binary information string!"); - } - - List result = new LinkedList(); - ByteBuffer peers = ByteBuffer.wrap(data); - - for (int i=0; i < data.length / 6 ; i++) { - byte[] ipBytes = new byte[4]; - peers.get(ipBytes); - InetAddress ip = InetAddress.getByAddress(ipBytes); - int port = - (0xFF & (int)peers.get()) << 8 | - (0xFF & (int)peers.get()); - result.add(new Peer(new InetSocketAddress(ip, port))); - } - - return result; - } - - /** - * Craft a compact announce response message. - * - * @param interval - * @param minInterval - * @param trackerId - * @param complete - * @param incomplete - * @param peers - */ - public static HTTPAnnounceResponseMessage craft(int interval, - int minInterval, String trackerId, int complete, int incomplete, - List peers) throws IOException, UnsupportedEncodingException { - Map response = new HashMap(); - response.put("interval", new BEValue(interval)); - response.put("complete", new BEValue(complete)); - response.put("incomplete", new BEValue(incomplete)); - - ByteBuffer data = ByteBuffer.allocate(peers.size() * 6); - for (Peer peer : peers) { - byte[] ip = peer.getRawIp(); - if (ip == null || ip.length != 4) { - continue; - } - data.put(ip); - data.putShort((short)peer.getPort()); - } - response.put("peers", new BEValue(data.array())); - - return new HTTPAnnounceResponseMessage( - BEncoder.bencode(response), - interval, complete, incomplete, peers); - } -} diff --git a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerErrorMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerErrorMessage.java deleted file mode 100644 index 96166ec65..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerErrorMessage.java +++ /dev/null @@ -1,87 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.http; - -import com.turn.ttorrent.bcodec.BDecoder; -import com.turn.ttorrent.bcodec.BEValue; -import com.turn.ttorrent.bcodec.BEncoder; -import com.turn.ttorrent.bcodec.InvalidBEncodingException; -import com.turn.ttorrent.common.Torrent; -import com.turn.ttorrent.common.protocol.TrackerMessage.ErrorMessage; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.Map; - - -/** - * An error message from an HTTP tracker. - * - * @author mpetazzoni - */ -public class HTTPTrackerErrorMessage extends HTTPTrackerMessage - implements ErrorMessage { - - private final String reason; - - private HTTPTrackerErrorMessage(ByteBuffer data, String reason) { - super(Type.ERROR, data); - this.reason = reason; - } - - @Override - public String getReason() { - return this.reason; - } - - public static HTTPTrackerErrorMessage parse(ByteBuffer data) - throws IOException, MessageValidationException { - BEValue decoded = BDecoder.bdecode(data); - if (decoded == null) { - throw new MessageValidationException( - "Could not decode tracker message (not B-encoded?)!"); - } - - Map params = decoded.getMap(); - - try { - return new HTTPTrackerErrorMessage( - data, - params.get("failure reason") - .getString(Torrent.BYTE_ENCODING)); - } catch (InvalidBEncodingException ibee) { - throw new MessageValidationException("Invalid tracker error " + - "message!", ibee); - } - } - - public static HTTPTrackerErrorMessage craft( - ErrorMessage.FailureReason reason) throws IOException, - MessageValidationException { - return HTTPTrackerErrorMessage.craft(reason.getMessage()); - } - - public static HTTPTrackerErrorMessage craft(String reason) - throws IOException, MessageValidationException { - Map params = new HashMap(); - params.put("failure reason", - new BEValue(reason, Torrent.BYTE_ENCODING)); - return new HTTPTrackerErrorMessage( - BEncoder.bencode(params), - reason); - } -} diff --git a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerMessage.java deleted file mode 100644 index 221826ca9..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerMessage.java +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.http; - -import com.turn.ttorrent.bcodec.BDecoder; -import com.turn.ttorrent.bcodec.BEValue; -import com.turn.ttorrent.common.protocol.TrackerMessage; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Map; - - -/** - * Base class for HTTP tracker messages. - * - * @author mpetazzoni - */ -public abstract class HTTPTrackerMessage extends TrackerMessage { - - protected HTTPTrackerMessage(Type type, ByteBuffer data) { - super(type, data); - } - - public static HTTPTrackerMessage parse(ByteBuffer data) - throws IOException, MessageValidationException { - BEValue decoded = BDecoder.bdecode(data); - if (decoded == null) { - throw new MessageValidationException( - "Could not decode tracker message (not B-encoded?)!"); - } - - Map params = decoded.getMap(); - - if (params.containsKey("info_hash")) { - return HTTPAnnounceRequestMessage.parse(data); - } else if (params.containsKey("peers")) { - return HTTPAnnounceResponseMessage.parse(data); - } else if (params.containsKey("failure reason")) { - return HTTPTrackerErrorMessage.parse(data); - } - - throw new MessageValidationException("Unknown HTTP tracker message!"); - } -} diff --git a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceRequestMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceRequestMessage.java deleted file mode 100644 index 3150ea890..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceRequestMessage.java +++ /dev/null @@ -1,253 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.udp; - -import com.turn.ttorrent.common.Torrent; -import com.turn.ttorrent.common.protocol.TrackerMessage; - -import java.net.InetAddress; -import java.net.Inet4Address; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; - - -/** - * The announce request message for the UDP tracker protocol. - * - * @author mpetazzoni - */ -public class UDPAnnounceRequestMessage - extends UDPTrackerMessage.UDPTrackerRequestMessage - implements TrackerMessage.AnnounceRequestMessage { - - private static final int UDP_ANNOUNCE_REQUEST_MESSAGE_SIZE = 98; - - private final long connectionId; - private final int actionId = Type.ANNOUNCE_REQUEST.getId(); - private final int transactionId; - private final byte[] infoHash; - private final byte[] peerId; - private final long downloaded; - private final long uploaded; - private final long left; - private final RequestEvent event; - private final InetAddress ip; - private final int numWant; - private final int key; - private final short port; - - private UDPAnnounceRequestMessage(ByteBuffer data, long connectionId, - int transactionId, byte[] infoHash, byte[] peerId, long downloaded, - long uploaded, long left, RequestEvent event, InetAddress ip, - int key, int numWant, short port) { - super(Type.ANNOUNCE_REQUEST, data); - this.connectionId = connectionId; - this.transactionId = transactionId; - this.infoHash = infoHash; - this.peerId = peerId; - this.downloaded = downloaded; - this.uploaded = uploaded; - this.left = left; - this.event = event; - this.ip = ip; - this.key = key; - this.numWant = numWant; - this.port = port; - } - - public long getConnectionId() { - return this.connectionId; - } - - @Override - public int getActionId() { - return this.actionId; - } - - @Override - public int getTransactionId() { - return this.transactionId; - } - - @Override - public byte[] getInfoHash() { - return this.infoHash; - } - - @Override - public String getHexInfoHash() { - return Torrent.byteArrayToHexString(this.infoHash); - } - - @Override - public byte[] getPeerId() { - return this.peerId; - } - - @Override - public String getHexPeerId() { - return Torrent.byteArrayToHexString(this.peerId); - } - - @Override - public int getPort() { - return this.port; - } - - @Override - public long getUploaded() { - return this.uploaded; - } - - @Override - public long getDownloaded() { - return this.downloaded; - } - - @Override - public long getLeft() { - return this.left; - } - - @Override - public boolean getCompact() { - return true; - } - - @Override - public boolean getNoPeerIds() { - return true; - } - - @Override - public RequestEvent getEvent() { - return this.event; - } - - @Override - public String getIp() { - return this.ip.toString(); - } - - @Override - public int getNumWant() { - return this.numWant; - } - - public int getKey() { - return this.key; - } - - public static UDPAnnounceRequestMessage parse(ByteBuffer data) - throws MessageValidationException { - if (data.remaining() != UDP_ANNOUNCE_REQUEST_MESSAGE_SIZE) { - throw new MessageValidationException( - "Invalid announce request message size!"); - } - - long connectionId = data.getLong(); - - if (data.getInt() != Type.ANNOUNCE_REQUEST.getId()) { - throw new MessageValidationException( - "Invalid action code for announce request!"); - } - - int transactionId = data.getInt(); - byte[] infoHash = new byte[20]; - data.get(infoHash); - byte[] peerId = new byte[20]; - data.get(peerId); - long downloaded = data.getLong(); - long uploaded = data.getLong(); - long left = data.getLong(); - - RequestEvent event = RequestEvent.getById(data.getInt()); - if (event == null) { - throw new MessageValidationException( - "Invalid event type in announce request!"); - } - - InetAddress ip = null; - try { - byte[] ipBytes = new byte[4]; - data.get(ipBytes); - ip = InetAddress.getByAddress(ipBytes); - } catch (UnknownHostException uhe) { - throw new MessageValidationException( - "Invalid IP address in announce request!"); - } - - int key = data.getInt(); - int numWant = data.getInt(); - short port = data.getShort(); - - return new UDPAnnounceRequestMessage(data, - connectionId, - transactionId, - infoHash, - peerId, - downloaded, - uploaded, - left, - event, - ip, - key, - numWant, - port); - } - - public static UDPAnnounceRequestMessage craft(long connectionId, - int transactionId, byte[] infoHash, byte[] peerId, long downloaded, - long uploaded, long left, RequestEvent event, InetAddress ip, - int key, int numWant, int port) { - if (infoHash.length != 20 || peerId.length != 20) { - throw new IllegalArgumentException(); - } - - if (! (ip instanceof Inet4Address)) { - throw new IllegalArgumentException("Only IPv4 addresses are " + - "supported by the UDP tracer protocol!"); - } - - ByteBuffer data = ByteBuffer.allocate(UDP_ANNOUNCE_REQUEST_MESSAGE_SIZE); - data.putLong(connectionId); - data.putInt(Type.ANNOUNCE_REQUEST.getId()); - data.putInt(transactionId); - data.put(infoHash); - data.put(peerId); - data.putLong(downloaded); - data.putLong(uploaded); - data.putLong(left); - data.putInt(event.getId()); - data.put(ip.getAddress()); - data.putInt(key); - data.putInt(numWant); - data.putShort((short)port); - return new UDPAnnounceRequestMessage(data, - connectionId, - transactionId, - infoHash, - peerId, - downloaded, - uploaded, - left, - event, - ip, - key, - numWant, - (short)port); - } -} diff --git a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessage.java deleted file mode 100644 index 612012d3b..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessage.java +++ /dev/null @@ -1,160 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.udp; - -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.protocol.TrackerMessage; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.List; - -/** - * The announce response message for the UDP tracker protocol. - * - * @author mpetazzoni - */ -public class UDPAnnounceResponseMessage - extends UDPTrackerMessage.UDPTrackerResponseMessage - implements TrackerMessage.AnnounceResponseMessage { - - private static final int UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE = 20; - - private final int actionId = Type.ANNOUNCE_RESPONSE.getId(); - private final int transactionId; - private final int interval; - private final int complete; - private final int incomplete; - private final List peers; - - private UDPAnnounceResponseMessage(ByteBuffer data, int transactionId, - int interval, int complete, int incomplete, List peers) { - super(Type.ANNOUNCE_REQUEST, data); - this.transactionId = transactionId; - this.interval = interval; - this.complete = complete; - this.incomplete = incomplete; - this.peers = peers; - } - - @Override - public int getActionId() { - return this.actionId; - } - - @Override - public int getTransactionId() { - return this.transactionId; - } - - @Override - public int getInterval() { - return this.interval; - } - - @Override - public int getComplete() { - return this.complete; - } - - @Override - public int getIncomplete() { - return this.incomplete; - } - - @Override - public List getPeers() { - return this.peers; - } - - public static UDPAnnounceResponseMessage parse(ByteBuffer data) - throws MessageValidationException { - if (data.remaining() < UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE || - (data.remaining() - UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE) % 6 != 0) { - throw new MessageValidationException( - "Invalid announce response message size!"); - } - - if (data.getInt() != Type.ANNOUNCE_RESPONSE.getId()) { - throw new MessageValidationException( - "Invalid action code for announce response!"); - } - - int transactionId = data.getInt(); - int interval = data.getInt(); - int incomplete = data.getInt(); - int complete = data.getInt(); - - List peers = new LinkedList(); - for (int i=0; i < data.remaining() / 6; i++) { - try { - byte[] ipBytes = new byte[4]; - data.get(ipBytes); - InetAddress ip = InetAddress.getByAddress(ipBytes); - int port = - (0xFF & (int)data.get()) << 8 | - (0xFF & (int)data.get()); - peers.add(new Peer(new InetSocketAddress(ip, port))); - } catch (UnknownHostException uhe) { - throw new MessageValidationException( - "Invalid IP address in announce request!"); - } - } - - return new UDPAnnounceResponseMessage(data, - transactionId, - interval, - complete, - incomplete, - peers); - } - - public static UDPAnnounceResponseMessage craft(int transactionId, - int interval, int complete, int incomplete, List peers) { - ByteBuffer data = ByteBuffer - .allocate(UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE + 6*peers.size()); - data.putInt(Type.ANNOUNCE_RESPONSE.getId()); - data.putInt(transactionId); - data.putInt(interval); - - /** - * Leechers (incomplete) are first, before seeders (complete) in the packet. - */ - data.putInt(incomplete); - data.putInt(complete); - - for (Peer peer : peers) { - byte[] ip = peer.getRawIp(); - if (ip == null || ip.length != 4) { - continue; - } - - data.put(ip); - data.putShort((short)peer.getPort()); - } - - return new UDPAnnounceResponseMessage(data, - transactionId, - interval, - complete, - incomplete, - peers); - } -} - diff --git a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectRequestMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectRequestMessage.java deleted file mode 100644 index b1b0c2ecb..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectRequestMessage.java +++ /dev/null @@ -1,89 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.udp; - -import com.turn.ttorrent.common.protocol.TrackerMessage; - -import java.nio.ByteBuffer; - - -/** - * The connection request message for the UDP tracker protocol. - * - * @author mpetazzoni - */ -public class UDPConnectRequestMessage - extends UDPTrackerMessage.UDPTrackerRequestMessage - implements TrackerMessage.ConnectionRequestMessage { - - private static final int UDP_CONNECT_REQUEST_MESSAGE_SIZE = 16; - private static final long UDP_CONNECT_REQUEST_MAGIC = 0x41727101980L; - - private final long connectionId = UDP_CONNECT_REQUEST_MAGIC; - private final int actionId = Type.CONNECT_REQUEST.getId(); - private final int transactionId; - - private UDPConnectRequestMessage(ByteBuffer data, int transactionId) { - super(Type.CONNECT_REQUEST, data); - this.transactionId = transactionId; - } - - public long getConnectionId() { - return this.connectionId; - } - - @Override - public int getActionId() { - return this.actionId; - } - - @Override - public int getTransactionId() { - return this.transactionId; - } - - public static UDPConnectRequestMessage parse(ByteBuffer data) - throws MessageValidationException { - if (data.remaining() != UDP_CONNECT_REQUEST_MESSAGE_SIZE) { - throw new MessageValidationException( - "Invalid connect request message size!"); - } - - if (data.getLong() != UDP_CONNECT_REQUEST_MAGIC) { - throw new MessageValidationException( - "Invalid connection ID in connection request!"); - } - - if (data.getInt() != Type.CONNECT_REQUEST.getId()) { - throw new MessageValidationException( - "Invalid action code for connection request!"); - } - - return new UDPConnectRequestMessage(data, - data.getInt() // transactionId - ); - } - - public static UDPConnectRequestMessage craft(int transactionId) { - ByteBuffer data = ByteBuffer - .allocate(UDP_CONNECT_REQUEST_MESSAGE_SIZE); - data.putLong(UDP_CONNECT_REQUEST_MAGIC); - data.putInt(Type.CONNECT_REQUEST.getId()); - data.putInt(transactionId); - return new UDPConnectRequestMessage(data, - transactionId); - } -} diff --git a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectResponseMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectResponseMessage.java deleted file mode 100644 index 0a9f4bf64..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectResponseMessage.java +++ /dev/null @@ -1,88 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.udp; - -import com.turn.ttorrent.common.protocol.TrackerMessage; - -import java.nio.ByteBuffer; - - -/** - * The connection response message for the UDP tracker protocol. - * - * @author mpetazzoni - */ -public class UDPConnectResponseMessage - extends UDPTrackerMessage.UDPTrackerResponseMessage - implements TrackerMessage.ConnectionResponseMessage { - - private static final int UDP_CONNECT_RESPONSE_MESSAGE_SIZE = 16; - - private final int actionId = Type.CONNECT_RESPONSE.getId(); - private final int transactionId; - private final long connectionId; - - private UDPConnectResponseMessage(ByteBuffer data, int transactionId, - long connectionId) { - super(Type.CONNECT_RESPONSE, data); - this.transactionId = transactionId; - this.connectionId = connectionId; - } - - @Override - public int getActionId() { - return this.actionId; - } - - @Override - public int getTransactionId() { - return this.transactionId; - } - - public long getConnectionId() { - return this.connectionId; - } - - public static UDPConnectResponseMessage parse(ByteBuffer data) - throws MessageValidationException { - if (data.remaining() != UDP_CONNECT_RESPONSE_MESSAGE_SIZE) { - throw new MessageValidationException( - "Invalid connect response message size!"); - } - - if (data.getInt() != Type.CONNECT_RESPONSE.getId()) { - throw new MessageValidationException( - "Invalid action code for connection response!"); - } - - return new UDPConnectResponseMessage(data, - data.getInt(), // transactionId - data.getLong() // connectionId - ); - } - - public static UDPConnectResponseMessage craft(int transactionId, - long connectionId) { - ByteBuffer data = ByteBuffer - .allocate(UDP_CONNECT_RESPONSE_MESSAGE_SIZE); - data.putInt(Type.CONNECT_RESPONSE.getId()); - data.putInt(transactionId); - data.putLong(connectionId); - return new UDPConnectResponseMessage(data, - transactionId, - connectionId); - } -} diff --git a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerErrorMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerErrorMessage.java deleted file mode 100644 index 01bf567db..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerErrorMessage.java +++ /dev/null @@ -1,102 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.udp; - -import com.turn.ttorrent.common.Torrent; -import com.turn.ttorrent.common.protocol.TrackerMessage; - -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; - - -/** - * The error message for the UDP tracker protocol. - * - * @author mpetazzoni - */ -public class UDPTrackerErrorMessage - extends UDPTrackerMessage.UDPTrackerResponseMessage - implements TrackerMessage.ErrorMessage { - - private static final int UDP_TRACKER_ERROR_MIN_MESSAGE_SIZE = 8; - - private final int actionId = Type.ERROR.getId(); - private final int transactionId; - private final String reason; - - private UDPTrackerErrorMessage(ByteBuffer data, int transactionId, - String reason) { - super(Type.ERROR, data); - this.transactionId = transactionId; - this.reason = reason; - } - - @Override - public int getActionId() { - return this.actionId; - } - - @Override - public int getTransactionId() { - return this.transactionId; - } - - @Override - public String getReason() { - return this.reason; - } - - public static UDPTrackerErrorMessage parse(ByteBuffer data) - throws MessageValidationException { - if (data.remaining() < UDP_TRACKER_ERROR_MIN_MESSAGE_SIZE) { - throw new MessageValidationException( - "Invalid tracker error message size!"); - } - - if (data.getInt() != Type.ERROR.getId()) { - throw new MessageValidationException( - "Invalid action code for tracker error!"); - } - - int transactionId = data.getInt(); - byte[] reasonBytes = new byte[data.remaining()]; - data.get(reasonBytes); - - try { - return new UDPTrackerErrorMessage(data, - transactionId, - new String(reasonBytes, Torrent.BYTE_ENCODING) - ); - } catch (UnsupportedEncodingException uee) { - throw new MessageValidationException( - "Could not decode error message!", uee); - } - } - - public static UDPTrackerErrorMessage craft(int transactionId, - String reason) throws UnsupportedEncodingException { - byte[] reasonBytes = reason.getBytes(Torrent.BYTE_ENCODING); - ByteBuffer data = ByteBuffer - .allocate(UDP_TRACKER_ERROR_MIN_MESSAGE_SIZE + - reasonBytes.length); - data.putInt(Type.ERROR.getId()); - data.putInt(transactionId); - data.put(reasonBytes); - return new UDPTrackerErrorMessage(data, - transactionId, - reason); - } -} diff --git a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerMessage.java b/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerMessage.java deleted file mode 100644 index afde16924..000000000 --- a/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerMessage.java +++ /dev/null @@ -1,107 +0,0 @@ -/** - * 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. - */ -package com.turn.ttorrent.common.protocol.udp; - -import com.turn.ttorrent.common.protocol.TrackerMessage; - -import java.nio.ByteBuffer; - -/** - * Base class for UDP tracker messages. - * - * @author mpetazzoni - */ -public abstract class UDPTrackerMessage extends TrackerMessage { - - private UDPTrackerMessage(Type type, ByteBuffer data) { - super(type, data); - } - - public abstract int getActionId(); - public abstract int getTransactionId(); - - public static abstract class UDPTrackerRequestMessage - extends UDPTrackerMessage { - - private static final int UDP_MIN_REQUEST_PACKET_SIZE = 16; - - protected UDPTrackerRequestMessage(Type type, ByteBuffer data) { - super(type, data); - } - - public static UDPTrackerRequestMessage parse(ByteBuffer data) - throws MessageValidationException { - if (data.remaining() < UDP_MIN_REQUEST_PACKET_SIZE) { - throw new MessageValidationException("Invalid packet size!"); - } - - /** - * UDP request packets always start with the connection ID (8 bytes), - * followed by the action (4 bytes). Extract the action code - * accordingly. - */ - data.mark(); - data.getLong(); - int action = data.getInt(); - data.reset(); - - if (action == Type.CONNECT_REQUEST.getId()) { - return UDPConnectRequestMessage.parse(data); - } else if (action == Type.ANNOUNCE_REQUEST.getId()) { - return UDPAnnounceRequestMessage.parse(data); - } - - throw new MessageValidationException("Unknown UDP tracker " + - "request message!"); - } - }; - - public static abstract class UDPTrackerResponseMessage - extends UDPTrackerMessage { - - private static final int UDP_MIN_RESPONSE_PACKET_SIZE = 8; - - protected UDPTrackerResponseMessage(Type type, ByteBuffer data) { - super(type, data); - } - - public static UDPTrackerResponseMessage parse(ByteBuffer data) - throws MessageValidationException { - if (data.remaining() < UDP_MIN_RESPONSE_PACKET_SIZE) { - throw new MessageValidationException("Invalid packet size!"); - } - - /** - * UDP response packets always start with the action (4 bytes), so - * we can extract it immediately. - */ - data.mark(); - int action = data.getInt(); - data.reset(); - - if (action == Type.CONNECT_RESPONSE.getId()) { - return UDPConnectResponseMessage.parse(data); - } else if (action == Type.ANNOUNCE_RESPONSE.getId()) { - return UDPAnnounceResponseMessage.parse(data); - } else if (action == Type.ERROR.getId()) { - return UDPTrackerErrorMessage.parse(data); - } - - throw new MessageValidationException("Unknown UDP tracker " + - "response message!"); - } - }; -} diff --git a/src/main/java/com/turn/ttorrent/tracker/TrackedPeer.java b/src/main/java/com/turn/ttorrent/tracker/TrackedPeer.java deleted file mode 100644 index f866580e8..000000000 --- a/src/main/java/com/turn/ttorrent/tracker/TrackedPeer.java +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.tracker; - -import com.turn.ttorrent.bcodec.BEValue; -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.Torrent; - -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * A BitTorrent tracker peer. - * - *

- * Represents a peer exchanging on a given torrent. In this implementation, - * we don't really care about the status of the peers and how much they - * have downloaded / exchanged because we are not a torrent exchange and - * don't need to keep track of what peers are doing while they're - * downloading. We only care about when they start, and when they are done. - *

- * - *

- * We also never expire peers automatically. Unless peers send a STOPPED - * announce request, they remain as long as the torrent object they are a - * part of. - *

- */ -public class TrackedPeer extends Peer { - - private static final Logger logger = - LoggerFactory.getLogger(TrackedPeer.class); - - private static final int FRESH_TIME_SECONDS = 30; - - private long uploaded; - private long downloaded; - private long left; - private Torrent torrent; - - /** - * Represents the state of a peer exchanging on this torrent. - * - *

- * Peers can be in the STARTED state, meaning they have announced - * themselves to us and are eventually exchanging data with other peers. - * Note that a peer starting with a completed file will also be in the - * started state and will never notify as being in the completed state. - * This information can be inferred from the fact that the peer reports 0 - * bytes left to download. - *

- * - *

- * Peers enter the COMPLETED state when they announce they have entirely - * downloaded the file. As stated above, we may also elect them for this - * state if they report 0 bytes left to download. - *

- * - *

- * Peers enter the STOPPED state very briefly before being removed. We - * still pass them to the STOPPED state in case someone else kept a - * reference on them. - *

- */ - public enum PeerState { - UNKNOWN, - STARTED, - COMPLETED, - STOPPED; - }; - - private PeerState state; - private Date lastAnnounce; - - /** - * Instantiate a new tracked peer for the given torrent. - * - * @param torrent The torrent this peer exchanges on. - * @param ip The peer's IP address. - * @param port The peer's port. - * @param peerId The byte-encoded peer ID. - */ - public TrackedPeer(Torrent torrent, String ip, int port, - ByteBuffer peerId) { - super(ip, port, peerId); - this.torrent = torrent; - - // Instantiated peers start in the UNKNOWN state. - this.state = PeerState.UNKNOWN; - this.lastAnnounce = null; - - this.uploaded = 0; - this.downloaded = 0; - this.left = 0; - } - - /** - * Update this peer's state and information. - * - *

- * Note: if the peer reports 0 bytes left to download, its state will - * be automatically be set to COMPLETED. - *

- * - * @param state The peer's state. - * @param uploaded Uploaded byte count, as reported by the peer. - * @param downloaded Downloaded byte count, as reported by the peer. - * @param left Left-to-download byte count, as reported by the peer. - */ - public void update(PeerState state, long uploaded, long downloaded, - long left) { - if (PeerState.STARTED.equals(state) && left == 0) { - state = PeerState.COMPLETED; - } - - if (!state.equals(this.state)) { - logger.info("Peer {} {} download of {}.", - new Object[] { - this, - state.name().toLowerCase(), - this.torrent, - }); - } - - this.state = state; - this.lastAnnounce = new Date(); - this.uploaded = uploaded; - this.downloaded = downloaded; - this.left = left; - } - - /** - * Tells whether this peer has completed its download and can thus be - * considered a seeder. - */ - public boolean isCompleted() { - return PeerState.COMPLETED.equals(this.state); - } - - /** - * Returns how many bytes the peer reported it has uploaded so far. - */ - public long getUploaded() { - return this.uploaded; - } - - /** - * Returns how many bytes the peer reported it has downloaded so far. - */ - public long getDownloaded() { - return this.downloaded; - } - - /** - * Returns how many bytes the peer reported it needs to retrieve before - * its download is complete. - */ - public long getLeft() { - return this.left; - } - - /** - * Tells whether this peer has checked in with the tracker recently. - * - *

- * Non-fresh peers are automatically terminated and collected by the - * Tracker. - *

- */ - public boolean isFresh() { - return (this.lastAnnounce != null && - (this.lastAnnounce.getTime() + (FRESH_TIME_SECONDS * 1000) > - new Date().getTime())); - } - - /** - * Returns a BEValue representing this peer for inclusion in an - * announce reply from the tracker. - * - * The returned BEValue is a dictionary containing the peer ID (in its - * original byte-encoded form), the peer's IP and the peer's port. - */ - public BEValue toBEValue() throws UnsupportedEncodingException { - Map peer = new HashMap(); - if (this.hasPeerId()) { - peer.put("peer id", new BEValue(this.getPeerId().array())); - } - peer.put("ip", new BEValue(this.getIp(), Torrent.BYTE_ENCODING)); - peer.put("port", new BEValue(this.getPort())); - return new BEValue(peer); - } -} diff --git a/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java b/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java deleted file mode 100644 index 98a2734b5..000000000 --- a/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java +++ /dev/null @@ -1,312 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.tracker; - -import com.turn.ttorrent.common.Peer; -import com.turn.ttorrent.common.Torrent; -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; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * Tracked torrents are torrent for which we don't expect to have data files - * for. - * - *

- * {@link TrackedTorrent} objects are used by the BitTorrent tracker to - * represent a torrent that is announced by the tracker. As such, it is not - * expected to point to any valid local data like. It also contains some - * additional information used by the tracker to keep track of which peers - * exchange on it, etc. - *

- * - * @author mpetazzoni - */ -public class TrackedTorrent extends Torrent { - - private static final Logger logger = - LoggerFactory.getLogger(TrackedTorrent.class); - - /** Minimum announce interval requested from peers, in seconds. */ - public static final int MIN_ANNOUNCE_INTERVAL_SECONDS = 5; - - /** Default number of peers included in a tracker response. */ - private static final int DEFAULT_ANSWER_NUM_PEERS = 30; - - /** Default announce interval requested from peers, in seconds. */ - private static final int DEFAULT_ANNOUNCE_INTERVAL_SECONDS = 10; - - private int answerPeers; - private int announceInterval; - - /** Peers currently exchanging on this torrent. */ - private ConcurrentMap peers; - - /** - * Create a new tracked torrent from meta-info binary data. - * - * @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 { - super(torrent, false); - - this.peers = new ConcurrentHashMap(); - this.answerPeers = TrackedTorrent.DEFAULT_ANSWER_NUM_PEERS; - this.announceInterval = TrackedTorrent.DEFAULT_ANNOUNCE_INTERVAL_SECONDS; - } - - public TrackedTorrent(Torrent torrent) - throws IOException, NoSuchAlgorithmException { - this(torrent.getEncoded()); - } - - /** - * Returns the map of all peers currently exchanging on this torrent. - */ - public Map getPeers() { - return this.peers; - } - - /** - * Add a peer exchanging on this torrent. - * - * @param peer The new Peer involved with this torrent. - */ - public void addPeer(TrackedPeer peer) { - this.peers.put(peer.getHexPeerId(), peer); - } - - /** - * Retrieve a peer exchanging on this torrent. - * - * @param peerId The hexadecimal representation of the peer's ID. - */ - public TrackedPeer getPeer(String peerId) { - return this.peers.get(peerId); - } - - /** - * Remove a peer from this torrent's swarm. - * - * @param peerId The hexadecimal representation of the peer's ID. - */ - public TrackedPeer removePeer(String peerId) { - return this.peers.remove(peerId); - } - - /** - * Count the number of seeders (peers in the COMPLETED state) on this - * torrent. - */ - public int seeders() { - int count = 0; - for (TrackedPeer peer : this.peers.values()) { - if (peer.isCompleted()) { - count++; - } - } - return count; - } - - /** - * Count the number of leechers (non-COMPLETED peers) on this torrent. - */ - public int leechers() { - int count = 0; - for (TrackedPeer peer : this.peers.values()) { - if (!peer.isCompleted()) { - count++; - } - } - return count; - } - - /** - * Remove unfresh peers from this torrent. - * - *

- * Collect and remove all non-fresh peers from this torrent. This is - * usually called by the periodic peer collector of the BitTorrent tracker. - *

- */ - public void collectUnfreshPeers() { - for (TrackedPeer peer : this.peers.values()) { - if (!peer.isFresh()) { - this.peers.remove(peer.getHexPeerId()); - } - } - } - - /** - * Get the announce interval for this torrent. - */ - public int getAnnounceInterval() { - return this.announceInterval; - } - - /** - * Set the announce interval for this torrent. - * - * @param interval New announce interval, in seconds. - */ - public void setAnnounceInterval(int interval) { - if (interval <= 0) { - throw new IllegalArgumentException("Invalid announce interval"); - } - - this.announceInterval = interval; - } - - /** - * Update this torrent's swarm from an announce event. - * - *

- * This will automatically create a new peer on a 'started' announce event, - * and remove the peer on a 'stopped' announce event. - *

- * - * @param event The reported event. If null, means a regular - * interval announce event, as defined in the BitTorrent specification. - * @param peerId The byte-encoded peer ID. - * @param hexPeerId The hexadecimal representation of the peer's ID. - * @param ip The peer's IP address. - * @param port The peer's inbound port. - * @param uploaded The peer's reported uploaded byte count. - * @param downloaded The peer's reported downloaded byte count. - * @param left The peer's reported left to download byte count. - * @return The peer that sent us the announce request. - */ - public TrackedPeer update(RequestEvent event, ByteBuffer peerId, - String hexPeerId, String ip, int port, long uploaded, long downloaded, - long left) throws UnsupportedEncodingException { - TrackedPeer peer; - TrackedPeer.PeerState state = TrackedPeer.PeerState.UNKNOWN; - - if (RequestEvent.STARTED.equals(event)) { - peer = new TrackedPeer(this, ip, port, peerId); - state = TrackedPeer.PeerState.STARTED; - this.addPeer(peer); - } else if (RequestEvent.STOPPED.equals(event)) { - peer = this.removePeer(hexPeerId); - state = TrackedPeer.PeerState.STOPPED; - } else if (RequestEvent.COMPLETED.equals(event)) { - peer = this.getPeer(hexPeerId); - state = TrackedPeer.PeerState.COMPLETED; - } else if (RequestEvent.NONE.equals(event)) { - peer = this.getPeer(hexPeerId); - state = TrackedPeer.PeerState.STARTED; - } else { - throw new IllegalArgumentException("Unexpected announce event type!"); - } - - peer.update(state, uploaded, downloaded, left); - return peer; - } - - /** - * Get a list of peers we can return in an announce response for this - * torrent. - * - * @param peer The peer making the request, so we can exclude it from the - * list of returned peers. - * @return A list of peers we can include in an announce response. - */ - public List getSomePeers(TrackedPeer peer) { - List peers = new LinkedList(); - - // Extract answerPeers random peers - List candidates = - new LinkedList(this.peers.values()); - Collections.shuffle(candidates); - - int count = 0; - for (TrackedPeer candidate : candidates) { - // Collect unfresh peers, and obviously don't serve them as well. - if (!candidate.isFresh() || - (candidate.looksLike(peer) && !candidate.equals(peer))) { - logger.debug("Collecting stale peer {}...", candidate); - this.peers.remove(candidate.getHexPeerId()); - continue; - } - - // Don't include the requesting peer in the answer. - if (peer.looksLike(candidate)) { - continue; - } - - // Collect unfresh peers, and obviously don't serve them as well. - if (!candidate.isFresh()) { - logger.debug("Collecting stale peer {}...", - candidate.getHexPeerId()); - this.peers.remove(candidate.getHexPeerId()); - continue; - } - - // Only serve at most ANSWER_NUM_PEERS peers - if (count++ > this.answerPeers) { - break; - } - - peers.add(candidate); - } - - return peers; - } - - /** - * Load a tracked torrent from the given torrent file. - * - * @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(); - } - } - } -} diff --git a/src/main/java/com/turn/ttorrent/tracker/Tracker.java b/src/main/java/com/turn/ttorrent/tracker/Tracker.java deleted file mode 100644 index 6ba05d8b1..000000000 --- a/src/main/java/com/turn/ttorrent/tracker/Tracker.java +++ /dev/null @@ -1,399 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.tracker; - -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.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; - -/** - * BitTorrent tracker. - * - *

- * The tracker usually listens on port 6969 (the standard BitTorrent tracker - * port). Torrents must be registered directly to this tracker with the - * {@link #announce(TrackedTorrent torrent)} method. - *

- * - * @author mpetazzoni - */ -public class Tracker { - - private static final Logger logger = - LoggerFactory.getLogger(Tracker.class); - - /** Request path handled by the tracker announce request handler. */ - public static final String ANNOUNCE_URL = "/announce"; - - /** Default tracker listening port (BitTorrent's default is 6969). */ - public static final int DEFAULT_TRACKER_PORT = 6969; - - /** Default server name and version announced by the tracker. */ - public static final String DEFAULT_VERSION_STRING = - "BitTorrent Tracker (ttorrent)"; - - private final Connection connection; - private final InetSocketAddress address; - - /** The in-memory repository of torrents tracked. */ - private final ConcurrentMap torrents; - - private Thread tracker; - private Thread collector; - private boolean stop; - - /** - * Create a new BitTorrent tracker listening at the given address on the - * default port. - * - * @param address The address to bind to. - * @throws IOException Throws an IOException if the tracker - * cannot be initialized. - */ - public Tracker(InetAddress address) throws IOException { - this(new InetSocketAddress(address, DEFAULT_TRACKER_PORT), - DEFAULT_VERSION_STRING); - } - - /** - * Create a new BitTorrent tracker listening at the given address. - * - * @param address The address to bind to. - * @throws IOException Throws an IOException if the tracker - * cannot be initialized. - */ - public Tracker(InetSocketAddress address) throws IOException { - this(address, DEFAULT_VERSION_STRING); - } - - /** - * Create a new BitTorrent tracker listening at the given address. - * - * @param address The address to bind to. - * @param version A version string served in the HTTP headers - * @throws IOException Throws an IOException if the tracker - * cannot be initialized. - */ - public Tracker(InetSocketAddress address, String version) - throws IOException { - this.address = address; - - this.torrents = new ConcurrentHashMap(); - this.connection = new SocketConnection( - new TrackerService(version, this.torrents)); - } - - /** - * Returns the full announce URL served by this tracker. - * - *

- * This has the form http://host:port/announce. - *

- */ - public URL getAnnounceUrl() { - try { - return new URL("http", - this.address.getAddress().getCanonicalHostName(), - this.address.getPort(), - Tracker.ANNOUNCE_URL); - } catch (MalformedURLException mue) { - logger.error("Could not build tracker URL: {}!", mue, mue); - } - - return null; - } - - /** - * Start the tracker thread. - */ - public void start() { - if (this.tracker == null || !this.tracker.isAlive()) { - this.tracker = new TrackerThread(); - this.tracker.setName("tracker:" + this.address.getPort()); - this.tracker.start(); - } - - if (this.collector == null || !this.collector.isAlive()) { - this.collector = new PeerCollectorThread(); - this.collector.setName("peer-collector:" + this.address.getPort()); - this.collector.start(); - } - } - - /** - * Stop the tracker. - * - *

- * This effectively closes the listening HTTP connection to terminate - * the service, and interrupts the peer collector thread as well. - *

- */ - public void stop() { - this.stop = true; - - try { - this.connection.close(); - logger.info("BitTorrent tracker closed."); - } catch (IOException ioe) { - logger.error("Could not stop the tracker: {}!", ioe.getMessage()); - } - - if (this.collector != null && this.collector.isAlive()) { - this.collector.interrupt(); - logger.info("Peer collection terminated."); - } - } - - /** - * Announce a new torrent on this tracker. - * - *

- * The fact that torrents must be announced here first makes this tracker a - * closed BitTorrent tracker: it will only accept clients for torrents it - * knows about, and this list of torrents is managed by the program - * instrumenting this Tracker class. - *

- * - * @param torrent The Torrent object to start tracking. - * @return The torrent object for this torrent on this tracker. This may be - * different from the supplied Torrent object if the tracker already - * contained a torrent with the same hash. - */ - public synchronized TrackedTorrent announce(TrackedTorrent torrent) { - TrackedTorrent existing = this.torrents.get(torrent.getHexInfoHash()); - - if (existing != null) { - logger.warn("Tracker already announced torrent for '{}' " + - "with hash {}.", existing.getName(), existing.getHexInfoHash()); - return existing; - } - - this.torrents.put(torrent.getHexInfoHash(), torrent); - logger.info("Registered new torrent for '{}' with hash {}.", - torrent.getName(), torrent.getHexInfoHash()); - return torrent; - } - - /** - * Stop announcing the given torrent. - * - * @param torrent The Torrent object to stop tracking. - */ - public synchronized void remove(Torrent torrent) { - if (torrent == null) { - return; - } - - this.torrents.remove(torrent.getHexInfoHash()); - } - - /** - * Stop announcing the given torrent after a delay. - * - * @param torrent The Torrent object to stop tracking. - * @param delay The delay, in milliseconds, before removing the torrent. - */ - public synchronized void remove(Torrent torrent, long delay) { - if (torrent == null) { - return; - } - - new Timer().schedule(new TorrentRemoveTimer(this, torrent), delay); - } - - /** - * Timer task for removing a torrent from a tracker. - * - *

- * This task can be used to stop announcing a torrent after a certain delay - * through a Timer. - *

- */ - private static class TorrentRemoveTimer extends TimerTask { - - private Tracker tracker; - private Torrent torrent; - - TorrentRemoveTimer(Tracker tracker, Torrent torrent) { - this.tracker = tracker; - this.torrent = torrent; - } - - @Override - public void run() { - this.tracker.remove(torrent); - } - } - - /** - * The main tracker thread. - * - *

- * The core of the BitTorrent tracker run by the controller is the - * SimpleFramework HTTP service listening on the configured address. It can - * be stopped with the stop() method, which closes the listening - * socket. - *

- */ - private class TrackerThread extends Thread { - - @Override - public void run() { - logger.info("Starting BitTorrent tracker on {}...", - getAnnounceUrl()); - - try { - connection.connect(address); - } catch (IOException ioe) { - logger.error("Could not start the tracker: {}!", ioe.getMessage()); - Tracker.this.stop(); - } - } - } - - /** - * The unfresh peer collector thread. - * - *

- * Every PEER_COLLECTION_FREQUENCY_SECONDS, this thread will collect - * unfresh peers from all announced torrents. - *

- */ - private class PeerCollectorThread extends Thread { - - private static final int PEER_COLLECTION_FREQUENCY_SECONDS = 15; - - @Override - public void run() { - logger.info("Starting tracker peer collection for tracker at {}...", - getAnnounceUrl()); - - while (!stop) { - for (TrackedTorrent torrent : torrents.values()) { - torrent.collectUnfreshPeers(); - } - - try { - Thread.sleep(PeerCollectorThread - .PEER_COLLECTION_FREQUENCY_SECONDS * 1000); - } catch (InterruptedException ie) { - // Ignore - } - } - } - } - - /** - * 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 deleted file mode 100644 index 014ec5da6..000000000 --- a/src/main/java/com/turn/ttorrent/tracker/TrackerService.java +++ /dev/null @@ -1,359 +0,0 @@ -/** - * Copyright (C) 2011-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. - */ -package com.turn.ttorrent.tracker; - -import com.turn.ttorrent.bcodec.BEValue; -import com.turn.ttorrent.bcodec.BEncoder; -import com.turn.ttorrent.common.protocol.TrackerMessage.*; -import com.turn.ttorrent.common.protocol.http.*; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.simpleframework.http.Request; -import org.simpleframework.http.Response; -import org.simpleframework.http.Status; -import org.simpleframework.http.core.Container; - - -/** - * Tracker service to serve the tracker's announce requests. - * - *

- * It only serves announce requests on /announce, and only serves torrents the - * {@link Tracker} it serves knows about. - *

- * - *

- * The list of torrents {@link #torrents} is a map of torrent hashes to their - * corresponding Torrent objects, and is maintained by the {@link Tracker} this - * service is part of. The TrackerService only has a reference to this map, and - * does not modify it. - *

- * - * @author mpetazzoni - * @see BitTorrent protocol specification - */ -public class TrackerService implements Container { - - private static final Logger logger = - LoggerFactory.getLogger(TrackerService.class); - - /** - * The list of announce request URL fields that need to be interpreted as - * numeric and thus converted as such in the request message parsing. - */ - private static final String[] NUMERIC_REQUEST_FIELDS = - new String[] { - "port", "uploaded", "downloaded", "left", - "compact", "no_peer_id", "numwant" - }; - - private final String version; - private final ConcurrentMap torrents; - - - /** - * Create a new TrackerService serving the given torrents. - * - * @param torrents The torrents this TrackerService should serve requests - * for. - */ - TrackerService(String version, - ConcurrentMap torrents) { - this.version = version; - this.torrents = torrents; - } - - /** - * Handle the incoming request on the tracker service. - * - *

- * This makes sure the request is made to the tracker's announce URL, and - * delegates handling of the request to the process() method after - * preparing the response object. - *

- * - * @param request The incoming HTTP request. - * @param response The response object. - */ - public void handle(Request request, Response response) { - // Reject non-announce requests - if (!Tracker.ANNOUNCE_URL.equals(request.getPath().toString())) { - response.setCode(404); - response.setText("Not Found"); - return; - } - - OutputStream body = null; - try { - body = response.getOutputStream(); - this.process(request, response, body); - body.flush(); - } catch (IOException ioe) { - logger.warn("Error while writing response: {}!", ioe.getMessage()); - } finally { - if (body != null) { - try { - body.close(); - } catch (IOException ioe) { - // Ignore - } - } - } - } - - /** - * Process the announce request. - * - *

- * This method attemps to read and parse the incoming announce request into - * an announce request message, then creates the appropriate announce - * response message and sends it back to the client. - *

- * - * @param request The incoming announce request. - * @param response The response object. - * @param body The validated response body output stream. - */ - private void process(Request request, Response response, - OutputStream body) throws IOException { - // Prepare the response headers. - response.set("Content-Type", "text/plain"); - response.set("Server", this.version); - response.setDate("Date", System.currentTimeMillis()); - - /** - * Parse the query parameters into an announce request message. - * - * We need to rely on our own query parsing function because - * SimpleHTTP's Query map will contain UTF-8 decoded parameters, which - * doesn't work well for the byte-encoded strings we expect. - */ - HTTPAnnounceRequestMessage announceRequest = null; - try { - announceRequest = this.parseQuery(request); - } catch (MessageValidationException mve) { - this.serveError(response, body, Status.BAD_REQUEST, - mve.getMessage()); - return; - } - - // The requested torrent must be announced by the tracker. - TrackedTorrent torrent = this.torrents.get( - announceRequest.getHexInfoHash()); - if (torrent == null) { - logger.warn("Requested torrent hash was: {}", - announceRequest.getHexInfoHash()); - this.serveError(response, body, Status.BAD_REQUEST, - ErrorMessage.FailureReason.UNKNOWN_TORRENT); - return; - } - - AnnounceRequestMessage.RequestEvent event = announceRequest.getEvent(); - String peerId = announceRequest.getHexPeerId(); - - // When no event is specified, it's a periodic update while the client - // is operating. If we don't have a peer for this announce, it means - // the tracker restarted while the client was running. Consider this - // announce request as a 'started' event. - if ((event == null || - AnnounceRequestMessage.RequestEvent.NONE.equals(event)) && - torrent.getPeer(peerId) == null) { - event = AnnounceRequestMessage.RequestEvent.STARTED; - } - - // If an event other than 'started' is specified and we also haven't - // seen the peer on this torrent before, something went wrong. A - // previous 'started' announce request should have been made by the - // client that would have had us register that peer on the torrent this - // request refers to. - if (event != null && torrent.getPeer(peerId) == null && - !AnnounceRequestMessage.RequestEvent.STARTED.equals(event)) { - this.serveError(response, body, Status.BAD_REQUEST, - ErrorMessage.FailureReason.INVALID_EVENT); - return; - } - - // Update the torrent according to the announce event - TrackedPeer peer = null; - try { - peer = torrent.update(event, - ByteBuffer.wrap(announceRequest.getPeerId()), - announceRequest.getHexPeerId(), - announceRequest.getIp(), - announceRequest.getPort(), - announceRequest.getUploaded(), - announceRequest.getDownloaded(), - announceRequest.getLeft()); - } catch (IllegalArgumentException iae) { - this.serveError(response, body, Status.BAD_REQUEST, - ErrorMessage.FailureReason.INVALID_EVENT); - return; - } - - // Craft and output the answer - HTTPAnnounceResponseMessage announceResponse = null; - try { - announceResponse = HTTPAnnounceResponseMessage.craft( - torrent.getAnnounceInterval(), - TrackedTorrent.MIN_ANNOUNCE_INTERVAL_SECONDS, - this.version, - torrent.seeders(), - torrent.leechers(), - torrent.getSomePeers(peer)); - WritableByteChannel channel = Channels.newChannel(body); - channel.write(announceResponse.getData()); - } catch (Exception e) { - this.serveError(response, body, Status.INTERNAL_SERVER_ERROR, - e.getMessage()); - } - } - - /** - * Parse the query parameters using our defined BYTE_ENCODING. - * - *

- * Because we're expecting byte-encoded strings as query parameters, we - * can't rely on SimpleHTTP's QueryParser which uses the wrong encoding for - * the job and returns us unparsable byte data. We thus have to implement - * our own little parsing method that uses BYTE_ENCODING to decode - * parameters from the URI. - *

- * - *

- * Note: array parameters are not supported. If a key is present - * multiple times in the URI, the latest value prevails. We don't really - * need to implement this functionality as this never happens in the - * Tracker HTTP protocol. - *

- * - * @param uri The request's full URI, including query parameters. - * @return The {@link AnnounceRequestMessage} representing the client's - * announce request. - */ - private HTTPAnnounceRequestMessage parseQuery(Request request) - throws IOException, MessageValidationException { - Map params = new HashMap(); - - try { - String uri = request.getAddress().toString(); - for (String pair : uri.split("[?]")[1].split("&")) { - String[] keyval = pair.split("[=]", 2); - if (keyval.length == 1) { - this.recordParam(params, keyval[0], null); - } else { - this.recordParam(params, keyval[0], keyval[1]); - } - } - } catch (ArrayIndexOutOfBoundsException e) { - params.clear(); - } - - // Make sure we have the peer IP, fallbacking on the request's source - // address if the peer didn't provide it. - if (params.get("ip") == null) { - params.put("ip", new BEValue( - request.getClientAddress().getAddress().getHostAddress(), - TrackedTorrent.BYTE_ENCODING)); - } - - - return HTTPAnnounceRequestMessage.parse(BEncoder.bencode(params)); - } - - private void recordParam(Map params, String key, - String value) { - try { - value = URLDecoder.decode(value, TrackedTorrent.BYTE_ENCODING); - - for (String f : NUMERIC_REQUEST_FIELDS) { - if (f.equals(key)) { - params.put(key, new BEValue(Long.valueOf(value))); - return; - } - } - - params.put(key, new BEValue(value, TrackedTorrent.BYTE_ENCODING)); - } catch (UnsupportedEncodingException uee) { - // Ignore, act like parameter was not there - return; - } - } - - /** - * Write a {@link HTTPTrackerErrorMessage} to the response with the given - * HTTP status code. - * - * @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 error reported by the tracker. - */ - private void serveError(Response response, OutputStream body, - Status status, HTTPTrackerErrorMessage error) throws IOException { - response.setCode(status.getCode()); - response.setText(status.getDescription()); - logger.warn("Could not process announce request ({}) !", - error.getReason()); - - WritableByteChannel channel = Channels.newChannel(body); - channel.write(error.getData()); - } - - /** - * Write an error message to the response with the given HTTP status code. - * - * @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 error message reported by the tracker. - */ - private void serveError(Response response, OutputStream body, - Status status, String error) throws IOException { - try { - this.serveError(response, body, status, - HTTPTrackerErrorMessage.craft(error)); - } catch (MessageValidationException mve) { - logger.warn("Could not craft tracker error message!", mve); - } - } - - /** - * Write a tracker failure reason code to the response with the given HTTP - * status code. - * - * @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. - */ - private void serveError(Response response, OutputStream body, - Status status, ErrorMessage.FailureReason reason) throws IOException { - this.serveError(response, body, status, reason.getMessage()); - } -} diff --git a/ttorrent-client/build.gradle b/ttorrent-client/build.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/Client.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/Client.java new file mode 100644 index 000000000..d743be75a --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/Client.java @@ -0,0 +1,308 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client; + +import com.turn.ttorrent.tracker.client.HTTPTrackerClient; +import com.turn.ttorrent.client.io.PeerClient; +import com.turn.ttorrent.client.io.PeerServer; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.tracker.client.TorrentMetadataProvider; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A pure-java BitTorrent client. + * + *

+ * A BitTorrent client in its bare essence shares torrents. If the torrent is + * not complete locally, it will download pieces until it is complete, sharing + * pieces that it has with other clients. Once a torrent is complete, the client + * will seed the torrent until explicitly told to stop. + *

+ * + *

+ * This BitTorrent client implementation is made to be simple to embed and + * simple to use. Until a Client is started (with {@link #start}), it will not + * send or receive any data. At any point before or after starting the client, + * torrents can be added to it with the + * addTorrent methods. As soon as a torrent is registered and + * the client is started, it will begin sharing the torrent. {@link Torrent} objects + * can be created from .torrent files, and {@link TorrentHandler} objects add + * the business logic necessary to interpret and use the metadata in a .torrent + * file. + *

+ * + *

+ * TorrentHandlers can be retrieved or removed from a Client at any time, and + * listeners can be registered with a Client that will inform a caller when the + * Client or any torrent changes state. + *

+ * + * @author mpetazzoni + */ +public class Client implements TorrentRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(Client.class); + + public enum State { + + STOPPED, STARTING, STARTED, STOPPING; + } + private final ClientEnvironment environment; + @GuardedBy("lock") + private State state = State.STOPPED; + private PeerServer peerServer; + private PeerClient peerClient; + private HTTPTrackerClient httpTrackerClient; + // private UDPTrackerClient udpTrackerClient; + private long reportInterval = -1; + private Future reportFuture = null; + private final ConcurrentMap torrents = new ConcurrentHashMap(); + private final List listeners = new CopyOnWriteArrayList(); + private final Object lock = new Object(); + + /** + * Initialize the BitTorrent client. + */ + public Client() { + this(null); + } + + public Client(@CheckForNull String peerName) { + this.environment = new ClientEnvironment(peerName); + } + + @Nonnull + public ClientEnvironment getEnvironment() { + return environment; + } + + @Override + public byte[] getLocalPeerId() { + return getEnvironment().getLocalPeerId(); + } + + @Override + public String getLocalPeerName() { + return getEnvironment().getLocalPeerName(); + } + + @Nonnull + public State getState() { + synchronized (lock) { + return state; + } + } + + private void setState(@Nonnull State state) { + synchronized (lock) { + this.state = state; + } + fireClientState(state); + } + + @Nonnull + public PeerServer getPeerServer() { + return peerServer; + } + + @Nonnull + public PeerClient getPeerClient() { + return peerClient; + } + + @Nonnull + public HTTPTrackerClient getHttpTrackerClient() { + // Not locked, as should only be read after a happen-before event. + // Causes a deadlock against stop() as called from TrackerHandler. + HTTPTrackerClient h = httpTrackerClient; + if (h == null) + throw new IllegalStateException("No HTTPTrackerClient - bad state: " + this); + return h; + } + + public long getReportInterval() { + return reportInterval; + } + + public void setReportInterval(long reportInterval, TimeUnit unit) { + this.reportInterval = unit.toMillis(reportInterval); + rereport(); + } + + public void rereport() { + synchronized (lock) { + if (reportFuture != null) + reportFuture.cancel(false); + if (getState() == State.STARTED && reportInterval > 0) + reportFuture = getEnvironment().getEventService().scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + info(true); + } + }, 0, reportInterval, TimeUnit.MILLISECONDS); + else + reportFuture = null; + } + } + + /* + @Nonnull + public UDPTrackerClient getUdpTrackerClient() { + return udpTrackerClient; + } + */ + public void start() throws Exception { + LOG.info("BitTorrent client [{}] starting...", this); + synchronized (lock) { + setState(State.STARTING); + + environment.start(); + + peerServer = new PeerServer(getEnvironment(), this); + peerServer.start(); + peerClient = new PeerClient(getEnvironment()); + peerClient.start(); + + httpTrackerClient = new HTTPTrackerClient(peerServer); + httpTrackerClient.start(); + + // udpTrackerClient = new UDPTrackerClient(environment, peer); + // udpTrackerClient.start(); + + for (TorrentHandler torrent : torrents.values()) + torrent.start(); + + setState(State.STARTED); + + rereport(); + } + LOG.info("BitTorrent client [{}] started.", this); + } + + public void stop() throws Exception { + LOG.info("BitTorrent client [{}] stopping...", this); + synchronized (lock) { + setState(State.STOPPING); + + rereport(); + + for (TorrentHandler torrent : torrents.values()) + torrent.stop(); + + // if (udpTrackerClient != null) + // udpTrackerClient.stop(); + // udpTrackerClient = null; + if (httpTrackerClient != null) + httpTrackerClient.stop(); + httpTrackerClient = null; + if (peerClient != null) + peerClient.stop(); + peerClient = null; + if (peerServer != null) + peerServer.stop(); + environment.stop(); + + setState(State.STOPPED); + } + LOG.info("BitTorrent client [{}] stopped.", this); + } + + @Override + @CheckForNull + public TorrentHandler getTorrent(@Nonnull byte[] infoHash) { + String hexInfoHash = TorrentUtils.toHex(infoHash); + return torrents.get(hexInfoHash); + } + + @Nonnull + public void addTorrent(@Nonnull TorrentHandler torrent) throws IOException, InterruptedException { + // This lock guarantees that we are started or stopped. + if (torrent.getClient() != this) + throw new IllegalArgumentException("Wrong Client in TorrentHandler."); + synchronized (lock) { + torrents.put(TorrentUtils.toHex(torrent.getInfoHash()), torrent); + if (getState() == State.STARTED) + torrent.start(); + } + } + + @Nonnull + public TorrentHandler addTorrent(@Nonnull Torrent torrent, @Nonnull File destDir) throws IOException, InterruptedException { + TorrentHandler torrentHandler = new TorrentHandler(this, torrent, destDir); + addTorrent(torrentHandler); + return torrentHandler; + } + + public void removeTorrent(@Nonnull TorrentHandler torrent) throws IOException { + synchronized (lock) { + torrents.remove(TorrentUtils.toHex(torrent.getInfoHash()), torrent); + if (getState() == State.STARTED) + torrent.stop(); + } + } + + @CheckForNull + public TorrentHandler removeTorrent(@Nonnull Torrent torrent) throws IOException { + return removeTorrent(torrent.getInfoHash()); + } + + @CheckForNull + public TorrentHandler removeTorrent(@Nonnull byte[] infoHash) throws IOException { + TorrentHandler torrent = torrents.get(TorrentUtils.toHex(infoHash)); + if (torrent != null) + removeTorrent(torrent); + return torrent; + } + + public void addClientListener(@Nonnull ClientListener listener) { + listeners.add(listener); + } + + private void fireClientState(@Nonnull State state) { + for (ClientListener listener : listeners) + listener.clientStateChanged(this, state); + } + + public void fireTorrentState(@Nonnull TorrentHandler torrent, @Nonnull TorrentMetadataProvider.State state) { + for (ClientListener listener : listeners) + listener.torrentStateChanged(this, torrent, state); + } + + public void info(boolean verbose) { + for (Map.Entry e : torrents.entrySet()) { + e.getValue().info(verbose); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + getLocalPeerName() + ")"; + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientEnvironment.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientEnvironment.java new file mode 100644 index 000000000..e77cdcd92 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientEnvironment.java @@ -0,0 +1,237 @@ +/* + * Copyright 2014 shevek. + * + * 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.client; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Preconditions; +import com.turn.ttorrent.client.io.PeerServer; +import com.turn.ttorrent.client.peer.Instrumentation; +import com.turn.ttorrent.protocol.PeerIdentityProvider; +import com.turn.ttorrent.protocol.torrent.TorrentCreator; +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.protocol.bcodec.BEUtils; +import com.turn.ttorrent.tracker.client.PeerAddressProvider; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.oio.OioEventLoopGroup; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.channel.socket.oio.OioServerSocketChannel; +import io.netty.channel.socket.oio.OioSocketChannel; +import io.netty.util.concurrent.DefaultThreadFactory; +import java.net.SocketAddress; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * The ClientEnvironment is created by the Client and may be consumed by other beans. + * + * @author shevek + */ +public class ClientEnvironment implements PeerIdentityProvider { + + public static final String BITTORRENT_ID_PREFIX = "-TO0042-"; + + public static enum EventLoopType { + + OIO { + @Override + public EventLoopGroup newEventLoopGroup(ThreadFactory factory) { + return new OioEventLoopGroup(0, factory); + } + + @Override + public Class getClientChannelType() { + return OioSocketChannel.class; + } + + @Override + public Class getServerChannelType() { + return OioServerSocketChannel.class; + } + }, NIO { + @Override + public EventLoopGroup newEventLoopGroup(ThreadFactory factory) { + return new NioEventLoopGroup(0, factory); + } + + @Override + public Class getClientChannelType() { + return NioSocketChannel.class; + } + + @Override + public Class getServerChannelType() { + return NioServerSocketChannel.class; + } + }, EPOLL { + @Override + public EventLoopGroup newEventLoopGroup(ThreadFactory factory) { + return new EpollEventLoopGroup(0, factory); + } + + @Override + public Class getClientChannelType() { + return EpollSocketChannel.class; + } + + @Override + public Class getServerChannelType() { + return EpollServerSocketChannel.class; + } + }; + + @Nonnull + public abstract EventLoopGroup newEventLoopGroup(ThreadFactory factory); + + @Nonnull + public abstract Class getClientChannelType(); + + @Nonnull + public abstract Class getServerChannelType(); + } + private final Random random = new Random(); + private final byte[] peerId; + private SocketAddress peerListenAddress; + private MetricRegistry metricRegistry = new MetricRegistry(); + private EventLoopType eventLoopType = EventLoopType.NIO; + private ThreadPoolExecutor executorService; + private EventLoopGroup eventService; + private Instrumentation peerInstrumentation = new Instrumentation(); + private final Object lock = new Object(); + + public ClientEnvironment(@CheckForNull String peerName) { + // String id = BITTORRENT_ID_PREFIX + UUID.randomUUID().toString().split("-")[4]; + byte[] tmp = new byte[20]; // Far too many, but who cares. + random.nextBytes(tmp); + String id = BITTORRENT_ID_PREFIX + (peerName != null ? peerName : "") + TorrentUtils.toHex(tmp); + this.peerId = Arrays.copyOf(id.getBytes(BEUtils.BYTE_ENCODING), 20); + } + + /** + * Get this client's peer specification. + */ + @Override + @SuppressFBWarnings("EI_EXPOSE_REP") + public byte[] getLocalPeerId() { + return peerId; + } + + @Override + public String getLocalPeerName() { + return TorrentUtils.toText(getLocalPeerId()); + } + + /** + * You probably want {@link PeerServer#getLocalAddresses()}. + * + * @see PeerAddressProvider + */ + @CheckForNull + public SocketAddress getLocalPeerListenAddress() { + return peerListenAddress; + } + + public void setLocalPeerListenAddress(@CheckForNull SocketAddress peerListenAddress) { + this.peerListenAddress = peerListenAddress; + } + + @Nonnull + public MetricRegistry getMetricRegistry() { + return metricRegistry; + } + + public void setMetricRegistry(@Nonnull MetricRegistry metricRegistry) { + this.metricRegistry = Preconditions.checkNotNull(metricRegistry, "MetricRegistry was null."); + } + + @Nonnull + public EventLoopType getEventLoopType() { + return eventLoopType; + } + + public void setEventLoopType(@Nonnull EventLoopType eventLoopType) { + this.eventLoopType = Preconditions.checkNotNull(eventLoopType, "EventLoopType was null."); + } + + public void start() throws Exception { + synchronized (lock) { + { + executorService = TorrentCreator.newExecutor(getLocalPeerName()); + } + { + ThreadFactory factory = new DefaultThreadFactory("bittorrent-event-" + getLocalPeerName(), true); + eventService = getEventLoopType().newEventLoopGroup(factory); + } + } + } + + private void shutdown(@CheckForNull ExecutorService service) throws InterruptedException { + if (service != null && !service.isShutdown()) { + service.shutdown(); + service.awaitTermination(1, TimeUnit.SECONDS); + } + } + + /** + * Closes this context. + */ + public void stop() throws Exception { + synchronized (lock) { + if (eventService != null) + eventService.shutdownGracefully(1, 4, TimeUnit.SECONDS); + eventService = null; + shutdown(executorService); + executorService = null; + } + } + + @Nonnull + public Random getRandom() { + return random; + } + + @Nonnull + public ThreadPoolExecutor getExecutorService() { + return executorService; + } + + @Nonnull + public EventLoopGroup getEventService() { + return eventService; + } + + @Nonnull + public Instrumentation getInstrumentation() { + return peerInstrumentation; + } + + public void setInstrumentation(@Nonnull Instrumentation peerInstrumentation) { + this.peerInstrumentation = peerInstrumentation; + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientListener.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientListener.java new file mode 100644 index 000000000..80ebf21d1 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientListener.java @@ -0,0 +1,18 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public interface ClientListener { + + public void clientStateChanged(@Nonnull Client client, @Nonnull Client.State state); + + public void torrentStateChanged(@Nonnull Client client, @Nonnull TorrentHandler torrent, @Nonnull TorrentHandler.State state); +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientListenerAdapter.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientListenerAdapter.java new file mode 100644 index 000000000..238c0ac62 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientListenerAdapter.java @@ -0,0 +1,20 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +/** + * + * @author shevek + */ +public class ClientListenerAdapter implements ClientListener { + + @Override + public void clientStateChanged(Client client, Client.State state) { + } + + @Override + public void torrentStateChanged(Client client, TorrentHandler torrent, TorrentHandler.State state) { + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/ErrorListener.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/ErrorListener.java new file mode 100644 index 000000000..a71c5f973 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/ErrorListener.java @@ -0,0 +1,17 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public interface ErrorListener { + + // TODO: Use this for all protocol violations, stats, etc. + public void error(@Nonnull String message); +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/PeerPieceProvider.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/PeerPieceProvider.java new file mode 100644 index 000000000..20e16e594 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/PeerPieceProvider.java @@ -0,0 +1,75 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import com.turn.ttorrent.client.peer.PieceHandler; +import com.turn.ttorrent.client.peer.PeerHandler; +import com.turn.ttorrent.client.peer.Instrumentation; +import com.turn.ttorrent.protocol.PeerIdentityProvider; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.BitSet; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public interface PeerPieceProvider extends PeerIdentityProvider { + + @Nonnull + public Instrumentation getInstrumentation(); + + @Nonnegative + public int getPieceCount(); + + @Nonnull + public int getPieceLength(@Nonnegative int index); + + @Nonnegative + public int getBlockLength(); + + @Nonnull + public BitSet getCompletedPieces(); + + public boolean isCompletedPiece(@Nonnegative int index); + + /** Zero-copy. */ + public void andNotCompletedPieces(@Nonnull BitSet out); + + @CheckForNull + public Iterable getNextPieceHandler(@Nonnull PeerHandler peer, @Nonnull BitSet interesting); + + public int addRequestTimeout(@Nonnull Iterable requests); + + /** + * Read a piece block from the underlying byte storage. + * + *

+ * This is the public method for reading this piece's data, and it will + * only succeed if the piece is complete and valid on disk, thus ensuring + * any data that comes out of this function is valid piece data we can send + * to other peers. + *

+ * + * @param offset Offset inside this piece where to start reading. + * @throws IllegalArgumentException If offset + length goes over + * the piece boundary. + * @throws IllegalStateException If the piece is not valid when attempting + * to read it. + * @throws IOException If the read can't be completed (I/O error, or EOF + * reached, which can happen if the piece is not complete). + */ + public void readBlock(@Nonnull ByteBuffer block, @Nonnegative int piece, @Nonnegative int offset) throws IOException; + + /** + * Consumes the block. + */ + public void writeBlock(@Nonnull ByteBuffer block, @Nonnegative int piece, @Nonnegative int offset) throws IOException; + + public boolean validateBlock(@Nonnegative ByteBuffer block, @Nonnegative int piece) throws IOException; +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/PieceValidator.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/PieceValidator.java new file mode 100644 index 000000000..7012a0383 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/PieceValidator.java @@ -0,0 +1,59 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import com.turn.ttorrent.protocol.torrent.Torrent; +import java.nio.ByteBuffer; +import java.util.BitSet; +import java.util.concurrent.CountDownLatch; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link Runnable} to call the piece validation function. + * + *

+ * This {@link Runnable} implementation allows for the calling of the piece + * validation function in a controlled context like a thread or an + * executor. + *

+ * + * @author mpetazzoni + */ +/* pp */ class PieceValidator implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(PieceValidator.class); + private final Torrent torrent; + private final int piece; + private final ByteBuffer data; + private final BitSet valid; + private final CountDownLatch latch; + + public PieceValidator(@Nonnull Torrent torrent, @Nonnegative int piece, @Nonnull ByteBuffer data, @Nonnull BitSet valid, @Nonnull CountDownLatch latch) { + this.torrent = torrent; + this.piece = piece; + this.data = data; + this.valid = valid; + this.latch = latch; + } + + @Override + public void run() { + try { + if (torrent.isPieceValid(piece, data)) { + // TODO: Synchronization on this lock may slow this down a lot. + synchronized (valid) { + valid.set(piece); + } + } + } catch (Exception e) { + LOG.error("Failed validation of " + this, e); + } finally { + latch.countDown(); + } + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/SwarmHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/SwarmHandler.java new file mode 100644 index 000000000..1c4b43957 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/SwarmHandler.java @@ -0,0 +1,1332 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client; + +import com.google.common.base.Function; +import com.google.common.collect.Maps; +import com.turn.ttorrent.client.io.PeerMessage; +import com.turn.ttorrent.client.io.PeerServer; +import com.turn.ttorrent.client.peer.Instrumentation; +import com.turn.ttorrent.client.peer.PeerActivityListener; +import com.turn.ttorrent.client.peer.PeerConnectionListener; +import com.turn.ttorrent.client.peer.PeerExistenceListener; +import com.turn.ttorrent.client.peer.PeerHandler; +import com.turn.ttorrent.client.peer.PieceHandler; +import com.turn.ttorrent.client.peer.Rate; +import com.turn.ttorrent.client.peer.RateComparator; +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.protocol.tracker.Peer; +import com.turn.ttorrent.tracker.client.PeerAddressProvider; +import io.netty.channel.Channel; +import io.netty.util.internal.PlatformDependent; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages swarm-level coordination as a periodically-executed Runnable. + * + *

+ * This class implements several interfaces that all share a 1:1 ratio for + * object lifetime. Known peers in the swarm are tracked with a simple map, but + * connected peers have {@link PeerHandler} objects created for them, and these + * objects allow direct control of the wire protocol. Most of the interfaces + * implemented by this class are then used by PeerHandlers to learn about the + * torrent status, the peer's addresses, and other peers, as well as to handle + * various peer and piece state changes. Pieces of the torrent are similarly + * tracked in a simple array, but in progress pieces have {@link PieceHandler} + * objects allocated that handle reading and writing blocks on the wire. The + * SwarmHandler is used by PieceHandlers to maintain piece consistency. + *

+ * + *

+ * The SwarmHandler for a torrent is also used by the {@link PeerClent} as a + * listener that handles new connections and by the {@link TrackerHandler} s a + * place to register peers returned by a tracker. + *

+ * + * @author mpetazzoni + */ +public class SwarmHandler implements Runnable, + PeerAddressProvider, PeerPieceProvider, + PeerExistenceListener, PeerConnectionListener, PeerActivityListener { + + private static final Logger LOG = LoggerFactory.getLogger(SwarmHandler.class); + + private static class PeerInformation { + + // Yes, the reference is volatile, not the data. + @CheckForNull + private volatile byte[] remotePeerId; + private volatile long keepAliveTime; + private volatile long reconnectTime; + + public void setReconnectTime(@Nonnull Random r, long reconnectTime) { + // 5000 estimates that we won't have more than 5 valid IPs for a given target. + this.reconnectTime = reconnectTime + r.nextInt(5000); + } + + @Override + public String toString() { + return "RemotePeerId=" + TorrentUtils.toHexOrNull(remotePeerId) + ", reconnectTime=" + reconnectTime; + } + } + /** Peers unchoking frequency, in seconds. Current BitTorrent specification + * recommends 10 seconds to avoid choking fibrilation. */ + private static final long UNCHOKE_DELAY = TimeUnit.SECONDS.toMillis(10); + /** Optimistic unchokes are done every 2 loop iterations, i.e. every + * 2*UNCHOKING_FREQUENCY seconds. */ + private static final long OPTIMISTIC_UNCHOKE_DELAY = TimeUnit.SECONDS.toMillis(32); + private static final int MAX_DOWNLOADERS_UNCHOKE = 4; + private static final long RECONNECT_DELAY_TEMPORARY = TimeUnit.MINUTES.toMillis(1); + private static final long RECONNECT_DELAY_PERMANENT = TimeUnit.MINUTES.toMillis(10); + /** End-game trigger ratio. + * + *

+ * End-game behavior (requesting already requested pieces from available + * and ready peers to try to speed-up the end of the transfer) will only be + * enabled when the ratio of completed pieces over total pieces in the + * torrent is over this value. + *

+ */ + private static final float END_GAME_COMPLETION_RATIO = 0.95f; + private final TorrentHandler torrent; + // Keys are InetSocketAddress or HexPeerId + private final ConcurrentMap knownPeers = PlatformDependent.newConcurrentHashMap(); + private final ConcurrentMap connectedPeers = PlatformDependent.newConcurrentHashMap(); + private final AtomicLong uploaded = new AtomicLong(0); + private final AtomicLong downloaded = new AtomicLong(0); + private final AtomicIntegerArray availablePieces; + @GuardedBy("lock") + private final Set partialPieces = new HashSet(); + // We only care about global rarest pieces for peer selection or opportunistic unchoking. + // private final BitSet rarestPieces; + // private int rarestPiecesAvailability = 0; + @GuardedBy("future") + private Future future; + @GuardedBy("lock") + private long unchokeTime = 0; + @GuardedBy("lock") + private long optimisticUnchokeTime = 0; + private long tickTime = 0; + private final Object lock = new Object(); + + SwarmHandler(@Nonnull TorrentHandler torrent) { + this.torrent = torrent; + this.availablePieces = new AtomicIntegerArray(torrent.getPieceCount()); + // this.rarestPieces = new BitSet(torrent.getPieceCount()); + } + + @Nonnull + public Client getClient() { + return torrent.getClient(); + } + + @Override + public byte[] getLocalPeerId() { + return getClient().getEnvironment().getLocalPeerId(); + } + + @Override + public String getLocalPeerName() { + return getClient().getEnvironment().getLocalPeerName(); + } + + @Override + public Set getLocalAddresses() { + PeerServer server = getClient().getPeerServer(); + // This can happen if we try to seed PEX before calling Client.start(). + if (server == null) + return Collections.emptySet(); + return server.getLocalAddresses(); + } + + @Override + public Instrumentation getInstrumentation() { + return getClient().getEnvironment().getInstrumentation(); + } + + @Nonnull + private Random getRandom() { + return getClient().getEnvironment().getRandom(); + } + + @Nonnegative + public int getPeerCount() { + return knownPeers.size(); + } + + @Override + public Map getPeers() { + return Maps.transformValues(knownPeers, new Function() { + @Override + public byte[] apply(PeerInformation input) { + return input.remotePeerId; + } + }); + } + + private static boolean isInetAddress(@Nonnull SocketAddress socketAddress, @Nonnull Class type) { + if (!(socketAddress instanceof InetSocketAddress)) + return false; + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + return type.isInstance(inetSocketAddress.getAddress()); + } + + // @Nonnull + private void addPeer(@Nonnull SocketAddress peerAddress, @CheckForNull byte[] peerId, long now) { + // if (!isInetAddress(peerAddress, Inet4Address.class)) return; + + PeerInformation peerInformation = new PeerInformation(); + peerInformation.setReconnectTime(getRandom(), now); + PUT: + { + PeerInformation tmp = knownPeers.putIfAbsent(peerAddress, peerInformation); + if (tmp != null) + peerInformation = tmp; + } + if (peerId != null) + peerInformation.remotePeerId = peerId; + // TODO: Update stats about 'reported', 'connected', etc. + // return peerInformation; + } + + @Override + public void addPeers(@Nonnull Map peers, @Nonnull String source) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Adding peers from {}: {}", new Object[]{ + getLocalPeerName(), source, peers + }); + // PeerServer server = getClient().getPeerServer(); + Set localAddresses = getLocalAddresses(); + long now = System.currentTimeMillis(); + for (Map.Entry e : peers.entrySet()) { + SocketAddress remoteAddress = e.getKey(); + if (false && localAddresses.contains(remoteAddress)) { + // TODO: Probably ignore silently as it's bound to happen and it's not actionable. + LOG.warn("Attempted to add local address " + remoteAddress + " to known peer set."); + continue; + } + addPeer(remoteAddress, e.getValue(), now); + } + // run(); // If you want very low latency, call run() manually after calling this. + } + + @Nonnull + public Iterable getConnectedPeers() { + return connectedPeers.values(); + } + + @Nonnegative + public int getConnectedPeerCount() { + return connectedPeers.size(); + } + + /** + * Get the number of bytes uploaded for this torrent. + */ + @Nonnegative + public long getUploaded() { + return uploaded.get(); + } + + /** + * Get the number of bytes downloaded for this torrent. + * + *

+ * Note: this could be more than the torrent's length, and should + * not be used to determine a completion percentage. + *

+ */ + @Nonnegative + public long getDownloaded() { + return downloaded.get(); + } + + @Nonnegative + public int getAvailablePieceCount() { + int count = 0; + for (int i = 0; i < torrent.getPieceCount(); i++) { + if (this.availablePieces.get(i) > 0) + count++; + } + return count; + } + + public int setAvailablePiece(@Nonnegative int piece, boolean available) { + if (available) { + return availablePieces.incrementAndGet(piece); + } else { + // Implement an unsigned CAS. + for (;;) { + int current = availablePieces.get(piece); + if (current <= 0) + return 0; + int next = current - 1; + if (availablePieces.compareAndSet(piece, current, next)) + return next; + } + } + } + + /** + * Return a BitSet describing the currently requested pieces. + */ + @Nonnull + public BitSet getRequestedPieces() { + BitSet requestedPieces = new BitSet(torrent.getPieceCount()); + for (PeerHandler peerHandler : connectedPeers.values()) { + for (PieceHandler.AnswerableRequestMessage request : peerHandler.getRequestsSent()) { + requestedPieces.set(request.getPiece()); + } + } + return requestedPieces; + } + + @Nonnegative + public int getRequestedPieceCount() { + return getRequestedPieces().cardinality(); + } + + /* + public boolean isRequestedPiece(int index) { + synchronized (lock) { + return requestedPieces.get(index); + } + } + */ + private void andNotRequestedPieces(@Nonnull BitSet b) { + for (PeerHandler peerHandler : connectedPeers.values()) { + for (PieceHandler.AnswerableRequestMessage request : peerHandler.getRequestsSent()) { + b.clear(request.getPiece()); + } + } + } + + /** + * Connect to the given peer and perform the BitTorrent handshake. + * + *

+ * Submits an asynchronous connection task to the outbound connections + * executor to connect to the given peer. + *

+ * + * @param peer The peer to connect to. + */ + public void connect(SocketAddress address) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Attempting to connect to {} for {}", new Object[]{ + getLocalPeerName(), + address, torrent + }); + getClient().getPeerClient().connect(this, torrent.getInfoHash(), address); + } + + public void start() { + synchronized (lock) { + long ms = Rate.INTERVAL_MS; + ms = Math.min(ms, UNCHOKE_DELAY); + ms = Math.min(ms, OPTIMISTIC_UNCHOKE_DELAY); + ms = Math.min(ms, RECONNECT_DELAY_TEMPORARY); + future = getClient().getEnvironment().getEventService().scheduleWithFixedDelay(this, 0, ms, TimeUnit.MILLISECONDS); + } + } + + public void stop() { + synchronized (lock) { + if (future != null) { + future.cancel(true); + future = null; + } + } + } + + // TODO: Periodic / step function. + @Override + public void run() { + // Stopwatch stopwatch = Stopwatch.createStarted(); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Run: peers={}, connected={}, completed={}/{}", + new Object[]{ + getLocalPeerName(), + knownPeers.keySet(), getConnectedPeers(), + torrent.getCompletedPieceCount(), torrent.getPieceCount() + }); + boolean unchoke = false; + boolean optimisticUnchoke = false; + boolean tick = false; + long now = System.currentTimeMillis(); + synchronized (lock) { + if (unchokeTime + UNCHOKE_DELAY < now) { + unchokeTime = now; + unchoke = true; + } + if (optimisticUnchokeTime + OPTIMISTIC_UNCHOKE_DELAY < now) { + optimisticUnchokeTime = now; + optimisticUnchoke = true; + } + if (tickTime + Rate.INTERVAL_MS < now) { + tickTime = now; + tick = true; + } + } + + if (!torrent.isComplete()) { + // Attempt to connect to the peer if and only if: + // - We're not already connected or connecting to it; + // - We're not a seeder (we leave the responsibility + // of connecting to peers that need to download + // something). + for (Map.Entry e : knownPeers.entrySet()) { + PeerInformation peerInformation = e.getValue(); + if (LOG.isTraceEnabled()) + LOG.trace("{}: At {}, considering {} -> {}", new Object[]{ + getLocalPeerName(), now, + e.getKey(), peerInformation + }); + if (peerInformation.reconnectTime > now) + continue; + byte[] remotePeerId = peerInformation.remotePeerId; + if (remotePeerId != null) { + if (connectedPeers.containsKey(TorrentUtils.toHex(remotePeerId))) + continue; + if (Arrays.equals(remotePeerId, getLocalPeerId())) + continue; + } + peerInformation.setReconnectTime(getRandom(), now + RECONNECT_DELAY_TEMPORARY); + connect(e.getKey()); + } + } + + if (unchoke) + unchokePeers(optimisticUnchoke); + + for (PeerHandler peer : getConnectedPeers()) { + try { + // This is the call which is likely to cause the most trouble. + peer.run("swarm tick"); + } catch (IOException e) { + LOG.error(getLocalPeerName() + ": Peer " + peer + " threw.", e); + } + if (tick) + peer.tick(); + } + // LOG.debug("{}: Swarm tick took {}", getLocalPeerName(), stopwatch); + } + + /** + * Retrieve a peer comparator. + * + *

+ * Returns a peer comparator based on either the download rate or the + * upload rate of each peer depending on our state. While sharing, we rely + * on the download rate we get from each peer. When our download is + * complete and we're only seeding, we use the upload rate instead. + *

+ * + * @return A SharingPeer comparator that can be used to sort peers based on + * the download or upload rate we get from them. + */ + @Nonnull + private Comparator getPeerRateComparator() { + switch (torrent.getState()) { + case SHARING: + return new RateComparator.DLRateComparator(); + case SEEDING: + return new RateComparator.ULRateComparator(); + default: + throw new IllegalStateException("Client is neither sharing nor " + + "seeding, we shouldn't be comparing peers at this point."); + } + } + + /** + * Unchoke connected peers. + * + *

+ * This is one of the "clever" places of the BitTorrent client. Every + * OPTIMISTIC_UNCHOKING_FREQUENCY seconds, we decide which peers should be + * unchoked and authorized to grab pieces from us. + *

+ * + *

+ * Reciprocation (tit-for-tat) and upload capping is implemented here by + * carefully choosing which peers we unchoke, and which peers we choke. + *

+ * + *

+ * The four peers with the best download rate and are interested in us get + * unchoked. This maximizes our download rate as we'll be able to get data + * from the four "best" peers quickly, while allowing these peers to + * download from us and thus reciprocate their generosity. + *

+ * + *

+ * Peers that have a better download rate than these four downloaders but + * are not interested get unchoked too, we want to be able to download from + * them to get more data more quickly. If one becomes interested, it takes + * a downloader's place as one of the four top downloaders (i.e. we choke + * the downloader with the worst upload rate). + *

+ * + * @param optimistic Whether to perform an optimistic unchoke as well. + */ + private void unchokePeers(boolean optimistic) { + // Build a set of all connected peers, we don't care about peers we're + // not connected to. + List candidates = new ArrayList(); + for (PeerHandler peer : getConnectedPeers()) { + // TODO: Panic-check that it's still connected? + if (peer.isInterested()) + candidates.add(peer); + else + peer.choke(); + } + + if (LOG.isTraceEnabled()) + LOG.trace("{}: Running unchokePeers() on {} connected peers.", getLocalPeerName(), candidates.size()); + // Collections.shuffle(candidates); // Make the sort unstable, so if we have no downloaders, we select at random. + Collections.sort(candidates, getPeerRateComparator()); + // LOG.info("{}: Candidates are {}", getLocalPeerName(), candidates); + + int downloaders = 0; + // We're interested in the top downloaders first, so use a descending set. + int i = 0; + for (/* */; i < candidates.size(); i++) { + if (++downloaders > MAX_DOWNLOADERS_UNCHOKE) + break; + PeerHandler peer = candidates.get(i); + peer.unchoke(); + } + + // Actually choke all chosen peers (if any), except the eventual + // optimistic unchoke. + int nchoked = candidates.size() - i; + if (nchoked > 0) { + int unchoke; + if (optimistic) { + unchoke = i + getRandom().nextInt(nchoked); + if (LOG.isTraceEnabled()) { + PeerHandler peer = candidates.get(unchoke); + LOG.trace("Optimistic unchoke of {}.", peer); + } + } else { + unchoke = -1; + } + for (/* */; i < candidates.size(); i++) { + if (i == unchoke) + candidates.get(i).unchoke(); + else + candidates.get(i).choke(); + } + } + } + + /** + * Computes the set of rarest pieces from the interesting set. + */ + private int computeRarestPieces(@Nonnull BitSet rarest, @Nonnull BitSet interesting) { + rarest.clear(); + int rarestAvailability = Integer.MAX_VALUE; + for (int i = interesting.nextSetBit(0); i >= 0; + i = interesting.nextSetBit(i + 1)) { + int availability = availablePieces.get(i); + // This looks weird, but: The bit wouldn't be set in interesting if availability was 0. + // So we got a miscount somewhere (entirely possible) and we patch up here. + if (availability == 0) + availability = 1; + // Now, !interesting.isEmpty() -> !rarest.isEmpty() + if (availability > rarestAvailability) + continue; + if (availability < rarestAvailability) { + rarestAvailability = availability; + rarest.clear(); + } + rarest.set(i); + } + return rarestAvailability; + } + + @Override + public int getPieceCount() { + return torrent.getPieceCount(); + } + + @Override + public int getPieceLength(int index) { + return torrent.getPieceLength(index); + } + + @Override + public int getBlockLength() { + return torrent.getBlockLength(); + } + + @Override + public BitSet getCompletedPieces() { + return torrent.getCompletedPieces(); + } + + @Override + public boolean isCompletedPiece(int index) { + return torrent.isCompletedPiece(index); + } + + @Override + public void andNotCompletedPieces(BitSet out) { + torrent.andNotCompletedPieces(out); + } + + @Override + public Iterable getNextPieceHandler( + @Nonnull PeerHandler peer, + @Nonnull BitSet peerInteresting) { + int peerAvailable = peer.getAvailablePieceCount(); + // LOG.debug("Peer interesting is {}", peerInteresting); + + // TODO: We hold this lock for a LONG time. :-( + // I'm fairly sure our lock acquisition order is peer then torrent. + // We can't drop the lock earlier, else two peers will get the + // same DownloadingPiece, and we don't reference those. + synchronized (lock) { + PARTIAL: + { + List piece = new ArrayList(); + Iterator it = partialPieces.iterator(); + while (it.hasNext()) { + if (piece.size() > 20) + break; + PieceHandler.AnswerableRequestMessage request = it.next(); + if (peerInteresting.get(request.getPiece())) { + // An endgame might have requested it elsewhere. + if (!isCompletedPiece(request.getPiece())) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Peer {} retrying request {}", new Object[]{ + getLocalPeerName(), + peer, request + }); + piece.add(request); + } + + it.remove(); + } + } + // LOG.info("Looking for partials generated " + piece); + if (!piece.isEmpty()) + return piece; + } + + // TODO: Should this be before or after PARTIAL? + BitSet interesting = (BitSet) peerInteresting.clone(); + this.andNotRequestedPieces(interesting); + + // If we didn't find interesting pieces, we need to check if we're in + // an end-game situation. If yes, we request an already requested piece + // to try to speed up the end. + if (interesting.isEmpty()) { + if (torrent.getCompletedPieceCount() < END_GAME_COMPLETION_RATIO * torrent.getPieceCount()) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Not far along enough to warrant end-game mode.", getLocalPeerName()); + return null; + } + + interesting = (BitSet) peerInteresting.clone(); + torrent.andNotCompletedPieces(interesting); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Possible end-game, we're about to request a piece " + + "that was already requested from another peer.", + getLocalPeerName()); + } + + if (interesting.isEmpty()) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: No interesting piece from {}!", getLocalPeerName(), peer); + return null; + } + + BitSet rarestPieces = new BitSet(interesting.length()); + computeRarestPieces(rarestPieces, interesting); + // Since interesting is nonempty, rarestPieces should be nonempty. + // We can violate that if we have a miscount of 0 in availablePieces. + // Pick a random piece from the rarest pieces from this peer. + int rarestIndex = getRandom().nextInt(rarestPieces.cardinality()); + SEARCH: + { + // This loop will NEVER terminate "normally" because rarestIndex < rarestPieces.cardinality(); + for (int i = rarestPieces.nextSetBit(0); i >= 0; + i = rarestPieces.nextSetBit(i + 1)) { + if (rarestIndex-- == 0) { + rarestIndex = i; + break SEARCH; + } + } + // NOTREACHED + LOG.error("{}: No rare piece from {}!", getLocalPeerName(), peer); + return null; + } + + if (LOG.isTraceEnabled()) + LOG.trace("{}: Peer {} has {}/{}/{} interesting piece(s); requesting {}, requests {}", new Object[]{ + getLocalPeerName(), + peer, + interesting.cardinality(), peerAvailable, torrent.getPieceCount(), + rarestIndex, + getRequestedPieces() + }); + + // TODO: We don't keep track of these, so it is possible to have more than one + // PieceHandler for a given piece. We make some attempt with rejected or timed out + // requests, but it isn't great. + return new PieceHandler(/* this, */this, rarestIndex); + } + } + + @Override + public int addRequestTimeout(Iterable requests) { + int count = 0; + synchronized (lock) { + for (PieceHandler.AnswerableRequestMessage request : requests) { + if (!isCompletedPiece(request.getPiece())) { + partialPieces.add(request); + count++; + } + } + } + return count; + } + + @Nonnegative + private int getPartialPieceCount() { + synchronized (lock) { + return partialPieces.size(); + } + } + + private void ioBlock(@Nonnull ByteBuffer block, @Nonnegative int piece, @Nonnegative int offset, boolean completed) throws IOException { + int rawLength = torrent.getTorrent().getPieceLength(piece); + if (offset + block.remaining() > rawLength) + throw new IllegalArgumentException("Offset " + + offset + "+" + block.remaining() + + " too large for piece " + piece + + " of length " + rawLength); + if (completed && !isCompletedPiece(piece)) + throw new IllegalArgumentException("Attempt to read piece " + piece + " which is not complete."); + } + + @Override + public void readBlock(ByteBuffer block, int piece, int offset) throws IOException { + ioBlock(block, piece, offset, true); + long rawOffset = torrent.getTorrent().getPieceOffset(piece) + offset; + torrent.getBucket().read(block, rawOffset); + } + + @Override + public void writeBlock(ByteBuffer block, int piece, int offset) throws IOException { + ioBlock(block, piece, offset, false); + long rawOffset = torrent.getTorrent().getPieceOffset(piece) + offset; + torrent.getBucket().write(block, rawOffset); + } + + @Override + public boolean validateBlock(ByteBuffer block, int piece) throws IOException { + // LOG.trace("Validating data for {}...", this); + return torrent.getTorrent().isPieceValid(piece, block); + } + + /** PeerConnectionListener handler(s). ********************************/ + /** + * Retrieves a {@link PeerHandler} object from the given peer specification. + * + *

+ * This function tries to retrieve an existing peer object based on the + * provided peer specification or otherwise instantiates a new one and adds + * it to our peer repository. + *

+ * + * This method takes two {@link Nonnull} arguments instead of a {@link Peer}, + * because Peer has a {@link CheckForNull} on {@link Peer#getPeerId()}. + */ + @Override + public PeerHandler handlePeerConnectionCreated(Channel channel, byte[] remotePeerId, byte[] remoteReserved) { + SocketAddress remoteAddress = channel.remoteAddress(); + + // This is almost always an ephemeral outgoing port so don't add it here. + // If the peer supports PeerExtendedMessage.HandshakeMessage, we will get its id then. + // addPeer(remoteAddress, remotePeerId, System.currentTimeMillis()); + PeerInformation peerInformation = knownPeers.get(remoteAddress); + if (peerInformation != null) + peerInformation.remotePeerId = remotePeerId; + + if (Arrays.equals(remotePeerId, getClient().getLocalPeerId())) + throw new IllegalArgumentException("Cannot connect to self."); + + String remoteHexPeerId = TorrentUtils.toHex(remotePeerId); + PeerHandler peerHandler = connectedPeers.get(remoteHexPeerId); + if (peerHandler != null) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Found existing peer for {}: {}.", new Object[]{ + getLocalPeerName(), remoteAddress, peerHandler + }); + return null; + } + + peerHandler = new PeerHandler(channel, remotePeerId, remoteReserved, this, this, this, this, this); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Created new peer: {}.", getLocalPeerName(), peerHandler); + + return peerHandler; + } + + /** + * Chooses, deterministically, between two PeerHandlers, such that both + * ends of a connection will make the same deterministic choice. + * + * Attempts to prefer link-local IPv6 addresses. + */ + private static class PeerConnectionComparator implements Comparator { + + public static final PeerConnectionComparator INSTANCE = new PeerConnectionComparator(); + + private static int score(@Nonnull InetAddress a) { + if (a.isLinkLocalAddress()) + return 2; // Most beloved, if we can get it. + if (a.isLoopbackAddress()) + return 10; // Only if we can't get something better. + if (a.isAnyLocalAddress()) + return 100; // Avoid. + if (a.isMulticastAddress()) + return 200; // Never. + return 4; // Regular address. + } + + private static int compare(@Nonnull InetAddress a1, @Nonnull InetAddress a2) { + byte[] b1 = a1.getAddress(); + byte[] b2 = a2.getAddress(); + // Use the longer address. + int cmp = -Integer.compare(b1.length, b2.length); + if (cmp != 0) + return cmp; + // Use the lower address. + for (int i = 0; i < b1.length; i++) { + cmp = b2[i] - b1[i]; + if (cmp != 0) + return cmp; + } + return 0; + } + + @Nonnull + private static InetAddress choose(@Nonnull InetAddress a1, @Nonnull InetAddress a2) { + int cmp = compare(a1, a2); + return cmp < 0 ? a1 : a2; + } + + @Override + public int compare(PeerHandler o1, PeerHandler o2) { + SocketAddress s1 = o1.getRemoteAddress(); + SocketAddress s2 = o2.getRemoteAddress(); + if (!(s1 instanceof InetSocketAddress)) { + if (s2 instanceof InetSocketAddress) + return 1; + return 0; + } else if (!(s2 instanceof InetSocketAddress)) { + return -1; + } + InetAddress a1 = ((InetSocketAddress) s1).getAddress(); + InetAddress a2 = ((InetSocketAddress) s2).getAddress(); + // Longer addresses are preferred: IPv6. + int cmp = -Integer.compare(a1.getAddress().length, a2.getAddress().length); + if (cmp != 0) + return cmp; + cmp = Integer.compare(score(a1), score(a2)); + if (cmp != 0) + return cmp; + + // OK, we ran out of smart ideas. Let's do something very deterministic. + InetAddress e1 = choose(a1, ((InetSocketAddress) o1.getLocalAddress()).getAddress()); + InetAddress e2 = choose(a2, ((InetSocketAddress) o2.getLocalAddress()).getAddress()); + return compare(e1, e2); + } + } + + /** + * Handle a new peer connection. + * + *

+ * This handler is called once the connection has been successfully + * established and the handshake exchange made. This generally simply means + * binding the peer to the socket, which will put in place the communication + * logic with this peer. + *

+ * + * @param peer The connected remote peer. Note + * that if the peer somehow rejected our handshake reply, this socket might + * very soon get closed, but this is handled down the road. + * + * @see PeerHandler + */ + @Override + public void handlePeerConnectionReady(PeerHandler peer) { + try { + if (LOG.isDebugEnabled()) + LOG.debug("{}: New peer connection with {} [{}/{}].", + new Object[]{ + getLocalPeerName(), peer, + getConnectedPeerCount(), getPeerCount() + }); + + peer.getRemoteAddress(); + + for (;;) { + // See whether we are already connected. + PeerHandler prev = connectedPeers.putIfAbsent(peer.getHexRemotePeerId(), peer); + // Simple success. + if (prev == null) + break; + // Some weird race which is simple success, but can't happen. + if (prev == peer) + break; + // If so, choose a connection deterministically. + int cmp = PeerConnectionComparator.INSTANCE.compare(prev, peer); + // We didn't like the new connection. + if (cmp <= 0) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Closing duplicate peer connection {} : {} [{}/{}]", new Object[]{ + getLocalPeerName(), peer, prev, + getConnectedPeerCount(), getPeerCount() + }); + peer.close("duplicate connection"); + return; + } + // Try to use the new connection. + // TODO: Do we just keep the old (active) one? + if (connectedPeers.replace(peer.getHexRemotePeerId(), prev, peer)) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Closing superceded peer connection {} : {} [{}/{}]", new Object[]{ + getLocalPeerName(), prev, peer, + getConnectedPeerCount(), getPeerCount() + }); + prev.close("superceded connection"); + break; + } + // We preferred the new connection, but replace() failed. Try again. + } + + // Give the peer a chance to send a bitfield message. + peer.run("new connection"); + } catch (Exception e) { + LOG.warn("Could not handle new peer connection " + + "with {}: {}", peer, e.getMessage()); + } + } + + /** + * Handle a failed peer connection. + * + *

+ * If an outbound connection failed (could not connect, invalid handshake, + * etc.), remove the peer from our known peers. + *

+ * + * @param peer The peer we were trying to connect with. + * @param cause The exception encountered when connecting with the peer. + */ + @Override + public void handlePeerConnectionFailed(SocketAddress remoteAddress, Throwable cause) { + String reason = (cause == null) ? "(cause not specified)" : cause.getMessage(); + LOG.warn("{}: Could not connect to {}: {}.", new Object[]{ + getLocalPeerName(), + remoteAddress, reason + }); + // No need to clean up the connectedPeers map here - + // PeerHandler is only created in PeerHandshakeHandler. + + PeerInformation peerInformation = knownPeers.get(remoteAddress); + if (peerInformation != null) { + long reconnectDelay; + if (cause != null) + reconnectDelay = RECONNECT_DELAY_PERMANENT; + else + reconnectDelay = RECONNECT_DELAY_TEMPORARY; + peerInformation.setReconnectTime(getRandom(), System.currentTimeMillis() + reconnectDelay); + } + LOG.debug("{}: PeerInformation {} -> {}", new Object[]{ + getLocalPeerName(), + remoteAddress, peerInformation + }); + } + + /** PeerActivityListener handler(s). *************************************/ + /** + * Peer choked handler. + * + *

+ * When a peer chokes, the requests made to it are cancelled and we need to + * mark the eventually piece we requested from it as available again for + * download tentative from another peer. + *

+ * + * @param peer The peer that choked. + */ + @Override + public void handlePeerChoking(PeerHandler peer) { + if (LOG.isTraceEnabled()) + LOG.trace("Peer {} choked, we now have {} outstanding " + + "request(s): {}", + new Object[]{ + peer, + getRequestedPieceCount(), + getRequestedPieces() + }); + } + + /** + * Peer ready handler. + * + *

+ * When a peer becomes ready to accept piece block requests, select a piece + * to download and go for it. + *

+ * + * @param peer The peer that became ready. + */ + @Override + public void handlePeerUnchoking(PeerHandler peer) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Peer {} is ready and has {}/{} piece(s).", new Object[]{ + getLocalPeerName(), + peer, + peer.getAvailablePieceCount(), + torrent.getPieceCount() + }); + } + + /** + * Piece availability handler. + * + *

+ * Handle updates in piece availability from a peer's HAVE message. When + * this happens, we need to mark that piece as available from the peer. + *

+ * + * @param peer The peer we got the update from. + * @param piece The piece that became available. + */ + @Override + public void handlePieceAvailability(PeerHandler peer, int piece) { + setAvailablePiece(piece, true); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Peer {} contributes {}/{} piece(s) " + + "[completed={}, available={}/{}] " + + "[connected={}/{}]", + new Object[]{ + getLocalPeerName(), + peer, + peer.getAvailablePieceCount(), + torrent.getPieceCount(), + torrent.getCompletedPieceCount(), + getAvailablePieceCount(), + torrent.getPieceCount(), + getConnectedPeerCount(), + getPeerCount() + }); + } + + /** + * Bit field availability handler. + * + *

+ * Handle updates in piece availability from a peer's BITFIELD message. + * When this happens, we need to mark in all the pieces the peer has that + * they can be reached through this peer, thus augmenting the global + * availability of pieces. + *

+ * + * @param peer The peer we got the update from. + * @param availablePieces The pieces availability bit field of the peer. + */ + @Override + public void handleBitfieldAvailability(PeerHandler peer, + BitSet prevAvailablePieces, + BitSet currAvailablePieces) { + + // Record that the peer no longer has all the pieces it previously told us it had. + for (int i = prevAvailablePieces.nextSetBit(0); i >= 0; + i = prevAvailablePieces.nextSetBit(i + 1)) { + if (!currAvailablePieces.get(i)) + setAvailablePiece(i, false); + } + + // Record that the peer has all the pieces it told us it had. + for (int i = currAvailablePieces.nextSetBit(0); i >= 0; + i = currAvailablePieces.nextSetBit(i + 1)) { + if (!prevAvailablePieces.get(i)) + setAvailablePiece(i, true); + } + + // Determine if the peer is interesting for us or not, and notify it. + BitSet interesting = currAvailablePieces; + torrent.andNotCompletedPieces(interesting); + this.andNotRequestedPieces(interesting); + + /* + if (interesting.isEmpty()) + peer.notInteresting(); + else + peer.interesting(); + */ + + if (LOG.isTraceEnabled()) + LOG.trace("{}: Peer {} contributes {} piece(s) ({} interesting) " + + "[completed={}; available={}/{}] " + + "[connected={}/{}]", + new Object[]{ + getLocalPeerName(), + peer, + currAvailablePieces.cardinality(), + interesting.cardinality(), + torrent.getCompletedPieceCount(), + getAvailablePieceCount(), + torrent.getPieceCount(), + getConnectedPeerCount(), + getPeerCount() + }); + + // Fast unchoking: TODO: Move to somewhere more useful. + if (connectedPeers.size() < MAX_DOWNLOADERS_UNCHOKE) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Fast-unchoking {}.", new Object[]{ + getLocalPeerName(), peer + }); + peer.unchoke(); + } + + } + + /** + * Block upload handler. + * + *

+ * When a block has been sent to a peer, we just record that we sent that + * many bytes. If the piece is valid on the peer's side, it will send us a + * HAVE message and we'll record that the piece is available on the peer at + * that moment (see handlePieceAvailability()). + *

+ * + * @param peer The peer we got this piece from. + * @param piece The piece in question. + */ + @Override + public void handleBlockSent(PeerHandler peer, int piece, int offset, int length) { + this.uploaded.addAndGet(length); + } + + @Override + public void handleBlockReceived(PeerHandler peer, int piece, int offset, int length) { + this.downloaded.addAndGet(length); + } + + /** + * Piece download completion handler. + * + *

+ * When a piece is completed, and valid, we announce to all connected peers + * that we now have this piece. + *

+ * + *

+ * We use this handler to identify when all of the pieces have been + * downloaded. When that's the case, we can start the seeding period, if + * any. + *

+ * + * @param peer The peer we got the piece from. + * @param piece The piece in question. + */ + @Override + public void handlePieceCompleted(PeerHandler peer, int piece, PieceHandler.Reception reception) + throws IOException { + // Regardless of validity, record the number of bytes downloaded and + // mark the piece as not requested anymore + synchronized (lock) { + // TODO: Not sure if this is required. + for (Iterator it = partialPieces.iterator(); it.hasNext(); /**/) { + PieceHandler.AnswerableRequestMessage request = it.next(); + if (request.getPiece() == piece) + it.remove(); + } + } + + if (reception == PieceHandler.Reception.VALID) { + // Make sure the piece is marked as completed in the torrent + // Note: this is required because the order the + // PeerActivityListeners are called is not defined, and we + // might be called before the torrent's piece completion + // handler is. + // Do this before we print the log message, else the counts are misleading. + torrent.setCompletedPiece(piece); + } + + if (LOG.isDebugEnabled()) + LOG.debug("{}: Completed download of piece {} from {} ({}). We now have {}/{} pieces and {} outstanding requests: {}", + new Object[]{ + getLocalPeerName(), + piece, peer, reception, + torrent.getCompletedPieceCount(), + torrent.getPieceCount(), + getRequestedPieceCount(), + getRequestedPieces() + }); + + if (reception == PieceHandler.Reception.VALID) { + // Send a HAVE message to all connected peers + PeerMessage have = new PeerMessage.HaveMessage(piece); + for (PeerHandler remote : getConnectedPeers()) + remote.send(have, true); + } else { + LOG.warn("{}, Downloaded piece#{} from {} was not valid: {} ;-(", new Object[]{ + getLocalPeerName(), + piece, peer, reception + }); + } + + // It's possible for more than one thread to get here simultaneously. + if (torrent.isComplete()) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: {}: Last piece ({}) validated and completed, finishing download.", new Object[]{ + getLocalPeerName(), torrent, piece + }); + + // Cancel all remaining outstanding requests + for (PeerHandler remote : getConnectedPeers()) + remote.cancelRequestsSent("torrent completed"); + + torrent.finish(); + } + } + + /** + * Peer disconnection handler. + * + *

+ * When a peer disconnects, we need to mark in all of the pieces it had + * available that they can't be reached through this peer anymore. + *

+ * + * @param peer The peer we got this piece from. + */ + @Override + public void handlePeerDisconnected(PeerHandler peer) { + BitSet peerAvailablePieces = peer.getAvailablePieces(); + for (int i = peerAvailablePieces.nextSetBit(0); i >= 0; + i = peerAvailablePieces.nextSetBit(i + 1)) { + setAvailablePiece(i, false); + } + + peer.rejectRequestsSent("peer disconnected"); + + if (LOG.isDebugEnabled()) + LOG.debug("{}: Peer {} went away with {} piece(s) " + + "[completed={}; available={}/{}] " + + "[connected={}/{}]", + new Object[]{ + getLocalPeerName(), + peer, + peer.getAvailablePieceCount(), + torrent.getCompletedPieceCount(), + getAvailablePieceCount(), + torrent.getPieceCount(), + getConnectedPeerCount(), + getPeerCount() + }); + + connectedPeers.remove(peer.getHexRemotePeerId(), peer); + + long now = System.currentTimeMillis(); + PeerInformation peerInformation = knownPeers.get(peer.getRemoteAddress()); + if (peerInformation != null) + peerInformation.setReconnectTime(getRandom(), now + RECONNECT_DELAY_TEMPORARY); + else + for (Map.Entry e : knownPeers.entrySet()) { + peerInformation = e.getValue(); + if (Arrays.equals(peerInformation.remotePeerId, peer.getRemotePeerId())) + peerInformation.setReconnectTime(getRandom(), now + RECONNECT_DELAY_TEMPORARY); + } + } + + @Override + public void handleIOException(PeerHandler peer, IOException ioe) { + LOG.warn("I/O error while exchanging data with " + peer + ", " + + "closing connection with it!", ioe); + peer.close("I/O error"); + // This should be done by handlePeerDisconnected but let's double up. + connectedPeers.remove(peer.getHexRemotePeerId(), peer); + } + + public void info(boolean verbose) { + double dl = 0; + double ul = 0; + for (PeerHandler peer : getConnectedPeers()) { + dl += peer.getDLRate().getRate(TimeUnit.SECONDS); + ul += peer.getULRate().getRate(TimeUnit.SECONDS); + } + + LOG.info("{} {} {} {}/{} pieces ({}%) [req {}/{} partial {}] with {}/{} peers at {}/{} kB/s.", + new Object[]{ + getLocalPeerName(), + torrent.getState().name(), + TorrentUtils.toHex(torrent.getInfoHash()), + torrent.getCompletedPieceCount(), + getPieceCount(), + String.format("%.2f", torrent.getCompletion()), + getRequestedPieceCount(), + getAvailablePieceCount(), + getPartialPieceCount(), + getConnectedPeerCount(), + getPeerCount(), + String.format("%.2f", dl / 1024.0), + String.format("%.2f", ul / 1024.0) + }); + + if (verbose) + for (PeerHandler peer : getConnectedPeers()) + LOG.debug(" | {}", peer); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentHandler.java new file mode 100644 index 000000000..5b37de605 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentHandler.java @@ -0,0 +1,583 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import com.google.common.io.Files; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.client.peer.PieceHandler; +import com.turn.ttorrent.client.storage.ByteStorage; +import com.turn.ttorrent.client.storage.FileStorage; +import com.turn.ttorrent.client.storage.FileCollectionStorage; + +import com.turn.ttorrent.tracker.client.TorrentMetadataProvider; +import com.turn.ttorrent.protocol.TorrentUtils; +import java.io.Closeable; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.util.BitSet; +import java.util.LinkedList; +import java.util.List; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadPoolExecutor; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages an individual torrent's state, providing an interface for reading and + * writing pieces. + * + *

+ * Piece completion and upload/download rates are tracked with this class. When + * all pieces are complete, {@link #isComplete()} will return true. + *

+ * + *

+ * A TorrentHandler instantiates a {@link SwarmHandler} on construction, but + * then waits for a call to it's {@link #start} method before doing anything. + * When the TorrentHandler is started, it will checksum the pieces that it finds + * on disk (if any) to determine which pieces are complete and incomplete, and + * then it calls {@link SwarmHandler#start()} to kickoff swarm-level management + * of the torrent. At the same time, it also calls + * {@link TrackerHandler#start()} to kickoff peer acquisition. + *

+ * + * @author mpetazzoni + */ +public class TorrentHandler implements TorrentMetadataProvider, Closeable { + + private static final Logger LOG = LoggerFactory.getLogger(TorrentHandler.class); + private final Client client; + private final Torrent torrent; + private final ByteStorage bucket; + private final SwarmHandler swarmHandler; + private final TrackerHandler trackerHandler; + @Nonnull + @GuardedBy("lock") + private State state = State.WAITING; + // private SortedSet rarest; + @Nonnull + @GuardedBy("lock") + private BitSet completedPieces = new BitSet(); + private int blockLength = PieceHandler.DEFAULT_BLOCK_SIZE; + private double maxUploadRate = 0.0; + private double maxDownloadRate = 0.0; + private final Object lock = new Object(); + + /** + * Create a new shared torrent from meta-info binary data. + * + * @param torrent The meta-info byte data. + * @param destDir The destination directory or location of the torrent + * files. + * @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. + */ + public TorrentHandler(@Nonnull Client client, @Nonnull byte[] torrent, @Nonnull File destDir) + throws IOException, URISyntaxException { + this(client, new Torrent(torrent), destDir); + } + + /* + public TorrentHandler(@Nonnull Client client, @Nonnull File torrent, @Nonnull File destDir) + throws IOException, URISyntaxException { + this(client, new Torrent(torrent), destDir); + } + */ + /** + * Create a new shared torrent from a base Torrent object. + * + *

+ * This will recreate a SharedTorrent object from the provided Torrent + * object's encoded meta-info data. + *

+ * + * @param torrent The Torrent object. + * @param destDir The destination directory or location of the torrent + * files. + * @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. + */ + public TorrentHandler(@Nonnull Client client, @Nonnull Torrent torrent, @Nonnull File destDir) + throws IOException { + this(client, torrent, toStorage(torrent, destDir)); + } + + @Nonnull + private static ByteStorage toStorage(@Nonnull Torrent torrent, @Nonnull File parent) + throws IOException { + Preconditions.checkNotNull(parent, "Parent directory was null."); + + String parentPath = parent.getCanonicalPath(); + + if (!torrent.isMultifile() && parent.isFile()) + return new FileStorage(parent, torrent.getSize()); + + List files = new LinkedList(); + long offset = 0L; + for (Torrent.TorrentFile file : torrent.getFiles()) { + // TODO: Files.simplifyPath() is a security check here to avoid jail-escape. + // However, it uses "/" not File.separator internally. + String path = Files.simplifyPath("/" + file.path); + File actual = new File(parent, path); + String actualPath = actual.getCanonicalPath(); + if (!actualPath.startsWith(parentPath)) + throw new SecurityException("Torrent file path attempted to break directory jail: " + actualPath + " is not within " + parentPath); + + FileUtils.forceMkdir(actual.getParentFile()); + files.add(new FileStorage(actual, offset, file.size)); + offset += file.size; + } + if (files.size() == 1) + return files.get(0); + return new FileCollectionStorage(files); + } + + /** + * Constructs a new TorrentHandler. + * + * @param torrent The meta-info byte data. + * @param bucket The storage bucket for the torrent data. + */ + public TorrentHandler(@Nonnull Client client, @Nonnull Torrent torrent, @Nonnull ByteStorage bucket) { + this.client = client; + this.torrent = torrent; + this.bucket = bucket; + this.swarmHandler = new SwarmHandler(this); + this.trackerHandler = new TrackerHandler(client, this, this.swarmHandler); + + if (bucket.isFinished()) + throw new IllegalStateException("ByteStorage is already finished."); + } + + @Nonnull + public Client getClient() { + return client; + } + + @Nonnull + private String getLocalPeerName() { + return getClient().getEnvironment().getLocalPeerName(); + } + + @Nonnull + public Torrent getTorrent() { + return torrent; + } + + @Nonnull + public ByteStorage getBucket() { + return bucket; + } + + @Nonnull + public String getName() { + return getTorrent().getName(); + } + + @Override + public byte[] getInfoHash() { + return getTorrent().getInfoHash(); + } + + @Nonnegative + public long getSize() { + return getTorrent().getSize(); + } + + @Nonnegative + public int getPieceCount() { + return getTorrent().getPieceCount(); + } + + @Nonnegative + public int getPieceLength() { + return getTorrent().getPieceLength(); + } + + @Nonnegative + public long getPieceOffset(@Nonnegative int index) { + return (long) index * (long) torrent.getPieceLength(); + } + + /** + * @see Torrent#getPieceLength(int) + */ + @Nonnegative + public int getPieceLength(@Nonnegative int index) { + return getTorrent().getPieceLength(index); + } + + @Nonnegative + public int getBlockLength() { + return blockLength; + } + + public void setBlockLength(@Nonnegative int blockLength) { + this.blockLength = blockLength; + } + + @Override + public List> getAnnounceList() { + return getTorrent().getAnnounceList(); + } + + @Nonnull + public SwarmHandler getSwarmHandler() { + return swarmHandler; + } + + @Nonnull + public TrackerHandler getTrackerHandler() { + return trackerHandler; + } + + @Nonnull + public State getState() { + synchronized (lock) { + return state; + } + } + + /** Idempotent. */ + public void setState(@Nonnull State state) { + synchronized (lock) { + if (this.state.equals(state)) + return; + this.state = state; + } + getClient().fireTorrentState(this, state); + } + + /** + * Return a copy of the completed pieces bitset. + */ + @Nonnull + public BitSet getCompletedPieces() { + if (!this.isInitialized()) + throw new IllegalStateException("Torrent not yet initialized!"); + synchronized (lock) { + return (BitSet) completedPieces.clone(); + } + } + + /** + * Mark a piece as completed, decrementing the piece size in bytes from our + * left bytes to download counter. + */ + public void setCompletedPiece(@Nonnegative int index) { + // A completed piece means that's that much data left to download for + // this torrent. + synchronized (lock) { + this.completedPieces.set(index); + } + } + + public boolean isCompletedPiece(@Nonnegative int index) { + synchronized (lock) { + return completedPieces.get(index); + } + } + + @Nonnegative + public int getCompletedPieceCount() { + synchronized (lock) { + return completedPieces.cardinality(); + } + } + + public void andNotCompletedPieces(BitSet b) { + synchronized (lock) { + b.andNot(completedPieces); + } + } + + /** + * Return the completion percentage of this torrent. + * + *

+ * This is computed from the number of completed pieces divided by the + * number of pieces in this torrent, times 100. + *

+ */ + public float getCompletion() { + return this.isInitialized() + ? (float) getCompletedPieceCount() / getPieceCount() * 100.0f + : 0.0f; + } + + 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; + } + + @Override + public long getUploaded() { + return swarmHandler.getUploaded(); + } + + @Override + public long getDownloaded() { + return swarmHandler.getDownloaded(); + } + + /** + * Get the number of bytes left to download for this torrent. + */ + @Override + public long getLeft() { + synchronized (lock) { + long count = getPieceCount() - getCompletedPieceCount(); + long left = count * getPieceLength(); + + int lastPieceIndex = getPieceCount() - 1; + if (!isCompletedPiece(lastPieceIndex)) { + left -= getPieceLength(); + left += getPieceLength(lastPieceIndex); + } + + return left; + } + } + + /** + * Tells whether this torrent has been fully initialized yet. + */ + public boolean isInitialized() { + switch (getState()) { + case WAITING: + case VALIDATING: + return false; + case SHARING: + case SEEDING: + return true; + case ERROR: + case DONE: + default: + return false; + } + } + + /** + * Build this torrent's pieces array. + * + *

+ * Hash and verify any potentially present local data and create this + * torrent's pieces array from their respective hash provided in the + * torrent meta-info. + *

+ * + *

+ * This function should be called soon after the constructor to initialize + * the pieces array. + *

+ */ + @VisibleForTesting + /* pp */ void init() throws InterruptedException, IOException { + { + State s = getState(); + if (s != State.WAITING) { + LOG.info("Restarting torrent from state " + s); + return; + } + } + setState(State.VALIDATING); + + // byte[] zeroHash = TorrentUtils.hash(new byte[torrent.getPieceLength()]); + + try { + int npieces = torrent.getPieceCount(); + + long size = getSize(); + // Store in a local so we can update with minimal synchronization. + BitSet completedPieces = new BitSet(npieces); + long completedSize = 0; + + ThreadPoolExecutor executor = client.getEnvironment().getExecutorService(); + // TorrentCreator.newExecutor("TorrentHandlerInit"); + try { + LOG.info("{}: Analyzing local data for {} ({} pieces)...", new Object[]{ + getLocalPeerName(), getName(), npieces + }); + + int step = 10; + CountDownLatch latch = new CountDownLatch(npieces); + for (int index = 0; index < npieces; index++) { + // TODO: Read the file sequentially and pass it to the validator. + // Otherwise we thrash the disk on validation. + ByteBuffer buffer = ByteBuffer.allocate(getPieceLength(index)); + bucket.read(buffer, getPieceOffset(index)); + buffer.flip(); + executor.execute(new PieceValidator(torrent, index, buffer, completedPieces, latch)); + + if (index / (float) npieces * 100f > step) { + LOG.info("{}: ... {}% complete", getLocalPeerName(), step); + step += 10; + } + } + latch.await(); + + for (int i = completedPieces.nextSetBit(0); i >= 0; + i = completedPieces.nextSetBit(i + 1)) { + completedSize += getPieceLength(i); + } + } finally { + // Request orderly executor shutdown and wait for hashing tasks to + // complete. + // executor.shutdown(); + // executor.awaitTermination(1, TimeUnit.SECONDS); + } + + LOG.debug("{}: {}: we have {}/{} bytes ({}%) [{}/{} pieces].", + new Object[]{ + getLocalPeerName(), getName(), + completedSize, size, + String.format("%.1f", (100f * (completedSize / (float) size))), + completedPieces.cardinality(), + getPieceCount() + }); + + synchronized (lock) { + this.completedPieces = completedPieces; + } + + if (isComplete()) { + setState(State.SEEDING); + finish(); + } else { + setState(State.SHARING); + } + } catch (Exception e) { + setState(State.ERROR); + Throwables.propagateIfPossible(e, InterruptedException.class, IOException.class); + throw Throwables.propagate(e); + } + } + + /** + * Display information about the BitTorrent client state. + * + *

+ * This emits an information line in the log about this client's state. It + * includes the number of choked peers, number of connected peers, number + * of known peers, information about the torrent availability and + * completion and current transmission rates. + *

+ */ + public void info(boolean verbose) { + getSwarmHandler().info(verbose); + } + + /** + * Finalize the download of this torrent. + * + *

+ * This realizes the final, pre-seeding phase actions on this torrent, + * which usually consists in putting the torrent data in their final form + * and at their target location. + *

+ * + * This call is idempotent. + * + * @see ByteStorage#finish() + */ + public void finish() throws IOException { + if (!isInitialized()) + throw new IllegalStateException("Torrent not yet initialized!"); + if (!isComplete()) + throw new IllegalStateException("Torrent download is not complete!"); + + bucket.finish(); + setState(State.SEEDING); + } + + public boolean isFinished() { + return isComplete() && bucket.isFinished(); + } + + /** + * Tells whether this torrent has been fully downloaded, or is fully + * available locally. + */ + public boolean isComplete() { + return getCompletedPieceCount() == getPieceCount(); + } + + @Override + public void close() throws IOException { + bucket.close(); + + // Determine final state + if (isFinished()) + setState(State.DONE); + else + setState(State.ERROR); + } + + public void start() throws InterruptedException, IOException { + init(); + swarmHandler.start(); + trackerHandler.start(); + } + + public void stop() throws IOException { + trackerHandler.stop(); + swarmHandler.stop(); + close(); + } + + @Override + public String toString() { + return TorrentUtils.toHex(getInfoHash()) + " [" + getCompletedPieceCount() + "/" + getPieceCount() + "]"; + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentRegistry.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentRegistry.java new file mode 100644 index 000000000..66bae39ee --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentRegistry.java @@ -0,0 +1,19 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import com.turn.ttorrent.protocol.PeerIdentityProvider; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public interface TorrentRegistry extends PeerIdentityProvider { + + @CheckForNull + public TorrentHandler getTorrent(@Nonnull byte[] infoHash); +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/TrackerHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/TrackerHandler.java new file mode 100644 index 000000000..3ebc4f9cf --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/TrackerHandler.java @@ -0,0 +1,472 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.turn.ttorrent.client.peer.PeerExistenceListener; +import com.turn.ttorrent.tracker.client.AnnounceResponseListener; +import com.turn.ttorrent.tracker.client.TorrentMetadataProvider; +import com.turn.ttorrent.tracker.client.TrackerClient; +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.protocol.tracker.Peer; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import javax.annotation.CheckForNull; +import javax.annotation.CheckForSigned; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * BitTorrent announce sub-system. + * + *

+ * A BitTorrent client must check-in to the torrent's tracker(s) to get peers + * and to report certain events. + *

+ * + *

+ * This TrackerHandler class maintains the state of each tracker known to the + * torrent, and manages a periodic announce event using the {@link Client} + * Client's {@link ScheduledExecutorService}. + *

+ * + *

+ * The announce state machine starts by making the initial 'started' announce + * request to register on the tracker and get the announce interval value. + * Subsequent announce requests are ordinary, event-less, periodic requests + * for peers. + *

+ * + * @author mpetazzoni + * @see TrackerMessage + */ +public class TrackerHandler implements Runnable, AnnounceResponseListener { + + private static final Logger LOG = LoggerFactory.getLogger(TrackerHandler.class); + public static final long DELAY_DEFAULT = 5000; + public static final long DELAY_MIN = 500; + public static final long DELAY_RESCHEDULE_DELTA = 100; + + @VisibleForTesting + /* pp */ static class TrackerState { + + private final URI uri; + private final int tier; + /** Epoch. */ + private long lastSend; + /** Epoch. */ + private long lastRecv; + private long lastErr; + /** Milliseconds, although protocol is in seconds. */ + private long interval = DELAY_DEFAULT; + + public TrackerState(@Nonnull URI uri, int tier) { + this.uri = uri; + this.tier = tier; + } + + @Nonnull + public URI getUri() { + return uri; + } + + /** In milliseconds. */ + public void setInterval(long interval) { + this.interval = interval; + } + + /** In milliseconds. */ + @CheckForSigned + public long getDelay() { + long last = Math.max(lastSend, lastRecv); + long then = last + interval; + return then - System.currentTimeMillis(); + } + + /** In milliseconds. */ + @Nonnegative + public long getRescheduleDelay() { + return Math.max(getDelay(), DELAY_MIN); // Could use 0 here. + } + + @Override + public String toString() { + return uri + " (tier=" + tier + ", interval=" + interval + ")"; + } + } + private final Client client; + private final TorrentMetadataProvider torrent; + private final PeerExistenceListener existenceListener; + private final List trackers = new ArrayList(); + private int trackerIndex = 0; + private ScheduledFuture future; + private final Object lock = new Object(); + + /** + * Initialize the base announce class members for the announcer. + * + * @param torrent The torrent we're announcing about. + * @param peer Our peer specification. + */ + public TrackerHandler(@Nonnull Client client, @Nonnull TorrentMetadataProvider torrent, @Nonnull PeerExistenceListener existenceListener) { + this.client = Preconditions.checkNotNull(client, "Client was null."); + this.torrent = Preconditions.checkNotNull(torrent, "TorrentMetadataProvider was null."); + this.existenceListener = Preconditions.checkNotNull(existenceListener, "PeerExistenceListener was null."); + } + + @Nonnull + private Client getClient() { + return client; + } + + @Nonnull + private String getLocalPeerName() { + return getClient().getEnvironment().getLocalPeerName(); + } + + @Nonnull + private ScheduledExecutorService getSchedulerService() { + return getClient().getEnvironment().getEventService(); + } + + @Nonnull + private String getTorrentName() { + return TorrentUtils.toHex(torrent.getInfoHash()); + } + + @Nonnull + private TrackerMessage.AnnounceEvent getAnnounceEvent() { + TorrentMetadataProvider.State state = torrent.getState(); + switch (state) { + case WAITING: + case VALIDATING: + case ERROR: + return TrackerMessage.AnnounceEvent.STOPPED; + case SHARING: + return TrackerMessage.AnnounceEvent.STARTED; + case SEEDING: + case DONE: + return TrackerMessage.AnnounceEvent.COMPLETED; + default: + throw new IllegalStateException("Unknown state " + state); + } + } + + /** + * Locate a {@link TrackerClient} announcing to the given tracker address. + * + * @param tracker The tracker address as a {@link URI}. + */ + @VisibleForTesting + /* pp */ TrackerClient getTrackerClient(@Nonnull URI tracker) { + String scheme = tracker.getScheme(); + // LOG.trace("Tracker scheme is " + scheme); + if ("http".equals(scheme) || "https".equals(scheme)) { + // LOG.trace("Looking for HttpTrackerClient"); + return getClient().getHttpTrackerClient(); + // } else if ("udp".equals(scheme)) { + // TODO: Check we have an ipv4 address before allowing the UDP protocol. + // return getClient().getUdpTrackerClient(); + } else { + return null; + } + } + + @CheckForNull + @VisibleForTesting + /* pp */ TrackerState getTracker(@Nonnull URI uri) { + // Needs synchronization against the swap() in promoteCurrentTracker() + synchronized (lock) { + // If we have no trackers, getCurrentTracker() throws OOBE. + /*{ + TrackerState tracker = getCurrentTracker(); + if (tracker.uri.equals(uri)) + return tracker; + }*/ + for (TrackerState tracker : trackers) + if (tracker.uri.equals(uri)) + return tracker; + } + LOG.warn("{}.{}: No tracker for {}: available are {}", new Object[]{ + getLocalPeerName(), getTorrentName(), + uri, trackers + }); + return null; + } + + /** + * Returns the current tracker client used for announces. + * + * Returns null if no trackers are available for this torrent. + */ + @Nonnull + @VisibleForTesting + /* pp */ TrackerState getCurrentTracker() { + synchronized (lock) { + return trackers.get(trackerIndex); + } + } + + /** + * Promotes the current tracker to the head of its tier. + * + * As defined by BEP#0012, when communication with a tracker is successful, + * it should be moved to the front of its tier. + */ + @VisibleForTesting + /* pp */ void promoteCurrentTracker() { + synchronized (lock) { + int currentTier = trackers.get(trackerIndex).tier; + int idx; + for (idx = trackerIndex - 1; idx >= 0; idx--) { + if (trackers.get(idx).tier != currentTier) { + idx++; + break; + } + } + Collections.swap(trackers, trackerIndex, idx); + trackerIndex = idx; + } + } + + /** + * Move to the next tracker client. + * + *

+ * If no more trackers are available in the current tier, move to the next + * tier. If we were on the last tier, restart from the first tier. + *

+ */ + @VisibleForTesting + /* pp */ boolean moveToNextTracker(@Nonnull TrackerState curr, @Nonnull String reason) { + TrackerState prev, next; + synchronized (lock) { + prev = getCurrentTracker(); + LOG.info("Moving from " + curr + " (currently " + prev); + if (curr != prev) + return false; + if (++trackerIndex >= trackers.size()) + trackerIndex = 0; + next = getCurrentTracker(); + } + if (LOG.isDebugEnabled()) + LOG.debug("{}.{}: Moved tracker: {} -> {}: {}", + new Object[]{ + getLocalPeerName(), getTorrentName(), + prev, next, reason + }); + if (LOG.isTraceEnabled()) + LOG.trace("{}", this); + return true; + } + + /********** Event drivers *****/ + public void start() { + if (LOG.isDebugEnabled()) + LOG.debug("{}.{}: Starting TrackerHandler for {}", new Object[]{ + getLocalPeerName(), getTorrentName(), + torrent + }); + synchronized (lock) { + + int tier = 0; + for (List announceTier : torrent.getAnnounceList()) { + if (LOG.isTraceEnabled()) + LOG.trace("{}.{}: Loading tier {}", new Object[]{ + getLocalPeerName(), getTorrentName(), + announceTier + }); + for (URI announceUri : announceTier) { + if (LOG.isTraceEnabled()) + LOG.trace("{}.{}: Loading client for {}", new Object[]{ + getLocalPeerName(), getTorrentName(), + announceUri + }); + if (getTrackerClient(announceUri) != null) { + trackers.add(new TrackerState(announceUri, tier)); + } else { + LOG.warn("{}.{}: No tracker client available for {}.", new Object[]{ + getLocalPeerName(), getTorrentName(), + announceUri + }); + } + } + tier++; + } + + LOG.info("{}.{}: Initialized announce sub-system with {} trackers on {}.", new Object[]{ + getLocalPeerName(), getTorrentName(), + trackers.size(), torrent + }); + + if (LOG.isDebugEnabled()) + LOG.debug("{}.{}: Started TrackerHandler {}", new Object[]{ + getLocalPeerName(), getTorrentName(), + this + }); + run(TrackerMessage.AnnounceEvent.STARTED); + } + } + + public void stop() { + LOG.info("{}.{}: Stopping TrackerHandler for {}", new Object[]{ + getLocalPeerName(), getTorrentName(), + torrent + }); + synchronized (lock) { + if (future != null) + future.cancel(false); + run_once(TrackerMessage.AnnounceEvent.STOPPED); + trackers.clear(); // Causes OOBE on future calls to getCurrentTracker(). + } + } + + private void reschedule(@Nonnegative long requestedDelay) { + // LOG.trace("Rescheduling tracker for {}", delay); + synchronized (lock) { + if (future != null) { + long actualDelay = future.getDelay(TimeUnit.MILLISECONDS); + long delta = actualDelay - requestedDelay; + // Don't reschedule if it's "soon". + if (LOG.isTraceEnabled()) + LOG.trace("{}.{}: Reschedule: requested={}, actual={}, delta={}", new Object[]{ + getLocalPeerName(), getTorrentName(), + requestedDelay, actualDelay, delta + }); + if (actualDelay > 0) + if (Math.abs(delta) < DELAY_RESCHEDULE_DELTA) + return; + future.cancel(false); + } + + if (requestedDelay < DELAY_MIN) + requestedDelay = DELAY_MIN; + future = getSchedulerService().schedule(this, requestedDelay, TimeUnit.MILLISECONDS); + } + } + + @CheckForNull + @VisibleForTesting + /* pp */ TrackerState run_once(TrackerMessage.AnnounceEvent event) { + TrackerState tracker; + synchronized (lock) { + if (trackers.isEmpty()) + return null; + tracker = getCurrentTracker(); + } + try { + TrackerClient client = getTrackerClient(tracker.uri); + client.announce(this, torrent, tracker.uri, event, false); + synchronized (lock) { + tracker.lastSend = System.currentTimeMillis(); + } + } catch (Exception e) { + LOG.error("{}.{}: Failed to announce to {}", new Object[]{ + getLocalPeerName(), getTorrentName(), + tracker + }, e); + moveToNextTracker(tracker, "Announce threw " + e); + } + return tracker; + } + + /** + * Performs a single step of the tracker state machine, then reschedules it. + * + * Broken out so that we can send an explicit STARTED on the first call. + */ + private void run(@CheckForNull TrackerMessage.AnnounceEvent event) { + if (event == null) + event = getAnnounceEvent(); + TrackerState tracker = run_once(event); + if (event != TrackerMessage.AnnounceEvent.STOPPED) + if (tracker != null) + reschedule(tracker.getRescheduleDelay()); + } + + /** + * Main scheduler callback. + */ + @Override + public void run() { + run(null); + } + + /** AnnounceResponseListener handler(s). **********************************/ + /** + * Handle an announce response event. + * + * @param interval The announce interval requested by the tracker. + * @param complete The number of seeders on this torrent. + * @param incomplete The number of leechers on this torrent. + */ + @Override + public void handleAnnounceResponse(URI uri, TrackerMessage.AnnounceEvent event, TrackerMessage.AnnounceResponseMessage message) { + Map peers = new HashMap(); + for (Peer peer : message.getPeers()) + peers.put(peer.getAddress(), peer.getPeerId()); + existenceListener.addPeers(peers, "tracker"); + + synchronized (lock) { + if (event == TrackerMessage.AnnounceEvent.STOPPED) + return; + TrackerState tracker = getTracker(uri); + if (tracker == null) + return; + tracker.lastRecv = System.currentTimeMillis(); + tracker.setInterval(TimeUnit.SECONDS.toMillis(message.getInterval())); + reschedule(tracker.getRescheduleDelay()); + } + } + + @Override + public void handleAnnounceFailed(URI uri, TrackerMessage.AnnounceEvent event, String reason) { + synchronized (lock) { + if (event == TrackerMessage.AnnounceEvent.STOPPED) + return; + TrackerState tracker = getTracker(uri); + if (tracker == null) + return; + tracker.lastErr = System.currentTimeMillis(); + moveToNextTracker(tracker, "Announce failed to " + uri + ": " + reason); + reschedule(getCurrentTracker().getRescheduleDelay()); + } + } + + @Override + public String toString() { + synchronized (lock) { + return Objects.toStringHelper(this) + .add("trackers", trackers) + .add("trackerIndex", trackerIndex) + .add("event", getAnnounceEvent()) + .toString(); + } + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerClient.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerClient.java new file mode 100644 index 000000000..c6f7c1dae --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerClient.java @@ -0,0 +1,102 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.io; + +import com.turn.ttorrent.client.ClientEnvironment; +import com.turn.ttorrent.client.peer.PeerConnectionListener; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelOption; +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Client for making outgoing connections to other peers. + * + *

+ * A PeerClient is a singleton per {@link Client}, and can be shared across + * torrents and swarms. + *

+ * + * @author shevek + */ +public class PeerClient extends PeerEndpoint { + + private static final Logger LOG = LoggerFactory.getLogger(PeerClient.class); + public static final int CLIENT_KEEP_ALIVE_MINUTES = PeerServer.CLIENT_KEEP_ALIVE_MINUTES; + private final ClientEnvironment environment; + private Bootstrap bootstrap; + private final Object lock = new Object(); + + public PeerClient(ClientEnvironment environment) { + this.environment = environment; + } + + public void start() throws Exception { + bootstrap = new Bootstrap(); + bootstrap.group(environment.getEventService()); + bootstrap.channel(environment.getEventLoopType().getClientChannelType()); + // SocketAddress address = environment.getLocalPeerListenAddress(); + // if (address != null) bootstrap.localAddress(address); + bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + bootstrap.option(ChannelOption.SO_KEEPALIVE, true); + bootstrap.option(ChannelOption.SO_REUSEADDR, true); + bootstrap.option(ChannelOption.TCP_NODELAY, true); + // bootstrap.option(ChannelOption.SO_TIMEOUT, (int) TimeUnit.MINUTES.toMillis(CLIENT_KEEP_ALIVE_MINUTES)); + bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) TimeUnit.SECONDS.toMillis(10)); + // TODO: Derive from PieceHandler.DEFAULT_BLOCK_SIZE + // bootstrap.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 1024 * 1024); + // bootstrap.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024); + } + + public void stop() throws InterruptedException { + bootstrap = null; + } + + @Nonnull + public ChannelFuture connect( + @Nonnull final PeerConnectionListener listener, + @Nonnull final byte[] infoHash, + @Nonnull final SocketAddress remoteAddress) { + final ChannelFuture future; + synchronized (lock) { + // connect -> initAndRegister grabs this, so we can safely synchronize here. + bootstrap.handler(new PeerClientHandshakeHandler(listener, infoHash, listener.getLocalPeerId())); + future = bootstrap.connect(remoteAddress); + } + future.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + try { + LOG.trace("Succeeded: {}", future.get()); + } catch (Exception e) { + // LOG.error("Connection to " + remoteAddress + " failed.", e); + listener.handlePeerConnectionFailed(remoteAddress, e); + Channel channel = future.channel(); + if (channel.isOpen()) + channel.close(); // .addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + } + } + }); + return future; + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerClientHandshakeHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerClientHandshakeHandler.java new file mode 100644 index 000000000..ae2900391 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerClientHandshakeHandler.java @@ -0,0 +1,89 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.io; + +import com.turn.ttorrent.client.peer.PeerConnectionListener; +import com.turn.ttorrent.protocol.TorrentUtils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.logging.LoggingHandler; +import java.util.Arrays; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class PeerClientHandshakeHandler extends PeerHandshakeHandler { + + private static final Logger logger = LoggerFactory.getLogger(PeerClientHandshakeHandler.class); + private static final LoggingHandler wireLogger = new LoggingHandler("client-wire"); + private static final LoggingHandler frameLogger = new LoggingHandler("client-frame"); + private static final LoggingHandler messageLogger = new LoggingHandler("client-message"); + @Nonnull + private final byte[] infoHash; + @Nonnull + private final byte[] peerId; + @Nonnull + private final PeerConnectionListener listener; + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public PeerClientHandshakeHandler( + @Nonnull PeerConnectionListener listener, + @Nonnull byte[] infoHash, + @Nonnull byte[] peerId) { + this.listener = listener; + this.infoHash = infoHash; + this.peerId = peerId; + } + + @Override + public LoggingHandler getWireLogger() { + return wireLogger; + } + + @Override + protected LoggingHandler getFrameLogger() { + return frameLogger; + } + + @Override + public LoggingHandler getMessageLogger() { + return messageLogger; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + PeerHandshakeMessage response = new PeerHandshakeMessage(infoHash, peerId); + ctx.writeAndFlush(toByteBuf(ctx, response), ctx.voidPromise()); + super.channelActive(ctx); + } + + @Override + protected void process(ChannelHandlerContext ctx, PeerHandshakeMessage message) { + // We were the connecting client. + if (!Arrays.equals(infoHash, message.getInfoHash())) { + logger.warn("InfoHash mismatch: requested " + TorrentUtils.toHex(infoHash) + " but received " + message); + ctx.close().addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + return; + } + + addPeer(ctx, message, listener); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerEndpoint.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerEndpoint.java new file mode 100644 index 000000000..b80ce4e46 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerEndpoint.java @@ -0,0 +1,12 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.io; + +/** + * + * @author shevek + */ +public class PeerEndpoint { +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerExtendedMessage.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerExtendedMessage.java new file mode 100644 index 000000000..953d8edf7 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerExtendedMessage.java @@ -0,0 +1,399 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.io; + +import com.google.common.base.Preconditions; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Shorts; +import com.google.common.primitives.UnsignedBytes; +import com.turn.ttorrent.protocol.bcodec.BEUtils; +import com.turn.ttorrent.protocol.bcodec.BEValue; +import com.turn.ttorrent.protocol.bcodec.NettyBDecoder; +import com.turn.ttorrent.protocol.bcodec.NettyBEncoder; +import com.turn.ttorrent.client.peer.PeerHandler; +import io.netty.buffer.ByteBuf; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.CheckForSigned; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.OverridingMethodsMustInvokeSuper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Supports extensions to the BitTorrent protocol with extended message types. + * + * Currently supported extensions: + * + *
    + *
  • BEP 10 Extended Handshake: Allows peers to tell each other which protocol + * extensions they support
  • + *
  • UT Peer Exchange (UT_PEX): PEX allows peers to tell each other about + * peers that they know about, lowering load on trackers and potentially + * obviating them. There are two competing PEX protocols; we implement the one + * that works within the BEP 10 extension protocol. Couldn't find a spec for it + * anywhere, but a reference implementation is at + * https://github.com/libtorrent/libtorrent/blob/master/src/ut_pex.cpp
  • + *
+ * + * @author shevek + */ +public abstract class PeerExtendedMessage extends PeerMessage { + + private static final Logger LOG = LoggerFactory.getLogger(PeerExtendedMessage.class); + + public static enum ExtendedType { + + // Must be ordinal zero. + handshake, + ut_pex; + } + + @Override + public Type getType() { + return Type.EXTENDED; + } + + @Nonnull + public abstract ExtendedType getExtendedType(); + + @Override + @OverridingMethodsMustInvokeSuper + public void toWire(ByteBuf out, Map extendedTypes) throws IOException { + super.toWire(out, extendedTypes); + Byte remoteType = extendedTypes.get(getExtendedType()); + if (remoteType == null) + throw new NullPointerException("Unsupported extension " + getExtendedType() + "; remote supports " + extendedTypes); + out.writeByte(remoteType); + } + + public static class HandshakeMessage extends PeerExtendedMessage { + + public static final String K_MESSAGE_TYPES = "m"; + public static final String K_SENDER_IPV4 = "ipv4"; + public static final String K_SENDER_IPV6 = "ipv6"; + public static final String K_SENDER_PORT = "p"; + public static final String K_SENDER_VERSION = "v"; + public static final String K_SENDER_REQUEST_QUEUE_LENGTH = "reqq"; + public static final String K_RECEIVER_IP = "yourip"; + private final Map senderExtendedTypeMap = new EnumMap(ExtendedType.class); + private byte[] senderIp4; + private byte[] senderIp6; + private int senderPort; + private String senderVersion; + private int senderRequestQueueLength; + private byte[] receiverIp; + + /** + * Constructed for remote, received at local. + */ + public HandshakeMessage() { + senderExtendedTypeMap.put(ExtendedType.handshake, (byte) 0); + } + + /** + * Constructed for local, transmitted to remote. + */ + public HandshakeMessage(int senderRequestQueueLength, Set senderAddresses) { + this(); + this.senderRequestQueueLength = senderRequestQueueLength; + for (ExtendedType type : ExtendedType.values()) + senderExtendedTypeMap.put(type, (byte) type.ordinal()); + for (SocketAddress senderAddress : senderAddresses) { + if (!(senderAddress instanceof InetSocketAddress)) + continue; + InetSocketAddress socketAddress = (InetSocketAddress) senderAddress; + InetAddress inetAddress = socketAddress.getAddress(); + if (inetAddress == null) + continue; + if (inetAddress instanceof Inet4Address) { + senderIp4 = inetAddress.getAddress(); + senderPort = socketAddress.getPort(); + } else if (inetAddress instanceof Inet6Address) { + senderIp6 = inetAddress.getAddress(); + senderPort = socketAddress.getPort(); + } + } + } + + @Override + public ExtendedType getExtendedType() { + return ExtendedType.handshake; + } + + @Nonnull + public Map getSenderExtendedTypeMap() { + return senderExtendedTypeMap; + } + + @CheckForNull + private InetAddress toAddress(@CheckForNull byte[] address) throws UnknownHostException { + if (address == null) + return null; + return InetAddress.getByAddress(address); + } + + @CheckForNull + private InetSocketAddress toSocketAddress(@CheckForNull byte[] address, @CheckForSigned int defaultPort) throws UnknownHostException { + if (address == null) + return null; + int port = senderPort; + if (port <= 0) + port = defaultPort; + if (port <= 0) + return null; + InetAddress inetAddress = toAddress(address); + if (inetAddress == null) + return null; + return new InetSocketAddress(inetAddress, port); + } + + @CheckForNull + public InetSocketAddress getSenderIp4Address(@CheckForSigned int defaultPort) throws UnknownHostException { + return toSocketAddress(senderIp4, defaultPort); + } + + @CheckForNull + public InetSocketAddress getSenderIp6Address(@CheckForSigned int defaultPort) throws UnknownHostException { + return toSocketAddress(senderIp6, defaultPort); + } + + @CheckForNull + public String getSenderVersion() { + return senderVersion; + } + + @CheckForSigned + public int getSenderRequestQueueLength() { + return senderRequestQueueLength; + } + + @Override + public void fromWire(ByteBuf in) throws IOException { + NettyBDecoder decoder = new NettyBDecoder(in); + Map payload = decoder.bdecodeMap().getMap(); + EXTMSG: { + BEValue tmpValue = payload.get(K_MESSAGE_TYPES); + if (tmpValue == null) + break EXTMSG; + Map tmp = tmpValue.getMap(); + senderExtendedTypeMap.clear(); + for (Map.Entry e : tmp.entrySet()) { + try { + ExtendedType type = ExtendedType.valueOf(e.getKey()); + senderExtendedTypeMap.put(type, UnsignedBytes.checkedCast(e.getValue().getInt())); + } catch (IllegalArgumentException _e) { + LOG.debug("Ignored unknown sender extended type " + e.getKey()); + } + } + } + + senderIp4 = BEUtils.getBytes(payload.get(K_SENDER_IPV4)); + senderIp6 = BEUtils.getBytes(payload.get(K_SENDER_IPV6)); + senderPort = BEUtils.getInt(payload.get(K_SENDER_PORT), -1); + senderVersion = BEUtils.getString(payload.get(K_SENDER_VERSION)); + senderRequestQueueLength = BEUtils.getInt(payload.get(K_SENDER_REQUEST_QUEUE_LENGTH), PeerHandler.MAX_REQUESTS_SENT); + receiverIp = BEUtils.getBytes(payload.get(K_RECEIVER_IP)); + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) throws IOException { + super.toWire(out, extendedTypes); + NettyBEncoder encoder = new NettyBEncoder(out); + Map payload = new HashMap(); + { + Map types = new HashMap(); + // This is always our data set, so we could just use ordinal(), but let's not cheat; it always bites us later. + for (Map.Entry e : senderExtendedTypeMap.entrySet()) + types.put(e.getKey().name(), new BEValue(e.getValue().byteValue() & 0xFF)); + for (ExtendedType extendedType : ExtendedType.values()) + types.put(extendedType.name(), new BEValue(extendedType.ordinal())); + payload.put(K_MESSAGE_TYPES, new BEValue(types)); + } + if (senderIp4 != null) + payload.put(K_SENDER_IPV4, new BEValue(senderIp4)); + if (senderIp6 != null) + payload.put(K_SENDER_IPV6, new BEValue(senderIp6)); + if (senderPort > 0) + payload.put(K_SENDER_PORT, new BEValue(senderPort)); + if (senderVersion != null) + payload.put(K_SENDER_PORT, new BEValue(senderVersion)); + if (senderRequestQueueLength > 0) + payload.put(K_SENDER_REQUEST_QUEUE_LENGTH, new BEValue(senderRequestQueueLength)); + if (receiverIp != null) + payload.put(K_RECEIVER_IP, new BEValue(receiverIp)); + encoder.bencode(payload); + } + + @Override + public String toString() { + String ret = super.toString() + " " + getSenderExtendedTypeMap(); + try { + ret = ret + " ip4=" + toAddress(senderIp4); + } catch (UnknownHostException e) { + ret = ret + " ip4-error=" + e; + } + try { + ret = ret + " ip6=" + toAddress(senderIp6); + } catch (UnknownHostException e) { + ret = ret + " ip6-error=" + e; + } + ret = ret + " port=" + senderPort; + return ret; + } + } + + public static class UtPexMessage extends PeerExtendedMessage { + + public static final String ADDED = "added"; + public static final String ADDED_F = "added.f"; + public static final String ADDED6 = "added6"; + public static final String ADDED6_F = "added6.f"; + public static final String DROPPED = "dropped"; + public static final String DROPPED6 = "dropped6"; + private final List added; + private final List dropped; + + public UtPexMessage() { + this.added = new ArrayList(); + this.dropped = new ArrayList(); + } + + public UtPexMessage(@Nonnull List added, @Nonnull List dropped) { + this.added = Preconditions.checkNotNull(added, "Added was null."); + this.dropped = Preconditions.checkNotNull(dropped, "Dropped was null."); + } + + @Nonnull + public List getAdded() { + return added; + } + + @Nonnull + public List getDropped() { + return dropped; + } + + private static void getAddresses(@Nonnull List out, @CheckForNull byte[] in, @Nonnegative int size) throws UnknownHostException { + if (in == null) + return; + byte[] value = new byte[size]; + int ptr = 0; + while (ptr <= (in.length - size - 2)) { + System.arraycopy(in, ptr, value, 0, size); + InetAddress address = InetAddress.getByAddress(value); + int port = Ints.fromBytes((byte) 0, (byte) 0, in[ptr + size], in[ptr + size + 1]); + out.add(new InetSocketAddress(address, port)); + ptr = ptr + size + 2; + } + } + + @Override + public void fromWire(ByteBuf in) throws IOException { + NettyBDecoder decoder = new NettyBDecoder(in); + Map map = decoder.bdecodeMap().getMap(); + + added.clear(); + getAddresses(added, BEUtils.getBytes(map.get(ADDED)), 4); + getAddresses(added, BEUtils.getBytes(map.get(ADDED6)), 16); + + dropped.clear(); + getAddresses(dropped, BEUtils.getBytes(map.get(DROPPED)), 4); + getAddresses(dropped, BEUtils.getBytes(map.get(DROPPED6)), 16); + } + + private static void put(@Nonnull Map out, @Nonnull String key, @Nonnull ByteArrayOutputStream data) { + if (data.size() == 0) + return; + out.put(key, new BEValue(data.toByteArray())); + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) throws IOException { + super.toWire(out, extendedTypes); + Map map = new HashMap(); + + ADDED: + { + ByteArrayOutputStream out_added = new ByteArrayOutputStream(); + ByteArrayOutputStream out_added_f = new ByteArrayOutputStream(); + ByteArrayOutputStream out_added6 = new ByteArrayOutputStream(); + ByteArrayOutputStream out_added6_f = new ByteArrayOutputStream(); + + for (InetSocketAddress socketAddress : added) { + InetAddress address = socketAddress.getAddress(); + if (address instanceof Inet4Address) { + out_added.write(address.getAddress()); + out_added.write(Shorts.toByteArray((short) socketAddress.getPort())); + out_added_f.write(0); + } else if (address instanceof Inet6Address) { + out_added6.write(address.getAddress()); + out_added6.write(Shorts.toByteArray((short) socketAddress.getPort())); + out_added6_f.write(0); + } else { + } + } + + put(map, ADDED, out_added); + put(map, ADDED_F, out_added_f); + put(map, ADDED6, out_added6); + put(map, ADDED6_F, out_added6_f); + } + + REMOVED: + { + ByteArrayOutputStream out_dropped = new ByteArrayOutputStream(); + ByteArrayOutputStream out_dropped6 = new ByteArrayOutputStream(); + + for (InetSocketAddress socketAddress : dropped) { + InetAddress address = socketAddress.getAddress(); + if (address instanceof Inet4Address) { + out_dropped.write(address.getAddress()); + out_dropped.write(Shorts.toByteArray((short) socketAddress.getPort())); + } else if (address instanceof Inet6Address) { + out_dropped6.write(address.getAddress()); + out_dropped6.write(Shorts.toByteArray((short) socketAddress.getPort())); + } else { + } + } + + put(map, DROPPED, out_dropped); + put(map, DROPPED6, out_dropped6); + } + + NettyBEncoder encoder = new NettyBEncoder(out); + encoder.bencode(map); + } + + @Override + public ExtendedType getExtendedType() { + return ExtendedType.ut_pex; + } + + @Override + public String toString() { + return super.toString() + "; added=" + getAdded() + "; dropped=" + getDropped(); + } + } + + @Override + public String toString() { + return super.toString() + "." + getExtendedType().name(); + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerFrameDecoder.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerFrameDecoder.java new file mode 100644 index 000000000..0def05e26 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerFrameDecoder.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.io; + +import com.google.common.annotations.VisibleForTesting; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; + +/** + * + * @author shevek + */ +public class PeerFrameDecoder extends LengthFieldBasedFrameDecoder { + + public PeerFrameDecoder() { + super(1048576, 0, PeerMessage.MESSAGE_LENGTH_FIELD_SIZE, 0, 4); + } + + @VisibleForTesting + /* pp */ ByteBuf _decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + return (ByteBuf) super.decode(ctx, in); + } +} diff --git a/src/main/java/com/turn/ttorrent/client/peer/MessageListener.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerFrameEncoder.java similarity index 55% rename from src/main/java/com/turn/ttorrent/client/peer/MessageListener.java rename to ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerFrameEncoder.java index 7db1d474e..c8ad5c459 100644 --- a/src/main/java/com/turn/ttorrent/client/peer/MessageListener.java +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerFrameEncoder.java @@ -1,11 +1,11 @@ -/** - * Copyright (C) 2011-2012 Turn, Inc. +/* + * Copyright 2014 shevek. * * 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 + * 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, @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.turn.ttorrent.client.peer; - -import com.turn.ttorrent.common.protocol.PeerMessage; - -import java.util.EventListener; +package com.turn.ttorrent.client.io; +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.LengthFieldPrepender; /** - * EventListener interface for objects that want to receive incoming messages - * from peers. * - * @author mpetazzoni + * @author shevek */ -public interface MessageListener extends EventListener { +@ChannelHandler.Sharable +public class PeerFrameEncoder extends LengthFieldPrepender { - public void handleMessage(PeerMessage msg); + public PeerFrameEncoder() { + super(PeerMessage.MESSAGE_LENGTH_FIELD_SIZE); + } } diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerHandshakeHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerHandshakeHandler.java new file mode 100644 index 000000000..83309a8dd --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerHandshakeHandler.java @@ -0,0 +1,102 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.io; + +import com.turn.ttorrent.client.peer.PeerConnectionListener; +import com.turn.ttorrent.client.peer.PeerHandler; +import com.turn.ttorrent.client.peer.PeerMessageListener; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.ReferenceCountUtil; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + * @see BitTorrent handshake specification + * @see PeerServerHandshakeHandler + * @see PeerClientHandshakeHandler + */ +public abstract class PeerHandshakeHandler extends LengthFieldBasedFrameDecoder { + + private static final Logger LOG = LoggerFactory.getLogger(PeerHandshakeHandler.class); + + // protected static final PeerFrameEncoder frameEncoder = new PeerFrameEncoder(); + public PeerHandshakeHandler() { + super(1024, 0, 1, PeerHandshakeMessage.BASE_HANDSHAKE_LENGTH - 1, 0); + } + + @Nonnull + protected abstract LoggingHandler getWireLogger(); + + @Nonnull + protected abstract LoggingHandler getFrameLogger(); + + @Nonnull + protected abstract LoggingHandler getMessageLogger(); + + @Nonnull + protected ByteBuf toByteBuf(@Nonnull ChannelHandlerContext ctx, @Nonnull PeerHandshakeMessage message) { + ByteBuf buf = ctx.alloc().buffer(PeerHandshakeMessage.BASE_HANDSHAKE_LENGTH + 64); + message.toWire(buf); + return buf; + } + + private void addMessageHandlers(@Nonnull ChannelPipeline pipeline, @Nonnull PeerMessageListener listener) { + // TODO: Merge LengthFieldPrepender into PeerMessageCodec and use only a PeerFrameDecoder here. + pipeline.addLast(new PeerFrameDecoder()); + // pipeline.addLast(frameEncoder); + // pipeline.addLast(getFrameLogger()); + pipeline.addLast(new PeerMessageCodec(listener)); + // pipeline.addLast(getMessageLogger()); + // pipeline.addLast(new PeerMessageTrafficShapingHandler()); + pipeline.addLast(new PeerMessageHandler(listener)); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + // ctx.pipeline().addFirst(getWireLogger()); + super.channelRegistered(ctx); + } + + protected abstract void process(@Nonnull ChannelHandlerContext ctx, @Nonnull PeerHandshakeMessage message); + + protected void addPeer(@Nonnull ChannelHandlerContext ctx, @Nonnull PeerHandshakeMessage message, + @Nonnull PeerConnectionListener listener) { + Channel channel = ctx.channel(); + PeerHandler peer = listener.handlePeerConnectionCreated(channel, message.getPeerId(), message.getReserved()); + if (peer == null) { + ctx.close().addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + return; + } + + addMessageHandlers(ctx.pipeline(), peer); + ctx.pipeline().remove(this); + + listener.handlePeerConnectionReady(peer); + } + + @Override + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + Object message = super.decode(ctx, in); + if (message instanceof ByteBuf) { + PeerHandshakeMessage request = new PeerHandshakeMessage(); + request.fromWire((ByteBuf) message); + process(ctx, request); + ReferenceCountUtil.release(message); + return null; + } else { + // Including null. + return message; + } + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerHandshakeMessage.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerHandshakeMessage.java new file mode 100644 index 000000000..7db0b31b5 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerHandshakeMessage.java @@ -0,0 +1,134 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.io; + +import com.turn.ttorrent.client.io.PeerExtendedMessage.ExtendedType; +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.protocol.bcodec.BEUtils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.netty.buffer.ByteBuf; +import java.util.Arrays; +import java.util.Map; +import javax.annotation.Nonnull; +import org.apache.commons.io.Charsets; + +/** + * + * @author shevek + */ +public class PeerHandshakeMessage extends PeerMessage { + + public static enum Feature { + + BEP10_EXTENSION_PROTOCOL { + @Override + public boolean get(byte[] reserved) { + return (reserved[5] & 0x10) != 0; + } + + @Override + public void set(byte[] reserved) { + reserved[5] |= 0x10; + } + }; + + public abstract boolean get(@Nonnull byte[] reserved); + + public abstract void set(@Nonnull byte[] reserved); + } + public static final int BASE_HANDSHAKE_LENGTH = 49; + private static final byte[] BITTORRENT_PROTOCOL_IDENTIFIER = "BitTorrent protocol".getBytes(BEUtils.BYTE_ENCODING); + private byte[] protocolName; + private final byte[] reserved = new byte[8]; + private byte[] infoHash; // 20 + private byte[] peerId; // 20 + + public PeerHandshakeMessage() { + Feature.BEP10_EXTENSION_PROTOCOL.set(reserved); // Overwritten by fromWire() + } + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public PeerHandshakeMessage(@Nonnull byte[] infoHash, @Nonnull byte[] peerId) { + this(); + if (infoHash.length != 20) + throw new IllegalArgumentException("InfoHash length should be 20, not " + infoHash.length); + if (peerId.length != 20) + throw new IllegalArgumentException("PeerId length should be 20, not " + peerId.length); + this.protocolName = BITTORRENT_PROTOCOL_IDENTIFIER; + this.infoHash = infoHash; + this.peerId = peerId; + } + + @Override + public Type getType() { + return Type.HANDSHAKE; + } + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public byte[] getInfoHash() { + return infoHash; + } + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public byte[] getPeerId() { + return peerId; + } + + @Nonnull + @SuppressFBWarnings("EI_EXPOSE_REP2") + public byte[] getReserved() { + return reserved; + } + + @Override + public void fromWire(ByteBuf in) { + int pstrlen = in.readUnsignedByte(); + if (pstrlen < 0 || in.readableBytes() < BASE_HANDSHAKE_LENGTH + pstrlen - 1) + throw new IllegalArgumentException("Incorrect handshake message length (pstrlen=" + pstrlen + ") !"); + + // Check the protocol identification string + protocolName = new byte[pstrlen]; + in.readBytes(protocolName); + if (!Arrays.equals(protocolName, BITTORRENT_PROTOCOL_IDENTIFIER)) + throw new IllegalArgumentException("Unknown protocol " + new String(protocolName, Charsets.ISO_8859_1)); + + // Ignore reserved bytes + in.readBytes(reserved); + + infoHash = new byte[20]; + in.readBytes(infoHash); + peerId = new byte[20]; + in.readBytes(peerId); + } + + public void toWire(@Nonnull ByteBuf out) { + out.writeByte(protocolName.length); + out.writeBytes(protocolName); + out.writeBytes(reserved); + out.writeBytes(infoHash); + out.writeBytes(peerId); + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) { + throw new UnsupportedOperationException("Message should not appear in normal protocol."); + } + + @Override + public String toString() { + return super.toString() + " P=" + TorrentUtils.toTextOrNull(getPeerId()) + " T=" + TorrentUtils.toHexOrNull(getInfoHash()); + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessage.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessage.java new file mode 100644 index 000000000..0e9d8aa0f --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessage.java @@ -0,0 +1,538 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client.io; + +import com.google.common.base.Preconditions; +import com.turn.ttorrent.client.PeerPieceProvider; +import com.turn.ttorrent.client.io.PeerExtendedMessage.ExtendedType; +import com.turn.ttorrent.protocol.TorrentUtils; +import io.netty.buffer.ByteBuf; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.text.ParseException; +import java.util.BitSet; +import java.util.Map; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * BitTorrent peer protocol messages representations. + * + *

+ * This class and its *Messages subclasses provide POJO + * representations of the peer protocol messages, along with easy parsing from + * an input ByteBuffer to quickly get a usable representation of an incoming + * message. + *

+ * + * @author mpetazzoni + * @see BitTorrent peer wire protocol + */ +public abstract class PeerMessage { + + private static final Logger LOG = LoggerFactory.getLogger(PeerMessage.class); + /** The size, in bytes, of the length field in a message (one 32-bit + * integer). */ + public static final int MESSAGE_LENGTH_FIELD_SIZE = 4; + + /** + * Message type. + * + *

+ * Note that the keep-alive messages don't actually have an type ID defined + * in the protocol as they are of length 0. + *

+ */ + public enum Type { + + HANDSHAKE(-2), + KEEP_ALIVE(-1), + CHOKE(0), + UNCHOKE(1), + INTERESTED(2), + NOT_INTERESTED(3), + HAVE(4), + BITFIELD(5), + REQUEST(6), + PIECE(7), + CANCEL(8), + EXTENDED(20); + private byte id; + + Type(int id) { + this.id = (byte) id; + } + + public byte getTypeByte() { + return id; + } + }; + + @Nonnull + public abstract Type getType(); + + /** Reads everything except the length and type code. */ + public abstract void fromWire(@Nonnull ByteBuf in) throws IOException; + + /** Writes everything except the length. */ + public void toWire(@Nonnull ByteBuf out, @Nonnull Map extendedTypes) throws IOException { + out.writeByte(getType().getTypeByte()); + } + + /** + * Validate that this message makes sense for the torrent it's related to. + * + *

+ * This method is meant to be overloaded by distinct message types, where + * it makes sense. Otherwise, it defaults to true. + *

+ * + * @param torrent The torrent this message is about. + */ + public PeerMessage validate(PeerPieceProvider torrent) + throws MessageValidationException { + return this; + } + + @Override + public String toString() { + return this.getType().name(); + } + + public static class MessageValidationException extends ParseException { + + static final long serialVersionUID = -1; + + public MessageValidationException(PeerMessage m) { + super("Message " + m + " is not valid!", 0); + } + } + + /** + * Keep alive message. + * + * + */ + public static class KeepAliveMessage extends PeerMessage { + + @Override + public Type getType() { + return Type.KEEP_ALIVE; + } + + @Override + public void fromWire(ByteBuf in) { + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) { + } + } + + /** + * Choke message. + * + * + */ + public static class ChokeMessage extends PeerMessage { + + @Override + public Type getType() { + return Type.CHOKE; + } + + @Override + public void fromWire(ByteBuf in) { + } + } + + /** + * Unchoke message. + * + * + */ + public static class UnchokeMessage extends PeerMessage { + + @Override + public Type getType() { + return Type.UNCHOKE; + } + + @Override + public void fromWire(ByteBuf in) { + } + } + + /** + * Interested message. + * + * + */ + public static class InterestedMessage extends PeerMessage { + + @Override + public Type getType() { + return Type.INTERESTED; + } + + @Override + public void fromWire(ByteBuf in) { + } + } + + /** + * Not interested message. + * + * + */ + public static class NotInterestedMessage extends PeerMessage { + + @Override + public Type getType() { + return Type.NOT_INTERESTED; + } + + @Override + public void fromWire(ByteBuf in) { + } + } + + /** + * Have message. + * + * + */ + public static class HaveMessage extends PeerMessage { + + private int piece; + + public HaveMessage() { + } + + public HaveMessage(@Nonnegative int piece) { + this.piece = piece; + } + + @Override + public Type getType() { + return Type.HAVE; + } + + @Nonnegative + public int getPiece() { + return this.piece; + } + + @Override + public void fromWire(ByteBuf in) { + piece = in.readInt(); + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) throws IOException { + super.toWire(out, extendedTypes); + out.writeInt(piece); + } + + @Override + public HaveMessage validate(PeerPieceProvider torrent) + throws MessageValidationException { + if (this.piece >= 0 && this.piece < torrent.getPieceCount()) + return this; + throw new MessageValidationException(this); + } + + @Override + public String toString() { + return super.toString() + " #" + this.getPiece(); + } + } + + /** + * Bitfield message. + * + * + */ + public static class BitfieldMessage extends PeerMessage { + + private BitSet bitfield; + + public BitfieldMessage() { + } + + public BitfieldMessage(@Nonnull BitSet bitfield) { + this.bitfield = bitfield; + } + + @Override + public Type getType() { + return Type.BITFIELD; + } + + public static byte reverse(byte b) { + int i = b & 0xFF; + i = (i & 0x55) << 1 | (i >>> 1) & 0x55; + i = (i & 0x33) << 2 | (i >>> 2) & 0x33; + i = (i & 0x0f) << 4 | (i >>> 4) & 0x0f; + return (byte) i; + } + + @Nonnull + public BitSet getBitfield() { + return this.bitfield; + } + + @Override + public void fromWire(ByteBuf in) { + byte[] bytes = new byte[in.readableBytes()]; + in.readBytes(bytes); + for (int i = 0; i < bytes.length; i++) + bytes[i] = reverse(bytes[i]); + bitfield = BitSet.valueOf(bytes); + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) throws IOException { + super.toWire(out, extendedTypes); + byte[] bytes = bitfield.toByteArray(); + for (int i = 0; i < bytes.length; i++) + bytes[i] = reverse(bytes[i]); + out.writeBytes(bytes); + // TODO: Extend this byte array to the required length. + // out.writeBytes(new byte[torrent.getPieceCount() / 8 - bytes.length]); + } + + @Override + public BitfieldMessage validate(PeerPieceProvider torrent) + throws MessageValidationException { + if (this.bitfield.length() > torrent.getPieceCount()) + throw new MessageValidationException(this); + return this; + } + + @Override + public String toString() { + return super.toString() + " " + this.getBitfield().cardinality(); + } + } + + public static abstract class AbstractPieceMessage extends PeerMessage { + + private int piece; + private int offset; + + public AbstractPieceMessage() { + } + + public AbstractPieceMessage(int piece, int offset) { + this.piece = piece; + this.offset = offset; + } + + @Nonnegative + public int getPiece() { + return this.piece; + } + + @Nonnegative + public int getOffset() { + return this.offset; + } + + @Nonnegative + public abstract int getLength(); + + @Override + public void fromWire(ByteBuf in) { + piece = in.readInt(); + offset = in.readInt(); + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) throws IOException { + super.toWire(out, extendedTypes); + out.writeInt(piece); + out.writeInt(offset); + } + + @Override + public AbstractPieceMessage validate(PeerPieceProvider torrent) + throws MessageValidationException { + if (getPiece() < 0) + throw new MessageValidationException(this); + if (getPiece() > torrent.getPieceCount()) + throw new MessageValidationException(this); + if (getOffset() + getLength() > torrent.getPieceLength(piece)) + throw new MessageValidationException(this); + return this; + } + + public boolean answers(@Nonnull AbstractPieceMessage message) { + Preconditions.checkNotNull(message, "Message was null."); + return getPiece() == message.getPiece() + && getOffset() == message.getOffset() + && getLength() == message.getLength(); + } + + @Override + public String toString() { + return super.toString() + " #" + this.getPiece() + + " (" + this.getLength() + "@" + this.getOffset() + ")"; + } + } + + /** + * Request message. + * + * + */ + public static class RequestMessage extends AbstractPieceMessage { + + private int length; + + public RequestMessage() { + } + + public RequestMessage(@Nonnegative int piece, @Nonnegative int offset, @Nonnegative int length) { + super(piece, offset); + this.length = length; + } + + @Override + public Type getType() { + return Type.REQUEST; + } + + @Override + public int getLength() { + return length; + } + + @Override + public void fromWire(ByteBuf in) { + super.fromWire(in); + length = in.readInt(); + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) throws IOException { + super.toWire(out, extendedTypes); + out.writeInt(length); + } + } + + /** + * Piece message. + * + * + */ + public static class PieceMessage extends AbstractPieceMessage { + + private static final int BASE_SIZE = 9; + // TODO: Use a FileRegion. + private ByteBuffer block; + + public PieceMessage() { + } + + public PieceMessage(int piece, int offset, ByteBuffer block) { + super(piece, offset); + this.block = block; + } + + @Override + public Type getType() { + return Type.PIECE; + } + + @Override + public int getLength() { + return getBlock().remaining(); + } + + public ByteBuffer getBlock() { + return this.block; + } + + @Override + public void fromWire(ByteBuf in) { + super.fromWire(in); + block = ByteBuffer.allocate(in.readableBytes()); + in.readBytes(block); + block.flip(); + // We can't do this because netty recycles the buffer. + // block = in.nioBuffer(); + // in.readerIndex(in.writerIndex()); + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) throws IOException { + super.toWire(out, extendedTypes); + out.writeBytes(block); + } + + @Override + public String toString() { + return super.toString() + " " + TorrentUtils.toString(block, 16); + } + } + + /** + * Cancel message. + * + * + */ + public static class CancelMessage extends AbstractPieceMessage { + + private int length; + + public CancelMessage() { + } + + public CancelMessage(@Nonnegative int piece, @Nonnegative int offset, @Nonnegative int length) { + super(piece, offset); + this.length = length; + } + + public CancelMessage(@Nonnull RequestMessage request) { + this(request.getPiece(), request.getOffset(), request.getLength()); + } + + @Override + public Type getType() { + return Type.CANCEL; + } + + @Override + public int getLength() { + return length; + } + + @Override + public void fromWire(ByteBuf in) { + super.fromWire(in); + length = in.readInt(); + } + + @Override + public void toWire(ByteBuf out, Map extendedTypes) throws IOException { + super.toWire(out, extendedTypes); + out.writeInt(length); + } + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessageCodec.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessageCodec.java new file mode 100644 index 000000000..78f3b8a3c --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessageCodec.java @@ -0,0 +1,111 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.io; + +import com.turn.ttorrent.client.peer.PeerMessageListener; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageCodec; +import java.io.IOException; +import java.util.List; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +// Not shareable +public class PeerMessageCodec extends ByteToMessageCodec { + + private static final Logger LOG = LoggerFactory.getLogger(PeerMessageCodec.class); + private final PeerMessageListener listener; + + public PeerMessageCodec(@Nonnull PeerMessageListener listener) { + this.listener = listener; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List out) throws Exception { + if (buf.readableBytes() == 0) { + out.add(new PeerMessage.KeepAliveMessage()); + return; + } + + byte type = buf.readByte(); + + PeerMessage message; + switch (type) { + case 0: + message = new PeerMessage.ChokeMessage(); + break; + case 1: + message = new PeerMessage.UnchokeMessage(); + break; + case 2: + message = new PeerMessage.InterestedMessage(); + break; + case 3: + message = new PeerMessage.NotInterestedMessage(); + break; + case 4: + message = new PeerMessage.HaveMessage(); + break; + case 5: + message = new PeerMessage.BitfieldMessage(); + break; + case 6: + message = new PeerMessage.RequestMessage(); + break; + case 7: + message = new PeerMessage.PieceMessage(); + break; + case 8: + message = new PeerMessage.CancelMessage(); + break; + case 20: + byte extendedType = buf.readByte(); // Throws on short packet. This is normal. + switch (extendedType) { + case 0: + message = new PeerExtendedMessage.HandshakeMessage(); + break; + case 1: + message = new PeerExtendedMessage.UtPexMessage(); + break; + default: + throw new IOException("Unknown extended message type " + extendedType); + } + break; + default: + throw new IOException("Unknown message type " + type); + } + message.fromWire(buf); + out.add(message); + + // if (buf.readableBytes() > 0) throw new IOException("Badly framed message " + message + "; remaining=" + buf.readableBytes()); + } + + @Override + protected void encode(ChannelHandlerContext ctx, PeerMessage value, ByteBuf out) throws Exception { + // LOG.info("encode: " + value); + int lengthIndex = out.writerIndex(); + out.writeInt(0); + int startIndex = out.writerIndex(); + value.toWire(out, listener.getExtendedMessageTypes()); + out.setInt(lengthIndex, out.writerIndex() - startIndex); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessageHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessageHandler.java new file mode 100644 index 000000000..7e215574e --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessageHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.io; + +import com.turn.ttorrent.client.peer.PeerMessageListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class PeerMessageHandler extends ChannelInboundHandlerAdapter { + + private static final Logger LOG = LoggerFactory.getLogger(PeerMessageHandler.class); + private final PeerMessageListener listener; + + public PeerMessageHandler(@Nonnull PeerMessageListener listener) { + this.listener = listener; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + // LOG.info("Received " + msg + " for " + listener); + PeerMessage message = (PeerMessage) msg; + listener.handleMessage(message); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + if (ctx.channel().isOpen()) + listener.handleReadComplete(); + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + if (ctx.channel().isWritable()) + listener.handleWritable(); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + listener.handleDisconnect(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + // TODO: Pass this to the application: An incoming message threw an exception. + listener.handleException(cause); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessageTrafficShapingHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessageTrafficShapingHandler.java new file mode 100644 index 000000000..3d68f16ff --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerMessageTrafficShapingHandler.java @@ -0,0 +1,26 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.io; + +import io.netty.handler.traffic.ChannelTrafficShapingHandler; +import java.util.concurrent.TimeUnit; + +/** + * + * @author shevek + */ +public class PeerMessageTrafficShapingHandler extends ChannelTrafficShapingHandler { + + public PeerMessageTrafficShapingHandler() { + super(TimeUnit.MINUTES.toMillis(1)); + } + + @Override + protected long calculateSize(Object msg) { + if (msg instanceof PeerMessage.PieceMessage) + return ((PeerMessage.PieceMessage) msg).getLength(); + return super.calculateSize(msg); + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerServer.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerServer.java new file mode 100644 index 000000000..88927dc67 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerServer.java @@ -0,0 +1,168 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.io; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import com.turn.ttorrent.client.ClientEnvironment; +import com.turn.ttorrent.client.TorrentRegistry; +import com.turn.ttorrent.tracker.client.PeerAddressProvider; +import com.turn.ttorrent.protocol.TorrentUtils; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.socket.ServerSocketChannel; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketException; +import java.util.Collections; +import java.util.Set; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Incoming peer connections service. + * + *

+ * Every BitTorrent client, BitTorrent being a peer-to-peer protocol, listens + * on a port for incoming connections from other peers sharing the same + * torrent. + *

+ * + *

+ * A PeerServer implements this service and starts a listening socket + * in the first available port in the default BitTorrent client port range + * 6881-6999. When a peer connects to it, it expects the BitTorrent handshake + * message, parses it and replies with our own handshake. + *

+ * + *

+ * A PeerServer is a singleton per {@link Client}, and can be shared across + * torrents and swarms. + *

+ * + * @author shevek + */ +public class PeerServer extends PeerEndpoint implements PeerAddressProvider { + + private static final Logger LOG = LoggerFactory.getLogger(PeerServer.class); + public static final int PORT_RANGE_START = 6881; + public static final int PORT_RANGE_END = 6999; + public static final int CLIENT_KEEP_ALIVE_MINUTES = 3; + @Nonnull + private final ClientEnvironment environment; + @Nonnull + private final TorrentRegistry torrents; + private ChannelFuture future; + + // This can stop taking Client if we sort out where the peerId will come from. + // One option is to make PeerAddressProvider NOT extend PeerIdentityProvider. + public PeerServer(@Nonnull ClientEnvironment environment, @Nonnull TorrentRegistry torrents) { + this.environment = Preconditions.checkNotNull(environment, "ClientEnvironment was null."); + this.torrents = Preconditions.checkNotNull(torrents, "TorrentRegistry was null."); + } + + /** + * Create and start a new listening service for our torrents, reporting + * with our peer ID on the given address. + * + *

+ * This binds to the first available port in the client port range + * PORT_RANGE_START to PORT_RANGE_END. + *

+ */ + public void start() throws Exception { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(environment.getEventService()); + bootstrap.channel(environment.getEventLoopType().getServerChannelType()); + bootstrap.option(ChannelOption.SO_BACKLOG, 128); + bootstrap.option(ChannelOption.SO_REUSEADDR, true); + bootstrap.option(ChannelOption.TCP_NODELAY, true); + bootstrap.childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast(new PeerServerHandshakeHandler(torrents)); + } + }); + bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); + // bootstrap.childOption(ChannelOption.SO_TIMEOUT, (int) TimeUnit.MINUTES.toMillis(CLIENT_KEEP_ALIVE_MINUTES)); + // TODO: Derive from PieceHandler.DEFAULT_BLOCK_SIZE + // bootstrap.childOption(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 1024 * 1024); + // bootstrap.childOption(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024); + SocketAddress address = environment.getLocalPeerListenAddress(); + if (address != null) { + future = bootstrap.bind(address).sync(); + } else { + BIND: + { + Exception x = new IOException("No available port for the BitTorrent client!"); + for (int i = PORT_RANGE_START; i <= PORT_RANGE_END; i++) { + try { + future = bootstrap.bind(i).sync(); + break BIND; + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + x = e; + } + } + throw new IOException("Failed to find an address to bind in range [" + PORT_RANGE_START + "," + PORT_RANGE_END + "]", x); + } + } + } + + public void stop() throws InterruptedException { + try { + Channel channel = future.channel(); + channel.close().sync(); + } finally { + future = null; + } + } + + @Override + public byte[] getLocalPeerId() { + return environment.getLocalPeerId(); + } + + @Override + public String getLocalPeerName() { + return environment.getLocalPeerName(); + } + + @Nonnull + public InetSocketAddress getLocalAddress() { + ServerSocketChannel channel = (ServerSocketChannel) future.channel(); + return channel.localAddress(); + } + + @Override + public Set getLocalAddresses() { + try { + // TODO: This call may be expensive on some operating systems. Cache it. + return Sets.newHashSet(TorrentUtils.getSpecificAddresses(getLocalAddress())); + } catch (SocketException e) { + LOG.error("Failed to get specific addresses", e); + return Collections.emptySet(); + } + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerServerHandshakeHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerServerHandshakeHandler.java new file mode 100644 index 000000000..82b67be29 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/io/PeerServerHandshakeHandler.java @@ -0,0 +1,86 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.io; + +import com.turn.ttorrent.client.TorrentRegistry; +import com.turn.ttorrent.client.peer.PeerConnectionListener; +import com.turn.ttorrent.client.TorrentHandler; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.logging.LoggingHandler; +import java.util.Arrays; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class PeerServerHandshakeHandler extends PeerHandshakeHandler { + + private static final Logger LOG = LoggerFactory.getLogger(PeerServerHandshakeHandler.class); + private static final LoggingHandler wireLogger = new LoggingHandler("server-wire"); + private static final LoggingHandler frameLogger = new LoggingHandler("server-frame"); + private static final LoggingHandler messageLogger = new LoggingHandler("server-message"); + private final TorrentRegistry torrentProvider; + + public PeerServerHandshakeHandler(@Nonnull TorrentRegistry torrentProvider) { + this.torrentProvider = torrentProvider; + } + + @Override + public LoggingHandler getWireLogger() { + return wireLogger; + } + + @Override + protected LoggingHandler getFrameLogger() { + return frameLogger; + } + + @Override + public LoggingHandler getMessageLogger() { + return messageLogger; + } + + @Override + protected void process(ChannelHandlerContext ctx, PeerHandshakeMessage message) { + if (LOG.isDebugEnabled()) + LOG.debug("Processing {}", message); + if (Arrays.equals(message.getPeerId(), torrentProvider.getLocalPeerId())) { + LOG.warn("Connected to self. Closing."); + ctx.close().addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + return; + } + + // We are a server. + TorrentHandler torrent = torrentProvider.getTorrent(message.getInfoHash()); + if (torrent == null) { + LOG.warn("Unknown torrent {}", message); + ctx.close().addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + return; + } + PeerConnectionListener listener = torrent.getSwarmHandler(); + if (LOG.isTraceEnabled()) + LOG.trace("Found torrent {}", torrent); + + PeerHandshakeMessage response = new PeerHandshakeMessage(torrent.getInfoHash(), torrentProvider.getLocalPeerId()); + ctx.writeAndFlush(toByteBuf(ctx, response), ctx.voidPromise()); + + addPeer(ctx, message, listener); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/main/ClientMain.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/main/ClientMain.java new file mode 100644 index 000000000..93eaaec77 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/main/ClientMain.java @@ -0,0 +1,175 @@ +/** + * 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.client.main; + +import com.turn.ttorrent.client.Client; + +import com.turn.ttorrent.client.ClientListenerAdapter; +import com.turn.ttorrent.client.TorrentHandler; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.tracker.client.TorrentMetadataProvider; +import java.io.File; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.channels.UnsupportedAddressTypeException; +import java.util.Collections; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import javax.annotation.CheckForNull; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +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. + */ + @CheckForNull + private static InetAddress getInterfaceAddress(@CheckForNull String ifaceName) + throws SocketException, UnsupportedAddressTypeException, UnknownHostException { + if (ifaceName != null) { + NetworkInterface iface = NetworkInterface.getByName(ifaceName); + if (iface == null) + throw new IllegalArgumentException("No such network interface '" + ifaceName + "'." + + " Available are " + Collections.list(NetworkInterface.getNetworkInterfaces())); + // Prefer Inet4Address if possible. + for (InetAddress address : Collections.list(iface.getInetAddresses())) + if (address instanceof Inet4Address) + return address; + for (InetAddress address : Collections.list(iface.getInetAddresses())) + return address; + } + + return null; + } + + /** + * Main client entry point for stand-alone operation. + */ + public static void main(String[] args) throws Exception { + // BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-25t] %-5p: %m%n"))); + + OptionParser parser = new OptionParser(); + OptionSpec helpOption = parser.accepts("help") + .forHelp(); + OptionSpec outputOption = parser.accepts("output") + .withRequiredArg().ofType(File.class) + .defaultsTo(new File(DEFAULT_OUTPUT_DIRECTORY)); + OptionSpec ifaceOption = parser.accepts("iface") + .withRequiredArg(); + // OptionSpec seedOption = parser.accepts("seed") + // .withRequiredArg().ofType(Integer.class) + // .defaultsTo(-1); + // OptionSpec uploadOption = parser.accepts("max-upload") + // .withRequiredArg().ofType(Double.class) + // .defaultsTo(0d); + // OptionSpec downloadOption = parser.accepts("max-download") + // .withRequiredArg().ofType(Double.class) + // .defaultsTo(0d); + OptionSpec torrentOption = parser.nonOptions() + .ofType(File.class) + .describedAs("file0.torrent file1.torrent ..."); + + OptionSet options = parser.parse(args); + List otherArgs = options.nonOptionArguments(); + + // Display help and exit if requested + if (options.has(helpOption) || otherArgs.size() != 1) { + System.out.println("Usage: Client [] "); + parser.printHelpOn(System.err); + System.exit(0); + } + + File outputValue = options.valueOf(outputOption); + + Client c = new Client(null); + + InetAddress address = getInterfaceAddress(options.valueOf(ifaceOption)); + if (address != null) + c.getEnvironment().setLocalPeerListenAddress(new InetSocketAddress(address, 0)); + + int count = 0; + for (File file : options.valuesOf(torrentOption)) { + Torrent torrent = new Torrent(file); + c.addTorrent(torrent, options.valueOf(outputOption)); + count++; + } + if (count == 0) + throw new IllegalArgumentException("No torrents given."); + + final CountDownLatch latch = new CountDownLatch(count); + c.addClientListener(new ClientListenerAdapter() { + @Override + public void torrentStateChanged(Client client, TorrentHandler torrent, TorrentMetadataProvider.State state) { + if (TorrentMetadataProvider.State.SEEDING.equals(state)) + latch.countDown(); + } + }); + + try { + c.start(); + + // c.setMaxDownloadRate(options.valueOf(downloadOption)); + // c.setMaxUploadRate(options.valueOf(uploadOption)); + + // 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))); + + latch.await(); + + } catch (Exception e) { + logger.error("Fatal error: {}", e.getMessage(), e); + System.exit(2); + } finally { + c.stop(); + } + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/main/TorrentMain.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/main/TorrentMain.java new file mode 100644 index 000000000..ebbdde2f8 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/main/TorrentMain.java @@ -0,0 +1,100 @@ +/** + * 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.client.main; + +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.torrent.TorrentCreator; +import java.io.File; +import java.io.OutputStream; +import java.net.URI; +import java.util.ArrayList; + +import java.util.Collections; +import java.util.List; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +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); + + /** + * Torrent creator. + * + *

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

+ */ + public static void main(String[] args) throws Exception { + // BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%-5p: %m%n"))); + + OptionParser parser = new OptionParser(); + OptionSpec helpOption = parser.accepts("help") + .forHelp(); + OptionSpec inputOption = parser.accepts("input") + .withRequiredArg().ofType(File.class) + .required() + .describedAs("The input file or directory for the torrent."); + OptionSpec outputOption = parser.accepts("output") + .withRequiredArg().ofType(File.class) + .required() + .describedAs("The output torrent file."); + OptionSpec announceOption = parser.accepts("announce") + .withRequiredArg().ofType(URI.class) + .required() + .describedAs("The announce URL for the torrent."); + parser.nonOptions().ofType(File.class) + .describedAs("Files to include in the torrent."); + + OptionSet options = parser.parse(args); + List otherArgs = options.nonOptionArguments(); + + // Display help and exit if requested + if (options.has(helpOption)) { + System.out.println("Usage: Torrent [] "); + parser.printHelpOn(System.err); + System.exit(0); + } + + List files = new ArrayList(); + for (Object o : otherArgs) + files.add((File) o); + Collections.sort(files); + + TorrentCreator creator = new TorrentCreator(options.valueOf(inputOption)); + if (!files.isEmpty()) + creator.setFiles(files); + creator.setAnnounceList(options.valuesOf(announceOption)); + Torrent torrent = creator.create(); + + File file = options.valueOf(outputOption); + OutputStream fos = FileUtils.openOutputStream(file); + try { + torrent.save(fos); + } finally { + IOUtils.closeQuietly(fos); + } + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/Instrumentation.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/Instrumentation.java new file mode 100644 index 000000000..34436355b --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/Instrumentation.java @@ -0,0 +1,26 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.peer; + +import com.turn.ttorrent.client.PeerPieceProvider; +import com.turn.ttorrent.client.io.PeerMessage; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * This is a hook class to interject cheeky behaviour into the client(s). + * + * @author shevek + */ +public class Instrumentation { + + public void instrumentThrowable(@Nonnull Object source, @Nonnull Throwable t) { + } + + @CheckForNull + public PeerMessage.RequestMessage instrumentBlockRequest(@Nonnull PeerHandler peer, @Nonnull PeerPieceProvider provider, @CheckForNull PeerMessage.RequestMessage request) { + return request; + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerActivityListener.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerActivityListener.java new file mode 100644 index 000000000..239d99cc2 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerActivityListener.java @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client.peer; + +import java.io.IOException; + +import java.util.BitSet; +import java.util.EventListener; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; + +/** + * EventListener interface for objects that want to handle peer activity + * events like piece availability, or piece completion events, and more. + * + * @author mpetazzoni + */ +public interface PeerActivityListener extends EventListener { + + /** + * Peer choked handler. + * + *

+ * This handler is fired when a peer choked and now refuses to send data to + * us. This means we should not try to request or expect anything from it + * until it becomes ready again. + *

+ * + * @param peer The peer that choked. + */ + public void handlePeerChoking(PeerHandler peer); + + /** + * Peer ready handler. + * + *

+ * This handler is fired when a peer notified that it is no longer choked. + * This means we can send piece block requests to it and start downloading. + *

+ * + * @param peer The peer that became ready. + */ + public void handlePeerUnchoking(PeerHandler peer); + + /** + * Piece availability handler. + * + *

+ * This handler is fired when an update in piece availability is received + * from a peer's HAVE message. + *

+ * + * @param peer The peer we got the update from. + * @param piece The piece that became available from this peer. + */ + public void handlePieceAvailability(@Nonnull PeerHandler peer, + @Nonnegative int piece); + + /** + * Bit field availability handler. + * + *

+ * This handler is fired when an update in piece availability is received + * from a peer's BITFIELD message. + *

+ * + * @param peer The peer we got the update from. + * @param availablePieces The pieces availability bit field of the peer. + */ + public void handleBitfieldAvailability(@Nonnull PeerHandler peer, + @Nonnull BitSet prevAvailablePieces, + @Nonnull BitSet availablePieces); + + /** + * Piece upload completion handler. + * + * @param peer The peer the piece was sent to. + * @param piece The piece in question. + */ + public void handleBlockSent(@Nonnull PeerHandler peer, + @Nonnegative int piece, + @Nonnegative int offset, @Nonnegative int length); + + public void handleBlockReceived(@Nonnull PeerHandler peer, + @Nonnegative int piece, + @Nonnegative int offset, @Nonnegative int length); + + /** + * Piece download completion handler. + * + *

+ * This handler is fired when a piece has been downloaded entirely and the + * piece data has been revalidated. + *

+ * + *

+ * Note: the piece may not be valid after it has been + * downloaded, in which case appropriate action should be taken to + * redownload the piece. + *

+ * + * @param peer The peer we got this piece from. + * @param piece The piece in question. + */ + public void handlePieceCompleted(@Nonnull PeerHandler peer, + @Nonnegative int piece, + @Nonnull PieceHandler.Reception reception) + throws IOException; +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerConnectionListener.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerConnectionListener.java new file mode 100644 index 000000000..7eb1eb71c --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerConnectionListener.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client.peer; + +import com.turn.ttorrent.protocol.PeerIdentityProvider; +import io.netty.channel.Channel; +import java.io.IOException; +import java.net.SocketAddress; +import java.util.EventListener; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * EventListener interface for objects that want to handle incoming peer + * connections. + * + * @author mpetazzoni + */ +public interface PeerConnectionListener extends PeerIdentityProvider, EventListener { + + public void handlePeerConnectionFailed(@Nonnull SocketAddress address, @CheckForNull Throwable cause); + + @CheckForNull + public PeerHandler handlePeerConnectionCreated(@Nonnull Channel channel, @Nonnull byte[] peerId, @Nonnull byte[] remoteReserved); + + public void handlePeerConnectionReady(@Nonnull PeerHandler peer); + + /** + * Peer disconnection handler. + * + *

+ * This handler is fired when a peer disconnects, or is disconnected due to + * protocol violation. + *

+ * + * @param peer The peer we got this piece from. + */ + public void handlePeerDisconnected(@Nonnull PeerHandler peer); + + /** + * Handler for IOException during peer operation. + * + * @param peer The peer whose activity trigger the exception. + * @param ioe The IOException object, for reporting. + */ + public void handleIOException(@Nonnull PeerHandler peer, @CheckForNull IOException ioe); +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerExistenceListener.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerExistenceListener.java new file mode 100644 index 000000000..505b503de --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerExistenceListener.java @@ -0,0 +1,23 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.peer; + +import java.net.SocketAddress; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public interface PeerExistenceListener { + + /** Returns all known peers, possibly including this peer's known addresses. */ + @Nonnull + public Map getPeers(); + + /** Adds SocketAddress -> PeerIds. The PeerId may be null if not known. */ + public void addPeers(@Nonnull Map peers, @Nonnull String source); +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerHandler.java new file mode 100644 index 000000000..efcc83958 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerHandler.java @@ -0,0 +1,1027 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client.peer; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterators; +import com.turn.ttorrent.client.PeerPieceProvider; +import com.turn.ttorrent.client.io.PeerExtendedMessage; +import com.turn.ttorrent.client.io.PeerHandshakeMessage; +import com.turn.ttorrent.client.io.PeerMessage; +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.tracker.client.PeerAddressProvider; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLongArray; +import javax.annotation.CheckForNull; +import javax.annotation.CheckForSigned; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages a connected peer for a specific torrent. + * + *

+ * Peers are defined by their peer ID, which is passed in, and their IP address + * and port number, which are retrieved from the passed in {@link Channel} + * object. Peers we exchange with also contain four crucial attributes: + *

+ * + *
    + *
  • choked, if the peer is choked, and we are + * not willing to send him anything for now;
  • + *
  • interesting, if the peer has a piece which is + * interesting to us.
  • + *
  • choking, if this peer is choking and won't send us + * anything right now;
  • + *
  • interested, if this peer is interested in something we + * have.
  • + *
+ * + *

+ * Peers start choked and uninterested. + *

+ * + * @author mpetazzoni + */ +public class PeerHandler implements PeerMessageListener { + + private static final Logger LOG = LoggerFactory.getLogger(PeerHandler.class); + public static final int MAX_REQUESTS_SENT = 100; + public static final int MIN_REQUESTS_SENT = 16; + public static final int MAX_REQUESTS_RCVD = 100; + public static final long MAX_REQUESTS_TIME = TimeUnit.SECONDS.toMillis(32); + public static final long MIN_PEX_DELAY = TimeUnit.SECONDS.toMillis(72); // Protocol requires minimum 60. + private static final Map DEFAULT_EXTENDED_MESSAGE_TYPE_MAP = Collections.singletonMap(PeerExtendedMessage.ExtendedType.handshake, (byte) 0); + + private static enum Flag { + // We decide about them: + + CHOKED, INTERESTING, + // They decide about us: + CHOKING, INTERESTED; + } + private final Channel channel; + private final byte[] remotePeerId; + private final byte[] remoteReserved; + private final PeerAddressProvider addressProvider; + private final PeerPieceProvider pieceProvider; + private final PeerExistenceListener existenceListener; + private final PeerConnectionListener connectionListener; + private final PeerActivityListener activityListener; + @GuardedBy("lock") + private final BitSet availablePieces; + @GuardedBy("lock") + private Map extendedMessageTypes = DEFAULT_EXTENDED_MESSAGE_TYPE_MAP; + // TODO: Convert to AtomicLongArray and allow some hysteresis on flag changes. + private final AtomicLongArray flags = new AtomicLongArray(4); + // @GuardedBy("requestsLock") + // private final BlockingQueue requests = new ArrayBlockingQueue(SharingPeer.MAX_REQUESTS_SENT); + private final Rate download = new Rate(60); + private final Rate upload = new Rate(60); + private final Object lock = new Object(); + + private static enum SendState { + + BITFIELD, EXTENDED_HANDSHAKE; + } + @GuardedBy("lock") + private Set sent = EnumSet.noneOf(SendState.class); + @Nonnull + @GuardedBy("lock") + private Iterator requestsSource = Iterators.emptyIterator(); + // @GuardedBy("lock") // It's now a concurrent structure. + // The limit should be irrelevant, it's just to protect us. + private final BlockingQueue requestsSent = new LinkedBlockingQueue(MAX_REQUESTS_SENT * 2); + @GuardedBy("lock") + private int requestsSentLimit = MAX_REQUESTS_SENT; + @GuardedBy("lock") + private long requestsExpiredAt = 0; + // @GuardedBy("lock") // Also now a concurrent structure. + private final BlockingQueue requestsReceived = new ArrayBlockingQueue(MAX_REQUESTS_RCVD); + @GuardedBy("lock") + private final Set peersExchanged = new HashSet(); + @GuardedBy("lock") + private long peersExchangedAt = 0; + + /** + * Create a new sharing peer on a given torrent. + * + *

+ * Initially, peers are considered choked, choking, and neither interested + * nor interesting. + *

+ */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public PeerHandler( + @Nonnull Channel channel, + @Nonnull byte[] remotePeerId, + @Nonnull byte[] remoteReserved, + // Deliberately specified in terms of interfaces, for testing. + @Nonnull PeerAddressProvider addressProvider, + @Nonnull PeerPieceProvider pieceProvider, + @Nonnull PeerExistenceListener existenceListener, + @Nonnull PeerConnectionListener connectionListener, + @Nonnull PeerActivityListener activityListener) { + this.channel = channel; + this.remotePeerId = remotePeerId; + this.remoteReserved = remoteReserved; + this.addressProvider = addressProvider; + this.pieceProvider = pieceProvider; + this.existenceListener = existenceListener; + this.connectionListener = connectionListener; + this.activityListener = activityListener; + + this.availablePieces = new BitSet(pieceProvider.getPieceCount()); + + setFlag(Flag.CHOKING, true); + setFlag(Flag.INTERESTING, false); + setFlag(Flag.CHOKED, true); + setFlag(Flag.INTERESTED, false); + } + + @Nonnull + private String getLocalPeerName() { + return addressProvider.getLocalPeerName(); + } + + @Nonnull + @SuppressFBWarnings("EI_EXPOSE_REP") + public byte[] getRemotePeerId() { + return remotePeerId; + } + + @Nonnull + public String getHexRemotePeerId() { + return TorrentUtils.toHex(getRemotePeerId()); + } + + @Nonnull + private String getTextRemotePeerId() { + return TorrentUtils.toText(getRemotePeerId()); + } + + @Nonnull + public SocketAddress getLocalAddress() { + return channel.localAddress(); + } + + @Nonnull + public SocketAddress getRemoteAddress() { + return channel.remoteAddress(); + } + + /** + * We might not be an InetSocketAddress, in which case this returns -1. + */ + @CheckForSigned + private int getRemotePort() { + SocketAddress remoteAddress = getRemoteAddress(); + if (!(remoteAddress instanceof InetSocketAddress)) + return -1; + return ((InetSocketAddress) remoteAddress).getPort(); + } + + @Nonnull + public Rate getDLRate() { + return download; + } + + @Nonnull + public Rate getULRate() { + return upload; + } + + /** + * Returns the available pieces from this peer. + * + * @return A clone of the available pieces bit field from this peer. + */ + @Nonnull + public BitSet getAvailablePieces() { + synchronized (lock) { + return (BitSet) this.availablePieces.clone(); + } + } + + @Override + public Map getExtendedMessageTypes() { + synchronized (lock) { + return extendedMessageTypes; + } + } + + private boolean isExtendedTypeSupported(@Nonnull PeerExtendedMessage.ExtendedType extendedType) { + if (extendedType == PeerExtendedMessage.ExtendedType.handshake) + return PeerHandshakeMessage.Feature.BEP10_EXTENSION_PROTOCOL.get(remoteReserved); + synchronized (lock) { + boolean ret = extendedMessageTypes.containsKey(extendedType); + // LOG.info("{}: {} supports {} = {}", new Object[]{getLocalPeerName(), getTextRemotePeerId(), extendedType, ret}); + return ret; + } + } + + @Nonnegative + public int getAvailablePieceCount() { + synchronized (lock) { + return availablePieces.cardinality(); + } + } + + /** + * @return true if this flag was set more than delta ms ago. + */ + private boolean getFlag(@Nonnull Flag flag, @Nonnegative int delta) { + // <= so that a fast set; get(0) is true. + long curr = flags.get(flag.ordinal()); + long now = System.currentTimeMillis(); + boolean ret = curr != 0 && curr + delta <= now; + // LOG.debug("{}: flag={}, curr={}, delta={}, now={}, ret={}", new Object[]{ getLocalPeerName(), flag, curr, delta, now, ret }); + return ret; + } + + private static boolean toBoolean(long value) { + return value != 0; + } + + /** + * @return true if the flag was changed, in a "boolean" sense. + */ + private boolean setFlag(@Nonnull Flag flag, boolean value) { + // Avoid updating the timestamp if we can. + if (value == toBoolean(flags.get(flag.ordinal()))) + return false; + long curr = value ? System.currentTimeMillis() : 0; + long prev = flags.getAndSet(flag.ordinal(), curr); + return value != toBoolean(prev); + // return flags.compareAndSet(flag.ordinal(), value ? 0 : 1, value ? 1 : 0); + } + + /** + * @return a read-only iterable of the requests sent to this peer + */ + @Nonnull + public Iterable getRequestsSent() { + return requestsSent; + } + + @Nonnegative + public int getRequestsSentCount() { + return requestsSent.size(); + } + + /** + * Choke this peer. + * + *

+ * We don't want to upload to this peer anymore, so mark that we're choking + * from this peer. + *

+ */ + public void choke() { + if (setFlag(Flag.CHOKED, true)) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Choking {}", getLocalPeerName(), this); + send(new PeerMessage.ChokeMessage(), true); + } + } + + /** + * Unchoke this peer. + * + *

+ * Mark that we are no longer choking from this peer and can resume + * uploading to it. + *

+ */ + public void unchoke() { + if (setFlag(Flag.CHOKED, false)) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Unchoking {}", getLocalPeerName(), this); + send(new PeerMessage.UnchokeMessage(), true); + // LOG.info("{}: Unchoking {}", getLocalPeerName(), this); + } + } + + public boolean isChoked(@Nonnegative int delta) { + return getFlag(Flag.CHOKED, delta); + } + + public void interesting() { + if (setFlag(Flag.INTERESTING, true)) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Telling {} we're interested.", getLocalPeerName(), this); + send(new PeerMessage.InterestedMessage(), true); + } + } + + public void notInteresting() { + if (setFlag(Flag.INTERESTING, false)) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Telling {} we're no longer interested.", getLocalPeerName(), this); + send(new PeerMessage.NotInterestedMessage(), true); + } + } + + public boolean isInteresting() { + return getFlag(Flag.INTERESTING, 0); + } + + public boolean isChoking() { + return getFlag(Flag.CHOKING, 0); + } + + public boolean isInterested() { + return getFlag(Flag.INTERESTED, 0); + } + + public void close(@Nonnull String reason) { + rejectRequestsSent("connection closed: " + reason); + LOG.debug("{}: Closing {}: {}", new Object[]{ + getLocalPeerName(), + this, reason + }); + channel.close().addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + } + + /** + * Send a message to the peer. + * + *

+ * Delivery of the message can only happen if the peer is connected. + *

+ * + * @param message The message to send to the remote peer through our peer + * exchange. + */ + public void send(@Nonnull PeerMessage message, boolean flush) throws IllegalStateException { + if (message instanceof PeerExtendedMessage) { + PeerExtendedMessage.ExtendedType extendedType = ((PeerExtendedMessage) message).getExtendedType(); + if (!isExtendedTypeSupported(extendedType)) { + LOG.warn("Extended message type not supported by remote end: " + message); + return; + } + } + // LOG.info("{}: -> {}", new Object[]{provider.getLocalPeerName(), message}); + if (flush) + channel.writeAndFlush(message, channel.voidPromise()); + else + channel.write(message, channel.voidPromise()); + } + + @GuardedBy("lock") + private static T removeRequestMessage( + @Nonnull PeerMessage.AbstractPieceMessage response, + @Nonnull Iterator requests) { + // int count = 0; + T out = null; + while (requests.hasNext()) { + T request = requests.next(); + if (response.answers(request)) { + out = request; + requests.remove(); + // count++; + } + } + // if (count > 1) LOG.error("Removed multiple requests for " + response, new Exception()); + return out; + } + + /** + * Remove the REQUEST message from the request pipeline matching this + * PIECE message. + * + *

+ * Upon reception of a piece block with a PIECE message, remove the + * corresponding request from the pipeline to make room for the next block + * requests. + *

+ * + * @param message The PIECE message received. + */ + @CheckForNull + private PieceHandler.AnswerableRequestMessage removeRequestSent(@Nonnull PeerMessage.PieceMessage response) { + return removeRequestMessage(response, requestsSent.iterator()); + } + + private void removeRequestReceived(@Nonnull PeerMessage.CancelMessage request) { + removeRequestMessage(request, requestsReceived.iterator()); + } + + /** + * Notifies the {@link PieceProvider} that requests that we sent have been + * rejected and will not be answered. + * + * @param requests a subset of the requests sent by this peer + * @param reason the reason to log that these requests are being rejected + */ + private void rejectRequests(@Nonnull Collection requests, @Nonnull String reason) { + // LOG.debug("{}: Rejecting {} requests.", provider.getLocalPeerName(), requests.size()); + if (!requests.isEmpty()) { + int count = pieceProvider.addRequestTimeout(requests); + if (LOG.isDebugEnabled()) + LOG.debug("{}: Rejecting {} requests; {} re-enqueued: {}", getLocalPeerName(), requests.size(), count, reason); + } + } + + /** + * Rejects all requests that we have sent. + * + * @see #rejectRequests(java.util.Collection, java.lang.String) + */ + public void rejectRequestsSent(@Nonnull String reason) { + List requestsRejected = new ArrayList(); + requestsSent.drainTo(requestsRejected); + rejectRequests(requestsRejected, reason); + } + + /** + * Cancel all pending requests that we have made. + * + *

+ * This queues CANCEL messages for all the requests in the queue, and + * returns the number of requests that were canceled + *

+ */ + public int cancelRequestsSent(@Nonnull String reason) { + // Set pieces = new HashSet(); + List requestsRejected = new ArrayList(); + requestsSent.drainTo(requestsRejected); + for (PieceHandler.AnswerableRequestMessage requestRejected : requestsRejected) + send(new PeerMessage.CancelMessage(requestRejected), false); + rejectRequests(requestsRejected, reason); + if (!requestsRejected.isEmpty()) + channel.flush(); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Cancelled {} remaining pending requests on {}.", new Object[]{ + getLocalPeerName(), + requestsRejected.size(), this + }); + return requestsRejected.size(); + } + + private boolean isWritable(@Nonnull Channel c, @Nonnull String message) { + if (c.isWritable()) + return true; + LOG.debug("{}: Peer {} channel {} not writable for {}.", new Object[]{ + getLocalPeerName(), + this, c, message + }); + return false; + } + + /** + * Run one step of the PeerHandler finite state machine. + * + *

+ * Re-fill the pipeline to get download the next blocks from the peer. + *

+ */ + public void run(@Nonnull String reason) throws IOException { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Step function in {}: {}", new Object[]{ + getLocalPeerName(), this, reason + }); + Channel c = channel; + boolean flush = false; + try { + // This locking could be more fine-grained. + synchronized (lock) { + + BITFIELD: + { + if (!sent.contains(SendState.BITFIELD)) { + if (!isWritable(c, "bitfield")) + return; + flush = true; + send(new PeerMessage.BitfieldMessage(pieceProvider.getCompletedPieces()), false); + sent.add(SendState.BITFIELD); + } + } + + EXTENDED_HANDSHAKE: + { + if (!isExtendedTypeSupported(PeerExtendedMessage.ExtendedType.handshake)) + break EXTENDED_HANDSHAKE; + if (!sent.contains(SendState.EXTENDED_HANDSHAKE)) { + if (!isWritable(c, "extended handshake")) + return; + flush = true; + PeerExtendedMessage.HandshakeMessage message = new PeerExtendedMessage.HandshakeMessage( + MAX_REQUESTS_RCVD, + addressProvider.getLocalAddresses()); + // We could add the InetSocketAddresses chosen by the HandshakeMessage to peersExchanged. + send(message, false); + sent.add(SendState.EXTENDED_HANDSHAKE); + } + } + + long now = System.currentTimeMillis(); + + PEX: + { + if (!isExtendedTypeSupported(PeerExtendedMessage.ExtendedType.ut_pex)) + break PEX; + // LOG.info("{}: {} PEX supported.", new Object[]{provider.getLocalPeerName(), getTextRemotePeerId()}); + if (peersExchangedAt > now - MIN_PEX_DELAY) + break PEX; + List peers = new ArrayList(); + for (Map.Entry e : existenceListener.getPeers().entrySet()) { + if (!(e.getKey() instanceof InetSocketAddress)) + continue; + if (peersExchanged.contains(e.getKey())) + continue; + if (Arrays.equals(e.getValue(), getRemotePeerId())) + continue; + peers.add((InetSocketAddress) e.getKey()); + if (peers.size() >= 100) + break; + } + if (peers.size() < 100) { + for (SocketAddress address : addressProvider.getLocalAddresses()) { + if (!(address instanceof InetSocketAddress)) + continue; + if (peersExchanged.contains(address)) + continue; + peers.add((InetSocketAddress) address); + if (peers.size() >= 100) + break; + } + } + // LOG.info("{}: {} PEX candidates are {}", new Object[]{provider.getLocalPeerName(), getTextRemotePeerId(), peers}); + if (peers.isEmpty()) + break PEX; + peersExchanged.addAll(peers); + peersExchangedAt = now; + flush = true; + send(new PeerExtendedMessage.UtPexMessage(peers, Collections.emptyList()), false); + } + + BitSet interesting = getAvailablePieces(); + pieceProvider.andNotCompletedPieces(interesting); + INTERESTING: + { + if (interesting.isEmpty()) + notInteresting(); + else + interesting(); + // This might have flushed. + } + + // Expires dead requests, and marks live ones uninteresting. + EXPIRE: + { + if (LOG.isTraceEnabled()) + LOG.trace("{}: requestsExpiredAt={}, now={}, comp={}, diff={}", new Object[]{ + getLocalPeerName(), + requestsExpiredAt, now, now - (MAX_REQUESTS_TIME >> 2), + (now - (MAX_REQUESTS_TIME >> 2)) - requestsExpiredAt + }); + if (requestsExpiredAt < now - (MAX_REQUESTS_TIME >> 2)) { + // LOG.debug("{}: Running request expiry.", provider.getLocalPeerName()); + long then = now - MAX_REQUESTS_TIME; + List requestsExpired = new ArrayList(); + Iterator it = requestsSent.iterator(); + while (it.hasNext()) { + PieceHandler.AnswerableRequestMessage requestSent = it.next(); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Awaiting sent message {} until {}", new Object[]{ + getLocalPeerName(), requestSent, MAX_REQUESTS_TIME + }); + if (requestSent.getRequestTime() < then) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Peer {} request {} timed out.", new Object[]{ + getLocalPeerName(), + getRemoteAddress(), requestSent + }); + requestsExpired.add(requestSent); + it.remove(); + } else { + interesting.clear(requestSent.getPiece()); + } + } + if (!requestsExpired.isEmpty()) { + rejectRequests(requestsExpired, "requests expired"); + requestsSentLimit = Math.max((int) (requestsSentLimit * 0.8), MIN_REQUESTS_SENT); + LOG.debug("{}: Lowered requestsSentLimit to {}", getLocalPeerName(), requestsSentLimit); + } + requestsExpiredAt = now; + } + } + + // Makes new requests. + REQUEST: + { + while (requestsSent.size() < requestsSentLimit) { + // A choke message can come in while we are iterating. + if (isChoking()) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: {}: Not sending requests because they are choking us.", new Object[]{ + getLocalPeerName(), this + }); + break REQUEST; + } + + if (!c.isWritable()) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Peer {} channel {} not writable for request; sent {}.", new Object[]{ + getLocalPeerName(), + this, c, + requestsSent.size() + }); + return; + } + + // Search for a block we can request. Ideally, this iterates 0 or 1 times. + while (!requestsSource.hasNext()) { + // This calls a significant piece of infrastructure elsewhere, + // and needs a proof against deadlock. + Iterable piece = pieceProvider.getNextPieceHandler(this, interesting); + if (piece == null) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Peer {} has no request source; breaking request loop.", new Object[]{ + getLocalPeerName(), + this + }); + requestsSource = Iterators.emptyIterator(); // Allow GC. + break REQUEST; + } + requestsSource = piece.iterator(); + } + + PieceHandler.AnswerableRequestMessage request = requestsSource.next(); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Adding {} from {}, queue={}/{}", new Object[]{ + getLocalPeerName(), + request, requestsSource, + requestsSent.size(), requestsSentLimit + }); + interesting.clear(request.getPiece()); // Don't pick up the same piece on the next iteration. + request.setRequestTime(); + requestsSent.add(request); + flush = true; + send(request, false); + } + } + } + + // This loop does I/O so we shouldn't hold the lock fully outside it. + RESPONSE: + while (c.isWritable()) { + PeerMessage.RequestMessage request = requestsReceived.poll(); + request = pieceProvider.getInstrumentation().instrumentBlockRequest(this, pieceProvider, request); + if (request == null) + break; + + if (!pieceProvider.isCompletedPiece(request.getPiece())) { + LOG.warn("{}: Peer {} requested invalid piece {}, terminating exchange.", new Object[]{ + getLocalPeerName(), + this, request.getPiece() + }); + close("requested piece we don't have"); + break; + } + + // At this point we agree to send the requested piece block to + // the remote peer, so let's queue a message with that block + ByteBuffer block = ByteBuffer.allocate(request.getLength()); + pieceProvider.readBlock(block, request.getPiece(), request.getOffset()); + block.flip(); + // ByteBuffer block = piece.read(request.getOffset(), request.getLength()); + PeerMessage.PieceMessage response = new PeerMessage.PieceMessage( + request.getPiece(), + request.getOffset(), + block); + // response = provider.getInstrumentation(). + flush = true; + send(response, false); + upload.update(request.getLength()); + + activityListener.handleBlockSent(this, request.getPiece(), request.getOffset(), request.getLength()); + } + } finally { + if (flush) + channel.flush(); + if (LOG.isTraceEnabled()) + LOG.trace("After run: requestsSent={}", requestsSent); + } + } + + /** + * Handle an incoming message from this peer. + * + * @param msg The incoming, parsed message. + */ + @Override + public void handleMessage(PeerMessage msg) throws IOException { + // LOG.info("{}: <- {}", new Object[]{provider.getLocalPeerName(), msg}); + switch (msg.getType()) { + case KEEP_ALIVE: + // Nothing to do, we're keeping the connection open anyways. + break; + + case CHOKE: + setFlag(Flag.CHOKING, true); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Peer {} is no longer accepting requests.", getLocalPeerName(), this); + cancelRequestsSent("remote peer choked us"); + activityListener.handlePeerChoking(this); + break; + + case UNCHOKE: + setFlag(Flag.CHOKING, false); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Peer {} is now accepting requests.", getLocalPeerName(), this); + activityListener.handlePeerUnchoking(this); + // run(); // We might want something. + break; + + case INTERESTED: + setFlag(Flag.INTERESTED, true); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Peer {} is now interested.", getLocalPeerName(), this); + break; + + case NOT_INTERESTED: + setFlag(Flag.INTERESTED, false); + if (LOG.isTraceEnabled()) + LOG.trace("{}: Peer {} is no longer interested.", getLocalPeerName(), this); + // TODO: Close if we are a seed? + break; + + case HAVE: { + // Record this peer has the given piece + PeerMessage.HaveMessage message = (PeerMessage.HaveMessage) msg; + + synchronized (lock) { + availablePieces.set(message.getPiece()); + } + + activityListener.handlePieceAvailability(this, message.getPiece()); + // run(); // We might now be interested, but we should get it in handleReadComplete. + break; + } + + case BITFIELD: { + // Augment the hasPiece bit field from this BITFIELD message + PeerMessage.BitfieldMessage message = (PeerMessage.BitfieldMessage) msg; + BitSet prevAvailablePieces; + + synchronized (lock) { + prevAvailablePieces = getAvailablePieces(); + availablePieces.clear(); + availablePieces.or(message.getBitfield()); + } + + // The copy from the message is independent, and thus threadsafe. + activityListener.handleBitfieldAvailability(this, prevAvailablePieces, message.getBitfield()); + // run(); // We might now be interested, but we should get it in handleReadComplete. + break; + } + + case REQUEST: { + PeerMessage.RequestMessage message = (PeerMessage.RequestMessage) msg; + + // If we are choking from this peer and it still sends us + // requests, it is a violation of the BitTorrent protocol. + // Similarly, if the peer requests a piece we don't have, it + // is a violation of the BitTorrent protocol. In these + // situation, terminate the connection. + if (isChoked(2000)) { + // TODO: This isn't synchronous. We need to remember WHEN we choked them. + long choked = flags.get(Flag.CHOKED.ordinal()); + long now = System.currentTimeMillis(); + LOG.warn("{}: Peer {} ignored choking, terminating exchange; choked at {} ({} ago), now {}", new Object[]{ + getLocalPeerName(), this, + choked, (now - choked), now + }); + close("ignored choking"); + break; + } + + // TODO: Ignore this condition for fast links. + if (message.getLength() > PieceHandler.MAX_BLOCK_SIZE) { + LOG.warn("{}: Peer {} requested a block too big ({}), terminating exchange.", new Object[]{ + getLocalPeerName(), this, + message.getLength() + }); + close("requested huge block"); + break; + } + + if (!requestsReceived.offer(message)) { + LOG.warn("{}: Peer {} requested too many blocks; dropping {}", new Object[]{ + getLocalPeerName(), + this, message + }); + break; + } + + // run(); + break; + } + + case PIECE: { + // Record the incoming piece block. + + // Should we keep track of the requested pieces and act when we + // get a piece we didn't ask for, or should we just stay + // greedy? + PeerMessage.PieceMessage message = (PeerMessage.PieceMessage) msg; + int blockLength = message.getLength(); + + // Remove the corresponding request from the request queue to + // make room for next block requests. + PieceHandler.AnswerableRequestMessage request = removeRequestSent(message); + PieceHandler.Reception reception = PieceHandler.Reception.WAT; + if (request != null) + reception = request.answer(message); + else if (LOG.isTraceEnabled()) + LOG.trace("{}: {}: Response received to unsent request: {}", new Object[]{ + getLocalPeerName(), + this, + message + }); + + download.update(blockLength); + activityListener.handleBlockReceived(this, message.getPiece(), message.getOffset(), blockLength); + switch (reception) { + case VALID: + case INVALID: + activityListener.handlePieceCompleted(this, message.getPiece(), reception); + break; + } + + // run(); + break; + } + + case CANCEL: { + PeerMessage.CancelMessage message = (PeerMessage.CancelMessage) msg; + removeRequestReceived(message); + break; + } + + case EXTENDED: { + handleExtendedMessage((PeerExtendedMessage) msg); + break; + } + + default: { + close("Unrecognized message " + msg); + break; + } + } + } + + @VisibleForTesting + public void handleExtendedMessage(@Nonnull PeerExtendedMessage msg) throws IOException { + switch (msg.getExtendedType()) { + case handshake: { + PeerExtendedMessage.HandshakeMessage message = (PeerExtendedMessage.HandshakeMessage) msg; + // this.requestsSentLimit = message.getRemoteRequestQueueLength(); + // existenceListener.addPeers(Arrays.asList()); + synchronized (lock) { + extendedMessageTypes = message.getSenderExtendedTypeMap(); + } + int remotePort = getRemotePort(); + Map remoteAddresses = new HashMap(); + SocketAddress remoteIp4Address = message.getSenderIp4Address(remotePort); + if (remoteIp4Address != null) + remoteAddresses.put(remoteIp4Address, getRemotePeerId()); + SocketAddress remoteIp6Address = message.getSenderIp6Address(remotePort); + if (remoteIp6Address != null) + remoteAddresses.put(remoteIp6Address, getRemotePeerId()); + existenceListener.addPeers(remoteAddresses, "extended-handshake"); + break; + } + case ut_pex: { + PeerExtendedMessage.UtPexMessage message = (PeerExtendedMessage.UtPexMessage) msg; + List added = message.getAdded(); + if (added != null && !added.isEmpty()) { + synchronized (lock) { + // The remote peer already knows about these. + peersExchanged.addAll(added); + } + Map peers = new HashMap(); + for (SocketAddress peer : added) + peers.put(peer, null); + // LOG.info("PEX adding peers " + peers); + existenceListener.addPeers(peers, "peer-exchange"); + } + break; + } + default: { + close("Unrecognized message " + msg); + break; + } + } + } + + @Override + public void handleReadComplete() throws IOException { + run("read complete"); + } + + @Override + public void handleWritable() throws IOException { + run("writable"); + } + + @Override + public void handleDisconnect() throws IOException { + connectionListener.handlePeerDisconnected(this); + } + + @Override + public void handleException(Throwable exception) { + LOG.error("{}: {}: Operation failed: {}", new Object[]{ + getLocalPeerName(), + getRemoteAddress(), + exception + }); + + if (!channel.isOpen()) // Then everything fails. + return; + if (channel.closeFuture().isDone()) + return; + + Throwable t = exception; + while (t != null) { + if (t instanceof IOException) { + if (t.getMessage().contains("Broken pipe")) + return; + if (t.getMessage().contains("Connection reset by peer")) + return; + } + t = t.getCause(); + } + LOG.error(getLocalPeerName() + ": Operation diagnostics", exception); + } + + public void tick() { + upload.tick(); + download.tick(); + // TODO: Keepalives. + } + + @Override + public String toString() { + // Channel c = getChannel(); + StringBuilder buf = new StringBuilder(getTextRemotePeerId()); + buf.append('(').append(getRemoteAddress()).append(')'); + buf + .append(" [R=") + .append((isChoking() ? "C" : "c")) + .append((isInterested() ? "I" : "i")) + .append("|L=") + .append((isChoked(0) ? "C" : "c")) + .append((isInteresting() ? "I" : "i")) + .append("|") + .append(getAvailablePieceCount()) + .append("]"); + buf.append(" queue=").append(requestsSent.size()).append("/"); + synchronized (lock) { + buf.append(requestsSentLimit); + } + buf.append(" ul/dl=").append(getULRate().getRate(TimeUnit.SECONDS)).append("/").append(getDLRate().getRate(TimeUnit.SECONDS)); + return buf.toString(); + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerMessageListener.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerMessageListener.java new file mode 100644 index 000000000..b9daa7fe4 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerMessageListener.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client.peer; + +import com.turn.ttorrent.client.io.PeerExtendedMessage; +import com.turn.ttorrent.client.io.PeerMessage; +import java.io.IOException; +import java.util.EventListener; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * EventListener interface for objects that want to receive incoming messages + * from peers. + * + * @author mpetazzoni + */ +public interface PeerMessageListener extends EventListener { + + @Nonnull + public Map getExtendedMessageTypes(); + + public void handleMessage(@Nonnull PeerMessage msg) throws IOException; + + public void handleReadComplete() throws IOException; + + public void handleWritable() throws IOException; + + public void handleDisconnect() throws IOException; + + public void handleException(@Nonnull Throwable exception); +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PieceHandler.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PieceHandler.java new file mode 100644 index 000000000..4a7798cea --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PieceHandler.java @@ -0,0 +1,232 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.peer; + +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.UnmodifiableIterator; +import com.turn.ttorrent.client.PeerPieceProvider; +import com.turn.ttorrent.client.io.PeerMessage; +import com.turn.ttorrent.client.peer.PieceHandler.AnswerableRequestMessage; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.BitSet; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Enables out-of-order reception of blocks within a piece. + * + * A {@link PeerHandler} sends {@link AnswerableRequestMessage}s to it's peer to + * request blocks within a piece. This class implements an iterable of these + * messages, enabling the PeerHandler to download a piece simply by calling + * {@link PeerHandler#send(com.turn.ttorrent.client.io.PeerMessage, boolean)} on + * the messages as it iterates over them. + * + * @author shevek + */ +public class PieceHandler implements Iterable { + + private static final Logger LOG = LoggerFactory.getLogger(PieceHandler.class); + /** Default block size is 2^14 bytes, or 16kB. */ + public static final int DEFAULT_BLOCK_SIZE = 16384; + /** Max block request size is 2^17 bytes, or 131kB. */ + public static final int MAX_BLOCK_SIZE = 128 * 1024; // This is 131072 in most implementations. + private final int piece; + // private final PeerIdentityProvider identityProvider; + private final PeerPieceProvider pieceProvider; + // TODO: Maintain the set of peers which sent us data, so we can bin bad peers. + @GuardedBy("lock") + private final byte[] pieceData; + @GuardedBy("lock") + private final BitSet pieceRequiredBytes; // We should do this with blocks. + private final Object lock = new Object(); + + public PieceHandler(/*@Nonnull PeerIdentityProvider identityProvider,*/ @Nonnull PeerPieceProvider pieceProvider, @Nonnegative int piece) { + // this.identityProvider = identityProvider; + this.pieceProvider = pieceProvider; + this.piece = piece; + this.pieceData = new byte[pieceProvider.getPieceLength(piece)]; + this.pieceRequiredBytes = new BitSet(pieceData.length); + this.pieceRequiredBytes.set(0, pieceData.length); // It's easier to find 1s than 0s. + } + + /** + * Returns the index of this piece in the torrent. + */ + @Nonnegative + public int getIndex() { + return piece; + } + + public static enum Reception { + + /** Corresponding request not found. */ + WAT, + /** Not required. */ + IGNORED, + /** Thankyou, but piece not complete. */ + INCOMPLETE, + /** Thank you, piece complete. */ + VALID, + /** Thank you, but you sent me a bum piece. */ + INVALID; + } + + /** + * Record the given block at the given offset in this piece. + * + * @param block The ByteBuffer containing the block data. + * @param offset The block offset in this piece. + */ + @Nonnull + private Reception receive(ByteBuffer block, int offset) throws IOException { + int length = block.remaining(); + // LOG.debug("Received {}[{}]", offset, length); + + synchronized (lock) { + // Make sure we actually needed any of these bytes. + if (pieceProvider.isCompletedPiece(piece)) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Discarding block of completed piece {}", pieceProvider.getLocalPeerName(), piece); + return Reception.IGNORED; + } + if (pieceRequiredBytes.nextSetBit(offset) >= offset + length) { + if (LOG.isDebugEnabled()) + LOG.debug("{}: Discarding non-required block for {}", pieceProvider.getLocalPeerName(), piece); + return Reception.IGNORED; + } + + block.get(pieceData, offset, length); + pieceRequiredBytes.clear(offset, offset + length); + + if (!pieceRequiredBytes.isEmpty()) + return Reception.INCOMPLETE; + + boolean valid = pieceProvider.validateBlock(ByteBuffer.wrap(pieceData), piece); + if (!valid) { + // LOG.warn("{}: Piece {} complete, but invalid. Not saving.", new Object[]{identityProvider.getLocalPeerName(), piece}); + this.pieceRequiredBytes.set(0, pieceData.length); + return Reception.INVALID; + } + } + + // if (LOG.isDebugEnabled()) + // LOG.debug("Piece {} complete, and valid.", piece); + pieceProvider.writeBlock(ByteBuffer.wrap(pieceData), piece, 0); + return Reception.VALID; + } + + public class AnswerableRequestMessage extends PeerMessage.RequestMessage { + + // This is written before PeerHandler.requestsSent and read afterwards. + private long requestTime = -1; + + public AnswerableRequestMessage(int piece, int offset, int length) { + super(piece, offset, length); + } + + @Nonnull + public PieceHandler getPieceHandler() { + return PieceHandler.this; + } + + public long getRequestTime() { + return requestTime; + } + + public void setRequestTime() { + requestTime = System.currentTimeMillis(); + } + + // TODO: Make public, and call when appropriate. + private void cancel() { + // TODO: Add to partial set. + } + + @Nonnull + public Reception answer(@Nonnull PeerMessage.PieceMessage response) throws IOException { + if (!response.answers(this)) + throw new IllegalArgumentException("Not an answer: request=" + this + ", response=" + response); + return receive(response.getBlock(), response.getOffset()); + } + + @Override + public int hashCode() { + return PieceHandler.this.hashCode() << 16 ^ getOffset() << 8 ^ getLength(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (null == obj) + return false; + if (!getClass().equals(obj.getClass())) + return false; + AnswerableRequestMessage other = (AnswerableRequestMessage) obj; + return getPieceHandler() == other.getPieceHandler() + && getOffset() == other.getOffset() + && getLength() == other.getLength(); + } + + @Override + public String toString() { + long offset = System.currentTimeMillis() - getRequestTime(); + return super.toString() + " (" + offset + " ms ago)"; + } + } + private static final int REQUEST_OFFSET_INIT = -1; + private static final int REQUEST_OFFSET_FINI = -2; + + private class AnswerableRequestIterator extends AbstractIterator { + + private int requestOffset = REQUEST_OFFSET_INIT; + + @Override + protected AnswerableRequestMessage computeNext() { + int blockLength = pieceProvider.getBlockLength(); + synchronized (lock) { + if (requestOffset == REQUEST_OFFSET_FINI) + return endOfData(); + else if (requestOffset == REQUEST_OFFSET_INIT) + requestOffset = pieceRequiredBytes.nextSetBit(0); + else + requestOffset = pieceRequiredBytes.nextSetBit(requestOffset + blockLength); + if (requestOffset < 0) { + requestOffset = REQUEST_OFFSET_FINI; + return endOfData(); + } + int length = Math.min( + blockLength, + pieceData.length - requestOffset); + return new AnswerableRequestMessage(piece, requestOffset, length); + } + } + } + + @Override + public UnmodifiableIterator iterator() { + return new AnswerableRequestIterator(); + } + + @Override + public String toString() { + return "PieceHandler(" + piece + ")"; + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/Rate.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/Rate.java new file mode 100644 index 000000000..4c9a42ef1 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/Rate.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.peer; + +import com.codahale.metrics.EWMA; +import java.util.concurrent.TimeUnit; +import static java.lang.Math.exp; + +/** + * + * @author shevek + */ +public class Rate extends EWMA { + + public static final int INTERVAL = 5; + public static final long INTERVAL_MS = TimeUnit.SECONDS.toMillis(INTERVAL); + + public Rate(double seconds) { + this(1 - exp(-INTERVAL / seconds), INTERVAL, TimeUnit.SECONDS); + } + + public Rate(double alpha, long interval, TimeUnit intervalUnit) { + super(alpha, interval, intervalUnit); + } + + @Override + public String toString() { + return getRate(TimeUnit.SECONDS) + "/s"; + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/RateComparator.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/RateComparator.java new file mode 100644 index 000000000..1cc23b7c8 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/RateComparator.java @@ -0,0 +1,87 @@ +/* + * Copyright 2014 shevek. + * + * 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.client.peer; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public abstract class RateComparator implements Comparator, Serializable { + + private static final long serialVersionUID = 1L; + + private static int compare(@Nonnull Rate a, @Nonnull Rate b) { + return Double.compare(a.getRate(TimeUnit.SECONDS), b.getRate(TimeUnit.SECONDS)); + } + + /** + * Download rate comparator. + * + *

+ * Compares sharing peers based on their current download rate. + *

+ * + * @author mpetazzoni + */ + public static class DLRateComparator extends RateComparator { + + private static double getRate(PeerHandler peer) { + double rate = peer.getDLRate().getRate(TimeUnit.SECONDS); + rate += peer.getRequestsSentCount(); + if (!peer.isChoked(0)) + rate = (rate + 100) * 1.5; + return rate; + } + + @Override + public int compare(PeerHandler a, PeerHandler b) { + double ra = getRate(a); + double rb = getRate(b); + return Double.compare(rb, ra); + } + } + + /** + * Upload rate comparator. + * + *

+ * Compares sharing peers based on their current upload rate. + *

+ * + * @author mpetazzoni + */ + public static class ULRateComparator extends RateComparator { + + private static double getRate(PeerHandler peer) { + double rate = peer.getDLRate().getRate(TimeUnit.SECONDS); + if (!peer.isChoked(0)) + rate = (rate + 100) * 1.5; + return rate; + } + + @Override + public int compare(PeerHandler a, PeerHandler b) { + double ra = getRate(a); + double rb = getRate(b); + return Double.compare(rb, ra); + } + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/ByteRangeStorage.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/ByteRangeStorage.java new file mode 100644 index 000000000..69e6cdf21 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/ByteRangeStorage.java @@ -0,0 +1,20 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.storage; + +import javax.annotation.Nonnegative; + +/** + * + * @author shevek + */ +public interface ByteRangeStorage extends ByteStorage { + + @Nonnegative + public long offset(); + + @Nonnegative + public long size(); +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/ByteStorage.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/ByteStorage.java new file mode 100644 index 000000000..9b7e451e2 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/ByteStorage.java @@ -0,0 +1,104 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client.storage; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.nio.ByteBuffer; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; + +/** + * Abstract torrent byte storage. + * + *

+ * This interface defines the methods for accessing an abstracted torrent byte + * storage. A torrent, especially when it contains multiple files, needs to be + * seen as one single continuous stream of bytes. Torrent pieces will most + * likely span across file boundaries. This abstracted byte storage aims at + * providing a simple interface for read/write access to the torrent data, + * regardless of how it is composed underneath the piece structure. + *

+ * + * @author mpetazzoni + * @author dgiffin + */ +public interface ByteStorage extends Flushable, Closeable { + + /** + * Read from the byte storage. + * + *

+ * Read {@code length} bytes at offset {@code offset} from the underlying + * byte storage and return them in a {@link ByteBuffer}. + * This method does NOT call {@link ByteBuffer#flip()}. + *

+ * + * @param buffer The buffer to read the bytes into. The buffer's limit will + * control how many bytes are read from the storage. + * @param offset The offset, in bytes, to read from. This must be within + * the storage boundary. + * @return The number of bytes read from the storage. + * @throws IOException If an I/O error occurs while reading from the + * byte storage. + */ + public int read(@Nonnull ByteBuffer buffer, @Nonnegative long offset) throws IOException; + + /** + * Write bytes to the byte storage. + * + *

+ *

+ * + * @param block A {@link ByteBuffer} containing the bytes to write to the + * storage. The buffer limit is expected to be set correctly: all bytes + * from the buffer will be used. + * @param offset Offset in the underlying byte storage to write the block + * at. + * @return The number of bytes written to the storage. + * @throws IOException If an I/O error occurs while writing to the byte + * storage. + */ + public int write(@Nonnull ByteBuffer block, @Nonnegative long offset) throws IOException; + + /** + * Close this byte storage. + * + * @throws IOException If closing the underlying storage (file(s) ?) + * failed. + */ + @Override + public void close() throws IOException; + + /** + * Finalize the byte storage when the download is complete. + * + *

+ * This gives the byte storage the opportunity to perform finalization + * operations when the download completes, like moving the files from a + * temporary location to their destination. + *

+ * + * @throws IOException If the finalization failed. + */ + public void finish() throws IOException; + + /** + * Tells whether this byte storage has been finalized. + */ + public boolean isFinished(); +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java new file mode 100644 index 000000000..5d8ba8457 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java @@ -0,0 +1,225 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client.storage; + +import com.google.common.base.Objects; +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Multi-file torrent byte storage. + * + *

+ * This implementation of the torrent byte storage provides support for + * multi-file torrents and completely abstracts the read/write operations from + * the notion of different files. The byte storage is represented as one + * continuous byte storage, directly accessible by offset regardless of which + * file this offset lands. + *

+ * + * @author mpetazzoni + * @author dgiffin + */ +public class FileCollectionStorage implements ByteStorage { + + private static final Logger LOG = LoggerFactory.getLogger(FileCollectionStorage.class); + private final List files; + + /** + * Initialize a new multi-file torrent byte storage. + * + * @param files The list of individual {@link ByteRangeStorage} + * objects making up the torrent. + * @param size The total size of the torrent data, in bytes. + */ + public FileCollectionStorage(@Nonnull List files) { + this.files = files; + + LOG.info("Initialized torrent byte storage on {} file(s) " + + "({} total byte(s)).", files.size(), size()); + } + + public long size() { + long size = 0; + for (ByteRangeStorage part : files) + size += part.size(); + return size; + } + + @Override + public int read(ByteBuffer buffer, long offset) throws IOException { + int requested = buffer.remaining(); + int bytes = 0; + + for (Fragment fo : this.select(offset, requested)) { + // TODO: remove cast to int when large ByteBuffer support is + // implemented in Java. + buffer.limit((int) (bytes + fo.length)); + bytes += fo.part.read(buffer, fo.offset); + } + + if (bytes < requested) { + throw new IOException("Storage collection read underrun!"); + } + + return bytes; + } + + @Override + public int write(ByteBuffer buffer, long offset) throws IOException { + int requested = buffer.remaining(); + if (requested <= 0) + throw new IllegalArgumentException("Suspicious write length " + requested); + + int bytes = 0; + + for (Fragment fo : this.select(offset, requested)) { + buffer.limit(bytes + (int) fo.length); + bytes += fo.part.write(buffer, fo.offset); + } + + if (bytes < requested) { + throw new IOException("Storage collection write underrun!"); + } + + return bytes; + } + + @Override + public void flush() throws IOException { + for (ByteRangeStorage part : files) + part.flush(); + } + + @Override + public void close() throws IOException { + for (Closeable part : this.files) + part.close(); + } + + @Override + public void finish() throws IOException { + for (ByteStorage part : this.files) + part.finish(); + } + + @Override + public boolean isFinished() { + for (ByteStorage part : this.files) + if (!part.isFinished()) + return false; + return true; + } + + /** + * File operation details holder. + * + *

+ * This simple inner class holds the details for a read or write operation + * on one of the underlying {@link FileStorage}s. + *

+ * + * @author dgiffin + * @author mpetazzoni + */ + private static class Fragment { + + public final ByteStorage part; + public final long offset; + public final long length; + + Fragment(ByteStorage part, long offset, long length) { + this.part = part; + this.offset = offset; + this.length = length; + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("part", part) + .add("offset", offset) + .add("length", length) + .toString(); + } + } + + /** + * Select the group of files impacted by an operation. + * + *

+ * This function selects which files are impacted by a read or write + * operation, with their respective relative offset and chunk length. + *

+ * + * @param offset The offset of the operation, in bytes, relative to the + * complete byte storage. + * @param length The number of bytes to read or write. + * @return A list of {@link FileOffset} objects representing the {@link + * FileStorage}s impacted by the operation, bundled with their + * respective relative offset and number of bytes to read or write. + * @throws IllegalArgumentException If the offset and length go over the + * byte storage size. + * @throws IllegalStateException If the files registered with this byte + * storage can't accommodate the request (should not happen, really). + */ + @Nonnull + private List select(@Nonnegative long offset, @Nonnegative long length) { + if (offset + length > size()) { + throw new IllegalArgumentException("Buffer overrun (" + + offset + " + " + length + " > " + size() + ") !"); + } + + List selected = new ArrayList(); + long bytes = 0; + + for (ByteRangeStorage part : this.files) { + // Our IO ends after this ByteRangeStorage. + if (part.offset() >= offset + length) { + break; + } + + // Our IO starts before this ByteRangeStorage. + if (part.offset() + part.size() <= offset) { + continue; + } + + long position = offset - part.offset(); + position = position > 0 ? position : 0; + long size = Math.min( + part.size() - position, + length - bytes); + selected.add(new Fragment(part, position, size)); + bytes += size; + } + + if (selected.isEmpty() || bytes < length) { + throw new IllegalStateException("Buffer underrun (only got " + + bytes + " out of " + length + " byte(s) requested)!"); + } + + return selected; + } +} diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileStorage.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileStorage.java new file mode 100644 index 000000000..e0aa9106d --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileStorage.java @@ -0,0 +1,239 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.client.storage; + +import com.google.common.base.Objects; +import com.turn.ttorrent.protocol.TorrentUtils; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import java.nio.file.StandardOpenOption; +import java.util.EnumSet; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Single-file torrent byte data storage. + * + *

+ * This implementation of TorrentByteStorageFile provides a torrent byte data + * storage relying on a single underlying file and uses a RandomAccessFile + * FileChannel to expose thread-safe read/write methods. + *

+ * + * @author mpetazzoni + */ +public class FileStorage implements ByteRangeStorage { + + private static final Logger LOG = LoggerFactory.getLogger(FileStorage.class); + public static final String PARTIAL_FILE_NAME_SUFFIX = ".part"; + private final File target; + private final long offset; + private final long size; + @GuardedBy("lock") + private FileChannel channel; + @GuardedBy("lock") + private File current; + @GuardedBy("lock") + private boolean finished; + private final Object lock = new Object(); + + public FileStorage(@Nonnull File file, @Nonnegative long size) throws IOException { + this(file, 0, size); + } + + public FileStorage(@Nonnull File file, @Nonnegative long offset, @Nonnegative long size) + throws IOException { + this.target = file; + this.offset = offset; + this.size = size; + + File partial = new File(file.getAbsolutePath() + PARTIAL_FILE_NAME_SUFFIX); + + if (partial.exists()) { + LOG.debug("{}: Partial download found at {}. Continuing...", + target.getAbsolutePath(), partial.getAbsolutePath()); + this.current = partial; + } else if (!this.target.exists()) { + LOG.debug("{}: Downloading new file to {}...", + target.getAbsolutePath(), partial.getAbsolutePath()); + this.current = partial; + } else { + LOG.debug("{}: Using existing file.", + target.getAbsolutePath(), target.getAbsolutePath()); + this.current = this.target; + } + + // Non-final variables are not guaranteed written before the end of a constructor. + synchronized (lock) { + // Set the file length to the appropriate size, eventually truncating + // or extending the file if it already exists with a different size. + RandomAccessFile raf = new RandomAccessFile(current, "rw"); + try { + raf.setLength(size); + } finally { + raf.close(); + } + + this.channel = FileChannel.open(current.toPath(), EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)); + this.finished = false; + } + LOG.info("{}: Initialized byte storage file at {} ({}+{} byte(s)).", + new Object[]{ + target.getAbsolutePath(), + current.getAbsolutePath(), + offset, size + }); + } + + @Nonnull + public File getFile() { + return target; + } + + @Override + public long offset() { + return this.offset; + } + + @Override + public long size() { + return this.size; + } + + @Override + public int read(ByteBuffer buffer, long offset) throws IOException { + synchronized (lock) { + if (channel == null) + throw new NullPointerException("Channel is null."); + + int length = buffer.remaining(); + if (offset + length > this.size) + throw new IllegalArgumentException(target.getAbsolutePath() + ": Invalid storage read request: offset=" + offset + ", length=" + length + " when size=" + this.size); + + int read = channel.read(buffer, offset); + if (read < length) + throw new IOException(target.getAbsolutePath() + ": Storage underrun: offset=" + offset + ", length=" + length + ", size=" + size + ", read=" + read); + + return read; + } + } + + @Override + public int write(ByteBuffer buffer, long offset) throws IOException { + synchronized (lock) { + if (LOG.isTraceEnabled()) + LOG.trace("{}: Write @{}: {} {}", new Object[]{ + target.getAbsolutePath(), + offset, current, TorrentUtils.toString(buffer, 16) + }); + if (isFinished()) + throw new IllegalStateException("Already finished."); + if (channel == null) + throw new NullPointerException("Channel is null."); + int length = buffer.remaining(); + if (length <= 0) + throw new IllegalArgumentException(target.getAbsolutePath() + ": Suspicious write length " + length); + if (offset + length > this.size) + throw new IllegalArgumentException(target.getAbsolutePath() + ": Invalid storage write request: offset=" + offset + ", length=" + length + " when size=" + this.size); + + return channel.write(buffer, offset); + } + } + + @Override + public void flush() throws IOException { + synchronized (lock) { + if (channel != null) + channel.force(true); + else + LOG.warn("{}: Not flushing {}: Not open.", target.getAbsolutePath(), current); + } + } + + @Override + public void close() throws IOException { + synchronized (lock) { + if (channel != null) { + LOG.debug("{}: Closing file channel to {}.", new Object[]{ + target.getAbsolutePath(), current.getName() + }); + flush(); // FileChannel does NOT flush on close. + channel.close(); + channel = null; + } else { + LOG.warn("{}: Not closing {}: Not open.", target.getAbsolutePath(), current); + } + } + } + + /** + * Move the partial file to its final location. + */ + @Override + public void finish() throws IOException { + synchronized (lock) { + // Nothing more to do if we're already on the target file. + if (isFinished()) + return; + + close(); + + if (!current.equals(target)) { + FileUtils.deleteQuietly(this.target); + FileUtils.moveFile(this.current, this.target); + LOG.info("{}: Moved torrent data from {} to {}.", new Object[]{ + target.getAbsolutePath(), + current.getName(), + target.getName() + }); + current = target; + } + + if (LOG.isDebugEnabled()) + LOG.debug("{}: Re-opening torrent byte storage.", + this.target.getAbsolutePath()); + + this.channel = FileChannel.open(target.toPath(), EnumSet.of(StandardOpenOption.READ)); + this.finished = true; + } + } + + @Override + public boolean isFinished() { + synchronized (lock) { + // We can't use target.equals(current) because we might be + // writing to an existing file. + return finished; + } + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("file", target) + .add("offset", offset) + .add("size", size) + .toString(); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/RawFileStorage.java b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/RawFileStorage.java new file mode 100644 index 000000000..2987213f8 --- /dev/null +++ b/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/RawFileStorage.java @@ -0,0 +1,72 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.storage; + +import com.google.common.base.Objects; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.StandardOpenOption; +import java.util.EnumSet; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public class RawFileStorage implements ByteStorage { + + private final File file; + private final FileChannel channel; + private boolean finished = false; + + public RawFileStorage(@Nonnull File file) throws IOException { + this.file = file; + this.channel = FileChannel.open(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)); + } + + @Override + public int read(ByteBuffer buffer, long offset) throws IOException { + int bytes = channel.read(buffer, offset); + buffer.position(buffer.limit()); + return bytes; + } + + @Override + public int write(ByteBuffer buffer, long offset) throws IOException { + return channel.write(buffer, offset); + } + + public void flush() throws IOException { + if (channel.isOpen()) + channel.force(true); + } + + @Override + public void close() throws IOException { + flush(); + if (channel.isOpen()) + channel.close(); + } + + @Override + public void finish() throws IOException { + flush(); + finished = true; + } + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("file", file) + .toString(); + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/AbstractReplicationTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/AbstractReplicationTest.java new file mode 100644 index 000000000..0c03e7b2c --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/AbstractReplicationTest.java @@ -0,0 +1,117 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import com.turn.ttorrent.tracker.simple.SimpleTracker; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.torrent.TorrentCreator; +import com.turn.ttorrent.protocol.test.TorrentTestUtils; +import com.turn.ttorrent.test.TorrentClientTestUtils; +import com.turn.ttorrent.tracker.TrackedTorrent; +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import org.junit.After; +import org.junit.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class AbstractReplicationTest { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractReplicationTest.class); + protected SimpleTracker tracker; + protected Torrent torrent; + protected TrackedTorrent trackedTorrent; + protected Client seed; + protected final List leechers = new ArrayList(); + + @Before + public void setUp() throws Exception { + tracker = new SimpleTracker(new InetSocketAddress("localhost", 0)); + tracker.start(); + + File dir = TorrentTestUtils.newTorrentDir(getClass().getSimpleName() + ".seed"); + + TorrentCreator creator = TorrentTestUtils.newTorrentCreator(dir, 126071); + // TorrentCreator creator = TorrentTestUtils.newTorrentCreator(dir, 126); + creator.setAnnounceList(tracker.getAnnounceUris()); + creator.setPieceLength(512); + torrent = creator.create(); + + trackedTorrent = tracker.addTorrent(torrent); + trackedTorrent.setAnnounceInterval(60, TimeUnit.SECONDS); + + seed = new Client("S-"); + TorrentHandler sharedTorrent = new TorrentHandler(seed, torrent, dir); + sharedTorrent.setBlockLength(64); + seed.addTorrent(sharedTorrent); + } + + @After + public void tearDown() throws Exception { + for (Client leecher : leechers) + leecher.stop(); + seed.stop(); + tracker.stop(); + Thread.sleep(1000); // Wait for socket release. + } + + @Nonnull + protected Client leech(@Nonnull CountDownLatch latch, int i) throws IOException, InterruptedException { + File d = TorrentTestUtils.newTorrentDir(getClass().getSimpleName() + ".client" + i); + Client c = new Client("L-" + i + "-"); + TorrentHandler sharedTorrent = new TorrentHandler(c, torrent, d); + sharedTorrent.setBlockLength(64); + c.addTorrent(sharedTorrent); + c.addClientListener(new ReplicationCompletionListener(latch, TorrentHandler.State.SEEDING)); + leechers.add(c); + return c; + } + + protected void await(@Nonnull CountDownLatch latch) throws InterruptedException { + for (;;) { + if (latch.await(30, TimeUnit.SECONDS)) + break; + seed.info(true); + for (Client c : leechers) + c.info(true); + } + } + + protected void testReplication(int seed_delay, int nclients) throws Exception { + if (seed_delay <= 0) { + seed.start(); + Thread.sleep(-seed_delay); + } + + CountDownLatch latch = new CountDownLatch(nclients); + + List clients = new ArrayList(); + for (int i = 0; i < nclients; i++) { + Client c = leech(latch, i); + c.start(); + clients.add(c); + } + + if (seed_delay > 0) { + Thread.sleep(seed_delay); + seed.start(); + } + + await(latch); + + for (Client peer : clients) + TorrentClientTestUtils.assertTorrentData(seed, peer, torrent.getInfoHash()); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/PeerExchangeTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/PeerExchangeTest.java new file mode 100644 index 000000000..cb0c486d9 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/PeerExchangeTest.java @@ -0,0 +1,130 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import com.turn.ttorrent.client.io.PeerServer; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.torrent.TorrentCreator; +import com.turn.ttorrent.protocol.test.TorrentTestUtils; +import com.turn.ttorrent.test.TorrentClientTestUtils; +import java.io.File; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class PeerExchangeTest { + + private static final Logger LOG = LoggerFactory.getLogger(PeerExchangeTest.class); + protected Torrent torrent; + protected Client seed; + protected Client[] peers; + protected CountDownLatch latch; + + @Before + public void setUp() throws Exception { + SEED: + { + File dir = TorrentTestUtils.newTorrentDir(getClass().getSimpleName() + ".seed"); + TorrentCreator creator = TorrentTestUtils.newTorrentCreator(dir, 126071); + creator.setPieceLength(512); + torrent = creator.create(); + + seed = new Client("S-"); + seed.addTorrent(torrent, dir); + } + + peers = new Client[8]; + latch = new CountDownLatch(peers.length); + + for (int i = 0; i < peers.length; i++) { + File dir = TorrentTestUtils.newTorrentDir(getClass().getSimpleName() + ".peer" + i); + Client peer = new Client("C" + i + "-"); + // This lets PeerServer.getPeerAddresses() return an explicit localhost + // which would otherwise be ignored as "local" while walking NetworkInterfaces. + // peer.getEnvironment().setLocalPeerListenAddress(new InetSocketAddress("localhost", 6882 + i)); + TorrentHandler handler = peer.addTorrent(torrent, dir); + peer.addClientListener(new ReplicationCompletionListener(latch, TorrentHandler.State.SEEDING)); + peers[i] = peer; + } + } + + @After + public void tearDown() throws Exception { + seed.stop(); + for (Client peer : peers) + peer.stop(); + Thread.sleep(1000); // Wait for socket release. + } + + protected void await() throws InterruptedException { + for (;;) { + if (latch.await(5, TimeUnit.SECONDS)) + break; + seed.info(true); + for (Client peer : peers) + peer.info(true); + } + } + + @Nonnull + private static InetSocketAddress toLocalhostAddress(@Nonnull PeerServer server) { + InetSocketAddress in = server.getLocalAddress(); + return new InetSocketAddress("localhost", in.getPort()); + } + + private static void tellLeftAboutRight(Client left, Client right, byte[] torrentId) { + SocketAddress peerAddress = toLocalhostAddress(right.getPeerServer()); + Map peers = Collections.singletonMap(peerAddress, null); + left.getTorrent(torrentId).getSwarmHandler().addPeers(Collections.singletonMap(peerAddress, (byte[]) null), "test"); + } + + @Test + public void testPeerExchange() throws Exception { + for (Client peer : peers) + peer.start(); + + byte[] torrentId = torrent.getInfoHash(); + for (int i = 1; i < peers.length; i++) { + // Tell each peer about the previous one. + tellLeftAboutRight(peers[i], peers[i - 1], torrentId); + } + + PEX: + { + SwarmHandler handler = peers[0].getTorrent(torrentId).getSwarmHandler(); + for (;;) { + LOG.info("Handler {} has {} peers, waiting for {}", handler, handler.getPeerCount(), peers.length - 1); + if (handler.getPeerCount() >= peers.length - 1) // It should know everyone but itself. + break; + for (Client peer : peers) { + peer.info(true); + LOG.info("Known: " + peer.getTorrent(torrentId).getSwarmHandler().getPeers().keySet()); + } + Thread.sleep(5000); + } + LOG.info("All peers exchanged!"); + } + + seed.start(); + tellLeftAboutRight(peers[0], seed, torrentId); + await(); + + for (Client peer : peers) + TorrentClientTestUtils.assertTorrentData(seed, peer, torrentId); + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationCompletionListener.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationCompletionListener.java new file mode 100644 index 000000000..60d6f6e07 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationCompletionListener.java @@ -0,0 +1,60 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class ReplicationCompletionListener extends ClientListenerAdapter { + + private final Logger LOG = LoggerFactory.getLogger(ReplicationCompletionListener.class); + private final CountDownLatch latch; + private final TorrentHandler.State state; + private final Set torrents = new HashSet(); + private final Object lock = new Object(); + + public ReplicationCompletionListener(@Nonnull CountDownLatch latch, @Nonnull TorrentHandler.State state) { + this.latch = latch; + this.state = state; + } + + @Override + public void clientStateChanged(Client client, Client.State state) { + LOG.info("ClientState: client=" + client + ", state=" + state); + } + + @Override + public void torrentStateChanged(Client client, TorrentHandler torrent, TorrentHandler.State state) { + LOG.info("TorrentState: client=" + client + ", torrent=" + torrent + ", state=" + state + ", latch=" + latch.toString()); + if (this.state.equals(state)) { + LOG.info("Counting down for " + client.getLocalPeerName()); + synchronized (lock) { + if (!torrents.add(torrent)) + throw new IllegalArgumentException("Duplicate count for " + client.getLocalPeerName() + " / " + torrent); + } + latch.countDown(); + } + /* + switch (state) { + case DONE: + case SEEDING: + try { + client.stop(); + } catch (Exception e) { + throw Throwables.propagate(e); + } + break; + } + */ + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationHugeSwarmTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationHugeSwarmTest.java new file mode 100644 index 000000000..d2f997db2 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationHugeSwarmTest.java @@ -0,0 +1,19 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import org.junit.Test; + +/** + * + * @author shevek + */ +public class ReplicationHugeSwarmTest extends AbstractReplicationTest { + + @Test + public void testHugeSwarm() throws Exception { + testReplication(-500, 16); + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationMultipleEarlyTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationMultipleEarlyTest.java new file mode 100644 index 000000000..aed4ec7cb --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationMultipleEarlyTest.java @@ -0,0 +1,21 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +/** + * + * @author shevek + */ +public class ReplicationMultipleEarlyTest extends AbstractReplicationTest { + + @Test + public void testReplicationMultipleEarly() throws Exception { + trackedTorrent.setAnnounceInterval(1, TimeUnit.MINUTES); + testReplication(-500, 3); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationMultipleLateTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationMultipleLateTest.java new file mode 100644 index 000000000..b6da55073 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationMultipleLateTest.java @@ -0,0 +1,19 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import org.junit.Test; + +/** + * + * @author shevek + */ +public class ReplicationMultipleLateTest extends AbstractReplicationTest { + + @Test + public void testReplicationMultipleLate() throws Exception { + testReplication(500, 3); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationSingleEarlyTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationSingleEarlyTest.java new file mode 100644 index 000000000..97186a2b6 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationSingleEarlyTest.java @@ -0,0 +1,21 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +/** + * + * @author shevek + */ +public class ReplicationSingleEarlyTest extends AbstractReplicationTest { + + @Test + public void testReplicationSingleEarly() throws Exception { + trackedTorrent.setAnnounceInterval(1, TimeUnit.MINUTES); + testReplication(-500, 1); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationSingleLateTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationSingleLateTest.java new file mode 100644 index 000000000..cfedb5067 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationSingleLateTest.java @@ -0,0 +1,19 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import org.junit.Test; + +/** + * + * @author shevek + */ +public class ReplicationSingleLateTest extends AbstractReplicationTest { + + @Test + public void testReplicationSingleLate() throws Exception { + testReplication(500, 1); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationTimeoutTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationTimeoutTest.java new file mode 100644 index 000000000..96e40fd3e --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/ReplicationTimeoutTest.java @@ -0,0 +1,61 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import com.turn.ttorrent.client.io.PeerMessage; +import com.turn.ttorrent.client.peer.Instrumentation; +import com.turn.ttorrent.client.peer.PeerHandler; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class ReplicationTimeoutTest extends AbstractReplicationTest { + + private static final Logger LOG = LoggerFactory.getLogger(ReplicationTimeoutTest.class); + + @Test + public void testReplicationTimeout() throws Exception { + trackedTorrent.setAnnounceInterval(10, TimeUnit.MINUTES); + + final Random r = seed.getEnvironment().getRandom(); + seed.getEnvironment().setInstrumentation(new Instrumentation() { + boolean dropped = false; + + @Override + public synchronized PeerMessage.RequestMessage instrumentBlockRequest(PeerHandler peer, PeerPieceProvider provider, PeerMessage.RequestMessage request) { + if (request == null) + return null; + if (!dropped) { + // Drop at least one + LOG.info("Drop " + request); + dropped = true; + return null; + } + if (r.nextFloat() < 0.02) { + // And bin 1% of remaining block requests + LOG.info("Drop " + request); + return null; + } + return super.instrumentBlockRequest(peer, provider, request); + } + }); + seed.start(); + + // Let the seed contact the tracker first. + Thread.sleep(100); + + CountDownLatch latch = new CountDownLatch(1); + Client c = leech(latch, 0); + c.start(); + await(latch); + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/TorrentHandlerTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/TorrentHandlerTest.java new file mode 100644 index 000000000..696f907b5 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/TorrentHandlerTest.java @@ -0,0 +1,79 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.torrent.TorrentCreator; +import com.turn.ttorrent.protocol.test.TorrentTestUtils; +import java.io.File; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class TorrentHandlerTest { + + private static final Logger LOG = LoggerFactory.getLogger(TorrentHandlerTest.class); + + private TorrentHandler test(Torrent torrent, File parent) throws Exception { + Client client = new Client(getClass().getSimpleName()); + TorrentHandler torrentHandler = client.addTorrent(torrent, parent); + client.getEnvironment().start(); + try { + torrentHandler.init(); + LOG.info("Available is " + torrentHandler.getCompletedPieces()); + assertTrue("We have pieces.", torrentHandler.getPieceCount() > 0); + return torrentHandler; + } finally { + client.getEnvironment().stop(); + } + } + + @Test + public void testMultiFileSeed() throws Exception { + File d_seed = TorrentTestUtils.newTorrentDir("TorrentHandlerTest"); + TorrentCreator creator = TorrentTestUtils.newTorrentCreator(d_seed, 12345678); + Torrent torrent = creator.create(); + TorrentHandler torrentHandler = test(torrent, d_seed); + assertTrue("We are complete, i.e. a seed.", torrentHandler.isComplete()); + } + + @Test + public void testSingleFileSeed() throws Exception { + File d_seed = TorrentTestUtils.newTorrentDir("TorrentHandlerTest"); + TorrentCreator creator = TorrentTestUtils.newTorrentCreator(d_seed, 12345678); + Torrent torrent = creator.create(); + File f_seed = new File(d_seed, TorrentTestUtils.FILENAME); + TorrentHandler torrentHandler = test(torrent, f_seed); + assertTrue("We are complete, i.e. a seed.", torrentHandler.isComplete()); + } + + @Test + public void testMultiFileLeech() throws Exception { + File d_seed = TorrentTestUtils.newTorrentDir("TorrentHandlerTest"); + TorrentCreator creator = TorrentTestUtils.newTorrentCreator(d_seed, 12345678); + Torrent torrent = creator.create(); + File d_leech = TorrentTestUtils.newTorrentDir("TorrentHandlerTest"); + TorrentHandler torrentHandler = test(torrent, d_leech); + assertEquals("We have no pieces.", 0, torrentHandler.getCompletedPieceCount()); + assertFalse("We are not complete, i.e. a seed.", torrentHandler.isComplete()); + } + + @Test + public void testSingleFileLeech() throws Exception { + File d_seed = TorrentTestUtils.newTorrentDir("TorrentHandlerTest"); + TorrentCreator creator = TorrentTestUtils.newTorrentCreator(d_seed, 12345678); + Torrent torrent = creator.create(); + File d_leech = TorrentTestUtils.newTorrentDir("TorrentHandlerTest"); + File f_leech = new File(d_leech, TorrentTestUtils.FILENAME); + TorrentHandler torrentHandler = test(torrent, f_leech); + assertEquals("We have no pieces.", 0, torrentHandler.getCompletedPieceCount()); + assertFalse("We are not complete, i.e. a seed.", torrentHandler.isComplete()); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/TrackerHandlerTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/TrackerHandlerTest.java new file mode 100644 index 000000000..18edd4b98 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/TrackerHandlerTest.java @@ -0,0 +1,155 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import com.turn.ttorrent.tracker.simple.SimpleTracker; +import com.turn.ttorrent.client.peer.PeerExistenceListener; +import com.turn.ttorrent.tracker.client.TorrentMetadataProvider; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.torrent.TorrentCreator; +import com.turn.ttorrent.test.TestPeerExistenceListener; +import com.turn.ttorrent.tracker.client.test.TestTorrentMetadataProvider; +import com.turn.ttorrent.protocol.test.TorrentTestUtils; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import com.turn.ttorrent.tracker.TrackedTorrent; +import java.io.File; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class TrackerHandlerTest { + + private static final Logger LOG = LoggerFactory.getLogger(TrackerHandlerTest.class); + + @Test + public void testTracking() throws Exception { + SimpleTracker tracker = new SimpleTracker(new InetSocketAddress("localhost", 5674)); + tracker.start(); + + try { + File dir = TorrentTestUtils.newTorrentDir("c_seed"); + TorrentCreator creator = TorrentTestUtils.newTorrentCreator(dir, 12345678); + List tier1 = new ArrayList(tracker.getAnnounceUris()); + List tier0 = Arrays.asList(URI.create("http://localhost:100/"), URI.create("http://1.1.1.1:101/")); + creator.setAnnounceTiers(Arrays.asList(tier0, tier1)); + Torrent torrent = creator.create(); + + TrackedTorrent trackedTorrent = tracker.addTorrent(torrent); + trackedTorrent.setAnnounceInterval(1, TimeUnit.MILLISECONDS); + + Client client = new Client(getClass().getSimpleName()); + client.start(); + + try { + final CountDownLatch latch = new CountDownLatch(2); + TorrentMetadataProvider torrentMetadataProvider = new TestTorrentMetadataProvider(torrent.getInfoHash(), torrent.getAnnounceList()); + PeerExistenceListener existenceListener = new TestPeerExistenceListener() { + @Override + public void addPeers(Map peers, String reason) { + super.addPeers(peers, reason); + latch.countDown(); + } + }; + final AtomicInteger moveCount = new AtomicInteger(0); + TrackerHandler trackerHandler = new TrackerHandler(client, torrentMetadataProvider, existenceListener) { + @Override + boolean moveToNextTracker(TrackerHandler.TrackerState curr, String reason) { + moveCount.getAndIncrement(); + return super.moveToNextTracker(curr, reason); + } + }; + LOG.info("TrackerHandler is " + trackerHandler); + trackerHandler.start(); + + latch.await(30, TimeUnit.SECONDS); + assertEquals(0, latch.getCount()); + assertEquals(2, moveCount.get()); + + trackerHandler.stop(); + } finally { + client.stop(); + } + } finally { + tracker.stop(); + } + } + + @Test + public void testFailover() throws Exception { + Client client = new Client(getClass().getSimpleName()); + client.start(); + + try { + byte[] infoHash = new byte[]{1, 2, 3, 4, 5, 6, 7, 8}; + URI uri0 = new URI("http://localhost:100/announce"); // Deliberately invalid. + URI uri1 = new URI("http://localhost:101/announce"); // Deliberately invalid. + List uris = Arrays.asList(uri0, uri1); + TestTorrentMetadataProvider metadataProvider = new TestTorrentMetadataProvider(infoHash, Arrays.asList(uris)); + PeerExistenceListener existenceListener = new TestPeerExistenceListener(); + TrackerHandler trackerHandler = new TrackerHandler(client, metadataProvider, existenceListener) { + @Override + /* pp */ TrackerHandler.TrackerState run_once(TrackerMessage.AnnounceEvent event) { + LOG.info("Running TrackerHandler: " + event); + return null; + } + + @Override + public void run() { + LOG.info("Running TrackerHandler."); + } + }; + LOG.info("TrackerHandler is " + trackerHandler); + trackerHandler.start(); + + Thread.sleep(1000); + + try { + + assertEquals("uri0: Initial", uri0, trackerHandler.getCurrentTracker().getUri()); + trackerHandler.handleAnnounceFailed(uri1, TrackerMessage.AnnounceEvent.NONE, "no-change fail"); + assertEquals("uri0: Announce-fail on not-used uri1", uri0, trackerHandler.getCurrentTracker().getUri()); + + trackerHandler.handleAnnounceFailed(uri0, TrackerMessage.AnnounceEvent.NONE, "change fail"); + assertEquals("uri1: Announced fail on current uri0", uri1, trackerHandler.getCurrentTracker().getUri()); + trackerHandler.handleAnnounceFailed(uri0, TrackerMessage.AnnounceEvent.NONE, "no-change fail"); + assertEquals("uri1: Announced second fail on non-current uri0", uri1, trackerHandler.getCurrentTracker().getUri()); + + trackerHandler.handleAnnounceFailed(uri1, TrackerMessage.AnnounceEvent.NONE, "change fail"); + assertEquals("uri0: Now we fail uri1", uri0, trackerHandler.getCurrentTracker().getUri()); + trackerHandler.handleAnnounceFailed(uri1, TrackerMessage.AnnounceEvent.NONE, "no-change fail"); + assertEquals("uri0: Now we fail uri1 again, but we aren't using it.", uri0, trackerHandler.getCurrentTracker().getUri()); + + LOG.info("Done."); + } finally { + trackerHandler.stop(); + } + + } finally { + client.stop(); + } + + } + + @Ignore("Not yet implemented.") + @Test + public void testMultipleTrackers() { + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/UbuntuImageDownloadTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/UbuntuImageDownloadTest.java new file mode 100644 index 000000000..b02954c6a --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/UbuntuImageDownloadTest.java @@ -0,0 +1,56 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client; + +import com.google.common.io.Files; +import com.google.common.io.Resources; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.tracker.client.TorrentMetadataProvider; +import java.io.File; +import java.net.URL; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Ignore; +import org.junit.Test; + +/** + * + * @author joel + */ +public class UbuntuImageDownloadTest { + + @Ignore + @Test + public void testDownloadImage() throws Exception { + File torrentFile = File.createTempFile("ttorrent-ubuntu", ".torrent"); + File image = File.createTempFile("ttorrent-ubuntu", ".img"); + //TODO(jfriedly): Remove when this works. + torrentFile.deleteOnExit(); + image.deleteOnExit(); + URL url = new URL("http://releases.ubuntu.com/14.10/ubuntu-14.10-desktop-amd64.iso.torrent"); + Resources.asByteSource(url).copyTo(Files.asByteSink(torrentFile)); + + Client c = new Client(null); + Torrent torrent = new Torrent(torrentFile); + c.addTorrent(torrent, image); + + CountDownLatch latch = new CountDownLatch(1); + c.addClientListener(new ReplicationCompletionListener(latch, TorrentMetadataProvider.State.SEEDING)); + + try { + c.start(); + for (;;) { + if (latch.await(10, TimeUnit.SECONDS)) + break; + c.info(true); + } + } finally { + c.stop(); + } + + torrentFile.delete(); + image.delete(); + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/io/PeerFrameDecoderTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/io/PeerFrameDecoderTest.java new file mode 100644 index 000000000..2489980ed --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/io/PeerFrameDecoderTest.java @@ -0,0 +1,41 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.io; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class PeerFrameDecoderTest { + + private static final Logger LOG = LoggerFactory.getLogger(PeerFrameDecoderTest.class); + + @Test + public void testDecoder() throws Exception { + ByteBuf in = Unpooled.buffer(27); + in.writeInt(6); + in.writeBytes(new byte[]{1, 2, 3, 4, 5, 6}); + + PeerFrameDecoder decoder = new PeerFrameDecoder() { + @Override + protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) { + ByteBuf frame = Unpooled.buffer(length); + frame.writeBytes(buffer, index, length); + return frame; + } + }; + ByteBuf out = decoder._decode(null, in); + + PeerMessageTest.Formatter formatter = new PeerMessageTest.Formatter(); + LOG.info(formatter.format("decoded", out)); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/io/PeerMessageTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/io/PeerMessageTest.java new file mode 100644 index 000000000..fa14f986d --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/io/PeerMessageTest.java @@ -0,0 +1,107 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.io; + +import com.google.common.primitives.UnsignedBytes; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.logging.LoggingHandler; +import java.util.BitSet; +import javax.annotation.Nonnull; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class PeerMessageTest { + + private static final Logger LOG = LoggerFactory.getLogger(PeerMessageTest.class); + + public static class Formatter extends LoggingHandler { + + public String format(String name, ByteBuf buf) { + return super.formatByteBuf(name, buf); + } + } + + @Nonnull + private T testMessage(@Nonnull T in) throws Exception { + Formatter formatter = new Formatter(); + + ByteBuf buf = Unpooled.buffer(1234); + + in.toWire(buf, null); + LOG.info(in + " -> " + formatter.format(in.getClass().getSimpleName(), buf)); + + T out = (T) in.getClass().newInstance(); + buf.readByte(); + out.fromWire(buf); + assertEquals(0, buf.readableBytes()); + + return out; + } + + @Test + public void testChokeMessage() throws Exception { + testMessage(new PeerMessage.ChokeMessage()); + } + + @Test + public void testBitfieldMessage() throws Exception { + testMessage(new PeerMessage.BitfieldMessage(new BitSet())); + BitSet set = new BitSet(); + set.set(0); + testMessage(new PeerMessage.BitfieldMessage(set)); + set.set(31); + testMessage(new PeerMessage.BitfieldMessage(set)); + } + + // I can't believe I have to write this. + private byte[] toBytes(int[] in) { + byte[] out = new byte[in.length]; + for (int i = 0; i < in.length; i++) + out[i] = UnsignedBytes.checkedCast(in[i]); + return out; + } + + @Test + public void testBitfieldMessageDecode() throws Exception { + int[] packet = new int[]{ + 0x00, 0x00, 0x00, 0xe6, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80 + }; + PeerMessage.BitfieldMessage message = new PeerMessage.BitfieldMessage(); + ByteBuf buf = Unpooled.wrappedBuffer(toBytes(packet)); + buf.readInt(); + message.fromWire(buf); + assertEquals(0, buf.readableBytes()); + + testMessage(message); + } + + @Test + public void testHaveMessage() throws Exception { + testMessage(new PeerMessage.HaveMessage(0)); + testMessage(new PeerMessage.HaveMessage(0x1234)); + testMessage(new PeerMessage.HaveMessage(0x12345678)); + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/peer/PeerHandlerTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/peer/PeerHandlerTest.java new file mode 100644 index 000000000..18a315380 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/peer/PeerHandlerTest.java @@ -0,0 +1,94 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.peer; + +import com.turn.ttorrent.client.Client; +import com.turn.ttorrent.client.io.PeerClientHandshakeHandler; +import com.turn.ttorrent.client.io.PeerServerHandshakeHandler; +import com.turn.ttorrent.protocol.test.TestPeerIdentityProvider; +import com.turn.ttorrent.test.TestPeerPieceProvider; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.test.TorrentTestUtils; +import com.turn.ttorrent.tracker.client.test.TestPeerAddressProvider; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalEventLoopGroup; +import io.netty.channel.local.LocalServerChannel; +import java.io.File; +import java.util.Arrays; +import org.easymock.EasyMock; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class PeerHandlerTest { + + private static final Logger LOG = LoggerFactory.getLogger(PeerHandlerTest.class); + + @Test + public void testPeerHandler() throws Exception { + byte[] peerId = Arrays.copyOf(new byte[]{1, 2, 3, 4, 5, 6}, 20); + File dir = TorrentTestUtils.newTorrentDir("PeerHandlerTest-server"); + Torrent torrent = TorrentTestUtils.newTorrent(dir, 12345); + + LocalAddress address = new LocalAddress("test"); + LocalEventLoopGroup group = new LocalEventLoopGroup(1); + SERVER: + { + Client client = new Client(); + client.addTorrent(torrent, dir); + ServerBootstrap b = new ServerBootstrap() + .group(group) + .channel(LocalServerChannel.class) + .childHandler(new PeerServerHandshakeHandler(client)); + b.bind(address).sync(); + } + + PeerConnectionListener connectionListener = EasyMock.createMock(PeerConnectionListener.class); + + Channel channel; + CLIENT: + { + Bootstrap b = new Bootstrap() + .group(group) + .channel(LocalChannel.class) + .handler(new PeerClientHandshakeHandler(connectionListener, torrent.getInfoHash(), peerId)); + channel = b.connect(address).sync().channel(); + } + + TestPeerAddressProvider addressProvider = new TestPeerAddressProvider(); + TestPeerPieceProvider pieceProvider = new TestPeerPieceProvider(torrent); + PeerExistenceListener existenceListener = EasyMock.createMock(PeerExistenceListener.class); + PeerActivityListener activityListener = EasyMock.createMock(PeerActivityListener.class); + PeerHandler peerHandler = new PeerHandler(channel, peerId, new byte[8], addressProvider, pieceProvider, existenceListener, connectionListener, activityListener); + + EasyMock.reset(activityListener, connectionListener); + EasyMock.replay(activityListener, connectionListener); + peerHandler.run("test 0"); + EasyMock.verify(activityListener, connectionListener); + + if (true) + return; + + EasyMock.reset(activityListener, connectionListener); + EasyMock.replay(activityListener, connectionListener); + pieceProvider.setPieceHandler(0); + peerHandler.run("test 1"); + EasyMock.verify(activityListener, connectionListener); + + EasyMock.reset(activityListener, connectionListener); + EasyMock.replay(activityListener, connectionListener); + peerHandler.run("test 2"); + EasyMock.verify(activityListener, connectionListener); + + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/peer/PieceHandlerTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/peer/PieceHandlerTest.java new file mode 100644 index 000000000..31ba37280 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/peer/PieceHandlerTest.java @@ -0,0 +1,50 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.peer; + +import com.turn.ttorrent.test.TestPeerPieceProvider; +import com.google.common.math.IntMath; +import com.turn.ttorrent.client.PeerPieceProvider; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.test.TorrentTestUtils; +import java.io.File; +import java.math.RoundingMode; +import java.util.Iterator; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class PieceHandlerTest { + + private static final Logger LOG = LoggerFactory.getLogger(PieceHandlerTest.class); + + @Test + public void testPiece() throws Exception { + File dir = TorrentTestUtils.newTorrentDir("PieceHandlerTest"); + Torrent torrent = TorrentTestUtils.newTorrent(dir, 465432); + + PeerPieceProvider provider = new TestPeerPieceProvider(torrent); + PieceHandler pieceHandler = new PieceHandler(provider, 0); + int blockCount = IntMath.divide(torrent.getPieceLength(0), PieceHandler.DEFAULT_BLOCK_SIZE, RoundingMode.UP); + Iterator it = pieceHandler.iterator(); + + for (int i = 0; i < blockCount; i++) { + assertTrue(it.hasNext()); + PieceHandler.AnswerableRequestMessage request = it.next(); + LOG.info("Request is " + request); + assertNotNull(request); + request.validate(provider); + } + + for (int i = 0; i < 2; i++) { + assertFalse(it.hasNext()); + } + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FileCollectionStorageTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FileCollectionStorageTest.java new file mode 100644 index 000000000..43f690629 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FileCollectionStorageTest.java @@ -0,0 +1,61 @@ +package com.turn.ttorrent.client.storage; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * User: loyd + * Date: 11/24/13 + */ +public class FileCollectionStorageTest { + + @Test + public void testSelect() throws Exception { + final File file1 = File.createTempFile(getClass().getSimpleName(), ".0.tmp"); + file1.deleteOnExit(); + final File file2 = File.createTempFile(getClass().getSimpleName(), ".1.tmp"); + file2.deleteOnExit(); + + final List files = new ArrayList(); + files.add(new FileStorage(file1, 0, 2)); + files.add(new FileStorage(file2, 2, 2)); + final FileCollectionStorage storage = new FileCollectionStorage(files); + // since all of these files already exist, we are considered finished + assertFalse(storage.isFinished()); + + // write to first file works + write(new byte[]{1, 2}, 0, storage); + check(new byte[]{1, 2}, file1); + + // write to second file works + write(new byte[]{5, 6}, 2, storage); + check(new byte[]{5, 6}, file2); + + // write to two files works + write(new byte[]{8, 9, 10, 11}, 0, storage); + check(new byte[]{8, 9}, file1); + check(new byte[]{10, 11}, file2); + + // make sure partial write into next file works + write(new byte[]{100, 101, 102}, 0, storage); + check(new byte[]{102, 11}, file2); + } + + private void write(byte[] bytes, int offset, FileCollectionStorage storage) throws IOException { + storage.write(ByteBuffer.wrap(bytes), offset); + storage.flush(); + } + + private void check(byte[] bytes, File f) throws IOException { + final byte[] temp = new byte[bytes.length]; + assertEquals(new FileInputStream(f).read(temp), temp.length); + assertArrayEquals(temp, bytes); + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FileStorageTest.java b/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FileStorageTest.java new file mode 100644 index 000000000..e21329e89 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FileStorageTest.java @@ -0,0 +1,81 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.client.storage; + +import com.google.common.base.Throwables; +import com.google.common.io.Files; +import com.turn.ttorrent.protocol.test.TorrentTestUtils; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class FileStorageTest { + + private static final Logger LOG = LoggerFactory.getLogger(FileStorageTest.class); + + @Test + public void testStorage() throws Exception { + File root = TorrentTestUtils.newTorrentDir("filestorage"); + + final List storage = new ArrayList(); + for (int i = 0; i < 5; i++) { + FileStorage s = new FileStorage(new File(root, "file" + i), 64); + storage.add(s); + } + + ExecutorService executor = Executors.newCachedThreadPool(); + final Random r = new Random(); + + int ntasks = 100; + final CountDownLatch latch = new CountDownLatch(ntasks); + for (int i = 0; i < ntasks; i++) { + executor.execute(new Runnable() { + @Override + public void run() { + try { + FileStorage s = storage.get(r.nextInt(storage.size())); + ByteBuffer buf = ByteBuffer.wrap(new byte[]{1, 2, 3, 4, 5, 6, 7, 8}); + s.write(buf, r.nextInt(48)); + } catch (IOException e) { + throw Throwables.propagate(e); + } finally { + latch.countDown(); + } + } + }); + } + latch.await(); + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.SECONDS); + + for (FileStorage s : storage) { + ByteBuffer buf = ByteBuffer.wrap(new byte[]{1, 2, 3, 4, 5, 6, 7, 8}); + s.write(buf, 0); + + s.finish(); + + byte[] data = Files.toByteArray(s.getFile()); + LOG.info("File contains " + Arrays.toString(data)); + for (int i = 0; i < 8; i++) + assertEquals(i + 1, data[i]); + } + } +} \ No newline at end of file diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/test/LoggingInvocationHandler.java b/ttorrent-client/src/test/java/com/turn/ttorrent/test/LoggingInvocationHandler.java new file mode 100644 index 000000000..9b86d55d9 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/test/LoggingInvocationHandler.java @@ -0,0 +1,37 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.test; + +import com.google.common.reflect.AbstractInvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class LoggingInvocationHandler extends AbstractInvocationHandler { + + private static final Logger LOG = LoggerFactory.getLogger(LoggingInvocationHandler.class); + + @Nonnull + public static T create(@Nonnull Class type) { + Object proxy = Proxy.newProxyInstance( + type.getClassLoader(), + new Class[]{type}, + new LoggingInvocationHandler()); + return type.cast(proxy); + } + + @Override + protected Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable { + LOG.info(method + "(" + Arrays.toString(args) + ")"); + return null; + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/test/TestPeerExistenceListener.java b/ttorrent-client/src/test/java/com/turn/ttorrent/test/TestPeerExistenceListener.java new file mode 100644 index 000000000..fe12411ae --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/test/TestPeerExistenceListener.java @@ -0,0 +1,41 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.test; + +import com.google.common.base.Functions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.turn.ttorrent.client.peer.PeerExistenceListener; +import java.net.SocketAddress; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class TestPeerExistenceListener implements PeerExistenceListener { + + private static final Logger LOG = LoggerFactory.getLogger(TestPeerExistenceListener.class); + private final Set addresses = new HashSet(); + + @Override + public Map getPeers() { + synchronized (addresses) { + return Maps.asMap(addresses, Functions.constant(null)); + } + } + + @Override + public void addPeers(Map peers, String reason) { + LOG.info("Added " + reason + ": " + peers); + synchronized (addresses) { + Iterables.addAll(addresses, peers.keySet()); + } + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/test/TestPeerPieceProvider.java b/ttorrent-client/src/test/java/com/turn/ttorrent/test/TestPeerPieceProvider.java new file mode 100644 index 000000000..0d5ac582c --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/test/TestPeerPieceProvider.java @@ -0,0 +1,115 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.test; + +import com.turn.ttorrent.client.PeerPieceProvider; +import com.turn.ttorrent.client.peer.PieceHandler; +import com.turn.ttorrent.client.peer.PeerHandler; +import com.turn.ttorrent.client.peer.Instrumentation; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.tracker.client.test.TestPeerAddressProvider; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.BitSet; + +/** + * + * @author shevek + */ +public class TestPeerPieceProvider extends TestPeerAddressProvider implements PeerPieceProvider { + + private final Instrumentation instrumentation = new Instrumentation(); + private final Torrent torrent; + private final BitSet completedPieces; + private PieceHandler pieceHandler; + private final Object lock = new Object(); + + public TestPeerPieceProvider(Torrent torrent) { + this.torrent = torrent; + this.completedPieces = new BitSet(torrent.getPieceCount()); + } + + @Override + public Instrumentation getInstrumentation() { + return instrumentation; + } + + @Override + public int getPieceCount() { + return torrent.getPieceCount(); + } + + @Override + public int getPieceLength(int index) { + return torrent.getPieceLength(index); + } + + @Override + public int getBlockLength() { + return PieceHandler.DEFAULT_BLOCK_SIZE; + } + + @Override + public BitSet getCompletedPieces() { + synchronized (lock) { + return (BitSet) completedPieces.clone(); + } + } + + @Override + public boolean isCompletedPiece(int index) { + synchronized (lock) { + return completedPieces.get(index); + } + } + + @Override + public void andNotCompletedPieces(BitSet out) { + synchronized (lock) { + out.andNot(completedPieces); + } + } + + @Override + public Iterable getNextPieceHandler(PeerHandler peer, BitSet interesting) { + synchronized (lock) { + PieceHandler out = pieceHandler; + pieceHandler = null; + return out; + } + } + + @Override + public int addRequestTimeout(Iterable requests) { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void setPieceHandler(PieceHandler pieceHandler) { + synchronized (lock) { + this.pieceHandler = pieceHandler; + } + } + + public void setPieceHandler(int piece) { + setPieceHandler(new PieceHandler(this, piece)); + } + + @Override + public void readBlock(ByteBuffer block, int piece, int offset) throws IOException { + // Apparently fill the block. + block.position(block.limit()); + } + + @Override + public void writeBlock(ByteBuffer block, int piece, int offset) throws IOException { + // Apparently consume the block. + block.position(block.limit()); + } + + @Override + public boolean validateBlock(ByteBuffer block, int piece) throws IOException { + return true; + } +} diff --git a/ttorrent-client/src/test/java/com/turn/ttorrent/test/TorrentClientTestUtils.java b/ttorrent-client/src/test/java/com/turn/ttorrent/test/TorrentClientTestUtils.java new file mode 100644 index 000000000..3a8c1b849 --- /dev/null +++ b/ttorrent-client/src/test/java/com/turn/ttorrent/test/TorrentClientTestUtils.java @@ -0,0 +1,54 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.test; + +import com.turn.ttorrent.client.Client; +import com.turn.ttorrent.client.TorrentHandler; +import com.turn.ttorrent.client.storage.ByteStorage; +import io.netty.util.ResourceLeakDetector; +import java.io.IOException; +import java.nio.ByteBuffer; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class TorrentClientTestUtils { + + private static final Logger LOG = LoggerFactory.getLogger(TorrentClientTestUtils.class); + + static { + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); + } + + public static void assertTorrentData(@Nonnull Client c0, @Nonnull Client c1, @Nonnull byte[] torrentId) throws IOException { + TorrentHandler h0 = c0.getTorrent(torrentId); + ByteStorage s0 = h0.getBucket(); + ByteBuffer b0 = ByteBuffer.allocate(64 * 1024); + + TorrentHandler h1 = c1.getTorrent(torrentId); + ByteStorage s1 = h1.getBucket(); + ByteBuffer b1 = ByteBuffer.allocate(b0.capacity()); + + for (long i = 0; i < h0.getSize(); i += b0.capacity()) { + long len = Math.min(h0.getSize() - i, b0.capacity()); + LOG.info("Compare " + len + " bytes at " + i); + + b0.clear(); + b0.limit((int) len); + s0.read(b0, i); + + b1.clear(); + b1.limit((int) len); + s1.read(b1, i); + + assertArrayEquals(s0 + " != " + s1 + " @" + i, b0.array(), b1.array()); + } + } +} diff --git a/ttorrent-protocol/build.gradle b/ttorrent-protocol/build.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/PeerIdentityProvider.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/PeerIdentityProvider.java new file mode 100644 index 000000000..091754936 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/PeerIdentityProvider.java @@ -0,0 +1,20 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol; + +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public interface PeerIdentityProvider { + + @Nonnull + public byte[] getLocalPeerId(); + + @Nonnull + public String getLocalPeerName(); +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/TorrentUtils.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/TorrentUtils.java new file mode 100644 index 000000000..446260676 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/TorrentUtils.java @@ -0,0 +1,175 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol; + +import com.google.common.base.Function; +import com.google.common.base.Throwables; +import com.google.common.collect.Iterables; +import com.google.common.io.BaseEncoding; +import com.google.common.primitives.UnsignedBytes; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public class TorrentUtils { + + private static final ThreadLocal DIGEST = new ThreadLocal() { + @Override + protected MessageDigest initialValue() { + try { + return MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw Throwables.propagate(e); + } + } + }; + + @Nonnull + public static byte[] hash(@Nonnull ByteBuffer data) { + MessageDigest digest = DIGEST.get(); + digest.reset(); + digest.update(data); + return digest.digest(); + } + + @Nonnull + public static byte[] hash(@Nonnull byte[] data) { + MessageDigest digest = DIGEST.get(); + digest.reset(); + digest.update(data); + return digest.digest(); + } + + public void toBitString(@Nonnull StringBuilder buf, @Nonnull BitSet b, char c0, char c1) { + int len = b.length(); + for (int i = 0; i < len; i++) + buf.append(b.get(i) ? c1 : c0); + } + + public void toBitString(@Nonnull StringBuilder buf, @Nonnull BitSet b) { + toBitString(buf, b, '0', '1'); + } + + @Nonnull + public String toBitString(@Nonnull BitSet b) { + StringBuilder buf = new StringBuilder(); + toBitString(buf, b); + return buf.toString(); + } + + /** + * Convert a byte string to a string containing an hexadecimal + * representation of the original data. + * + * @param bytes The byte array to convert. + */ + @Nonnull + public static String toHex(@Nonnull byte[] data) { + return BaseEncoding.base16().lowerCase().encode(data); + } + + @CheckForNull + public static String toHexOrNull(@CheckForNull byte[] data) { + if (data == null) + return null; + return toHex(data); + } + + @Nonnull + public static String toText(@Nonnull byte[] bytes) { + StringBuilder buf = new StringBuilder(); + for (byte b : bytes) { + if (Character.isValidCodePoint(b)) + buf.append((char) b); + else + buf.append("\\x").append(UnsignedBytes.toString(b, 16)); + } + return buf.toString(); + } + + @CheckForNull + public static String toTextOrNull(@CheckForNull byte[] data) { + if (data == null) + return null; + return toText(data); + } + + @Nonnull + public static String toString(@Nonnull ByteBuffer buf, int len) { + byte[] b = new byte[Math.min(buf.remaining(), len)]; + for (int i = 0; i < b.length; i++) + b[i] = buf.get(buf.position() + i); + return "[" + Arrays.toString(b) + "...(" + buf.remaining() + " bytes)]"; + } + + @Nonnull + public static Iterable getSpecificAddresses(@Nonnull InetAddress in) throws SocketException { + if (!in.isAnyLocalAddress()) + return Collections.singleton(in); + List out = new ArrayList(); + for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) { + if (iface.isLoopback()) + continue; + if (!iface.isUp()) + continue; + for (InetAddress ifaddr : Collections.list(iface.getInetAddresses())) { + // LOG.info("ifaddr=" + ifaddr + " iftype=" + ifaddr.getClass() + " atype=" + addr.getClass()); + if (ifaddr.isLoopbackAddress()) + continue; + // If we prefer the IPv6 stack, then addr.getClass() is Inet6Address, but listens on IPv4 as well. + // if (!ifaddr.getClass().equals(addr.getClass())) continue; + out.add(ifaddr); + } + } + if (out.isEmpty()) + out.add(InetAddress.getLoopbackAddress()); + return out; + } + + @Nonnull + public static Iterable getSpecificAddresses(@Nonnull final InetSocketAddress in) throws SocketException { + return Iterables.transform(getSpecificAddresses(in.getAddress()), new Function() { + @Override + public InetSocketAddress apply(InetAddress input) { + return new InetSocketAddress(input, in.getPort()); + } + }); + } + + @Deprecated // Not happy with this yet. + public static boolean isLegalPeerAddress(@CheckForNull SocketAddress socketAddress) { + if (socketAddress == null) + return false; + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + InetAddress inetAddress = inetSocketAddress.getAddress(); + if (inetAddress == null) + return false; + if (inetAddress.isAnyLocalAddress()) + return false; + if (inetAddress.isLoopbackAddress()) + return false; + if (inetAddress.isMulticastAddress()) + return false; + } + return true; + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/AbstractBDecoder.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/AbstractBDecoder.java new file mode 100644 index 000000000..2677ae030 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/AbstractBDecoder.java @@ -0,0 +1,236 @@ +/* + * Copyright 2014 shevek. + * + * 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.protocol.bcodec; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * B-encoding decoder. + * + *

+ * A b-encoded byte stream can represent byte arrays, numbers, lists and maps + * (dictionaries). This class implements a decoder of such streams into + * {@link BEValue}s. + *

+ * + * @see B-encoding specification + * @author shevek + */ +public abstract class AbstractBDecoder { + + protected abstract byte readByte() + throws IOException; + + @Nonnull + protected abstract byte[] readBytes(int length) + throws IOException; + + @Nonnull + public BEValue bdecode() + throws IOException { + // Cannot return null if called with false. + BEValue value = bdecode(false); + if (value == null) + throw new NullPointerException("Unexpected null from bdecode(false)"); + return value; + } + + @CheckForNull + private BEValue bdecode(boolean end) + throws IOException { + byte b = readByte(); + switch (b) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return _bdecodeBytes(b); + case 'i': + return _bdecodeNumber(); + case 'l': + return _bdecodeList(); + case 'd': + return _bdecodeMap(); + case 'e': + if (end) + return null; + throw new InvalidBEncodingException("Unexpected ending indicator."); + default: + throw new InvalidBEncodingException("Unknown indicator '" + ((char) b) + "'"); + } + } + + @Nonnull + public BEValue bdecodeBytes() + throws IOException { + byte b = readByte(); + return _bdecodeBytes(b); + } + + @Nonnull + private BEValue _bdecodeBytes(byte b) + throws IOException { + int digits = 1; + int length = b - '0'; + LENGTH: + for (;;) { + b = readByte(); + switch (b) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + length = (length * 10) + (b - '0'); + digits++; + break; + case ':': + if (digits == 0) + throw new InvalidBEncodingException("Length contained no digits."); + break LENGTH; + default: + throw new InvalidBEncodingException("Colon expected, not '" + (char) b + "'"); + } + } + + byte[] out = readBytes(length); + return new BEValue(out); + } + + @Nonnull + public BEValue bdecodeNumber() throws IOException { + byte b = readByte(); + if (b != 'i') + throw new InvalidBEncodingException("Expected 'i', not " + (char) b + "'"); + return _bdecodeNumber(); + } + + @Nonnull + private BEValue _bdecodeNumber() + throws IOException { + StringBuilder text = new StringBuilder(64); + boolean negative = false; + long value = 0; + int digits = 0; + DIGIT: + for (;;) { + byte b = readByte(); + switch (b) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + text.append((char) b); + value = (value * 10) + (b - '0'); + digits++; + break; + case '-': + if (digits > 0) + throw new InvalidBEncodingException("Negation must precede digits."); + text.append('-'); + break; + case 'e': + if (digits == 0) + throw new InvalidBEncodingException("Number contained no digits."); + break DIGIT; + default: + throw new InvalidBEncodingException("Expected 'e' not '" + (char) b + "'"); + } + } + if (negative) + value = -value; + if (digits < 16) // Lazy overflow check. + return new BEValue(Long.valueOf(value)); + return new BEValue(new BigInteger(text.toString())); + } + + @Nonnull + public BEValue bdecodeList() throws IOException { + byte b = readByte(); + if (b != 'l') + throw new InvalidBEncodingException("Expected 'l', not " + (char) b + "'"); + return _bdecodeList(); + } + + /** + * Returns the next b-encoded value on the stream and makes sure it is a + * list. + * + * @throws InvalidBEncodingException If it is not a list. + */ + @Nonnull + private BEValue _bdecodeList() throws IOException { + List out = new ArrayList(); + for (;;) { + BEValue value = bdecode(true); + if (value == null) + break; + out.add(value); + } + return new BEValue(out); + } + + @Nonnull + public BEValue bdecodeMap() throws IOException { + byte b = readByte(); + if (b != 'd') + throw new InvalidBEncodingException("Expected 'd', not " + (char) b + "'"); + return _bdecodeMap(); + } + + /** + * Returns the next b-encoded value on the stream and makes sure it is a + * map (dictionary). + * + * @throws InvalidBEncodingException If it is not a map. + */ + @Nonnull + private BEValue _bdecodeMap() throws IOException { + Map out = new HashMap(); + for (;;) { + BEValue key = bdecode(true); + if (key == null) + break; + BEValue value = bdecode(false); + out.put(key.getString(), value); + } + return new BEValue(out); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/AbstractBEncoder.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/AbstractBEncoder.java new file mode 100644 index 000000000..8770221f7 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/AbstractBEncoder.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014 shevek. + * + * 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.protocol.bcodec; + +import com.google.common.base.Charsets; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * B-encoding encoder. + * + *

+ * This class provides utility methods to encode objects and + * {@link BEValue}s to B-encoding into a provided output stream. + *

+ * + * @see B-encoding specification + * @author shevek + */ +public abstract class AbstractBEncoder { + + protected abstract void writeByte(int b) + throws IOException; + + protected abstract void writeBytes(@Nonnull byte[] b) + throws IOException; + + @SuppressWarnings("unchecked") + public void bencode(@Nonnull Object o) + throws IOException { + if (o instanceof BEValue) + o = ((BEValue) o).getValue(); + + if (o instanceof String) { + bencode((String) o); + } else if (o instanceof byte[]) { + bencode((byte[]) o); + } else if (o instanceof Number) { + bencode((Number) o); + } else if (o instanceof List) { + bencode((List) o); + } else if (o instanceof Map) { + bencode((Map) o); + } else { + throw new IllegalArgumentException("Cannot bencode: " + o.getClass()); + } + } + + public void bencode(@Nonnull String s) + throws IOException { + byte[] b = s.getBytes(Charsets.UTF_8); + bencode(b); + } + + public void bencode(@Nonnull Number n) + throws IOException { + writeByte('i'); + String s = n.toString(); + writeBytes(s.getBytes(Charsets.UTF_8)); + writeByte('e'); + } + + public void bencode(@Nonnull List l) + throws IOException { + writeByte('l'); + for (BEValue value : l) + bencode(value); + writeByte('e'); + } + + public void bencode(@Nonnull byte[] b) + throws IOException { + String l = Integer.toString(b.length); + writeBytes(l.getBytes(Charsets.UTF_8)); + writeByte(':'); + writeBytes(b); + } + + public void bencode(@Nonnull Map m) + throws IOException { + writeByte('d'); + // Keys must be sorted. + List keys = new ArrayList(m.keySet()); + Collections.sort(keys); + for (String key : keys) { + bencode(key); + bencode(m.get(key)); + } + writeByte('e'); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BEUtils.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BEUtils.java new file mode 100644 index 000000000..e5edea1ab --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BEUtils.java @@ -0,0 +1,46 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.bcodec; + +import com.google.common.base.Charsets; +import java.nio.charset.Charset; +import javax.annotation.CheckForNull; + +/** + * + * @author shevek + */ +public class BEUtils { + + /** The query parameters encoding when parsing byte strings. */ + public static final Charset BYTE_ENCODING = Charsets.ISO_8859_1; + public static final String BYTE_ENCODING_NAME = BYTE_ENCODING.name(); + + @CheckForNull + public static String getString(@CheckForNull BEValue value) + throws InvalidBEncodingException { + if (value == null) + return null; + return value.getString(BYTE_ENCODING); + } + + @CheckForNull + public static byte[] getBytes(@CheckForNull BEValue value) + throws InvalidBEncodingException { + if (value == null) + return null; + return value.getBytes(); + } + + public static int getInt(@CheckForNull BEValue value, int dflt) + throws InvalidBEncodingException { + if (value == null) + return dflt; + return value.getInt(); + } + + private BEUtils() { + } +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BEValue.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BEValue.java new file mode 100644 index 000000000..15598b170 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BEValue.java @@ -0,0 +1,181 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.protocol.bcodec; + +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * A type-agnostic container for B-encoded values. + * + * @author mpetazzoni + */ +public class BEValue { + + /** + * The B-encoded value can be a byte array, a Number, a List or a Map. + * Lists and Maps contains BEValues too. + */ + @Nonnull + private final Object value; + + public BEValue(@Nonnull byte[] value) { + this.value = Preconditions.checkNotNull(value); + } + + public BEValue(@Nonnull String value) { + this(value, Charsets.UTF_8); + } + + public BEValue(@Nonnull String value, @Nonnull Charset enc) { + this.value = value.getBytes(enc); + } + + public BEValue(int value) { + this.value = Integer.valueOf(value); + } + + public BEValue(long value) { + this.value = Long.valueOf(value); + } + + public BEValue(@Nonnull Number value) { + this.value = Preconditions.checkNotNull(value); + } + + public BEValue(@Nonnull List value) { + this.value = Preconditions.checkNotNull(value); + } + + public BEValue(@Nonnull Map value) { + this.value = Preconditions.checkNotNull(value); + } + + @Nonnull + public Object getValue() { + return value; + } + + /** + * Returns this BEValue as a String, interpreted as UTF-8. + * @throws InvalidBEncodingException If the value is not a byte[]. + */ + @Nonnull + public String getString() throws InvalidBEncodingException { + return getString(Charsets.UTF_8); + } + + /** + * Returns this BEValue as a String, interpreted with the specified + * encoding. + * + * @param encoding The encoding to interpret the bytes as when converting + * them into a {@link String}. + * @throws InvalidBEncodingException If the value is not a byte[]. + */ + @Nonnull + public String getString(Charset encoding) throws InvalidBEncodingException { + return new String(getBytes(), encoding); + } + + /** + * Returns this BEValue as a byte[]. + * + * @throws InvalidBEncodingException If the value is not a byte[]. + */ + public byte[] getBytes() throws InvalidBEncodingException { + try { + return (byte[]) value; + } catch (ClassCastException cce) { + throw new InvalidBEncodingException(cce); + } + } + + /** + * Returns this BEValue as a Number. + * + * @throws InvalidBEncodingException If the value is not a {@link Number}. + */ + @Nonnull + public Number getNumber() throws InvalidBEncodingException { + try { + return (Number) value; + } catch (ClassCastException cce) { + throw new InvalidBEncodingException(cce); + } + } + + /** + * Returns this BEValue as short. + * + * @throws InvalidBEncodingException If the value is not a {@link Number}. + */ + public short getShort() throws InvalidBEncodingException { + return getNumber().shortValue(); + } + + /** + * Returns this BEValue as int. + * + * @throws InvalidBEncodingException If the value is not a {@link Number}. + */ + public int getInt() throws InvalidBEncodingException { + return getNumber().intValue(); + } + + /** + * Returns this BEValue as long. + * + * @throws InvalidBEncodingException If the value is not a {@link Number}. + */ + public long getLong() throws InvalidBEncodingException { + return getNumber().longValue(); + } + + /** + * Returns this BEValue as a List of BEValues. + * + * @throws InvalidBEncodingException If the value is not a {@link List}. + */ + @Nonnull + @SuppressWarnings("unchecked") + public List getList() throws InvalidBEncodingException { + try { + return (List) value; + } catch (ClassCastException cce) { + throw new InvalidBEncodingException(cce); + } + } + + /** + * Returns this BEValue as a Map of String keys and BEValue values. + * + * @throws InvalidBEncodingException If the value is not a {@link Map}. + */ + @Nonnull + @SuppressWarnings("unchecked") + public Map getMap() throws InvalidBEncodingException { + try { + return (Map) value; + } catch (ClassCastException cce) { + throw new InvalidBEncodingException(cce); + } + } +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BytesBDecoder.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BytesBDecoder.java new file mode 100644 index 000000000..06b568a25 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BytesBDecoder.java @@ -0,0 +1,29 @@ +/* + * Copyright 2014 shevek. + * + * 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.protocol.bcodec; + +import java.io.ByteArrayInputStream; + +/** + * + * @author shevek + */ +public class BytesBDecoder extends StreamBDecoder { + + public BytesBDecoder(byte[] in) { + super(new ByteArrayInputStream(in)); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BytesBEncoder.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BytesBEncoder.java new file mode 100644 index 000000000..ac36fa9c0 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/BytesBEncoder.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 shevek. + * + * 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.protocol.bcodec; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public class BytesBEncoder extends AbstractBEncoder { + + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + @Override + protected void writeByte(int b) { + out.write(b); + } + + @Override + protected void writeBytes(byte[] b) throws IOException { + out.write(b); + } + + @Nonnull + public byte[] toByteArray() { + return out.toByteArray(); + } +} diff --git a/src/main/java/com/turn/ttorrent/bcodec/InvalidBEncodingException.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/InvalidBEncodingException.java similarity index 68% rename from src/main/java/com/turn/ttorrent/bcodec/InvalidBEncodingException.java rename to ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/InvalidBEncodingException.java index 07a53f44c..4fd7c413a 100644 --- a/src/main/java/com/turn/ttorrent/bcodec/InvalidBEncodingException.java +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/InvalidBEncodingException.java @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package com.turn.ttorrent.bcodec; +package com.turn.ttorrent.protocol.bcodec; import java.io.IOException; - /** * Exception thrown when a B-encoded stream cannot be decoded. * @@ -26,9 +24,17 @@ */ public class InvalidBEncodingException extends IOException { - public static final long serialVersionUID = -1; + public static final long serialVersionUID = -1; + + public InvalidBEncodingException(String message) { + super(message); + } + + public InvalidBEncodingException(Throwable cause) { + super(cause); + } - public InvalidBEncodingException(String message) { - super(message); - } + public InvalidBEncodingException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/NettyBDecoder.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/NettyBDecoder.java new file mode 100644 index 000000000..de26b901b --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/NettyBDecoder.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 shevek. + * + * 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.protocol.bcodec; + +import io.netty.buffer.ByteBuf; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public class NettyBDecoder extends AbstractBDecoder { + + private final ByteBuf in; + + public NettyBDecoder(@Nonnull ByteBuf in) { + this.in = in; + } + + @Override + protected byte readByte() { + return in.readByte(); + } + + @Override + protected byte[] readBytes(int length) { + byte[] out = new byte[length]; + in.readBytes(out); + return out; + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/NettyBEncoder.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/NettyBEncoder.java new file mode 100644 index 000000000..4df547bea --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/NettyBEncoder.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014 shevek. + * + * 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.protocol.bcodec; + +import io.netty.buffer.ByteBuf; + +/** + * + * @author shevek + */ +public class NettyBEncoder extends AbstractBEncoder { + + private final ByteBuf out; + + public NettyBEncoder(ByteBuf out) { + this.out = out; + } + + @Override + protected void writeByte(int b) { + out.writeByte(b); + } + + @Override + protected void writeBytes(byte[] b) { + out.writeBytes(b); + } +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/StreamBDecoder.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/StreamBDecoder.java new file mode 100644 index 000000000..2fd832101 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/StreamBDecoder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014 shevek. + * + * 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.protocol.bcodec; + +import com.google.common.io.ByteStreams; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * + * @author shevek + */ +public class StreamBDecoder extends AbstractBDecoder { + + private final InputStream in; + + public StreamBDecoder(InputStream in) { + this.in = in; + } + + @Override + protected byte readByte() throws IOException { + int value = in.read(); + if (value == -1) + throw new EOFException(); + return (byte) value; + } + + @Override + protected byte[] readBytes(int length) throws IOException { + byte[] bytes = new byte[length]; + ByteStreams.readFully(in, bytes); + return bytes; + } +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/StreamBEncoder.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/StreamBEncoder.java new file mode 100644 index 000000000..0a21d0575 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/bcodec/StreamBEncoder.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014 shevek. + * + * 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.protocol.bcodec; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * + * @author shevek + */ +public class StreamBEncoder extends AbstractBEncoder { + + private final OutputStream out; + + public StreamBEncoder(OutputStream out) { + this.out = out; + } + + @Override + protected void writeByte(int b) throws IOException { + out.write(b); + } + + @Override + protected void writeBytes(byte[] b) throws IOException { + out.write(b); + } +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/torrent/Torrent.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/torrent/Torrent.java new file mode 100644 index 000000000..3929ef9db --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/torrent/Torrent.java @@ -0,0 +1,444 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.protocol.torrent; + +import com.google.common.io.Closeables; +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.protocol.bcodec.BEValue; +import com.turn.ttorrent.protocol.bcodec.BytesBDecoder; +import com.turn.ttorrent.protocol.bcodec.BytesBEncoder; +import com.turn.ttorrent.protocol.bcodec.StreamBDecoder; +import com.turn.ttorrent.protocol.bcodec.StreamBEncoder; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.WillNotClose; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Metadata interface for a .torrent file. + * + *

+ * This class is also responsible for validating a ByteBuffer as a piece by + * checking its hash. + *

+ * + * @author mpetazzoni + * @see Torrent meta-info file structure specification + */ +public class Torrent { + + private static final Logger logger = LoggerFactory.getLogger(Torrent.class); + /** Torrent file piece length (in bytes), we use 512 kB. */ + public static final int PIECE_HASH_SIZE = 20; + + /** + * + * @author dgiffin + * @author mpetazzoni + */ + public static class TorrentFile { + + public final String path; + public final long size; + + public TorrentFile(@Nonnull String path, @Nonnegative long size) { + this.path = path; + this.size = size; + } + } + private final Map decoded; + private final Map decoded_info; + private final byte[] info_hash; + private final List> trackers = new ArrayList>(); + private final long creationTime; + private final String comment; + private final String createdBy; + private final String name; + private final long size; + private final List files = new ArrayList(); + private final int pieceLength; + private final byte[] piecesHashes; + + @Nonnull + private static Map load(@Nonnull File file) throws IOException { + InputStream in = new FileInputStream(file); + try { + return new StreamBDecoder(in).bdecodeMap().getMap(); + } finally { + Closeables.close(in, true); + } + } + + /** + * Load a torrent from the given torrent file. + * + * @param torrent The abstract {@link File} object representing the + * .torrent file to load. + * @throws IOException When the torrent file cannot be read. + */ + public Torrent(@Nonnull File torrent) throws IOException, URISyntaxException { + this(load(torrent)); + } + + /** + * Create a new torrent from meta-info binary data. + * + * Parses the meta-info data (which should be B-encoded as described in the + * BitTorrent specification) and create a Torrent object from it. + * + * @param torrent The meta-info byte data. + * @throws IOException When the info dictionary can't be read or + * encoded and hashed back to create the torrent's SHA-1 hash. + */ + public Torrent(@Nonnull byte[] torrent) throws IOException, URISyntaxException { + this(new BytesBDecoder(torrent).bdecodeMap().getMap()); + } + + public Torrent(@Nonnull Map torrent) throws IOException, URISyntaxException { + this.decoded = torrent; + this.decoded_info = this.decoded.get("info").getMap(); + + BytesBEncoder encoder = new BytesBEncoder(); + encoder.bencode(decoded_info); + this.info_hash = TorrentUtils.hash(encoder.toByteArray()); + + /** + * Parses the announce information from the decoded meta-info + * structure. + * + *

+ * If the torrent doesn't define an announce-list, use the mandatory + * announce field value as the single tracker in a single announce + * tier. Otherwise, the announce-list must be parsed and the trackers + * from each tier extracted. + *

+ * + * @see BitTorrent BEP#0012 "Multitracker Metadata Extension" + */ + Set allTrackers = new HashSet(); + + if (this.decoded.containsKey("announce-list")) { + List in_tiers = this.decoded.get("announce-list").getList(); + for (BEValue in_tier_bvalue : in_tiers) { + List in_tier = in_tier_bvalue.getList(); + if (in_tier.isEmpty()) + continue; + + List out_tier = new ArrayList(); + for (BEValue in_tracker : in_tier) { + URI uri = new URI(in_tracker.getString()); + + // Make sure we're not adding duplicate trackers. + if (!allTrackers.contains(uri)) { + out_tier.add(uri); + allTrackers.add(uri); + } + } + + // Only add the tier if it's not empty. + if (!out_tier.isEmpty()) + this.trackers.add(out_tier); + } + } else if (this.decoded.containsKey("announce")) { + URI tracker = new URI(this.decoded.get("announce").getString()); + // Build a single-tier announce list. + this.trackers.add(Arrays.asList(tracker)); + } + + this.creationTime = this.decoded.containsKey("creation date") + ? this.decoded.get("creation date").getLong() * 1000 + : -1L; + this.comment = this.decoded.containsKey("comment") + ? this.decoded.get("comment").getString() + : null; + this.createdBy = this.decoded.containsKey("created by") + ? this.decoded.get("created by").getString() + : null; + this.name = this.decoded_info.get("name").getString(); + + // Parse multi-file torrent file information structure. + if (this.decoded_info.containsKey("files")) { + for (BEValue file : this.decoded_info.get("files").getList()) { + Map fileInfo = file.getMap(); + StringBuilder path = new StringBuilder(this.name); + for (BEValue pathElement : fileInfo.get("path").getList()) { + path.append(File.separator).append(pathElement.getString()); + } + this.files.add(new TorrentFile( + path.toString(), + fileInfo.get("length").getLong())); + } + } else { + // For single-file torrents, the name of the torrent is + // directly the name of the file. + this.files.add(new TorrentFile( + this.name, + this.decoded_info.get("length").getLong())); + } + + // Calculate the total size of this torrent from its files' sizes. + long size = 0; + for (TorrentFile file : this.files) + size += file.size; + this.size = size; + + this.pieceLength = this.decoded_info.get("piece length").getInt(); + this.piecesHashes = this.decoded_info.get("pieces").getBytes(); + + if (this.piecesHashes.length / Torrent.PIECE_HASH_SIZE + * (long) this.pieceLength < this.size) { + throw new IllegalArgumentException("Torrent size does not " + + "match the number of pieces and the piece size!"); + } + + + logger.info("{}-file torrent information:", + this.isMultifile() ? "Multi" : "Single"); + logger.info(" Torrent name: {}", this.name); + logger.info(" Announced at:" + (trackers.isEmpty() ? " Seems to be trackerless" : "")); + for (int i = 0; i < this.trackers.size(); i++) { + List tier = this.trackers.get(i); + for (int j = 0; j < tier.size(); j++) { + logger.info(" {}{}", + (j == 0 ? String.format("%2d. ", i + 1) : " "), + tier.get(j)); + } + } + + if (this.creationTime > 0) + logger.info(" Created on..: {}", new Date(this.creationTime)); + if (this.comment != null) + logger.info(" Comment.....: {}", this.comment); + if (this.createdBy != null) + logger.info(" Created by..: {}", this.createdBy); + + if (this.isMultifile()) { + logger.info(" Found {} file(s) in multi-file torrent structure.", + this.files.size()); + int i = 0; + for (TorrentFile file : this.files) { + logger.debug(" {}. {} ({} byte(s))", + new Object[]{ + String.format("%2d", ++i), + file.path, + String.format("%,d", file.size) + }); + } + } + + logger.info(" Pieces......: {} piece(s) ({} byte(s)/piece)", + (this.size / this.decoded_info.get("piece length").getInt()) + 1, + this.decoded_info.get("piece length").getInt()); + logger.info(" Total size..: {} byte(s)", + String.format("%,d", this.size)); + } + + /** + * Get this torrent's name. + * + *

+ * For a single-file torrent, this is usually the name of the file. For a + * multi-file torrent, this is usually the name of a top-level directory + * containing those files. + *

+ */ + public String getName() { + return this.name; + } + + /** + * Get this torrent's comment string. + */ + public String getComment() { + return this.comment; + } + + /** + * Get this torrent's creator (user, software, whatever...). + */ + public String getCreatedBy() { + return this.createdBy; + } + + /** + * Get the total size of this torrent. + */ + @Nonnegative + public long getSize() { + return this.size; + } + + @Nonnull + public List getFiles() { + return files; + } + + /** + * Get the file names from this torrent. + * + * @return The list of relative filenames of all the files described in + * this torrent. + */ + public List getFilenames() { + List filenames = new ArrayList(files.size()); + for (TorrentFile file : this.files) + filenames.add(file.path); + return filenames; + } + + @Nonnegative + public int getPieceCount() { + return (int) (Math.ceil((double) getSize() / getPieceLength())); + } + + @Nonnegative + public long getPieceOffset(@Nonnegative int index) { + return (long) getPieceLength() * (long) index; + } + + @Nonnegative + public int getPieceLength() { + return pieceLength; + } + + /** + * Returns the size, in bytes, of the given piece. + * + *

+ * All pieces, except the last one, are expected to have the same size. + *

+ */ + @Nonnegative + public int getPieceLength(@Nonnegative int index) { + // The last piece may be shorter than the torrent's global piece + // length. Let's make sure we get the right piece length in any + // situation. + if (index < getPieceCount() - 1) + return getPieceLength(); + return (int) (getSize() % getPieceLength()); + } + + @Nonnull + @SuppressFBWarnings("EI_EXPOSE_REP") + public byte[] getPiecesHashes() { + return piecesHashes; + } + + @Nonnull + public byte[] getPieceHash(@Nonnegative int index) { + byte[] hashes = getPiecesHashes(); + int offset = index * PIECE_HASH_SIZE; + return Arrays.copyOfRange(hashes, offset, offset + PIECE_HASH_SIZE); + } + + public boolean isPieceValid(@Nonnegative int index, @Nonnull ByteBuffer data) { + if (data.remaining() != getPieceLength(index)) + throw new IllegalArgumentException("Validating piece " + index + " expected " + getPieceLength(index) + ", not " + data.remaining()); + return Arrays.equals(TorrentUtils.hash(data), getPieceHash(index)); + } + + /** + * Tells whether this torrent is multi-file or not. + */ + public boolean isMultifile() { + return this.files.size() > 1; + } + + /** + * Return the hash of the B-encoded meta-info structure of this torrent. + */ + @Nonnull + @SuppressFBWarnings("EI_EXPOSE_REP") + public byte[] getInfoHash() { + return this.info_hash; + } + + /** + * Get this torrent's info hash (as an hexadecimal-coded string). + */ + @Nonnull + public String getHexInfoHash() { + return TorrentUtils.toHex(this.info_hash); + } + + /** + * Return the trackers for this torrent. + */ + @Nonnull + public List> getAnnounceList() { + return this.trackers; + } + + /** + * Returns the number of trackers for this torrent. + */ + @Nonnegative + public int getTrackerCount() { + int count = 0; + for (List tier : getAnnounceList()) + count += tier.size(); + return count; + } + + @Nonnull + public byte[] toByteArray() throws IOException { + BytesBEncoder encoder = new BytesBEncoder(); + encoder.bencode(decoded); + return encoder.toByteArray(); + } + + /** + * Save this torrent meta-info structure into a .torrent file. + * + * @param output The stream to write to. + * @throws IOException If an I/O error occurs while writing the file. + */ + public void save(@Nonnull @WillNotClose OutputStream output) throws IOException { + StreamBEncoder encoder = new StreamBEncoder(output); + encoder.bencode(decoded); + output.flush(); + } + + /** + * Return a human-readable representation of this torrent object. + * + *

+ * The torrent's name is used. + *

+ */ + @Override + public String toString() { + return getName(); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/torrent/TorrentCreator.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/torrent/TorrentCreator.java new file mode 100644 index 000000000..45ebb6b8f --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/torrent/TorrentCreator.java @@ -0,0 +1,400 @@ +/* + * Copyright 2013 shevek. + * + * 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.protocol.torrent; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import com.google.common.math.LongMath; +import com.google.common.primitives.Ints; +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.protocol.bcodec.BEValue; +import io.netty.util.concurrent.DefaultThreadFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.math.RoundingMode; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class to create a {@link Torrent} object for a set of files. + * + *

+ * Hash the given files to create the multi-file {@link Torrent} object + * representing the Torrent meta-info about them, needed for announcing + * and/or sharing these files. Since we created the torrent, we're + * considering we'll be a full initial seeder for it. + *

+ * + * @author shevek + */ +public class TorrentCreator { + + private static final Logger logger = LoggerFactory.getLogger(TorrentCreator.class); + public static final int DEFAULT_PIECE_LENGTH = 512 * 1024; + + /** + * Determine how many threads to use for the piece hashing. + * + *

+ * If the environment variable TTORRENT_HASHING_THREADS is set to an + * integer value greater than 0, its value will be used. Otherwise, it + * defaults to the number of processors detected by the Java Runtime. + *

+ * + * @return How many threads to use for concurrent piece hashing. + */ + protected static int getHashingThreadsCount() { + String threads = System.getenv("TTORRENT_HASHING_THREADS"); + + if (threads != null) { + try { + int count = Integer.parseInt(threads); + if (count > 0) { + return count; + } + } catch (NumberFormatException nfe) { + // Pass + } + } + + return Runtime.getRuntime().availableProcessors(); + } + + /** + * Creates a new executor suitable for torrent hashing. + * + * This executor controls memory usage by using a bounded queue, and the + * CallerRunsPolicy slows down the producer if the queue bound is exceeded. + * The requirement is then to make the queue large enough to keep all the + * executor threads busy if the producer executes a task itself. + * + * In terms of memory, Executor.execute is much more efficient than + * ExecutorService.submit, and ByteBuffer(s) released by the ChunkHasher(s) + * remain in eden space, so are rapidly recycled for reading the next + * block(s). JVM ergonomics can make this much more efficient than any + * heap-based strategy we might devise. Now, we want the queue size small + * enough that JVM ergonomics keeps the eden size small. + */ + @Nonnull + public static ThreadPoolExecutor newExecutor(@Nonnull String peerName) { + int threads = getHashingThreadsCount(); + logger.info("Creating ExecutorService with {} threads", new Object[]{threads}); + ThreadFactory factory = new DefaultThreadFactory("bittorrent-executor-" + peerName, true); + ThreadPoolExecutor service = new ThreadPoolExecutor(0, threads, + 1L, TimeUnit.SECONDS, + new ArrayBlockingQueue(threads * 3), + factory, + new ThreadPoolExecutor.CallerRunsPolicy()); + service.allowCoreThreadTimeOut(true); + return service; + } + private Executor executor; + private final File parent; + private List files; + private int pieceLength = DEFAULT_PIECE_LENGTH; + private List> announce = new ArrayList>(); + private String createdBy = getClass().getName(); + + /* + * @param parent The parent directory or location of the torrent files, + * also used as the torrent's name. + */ + public TorrentCreator(@Nonnull File parent) { + this.parent = parent; + if (parent.isDirectory()) { + List tmp = new ArrayList(); + for (File file : parent.listFiles()) + if (file.isFile()) + tmp.add(file); + Collections.sort(tmp); + files = tmp; + } + } + + public void setExecutor(@Nonnull Executor executor) { + this.executor = executor; + } + + /* + * @param files The files to add into this torrent. + */ + public void setFiles(@Nonnull List files) { + this.files = files; + } + + @Nonnegative + public int getPieceLength() { + return pieceLength; + } + + public void setPieceLength(@Nonnegative int pieceLength) { + this.pieceLength = pieceLength; + } + + public void setAnnounceList(@Nonnull List announce) { + setAnnounceTiers(Arrays.asList(announce)); + } + + public void setAnnounceTiers(@Nonnull List> announce) { + this.announce = announce; + } + + /* + * @param announce The announce URIs organized as tiers that will + * be used for this torrent. + */ + public void setAnnounce(@Nonnull URI... announce) { + setAnnounceList(Arrays.asList(announce)); + } + + /* + * @param createdBy The creator's name, or any string identifying the + * torrent's creator. + */ + public void setCreatedBy(@Nonnull String createdBy) { + this.createdBy = createdBy; + } + + private void validate(@Nonnull File file) { + if (!file.isFile()) + throw new IllegalStateException("Not a file: " + file); + if (!file.canRead()) + throw new IllegalStateException("Not readable: " + file); + } + + private void validate() { + if (executor == null) + executor = newExecutor("TorrentCreator-" + parent); + if (files == null) { + validate(parent); + } else { + for (File file : files) + validate(file); + } + } + + /** + * Helper method to create a {@link Torrent} object for a set of files. + * + *

+ * Hash the given files to create the {@link Torrent} object + * representing the Torrent meta-info about them, needed for announcing + * and/or sharing these files. + *

+ */ + @Nonnull + public Torrent create() throws InterruptedException, IOException, URISyntaxException { + validate(); + + if (files == null || files.isEmpty()) + logger.info("Creating single-file torrent for {}...", parent.getName()); + else + logger.info("Creating {}-file torrent {}...", files.size(), parent.getName()); + + Map torrent = new HashMap(); + + ANNOUNCE: + if (announce != null) { + List announceFlat = new ArrayList(); + List announceTiers = new LinkedList(); + for (List trackers : announce) { + List announceTier = new LinkedList(); + for (URI tracker : trackers) { + announceFlat.add(tracker); + announceTier.add(new BEValue(tracker.toString())); + } + if (!announceTier.isEmpty()) + announceTiers.add(new BEValue(announceTier)); + } + if (announceFlat.size() == 1) + torrent.put("announce", new BEValue(announceFlat.get(0).toString())); + if (!announceTiers.isEmpty()) + torrent.put("announce-list", new BEValue(announceTiers)); + } + + torrent.put("creation date", new BEValue(new Date().getTime() / 1000)); + torrent.put("created by", new BEValue(createdBy)); + + Map info = new TreeMap(); + info.put("name", new BEValue(parent.getName())); + info.put("piece length", new BEValue(pieceLength)); + + if (files == null || files.isEmpty()) { + long nbytes = parent.length(); + info.put("length", new BEValue(nbytes)); + info.put("pieces", new BEValue(hashFiles(executor, Arrays.asList(parent), nbytes, pieceLength))); + } else { + List fileInfo = new LinkedList(); + long nbytes = 0L; + for (File file : files) { + Map fileMap = new HashMap(); + long length = file.length(); + fileMap.put("length", new BEValue(length)); + nbytes += length; + + LinkedList filePath = new LinkedList(); + while (file != null && !parent.equals(file)) { + filePath.addFirst(new BEValue(file.getName())); + file = file.getParentFile(); + } + + fileMap.put("path", new BEValue(filePath)); + fileInfo.add(new BEValue(fileMap)); + } + info.put("files", new BEValue(fileInfo)); + info.put("pieces", new BEValue(hashFiles(executor, files, nbytes, pieceLength))); + } + torrent.put("info", new BEValue(info)); + + return new Torrent(torrent); + } + + /** + * A {@link Runnable} to hash a data chunk. + * + * @author mpetazzoni + */ + private static class ChunkHasher implements Runnable { + + private final byte[] out; + private final int piece; + private final CountDownLatch latch; + private final ByteBuffer data; + + ChunkHasher(@Nonnull byte[] out, @Nonnegative int piece, @Nonnull CountDownLatch latch, @Nonnull ByteBuffer data) { + this.out = out; + this.piece = piece; + this.latch = latch; + this.data = data; + } + + @Override + public void run() { + try { + System.arraycopy(TorrentUtils.hash(this.data), 0, out, piece * Torrent.PIECE_HASH_SIZE, Torrent.PIECE_HASH_SIZE); + } finally { + latch.countDown(); + } + } + } + + /** + * Return the concatenation of the SHA-1 hashes of a file's pieces. + * + *

+ * Hashes the given file piece by piece using the default Torrent piece + * length (see {@link #PIECE_LENGTH}) and returns the concatenation of + * these hashes, as a string. + *

+ * + *

+ * This is used for creating Torrent meta-info structures from a file. + *

+ * + * @param files The file to hash. + */ + @Nonnull + @VisibleForTesting + public static byte[] hashFiles(@Nonnull Executor executor, + @Nonnull List files, @Nonnegative long nbytes, + @Nonnegative int pieceLength) + throws InterruptedException, IOException { + int npieces = Ints.checkedCast(LongMath.divide(nbytes, pieceLength, RoundingMode.CEILING)); + // (int) Math.ceil((double) nbytes / pieceLength); + byte[] out = new byte[Torrent.PIECE_HASH_SIZE * npieces]; + CountDownLatch latch = new CountDownLatch(npieces); + + ByteBuffer buffer = ByteBuffer.allocate(pieceLength); + + Stopwatch stopwatch = Stopwatch.createStarted(); + int piece = 0; + for (File file : files) { + logger.info("Hashing data from {} ({} pieces)...", new Object[]{ + file.getName(), + LongMath.divide(file.length(), pieceLength, RoundingMode.CEILING) + }); + + FileInputStream fis = new FileInputStream(file); + FileChannel channel = fis.getChannel(); + int step = 10; + + try { + while (channel.read(buffer) > 0) { + if (buffer.remaining() == 0) { + buffer.flip(); + executor.execute(new ChunkHasher(out, piece, latch, buffer)); + buffer = ByteBuffer.allocate(pieceLength); + piece++; + } + + if (channel.position() / (double) channel.size() * 100f > step) { + logger.info(" ... {}% complete", step); + step += 10; + } + } + } finally { + channel.close(); + fis.close(); + } + } + + // Hash the last bit, if any + if (buffer.position() > 0) { + buffer.flip(); + executor.execute(new ChunkHasher(out, piece, latch, buffer)); + piece++; + } + + // Wait for hashing tasks to complete. + latch.await(); + + logger.info("Hashed {} file(s) ({} bytes) in {} pieces ({} expected) in {}.", + new Object[]{ + files.size(), + nbytes, + piece, + npieces, + stopwatch + }); + + if (npieces != piece) + throw new IllegalStateException("Unexpected piece count " + piece + "; expected " + npieces); + + return out; + } +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/InetAddressComparator.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/InetAddressComparator.java new file mode 100644 index 000000000..6657bfcc8 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/InetAddressComparator.java @@ -0,0 +1,44 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.tracker; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.util.Comparator; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public class InetAddressComparator implements Comparator { + + public static final InetAddressComparator INSTANCE = new InetAddressComparator(); + + private static int score(@Nonnull InetAddress a) { + // Least likely to be a valid target address. + if (a.isAnyLocalAddress()) + return 10; + if (a.isMulticastAddress()) + return 6; + if (a.isLoopbackAddress()) + return 4; + if (a.isLinkLocalAddress()) + return 2; + return 0; + // Most likely to be a valid target address. + } + + @Override + public int compare(InetAddress o1, InetAddress o2) { + int cmp; + // Inet4Address is better than Inet6Address + cmp = -Boolean.compare(o1 instanceof Inet4Address, o2 instanceof Inet4Address); + if (cmp != 0) + return cmp; + // Avoid loopbacks. + return Integer.compare(score(o1), score(o2)); + } +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/InetSocketAddressComparator.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/InetSocketAddressComparator.java new file mode 100644 index 000000000..0d5110b9f --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/InetSocketAddressComparator.java @@ -0,0 +1,22 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.tracker; + +import java.net.InetSocketAddress; +import java.util.Comparator; + +/** + * + * @author shevek + */ +public class InetSocketAddressComparator implements Comparator { + + public static final InetSocketAddressComparator INSTANCE = new InetSocketAddressComparator(); + + @Override + public int compare(InetSocketAddress o1, InetSocketAddress o2) { + return InetAddressComparator.INSTANCE.compare(o1.getAddress(), o2.getAddress()); + } +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/Peer.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/Peer.java new file mode 100644 index 000000000..4d69b5ac9 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/Peer.java @@ -0,0 +1,212 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.protocol.tracker; + +import com.turn.ttorrent.protocol.TorrentUtils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Arrays; +import javax.annotation.CheckForNull; +import javax.annotation.CheckForSigned; +import javax.annotation.Nonnull; + +/** + * A basic BitTorrent peer. + * + *

+ * We can't use this class in as many places as we'd like because it does + * not have strong nonnull or type guarantees. + * + * Peer-to-peer traffic needs a SocketAddress and a PeerId, whereas the tracker + * needs an InetSocketAddress but PeerId is optional. + *

+ * + * @author mpetazzoni + */ +public class Peer { + + public static final int PEER_ID_LENGTH = 20; + + public static boolean isValidIpAddress(@CheckForNull SocketAddress sa) { + if (!(sa instanceof InetSocketAddress)) + return false; + InetSocketAddress isa = (InetSocketAddress) sa; + return isValidIpAddress(isa.getAddress()); + } + + public static boolean isValidIpAddress(@CheckForNull InetAddress ia) { + if (ia == null) + return false; + byte[] ba = ia.getAddress(); + return isValidIpAddress(ba); + } + + public static boolean isValidIpAddress(@CheckForNull byte[] ba) { + if (ba == null) + return false; + for (byte b : ba) + if (b != 0) + return true; + return false; + } + private final SocketAddress address; + // On UDP and HTTP-compact this is nullable. + @CheckForNull + private final byte[] peerId; + + /** + * Instantiate a new peer. + * + * @param address The peer's address, with port. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Peer(@Nonnull SocketAddress address, @CheckForNull byte[] peerId) { + if (!isValidIpAddress(address)) + throw new IllegalArgumentException("Invalid SocketAddress: " + address); + if (peerId != null && peerId.length != PEER_ID_LENGTH) + throw new IllegalArgumentException("PeerId length should be " + PEER_ID_LENGTH + ", not " + peerId.length); + this.address = address; + this.peerId = peerId; + } + + /** + * Returns the raw peer ID. + */ + @CheckForNull + @SuppressFBWarnings("EI_EXPOSE_REP") + public byte[] getPeerId() { + return this.peerId; + } + + public boolean hasPeerId() { + return peerId != null; + } + + /** + * Get the hexadecimal-encoded string representation of this peer's ID. + */ + @CheckForNull + public String getHexPeerId() { + byte[] peerId = getPeerId(); + if (peerId == null) + return null; + return TorrentUtils.toHex(peerId); + } + + /** + * Get the shortened hexadecimal-encoded peer ID. + */ + @CheckForNull + private String getShortHexPeerId() { + String hexPeerId = getHexPeerId(); + if (hexPeerId == null) + return null; + return hexPeerId.substring(hexPeerId.length() - 6); + } + + @Nonnull + public SocketAddress getAddress() { + return address; + } + + @CheckForNull + private InetAddress getInetAddress() { + SocketAddress sa = getAddress(); + if (!(sa instanceof InetSocketAddress)) + return null; + InetSocketAddress isa = (InetSocketAddress) sa; + return isa.getAddress(); + } + + @CheckForNull + public byte[] getIpBytes() { + InetAddress ia = getInetAddress(); + if (ia == null) + return null; + return ia.getAddress(); + } + + @CheckForNull + public String getIpString() { + InetAddress ia = getInetAddress(); + if (ia == null) + return null; + return ia.getHostAddress(); + } + + @CheckForSigned + public int getPort() { + SocketAddress sa = getAddress(); + if (!(sa instanceof InetSocketAddress)) + return -1; + InetSocketAddress isa = (InetSocketAddress) sa; + return isa.getPort(); + } + + /** + * Returns this peer's host identifier ("host:port"). + */ + @Nonnull + public String getHostIdentifier() { + return getIpString() + ":" + getPort(); + } + + @Override + public int hashCode() { + return getAddress().hashCode() ^ Arrays.hashCode(getPeerId()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (null == obj) + return false; + if (!getClass().equals(obj.getClass())) + return false; + Peer other = (Peer) obj; + return this.address.equals(other.address) + && Arrays.equals(peerId, other.peerId); + } + + public boolean matches(@Nonnull Peer other) { + if (!this.address.equals(other.address)) + return false; + if (!hasPeerId()) + return true; + return Arrays.equals(peerId, other.peerId); + } + + /** + * Returns a human-readable representation of this peer. + */ + @Override + public String toString() { + // TODO: Use InetAddresses.toUriString() when possible. + // Right now this generates peer:///1.2.3.4/ which has three slashes in it. + StringBuilder s = new StringBuilder("peer://") + .append(getAddress()) + .append("/"); + String hexPeerId = getShortHexPeerId(); + if (hexPeerId != null) + s.append(hexPeerId); + else + s.append("?"); + return s.toString(); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/TrackerMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/TrackerMessage.java new file mode 100644 index 000000000..890732801 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/TrackerMessage.java @@ -0,0 +1,220 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker; + +import java.util.Collection; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; + +/** + * BitTorrent tracker protocol messages representations. + * + *

+ * This class and its *TrackerMessage subclasses provide POJO + * representations of the tracker protocol messages, for at least HTTP and UDP + * trackers' protocols, along with easy parsing from an input ByteBuffer to + * quickly get a usable representation of an incoming message. + *

+ * + * @author mpetazzoni + */ +public abstract class TrackerMessage { + + /** + * Announce request event types. + * + *

+ * When the client starts exchanging on a torrent, it must contact the + * torrent's tracker with a 'started' announce request, which notifies the + * tracker this client now exchanges on this torrent (and thus allows the + * tracker to report the existence of this peer to other clients). + *

+ * + *

+ * When the client stops exchanging, or when its download completes, it must + * also send a specific announce request. Otherwise, the client must send an + * eventless (NONE), periodic announce request to the tracker at an + * interval specified by the tracker itself, allowing the tracker to + * refresh this peer's status and acknowledge that it is still there. + *

+ */ + public static enum AnnounceEvent { + + NONE(0), + COMPLETED(1), + STARTED(2), + STOPPED(3); + private final int id; + + AnnounceEvent(int id) { + this.id = id; + } + + public String getEventName() { + return this.name().toLowerCase(); + } + + public int getId() { + return this.id; + } + + @CheckForNull + public static AnnounceEvent getByName(@CheckForNull String name) { + // TODO: Use valueOf(toUpperCase()). + for (AnnounceEvent type : AnnounceEvent.values()) { + if (type.name().equalsIgnoreCase(name)) { + return type; + } + } + return null; + } + + @CheckForNull + public static AnnounceEvent getById(int id) { + for (AnnounceEvent type : AnnounceEvent.values()) { + if (type.getId() == id) { + return type; + } + } + return null; + } + }; + + /** + * Generic exception for message format and message validation exceptions. + */ + public static class MessageValidationException extends Exception { + + static final long serialVersionUID = -1; + + public MessageValidationException(@Nonnull String s) { + super(s); + } + + public MessageValidationException(@Nonnull String s, @Nonnull Throwable cause) { + super(s, cause); + } + } + + /** + * Base interface for announce request messages. + * + *

+ * This interface must be implemented by all subtypes of announce request + * messages for the various tracker protocols. + *

+ * + * @author mpetazzoni + */ + public interface AnnounceRequestMessage { + + public static final int DEFAULT_NUM_WANT = 50; + + @Nonnull + public byte[] getInfoHash(); + + @Nonnull + public String getHexInfoHash(); + + @Nonnull + public byte[] getPeerId(); + + // public InetSocketAddress getPeerAddress(); + public long getUploaded(); + + public long getDownloaded(); + + public long getLeft(); + + @Nonnull + public AnnounceEvent getEvent(); + + public int getNumWant(); + }; + + /** + * Base interface for announce response messages. + * + *

+ * This interface must be implemented by all subtypes of announce response + * messages for the various tracker protocols. + *

+ * + * @author mpetazzoni + */ + public interface AnnounceResponseMessage { + + /** In seconds. */ + @Nonnegative + public int getInterval(); + + @Nonnegative + public int getComplete(); + + @Nonnegative + public int getIncomplete(); + + @Nonnull + public Collection getPeers(); + }; + + /** + * Base interface for tracker error messages. + * + *

+ * This interface must be implemented by all subtypes of tracker error + * messages for the various tracker protocols. + *

+ * + * @author mpetazzoni + */ + public interface ErrorMessage { + + /** + * The various tracker error states. + * + *

+ * These errors are reported by the tracker to a client when expected + * parameters or conditions are not present while processing an + * announce request from a BitTorrent client. + *

+ */ + public enum FailureReason { + + UNKNOWN_TORRENT("The requested torrent does not exist on this tracker"), + MISSING_HASH("Missing info hash"), + MISSING_PEER_ADDRESS("Missing peer address"), + MISSING_PEER_ID("Missing peer ID"), + MISSING_PORT("Missing port"), + INVALID_EVENT("Unexpected event for peer state"), + UPDATE_FAILED("Failed to update torrent"), + NOT_IMPLEMENTED("Feature not implemented"), + SERVER_ERROR("Server error"); + private String message; + + FailureReason(String message) { + this.message = message; + } + + public String getMessage() { + return this.message; + } + }; + + public String getReason(); + }; +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceRequestMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceRequestMessage.java new file mode 100644 index 000000000..c56887531 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceRequestMessage.java @@ -0,0 +1,305 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.http; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.net.InetAddresses; +import com.turn.ttorrent.protocol.bcodec.BEUtils; +import com.turn.ttorrent.protocol.tracker.TrackerMessage.AnnounceRequestMessage; +import com.turn.ttorrent.protocol.TorrentUtils; +import java.io.UnsupportedEncodingException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The announce request message for the HTTP tracker protocol. + * + *

+ * This class represents the announce request message in the HTTP tracker + * protocol. It doesn't add any specific fields compared to the generic + * announce request message, but it provides the means to parse such + * messages and craft them. + *

+ * + * @author mpetazzoni + */ +public class HTTPAnnounceRequestMessage extends HTTPTrackerMessage + implements AnnounceRequestMessage { + + private static final Logger LOG = LoggerFactory.getLogger(HTTPAnnounceRequestMessage.class); + private final byte[] infoHash; + private final byte[] peerId; + private final List peerAddresses; + private final long uploaded; + private final long downloaded; + private final long left; + private final boolean compact; + private final boolean noPeerId; + private final AnnounceEvent event; + private final int numWant; + + public HTTPAnnounceRequestMessage( + byte[] infoHash, + byte[] peerId, List peerAddresses, + long uploaded, long downloaded, long left, + boolean compact, boolean noPeerId, AnnounceEvent event, int numWant) { + if (peerAddresses.isEmpty()) + throw new IllegalArgumentException("No PeerAddresses specified. Require at least an InetSocketAddress(port)."); + this.infoHash = infoHash; + this.peerId = peerId; + this.peerAddresses = peerAddresses; + this.downloaded = downloaded; + this.uploaded = uploaded; + this.left = left; + this.compact = compact; + this.noPeerId = noPeerId; + this.event = event; + this.numWant = numWant; + } + + @Override + public byte[] getInfoHash() { + return this.infoHash; + } + + @Override + public String getHexInfoHash() { + return TorrentUtils.toHex(this.infoHash); + } + + @Override + public byte[] getPeerId() { + return peerId; + } + + @Nonnull + public List getPeerAddresses() { + return peerAddresses; + } + + @Override + public long getUploaded() { + return this.uploaded; + } + + @Override + public long getDownloaded() { + return this.downloaded; + } + + @Override + public long getLeft() { + return this.left; + } + + public boolean getCompact() { + return this.compact; + } + + public boolean getNoPeerIds() { + return this.noPeerId; + } + + @Override + public AnnounceEvent getEvent() { + return this.event; + } + + @Override + public int getNumWant() { + return this.numWant; + } + + @Nonnull + @VisibleForTesting + /* pp */ static String toUrlString(@Nonnull byte[] data) throws UnsupportedEncodingException { + String text = new String(data, BEUtils.BYTE_ENCODING); + return URLEncoder.encode(text, BEUtils.BYTE_ENCODING_NAME); + } + + @Nonnull + @VisibleForTesting + /* pp */ static String toUrlString(@Nonnull InetAddress address, int port) throws UnsupportedEncodingException { + String text = InetAddresses.toUriString(address); + if (port != -1) + text = text + ":" + port; + return URLEncoder.encode(text, BEUtils.BYTE_ENCODING_NAME); + } + + /** + * Build the announce request URL for the given tracker announce URL. + * + * @param trackerAnnounceURL The tracker's announce URL. + * @return The URL object representing the announce request URL. + */ + @Nonnull + public URI toURI(@Nonnull URI trackerAnnounceURL) + throws UnsupportedEncodingException, URISyntaxException { + String base = trackerAnnounceURL.toString(); + StringBuilder url = new StringBuilder(base); + url.append(base.contains("?") ? "&" : "?") + .append("info_hash=").append(toUrlString(getInfoHash())) + .append("&peer_id=").append(toUrlString(getPeerId())) + // .append("&port=").append(getPeerAddress().getPort()) + .append("&uploaded=").append(getUploaded()) + .append("&downloaded=").append(getDownloaded()) + .append("&left=").append(getLeft()) + .append("&compact=").append(getCompact() ? 1 : 0) + .append("&no_peer_id=").append(getNoPeerIds() ? 1 : 0); + + if (getEvent() != null + && !AnnounceEvent.NONE.equals(getEvent())) { + url.append("&event=").append(getEvent().getEventName()); + } + + List addresses = new ArrayList(); + Iterables.addAll(addresses, getPeerAddresses()); + boolean port = false; + for (InetSocketAddress sockaddr : addresses) { + InetAddress inaddr = sockaddr.getAddress(); + if (!port) { + url.append("&port=").append(sockaddr.getPort()); + if (inaddr instanceof Inet4Address) + url.append("&ip=").append(toUrlString(inaddr, -1)); + else if (inaddr instanceof Inet6Address) + url.append("&ipv6=").append(toUrlString(inaddr, -1)); + port = true; + continue; + } + if (inaddr instanceof Inet4Address) + url.append("&ipv4=").append(toUrlString(inaddr, sockaddr.getPort())); + else if (inaddr instanceof Inet6Address) + url.append("&ipv6=").append(toUrlString(inaddr, sockaddr.getPort())); + } + + if (getNumWant() != AnnounceRequestMessage.DEFAULT_NUM_WANT) + url.append("&numwant=").append(getNumWant()); + + return new URI(url.toString()); + } + + @VisibleForTesting + @Nonnull + /* pp */ static InetSocketAddress toInetSocketAddress(@Nonnull String sockstr, int port) { + if (sockstr.indexOf(':') == -1) + return new InetSocketAddress(InetAddresses.forString(sockstr), port); + if (sockstr.startsWith("[")) { + int idx = sockstr.indexOf("]:"); + if (idx == -1) // Pure bracket-surrounded IPv6 address. + return new InetSocketAddress(InetAddresses.forUriString(sockstr), port); + int port6 = Integer.parseInt(sockstr.substring(idx + 2)); + return new InetSocketAddress(InetAddresses.forUriString(sockstr.substring(0, idx + 1)), port6); + } + int idx = sockstr.indexOf(':'); + if (idx != -1) { // IPv4 plus port + int port4 = Integer.parseInt(sockstr.substring(idx + 1)); + return new InetSocketAddress(InetAddresses.forUriString(sockstr.substring(0, idx)), port4); + } + return new InetSocketAddress(InetAddresses.forUriString(sockstr.substring(0, idx)), port); + } + + @Nonnull + public static HTTPAnnounceRequestMessage fromParams(@Nonnull Multimap params) + throws MessageValidationException { + + byte[] infoHash = toBytes(params, "info_hash", ErrorMessage.FailureReason.MISSING_HASH); + byte[] peerId = toBytes(params, "peer_id", ErrorMessage.FailureReason.MISSING_PEER_ID); + + // Default 'uploaded' and 'downloaded' to 0 if the client does + // not provide it (although it should, according to the spec). + long uploaded = toLong(params, "uploaded", 0, null); + long downloaded = toLong(params, "downloaded", 0, null); + // Default 'left' to -1 to avoid peers entering the COMPLETED + // state when they don't provide the 'left' parameter. + long left = toLong(params, "left", -1, null); + + boolean compact = toBoolean(params, "compact"); + boolean noPeerId = toBoolean(params, "no_peer_id"); + + int numWant = toInt(params, "numwant", AnnounceRequestMessage.DEFAULT_NUM_WANT, null); + + AnnounceEvent event = AnnounceEvent.NONE; + if (params.containsKey("event")) + event = AnnounceEvent.getByName(toString(params, "event", null)); + + List addresses = new ArrayList(); + int port = toInt(params, "port", -1, ErrorMessage.FailureReason.MISSING_PORT); + + MAIN: + { + String ip = toString(params, "ip", null); + if (ip != null) + addresses.add(new InetSocketAddress(InetAddresses.forString(ip), port)); + } + + IP4: + { + Collection ips = params.get("ipv4"); + if (ips != null) + for (String ip : ips) + addresses.add(toInetSocketAddress(ip, port)); + } + + IP6: + { + Collection ips = params.get("ipv6"); + if (ips != null) + for (String ip : ips) + addresses.add(toInetSocketAddress(ip, port)); + } + + DEFAULT: + { + if (addresses.isEmpty()) + addresses.add(new InetSocketAddress(port)); + } + + return new HTTPAnnounceRequestMessage(infoHash, + peerId, addresses, + uploaded, downloaded, left, compact, noPeerId, + event, numWant); + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("infoHash", getHexInfoHash()) + .add("peerId", TorrentUtils.toHex(peerId)) + .add("peerAddresses", peerAddresses) + .add("uploaded", uploaded) + .add("downloaded", downloaded) + .add("left", left) + .add("compact", compact) + .add("noPeerId", noPeerId) + .add("event", event) + .add("numWant", numWant) + .toString(); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceResponseMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceResponseMessage.java new file mode 100644 index 000000000..4a5138e73 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceResponseMessage.java @@ -0,0 +1,290 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.http; + +import com.google.common.base.Objects; +import com.google.common.net.InetAddresses; +import com.turn.ttorrent.protocol.bcodec.BEUtils; +import com.turn.ttorrent.protocol.bcodec.BEValue; +import com.turn.ttorrent.protocol.bcodec.InvalidBEncodingException; +import com.turn.ttorrent.protocol.tracker.Peer; +import com.turn.ttorrent.protocol.tracker.TrackerMessage.AnnounceResponseMessage; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The announce response message from an HTTP tracker. + * + * @author mpetazzoni + */ +public class HTTPAnnounceResponseMessage extends HTTPTrackerMessage + implements AnnounceResponseMessage { + + private static final Logger LOG = LoggerFactory.getLogger(HTTPAnnounceResponseMessage.class); + public static final String EXTERNAL_IP = "external ip"; + public static final String INTERVAL = "interval"; + public static final String COMPLETE = "complete"; + public static final String INCOMPLETE = "incomplete"; + public static final String PEERS = "peers"; + public static final String PEERS6 = "peers6"; + public static final String PEER_IP = "ip"; + public static final String PEER_PORT = "port"; + public static final String PEER_ID = "peer id"; + private final InetAddress clientAddress; + private final int interval; + private final int complete; + private final int incomplete; + private final List peers; + + public HTTPAnnounceResponseMessage( + @CheckForNull InetAddress clientAddress, + int interval, int complete, int incomplete, + @Nonnull List peers) { + this.clientAddress = clientAddress; + this.interval = interval; + this.complete = complete; + this.incomplete = incomplete; + this.peers = peers; + } + + @Override + public int getInterval() { + return this.interval; + } + + @Override + public int getComplete() { + return this.complete; + } + + @Override + public int getIncomplete() { + return this.incomplete; + } + + @Override + public Collection getPeers() { + return peers; + } + + @Nonnull + public static HTTPAnnounceResponseMessage fromBEValue(@Nonnull Map params) + throws IOException, MessageValidationException { + + if (params.get(INTERVAL) == null) { + throw new MessageValidationException( + "Tracker message missing mandatory field 'interval'!"); + } + + try { + byte[] clientAddressBytes = BEUtils.getBytes(params.get(EXTERNAL_IP)); + InetAddress clientAddress = null; + if (clientAddressBytes != null) + clientAddress = InetAddress.getByAddress(clientAddressBytes); + + List peers = new ArrayList(); + + BEValue peers4 = params.get(PEERS); + if (peers4 == null) { + } else if (peers4.getValue() instanceof List) { + toPeerList(peers, peers4.getList()); + } else if (peers4.getValue() instanceof byte[]) { + toPeerList(peers, peers4.getBytes(), 4); + } + + BEValue peers6 = params.get(PEERS6); + if (peers6 == null) { + } else if (peers6.getValue() instanceof byte[]) { + toPeerList(peers, peers6.getBytes(), 16); + } + + return new HTTPAnnounceResponseMessage( + clientAddress, + BEUtils.getInt(params.get(INTERVAL), 60), + BEUtils.getInt(params.get(COMPLETE), 0), + BEUtils.getInt(params.get(INCOMPLETE), 0), + peers); + } catch (InvalidBEncodingException ibee) { + throw new MessageValidationException("Invalid response " + + "from tracker!", ibee); + } catch (UnknownHostException uhe) { + throw new MessageValidationException("Invalid peer " + + "in tracker response!", uhe); + } + } + + /** + * Build a peer list as a list of {@link Peer}s from the + * announce response's peer list (in non-compact mode). + * + * @param peers The list of {@link BEValue}s dictionaries describing the + * peers from the announce response. + * @return A {@link List} of {@link Peer}s representing the + * peers' addresses. Peer IDs are lost, but they are not crucial. + */ + @Nonnull + private static void toPeerList(@Nonnull List out, @Nonnull List peers) { + for (BEValue peer : peers) { + try { + Map peerInfo = peer.getMap(); + String ip = BEUtils.getString(peerInfo.get(PEER_IP)); + int port = BEUtils.getInt(peerInfo.get(PEER_PORT), -1); + if (ip == null || port < 0) { + LOG.warn("Invalid peer " + peer); + continue; + } + InetAddress inaddr = InetAddresses.forString(ip); + InetSocketAddress saddr = new InetSocketAddress(inaddr, port); + + byte[] peerId = BEUtils.getBytes(peerInfo.get(PEER_ID)); + out.add(new Peer(saddr, peerId)); + } catch (InvalidBEncodingException e) { + LOG.error("Failed to parse peer from " + peer, e); + } catch (NullPointerException e) { + LOG.error("Failed to parse peer from " + peer, e); + } catch (IllegalArgumentException e) { + LOG.error("Failed to parse peer from " + peer, e); + } + } + } + + /** + * Build a peer list as a list of {@link Peer}s from the + * announce response's binary compact peer list. + * + * @param data The bytes representing the compact peer list from the + * announce response. + * @return A {@link List} of {@link Peer}s representing the + * peers' addresses. Peer IDs are lost, but they are not crucial. + */ + @Nonnull + private static void toPeerList(List out, @Nonnull byte[] data, int addrlen) + throws InvalidBEncodingException, UnknownHostException { + if (data.length % (addrlen + 2) != 0) { + throw new InvalidBEncodingException( + "Invalid peers binary information string!"); + } + + int addrcount = data.length / (addrlen + 2); + ByteBuffer peers = ByteBuffer.wrap(data); + + byte[] ipBytes = new byte[addrlen]; + for (int i = 0; i < addrcount; i++) { + peers.get(ipBytes); + int port = peers.getShort() & 0xFFFF; + try { + InetAddress ip = InetAddress.getByAddress(ipBytes); + out.add(new Peer(new InetSocketAddress(ip, port), null)); + } catch (IllegalArgumentException e) { + LOG.error("Failed to parse peer from " + Arrays.toString(ipBytes) + ", " + port, e); + } + } + } + + @Nonnull + public Map toBEValue(boolean compact, boolean noPeerIds) { + Map params = new HashMap(); + // TODO: "min interval", "tracker id" + if (Peer.isValidIpAddress(clientAddress)) + params.put(EXTERNAL_IP, new BEValue(clientAddress.getAddress())); + params.put(INTERVAL, new BEValue(interval)); + params.put(COMPLETE, new BEValue(complete)); + params.put(INCOMPLETE, new BEValue(incomplete)); + + if (compact) { + // This is a conservative overallocation. + ByteBuffer peer4Data = ByteBuffer.allocate(peers.size() * 6); + ByteBuffer peer6Data = ByteBuffer.allocate(peers.size() * 18); + + for (Peer peer : peers) { + // LOG.info("Adding peer " + peer); + byte[] ip = peer.getIpBytes(); + if (ip == null) + continue; + if (ip.length == 4) { + peer4Data.put(ip); + peer4Data.putShort((short) peer.getPort()); + } else if (ip.length == 16) { + peer6Data.put(ip); + peer6Data.putShort((short) peer.getPort()); + } else { + LOG.warn("Cannot encode peer " + peer); + } + } + + if (peer4Data.position() > 0) { + byte[] buf = Arrays.copyOf(peer4Data.array(), peer4Data.position()); + params.put(PEERS, new BEValue(buf)); + } + + if (peer6Data.position() > 0) { + byte[] buf = Arrays.copyOf(peer6Data.array(), peer6Data.position()); + params.put(PEERS6, new BEValue(buf)); + } + } else { + List peerList = new ArrayList(); + + for (Peer peer : peers) { + // LOG.info("Adding peer " + peer); + + Map peerItem = new HashMap(); + byte[] peerId = peer.getPeerId(); + if (peerId != null) + peerItem.put(PEER_ID, new BEValue(peerId)); + String ip = peer.getIpString(); + if (ip != null) + peerItem.put(PEER_IP, new BEValue(ip, BEUtils.BYTE_ENCODING)); + int port = peer.getPort(); + if (port != -1) + peerItem.put(PEER_PORT, new BEValue(port)); + peerList.add(new BEValue(peerItem)); + } + + params.put(PEERS, new BEValue(peerList)); + } + + return params; + } + + @Nonnull + public Map toBEValue(@Nonnull HTTPAnnounceRequestMessage request) { + return toBEValue(request.getCompact(), request.getNoPeerIds()); + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("clientAddress", clientAddress) + .add("interval", getInterval()) + .add("complete", getComplete()) + .add("incomplete", getIncomplete()) + .add("peers", getPeers()) + .toString(); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPTrackerErrorMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPTrackerErrorMessage.java new file mode 100644 index 000000000..589fc5e48 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPTrackerErrorMessage.java @@ -0,0 +1,67 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.http; + +import com.turn.ttorrent.protocol.bcodec.BEUtils; +import com.turn.ttorrent.protocol.bcodec.BEValue; +import com.turn.ttorrent.protocol.bcodec.InvalidBEncodingException; +import com.turn.ttorrent.protocol.tracker.TrackerMessage.ErrorMessage; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * An error message from an HTTP tracker. + * + * @author mpetazzoni + */ +public class HTTPTrackerErrorMessage extends HTTPTrackerMessage implements ErrorMessage { + + private final String reason; + + public HTTPTrackerErrorMessage(String reason) { + this.reason = reason; + } + + public HTTPTrackerErrorMessage(ErrorMessage.FailureReason reason) { + this(reason.getMessage()); + } + + @Override + public String getReason() { + return this.reason; + } + + @Nonnull + public static HTTPTrackerErrorMessage fromBEValue(@Nonnull Map params) + throws MessageValidationException { + + try { + String reason = params.get("failure reason").getString(BEUtils.BYTE_ENCODING); + return new HTTPTrackerErrorMessage(reason); + } catch (InvalidBEncodingException ibee) { + throw new MessageValidationException("Invalid tracker error " + + "message!", ibee); + } + } + + @Nonnull + public Map toBEValue() { + Map params = new HashMap(); + params.put("failure reason", new BEValue(getReason(), BEUtils.BYTE_ENCODING)); + return params; + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPTrackerMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPTrackerMessage.java new file mode 100644 index 000000000..cc8c6fc99 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/http/HTTPTrackerMessage.java @@ -0,0 +1,88 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.http; + +import com.google.common.collect.Multimap; +import com.turn.ttorrent.protocol.bcodec.BEUtils; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import java.util.Collection; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for HTTP tracker messages. + * + * @author mpetazzoni + */ +public abstract class HTTPTrackerMessage extends TrackerMessage { + + private static final Logger LOG = LoggerFactory.getLogger(HTTPTrackerMessage.class); + + @CheckForNull + protected static String toString(@Nonnull Multimap params, @Nonnull String key, @CheckForNull ErrorMessage.FailureReason error) throws MessageValidationException { + LOOKUP: + { + Collection texts = params.get(key); + if (texts == null) + break LOOKUP; + if (texts.isEmpty()) + break LOOKUP; + String text = texts.iterator().next(); + if (text == null) + break LOOKUP; + return text; + } + if (error != null) + throw new MessageValidationException("Invalid parameters " + params + ": " + error.getMessage()); + return null; + } + + @CheckForNull + protected static byte[] toBytes(@Nonnull Multimap params, @Nonnull String key, @CheckForNull ErrorMessage.FailureReason error) throws MessageValidationException { + String text = toString(params, key, error); + if (text == null) + return null; + return text.getBytes(BEUtils.BYTE_ENCODING); + } + + protected static int toInt(@Nonnull Multimap params, @Nonnull String key, int unknown, @CheckForNull ErrorMessage.FailureReason error) throws MessageValidationException { + try { + String text = toString(params, key, error); + if (text == null) + return unknown; + return Integer.parseInt(text); + } catch (NumberFormatException e) { + throw new MessageValidationException(e.getMessage(), e); + } + } + + protected static long toLong(@Nonnull Multimap params, @Nonnull String key, long unknown, @CheckForNull ErrorMessage.FailureReason error) throws MessageValidationException { + try { + String text = toString(params, key, error); + if (text == null) + return unknown; + return Long.parseLong(text); + } catch (NumberFormatException e) { + throw new MessageValidationException(e.getMessage(), e); + } + } + + protected static boolean toBoolean(@Nonnull Multimap params, @Nonnull String key) throws MessageValidationException { + return toInt(params, key, 0, null) != 0; + } +} diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPAnnounceRequestMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPAnnounceRequestMessage.java new file mode 100644 index 000000000..3fe49dcde --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPAnnounceRequestMessage.java @@ -0,0 +1,190 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.udp; + +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import com.turn.ttorrent.protocol.TorrentUtils; +import io.netty.buffer.ByteBuf; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import javax.annotation.Nonnull; + +/** + * The announce request message for the UDP tracker protocol. + * + * @author mpetazzoni + */ +public class UDPAnnounceRequestMessage + extends UDPTrackerMessage.UDPTrackerRequestMessage + implements TrackerMessage.AnnounceRequestMessage { + + @Nonnull + private static byte[] getIp4Address(InetSocketAddress peerAddress) { + InetAddress address = peerAddress.getAddress(); + if (address == null) + return new byte[4]; + byte[] ip = address.getAddress(); + if (ip.length != 4) + throw new IllegalArgumentException("Cannot express in UDP: " + peerAddress); + return ip; + } + private static final int UDP_ANNOUNCE_REQUEST_MESSAGE_SIZE = 98; + private byte[] infoHash; + private byte[] peerId; + private InetSocketAddress peerAddress; + private long downloaded; + private long uploaded; + private long left; + private AnnounceEvent event; + private int numWant; + private int key; + + public UDPAnnounceRequestMessage() { + super(Type.ANNOUNCE_REQUEST); + + /* + if (infoHash.length != 20 || peerId.length != 20) { + throw new IllegalArgumentException(); + } + + if (!(ip instanceof Inet4Address)) { + throw new IllegalArgumentException("Only IPv4 addresses are " + + "supported by the UDP tracer protocol!"); + } + */ + } + + public UDPAnnounceRequestMessage( + long connectionId, int transactionId, + byte[] infoHash, + byte[] peerId, InetSocketAddress peerAddress, + long downloaded, long uploaded, long left, + AnnounceEvent event, int numWant, int key) { + this(); + + getIp4Address(peerAddress); + + setConnectionId(connectionId); + setTransactionId(transactionId); + this.infoHash = infoHash; + this.peerId = peerId; + this.peerAddress = peerAddress; + this.downloaded = downloaded; + this.uploaded = uploaded; + this.left = left; + this.event = event; + this.numWant = numWant; + this.key = key; + } + + @Override + public byte[] getInfoHash() { + return this.infoHash; + } + + @Override + public String getHexInfoHash() { + return TorrentUtils.toHex(this.infoHash); + } + + @Override + public byte[] getPeerId() { + return peerId; + } + + // @Override + public InetSocketAddress getPeerAddress() { + return peerAddress; + } + + @Override + public long getUploaded() { + return this.uploaded; + } + + @Override + public long getDownloaded() { + return this.downloaded; + } + + @Override + public long getLeft() { + return this.left; + } + + @Override + public AnnounceEvent getEvent() { + return this.event; + } + + @Override + public int getNumWant() { + return this.numWant; + } + + public int getKey() { + return this.key; + } + + @Override + public void fromWire(ByteBuf in) throws MessageValidationException { + _fromWire(in, UDP_ANNOUNCE_REQUEST_MESSAGE_SIZE); + + infoHash = new byte[20]; + in.readBytes(infoHash); + peerId = new byte[20]; + in.readBytes(peerId); + + downloaded = in.readLong(); + uploaded = in.readLong(); + left = in.readLong(); + + event = AnnounceEvent.getById(in.readInt()); + if (event == null) + throw new MessageValidationException("Invalid event type in announce request!"); + + InetAddress address; + try { + byte[] ipBytes = new byte[4]; + in.readBytes(ipBytes); + address = InetAddress.getByAddress(ipBytes); + } catch (UnknownHostException e) { + throw new MessageValidationException("Invalid IP address in announce request!", e); + } + + key = in.readInt(); + numWant = in.readInt(); + int port = in.readShort() & 0xFFFF; + + peerAddress = new InetSocketAddress(address, port); + } + + @Override + public void toWire(ByteBuf out) { + _toWire(out); + out.writeBytes(infoHash); + out.writeBytes(getPeerId()); + out.writeLong(downloaded); + out.writeLong(uploaded); + out.writeLong(left); + out.writeInt(event.getId()); + out.writeBytes(getIp4Address(peerAddress)); + out.writeInt(key); + out.writeInt(numWant); + out.writeShort(peerAddress.getPort()); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPAnnounceResponseMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPAnnounceResponseMessage.java new file mode 100644 index 000000000..dcc42538a --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPAnnounceResponseMessage.java @@ -0,0 +1,113 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.udp; + +import com.turn.ttorrent.protocol.tracker.Peer; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import io.netty.buffer.ByteBuf; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * The announce response message for the UDP tracker protocol. + * + * @author mpetazzoni + */ +public class UDPAnnounceResponseMessage + extends UDPTrackerMessage.UDPTrackerResponseMessage + implements TrackerMessage.AnnounceResponseMessage { + + private static final int UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE = 20; + private int interval; + private int complete; + private int incomplete; + private final List peers = new ArrayList(); + + private UDPAnnounceResponseMessage() { + super(Type.ANNOUNCE_REQUEST); + } + + @Override + public int getInterval() { + return this.interval; + } + + @Override + public int getComplete() { + return this.complete; + } + + @Override + public int getIncomplete() { + return this.incomplete; + } + + @Override + public Collection getPeers() { + return this.peers; + } + + @Override + public void fromWire(ByteBuf in) throws MessageValidationException { + if (in.readableBytes() < UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE + || (in.readableBytes() - UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE) % 6 != 0) { + throw new MessageValidationException("Invalid announce response message size " + in.readableBytes()); + } + _fromWire(in, -1); + + interval = in.readInt(); + incomplete = in.readInt(); + complete = in.readInt(); + + peers.clear(); + while (in.readableBytes() > 0) { + try { + byte[] ipBytes = new byte[4]; + in.readBytes(ipBytes); + InetAddress ip = InetAddress.getByAddress(ipBytes); + int port = in.readShort() & 0xFFFF; + peers.add(new Peer(new InetSocketAddress(ip, port), null)); + } catch (UnknownHostException uhe) { + throw new MessageValidationException( + "Invalid IP address in announce request!"); + } + } + } + + @Override + public void toWire(ByteBuf out) { + _toWire(out); + out.writeInt(interval); + + /** + * Leechers (incomplete) are first, before seeders (complete) in the packet. + */ + out.writeInt(incomplete); + out.writeInt(complete); + + for (Peer peer : peers) { + byte[] ip = peer.getIpBytes(); + if (ip == null || ip.length != 4) + continue; + out.writeBytes(ip); + out.writeShort((short) peer.getPort()); + } + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPConnectRequestMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPConnectRequestMessage.java new file mode 100644 index 000000000..27a5935cf --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPConnectRequestMessage.java @@ -0,0 +1,53 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.udp; + + +import io.netty.buffer.ByteBuf; + +/** + * The connection request message for the UDP tracker protocol. + * + * @author mpetazzoni + */ +public class UDPConnectRequestMessage + extends UDPTrackerMessage.UDPTrackerRequestMessage { + + private static final int UDP_CONNECT_REQUEST_MESSAGE_SIZE = 16; + private static final long UDP_CONNECT_REQUEST_MAGIC = 0x41727101980L; + + public UDPConnectRequestMessage() { + super(Type.CONNECT_REQUEST); + setConnectionId(UDP_CONNECT_REQUEST_MAGIC); + } + + public UDPConnectRequestMessage(int transactionId) { + this(); + setTransactionId(transactionId); + } + + @Override + public void fromWire(ByteBuf in) throws MessageValidationException { + _fromWire(in, UDP_CONNECT_REQUEST_MESSAGE_SIZE); + if (getConnectionId() != UDP_CONNECT_REQUEST_MAGIC) + throw new MessageValidationException("Packet contained bad ConnectionId: " + this); + } + + @Override + public void toWire(ByteBuf out) { + _toWire(out); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPConnectResponseMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPConnectResponseMessage.java new file mode 100644 index 000000000..94b9cbe45 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPConnectResponseMessage.java @@ -0,0 +1,55 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.udp; + + +import io.netty.buffer.ByteBuf; + +/** + * The connection response message for the UDP tracker protocol. + * + * @author mpetazzoni + */ +public class UDPConnectResponseMessage + extends UDPTrackerMessage.UDPTrackerResponseMessage { + + private static final int UDP_CONNECT_RESPONSE_MESSAGE_SIZE = 16; + private long connectionId; + + public UDPConnectResponseMessage() { + super(Type.CONNECT_RESPONSE); + } + + public long getConnectionId() { + return this.connectionId; + } + + public void setConnectionId(long connectionId) { + this.connectionId = connectionId; + } + + @Override + public void fromWire(ByteBuf in) throws MessageValidationException { + _fromWire(in, UDP_CONNECT_RESPONSE_MESSAGE_SIZE); + setConnectionId(in.readLong()); + } + + @Override + public void toWire(ByteBuf out) { + _toWire(out); + out.writeLong(connectionId); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPTrackerErrorMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPTrackerErrorMessage.java new file mode 100644 index 000000000..9fc6bc118 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPTrackerErrorMessage.java @@ -0,0 +1,59 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.udp; + +import com.turn.ttorrent.protocol.bcodec.BEUtils; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import io.netty.buffer.ByteBuf; + +/** + * The error message for the UDP tracker protocol. + * + * @author mpetazzoni + */ +public class UDPTrackerErrorMessage + extends UDPTrackerMessage.UDPTrackerResponseMessage + implements TrackerMessage.ErrorMessage { + + private static final int UDP_TRACKER_ERROR_MIN_MESSAGE_SIZE = 8; + private String reason; + + private UDPTrackerErrorMessage() { + super(Type.ERROR); + } + + @Override + public String getReason() { + return this.reason; + } + + @Override + public void fromWire(ByteBuf in) throws MessageValidationException { + if (in.readableBytes() < UDP_TRACKER_ERROR_MIN_MESSAGE_SIZE) + throw new MessageValidationException("Invalid tracker error message size " + in.readableBytes()); + _fromWire(in, -1); + + byte[] reasonBytes = new byte[in.readableBytes()]; + in.readBytes(reasonBytes); + reason = new String(reasonBytes, BEUtils.BYTE_ENCODING); + } + + @Override + public void toWire(ByteBuf out) { + _toWire(out); + out.writeBytes(reason.getBytes(BEUtils.BYTE_ENCODING)); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPTrackerMessage.java b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPTrackerMessage.java new file mode 100644 index 000000000..1d8cbb150 --- /dev/null +++ b/ttorrent-protocol/src/main/java/com/turn/ttorrent/protocol/tracker/udp/UDPTrackerMessage.java @@ -0,0 +1,200 @@ +/** + * 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. + */ +package com.turn.ttorrent.protocol.tracker.udp; + +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import io.netty.buffer.ByteBuf; +import javax.annotation.CheckForSigned; +import javax.annotation.Nonnull; + +/** + * Base class for UDP tracker messages. + * + * @author mpetazzoni + */ +public abstract class UDPTrackerMessage extends TrackerMessage { + + /** + * Message type. + */ + public enum Type { + + UNKNOWN(-1), + CONNECT_REQUEST(0), + CONNECT_RESPONSE(0), + ANNOUNCE_REQUEST(1), + ANNOUNCE_RESPONSE(1), + SCRAPE_REQUEST(2), + SCRAPE_RESPONSE(2), + ERROR(3); + // This is the ActionId for the UDP protocol. Do not screw with it. + private final int id; + + Type(int id) { + this.id = id; + } + + public int getId() { + return this.id; + } + }; + private final Type type; + private int transactionId; + + private UDPTrackerMessage(Type type) { + this.type = type; + } + + /** + * Returns the type of this tracker message. + */ + @Nonnull + public Type getType() { + return type; + } + + public int getActionId() { + return getType().getId(); + } + + public int getTransactionId() { + return transactionId; + } + + public void setTransactionId(int transactionId) { + this.transactionId = transactionId; + } + + public abstract void fromWire(@Nonnull ByteBuf in) + throws MessageValidationException; + + public abstract void toWire(@Nonnull ByteBuf out); + + protected void _fromWire(@Nonnull ByteBuf in, @CheckForSigned int length) + throws MessageValidationException { + if (length != -1) + if (in.readableBytes() != length) + throw new MessageValidationException("Packet data had bad length: " + in.readableBytes() + "; expected " + length); + } + + public static abstract class UDPTrackerRequestMessage + extends UDPTrackerMessage { + + private static final int UDP_MIN_REQUEST_PACKET_SIZE = 16; + private long connectionId; + + protected UDPTrackerRequestMessage(@Nonnull Type type) { + super(type); + } + + public long getConnectionId() { + return connectionId; + } + + public void setConnectionId(long connectionId) { + this.connectionId = connectionId; + } + + protected void _toWire(@Nonnull ByteBuf out) { + out.writeLong(getConnectionId()); + out.writeInt(getActionId()); + out.writeInt(getTransactionId()); + } + + @Override + protected void _fromWire(@Nonnull ByteBuf in, @CheckForSigned int length) + throws MessageValidationException { + super._fromWire(in, length); + setConnectionId(in.readLong()); + int actionId = in.readInt(); + if (actionId != getActionId()) + throw new MessageValidationException("Packet contained bad ActionId: " + this); + setTransactionId(in.readInt()); + } + + public static UDPTrackerRequestMessage parse(ByteBuf data) + throws MessageValidationException { + if (data.readableBytes() < UDP_MIN_REQUEST_PACKET_SIZE) { + throw new MessageValidationException("Invalid packet size!"); + } + + /** + * UDP request packets always start with the connection ID (8 bytes), + * followed by the action (4 bytes). Extract the action code + * accordingly. + */ + int action = data.getInt(8); + + if (action == Type.CONNECT_REQUEST.getId()) { + return UDPConnectRequestMessage.parse(data); + } else if (action == Type.ANNOUNCE_REQUEST.getId()) { + return UDPAnnounceRequestMessage.parse(data); + } + + throw new MessageValidationException("Unknown UDP tracker " + + "request message!"); + } + }; + + public static abstract class UDPTrackerResponseMessage + extends UDPTrackerMessage { + + private static final int UDP_MIN_RESPONSE_PACKET_SIZE = 8; + + protected UDPTrackerResponseMessage(Type type) { + super(type); + } + + @Override + protected void _fromWire(@Nonnull ByteBuf in, @CheckForSigned int length) + throws MessageValidationException { + super._fromWire(in, length); + int actionId = in.readInt(); + if (actionId != getActionId()) + throw new MessageValidationException("Packet contained bad ActionId: " + this); + setTransactionId(in.readInt()); + } + + protected void _toWire(@Nonnull ByteBuf out) { + out.writeInt(getActionId()); + out.writeInt(getTransactionId()); + } + + public static UDPTrackerResponseMessage parse(ByteBuf data) + throws MessageValidationException { + if (data.readableBytes() < UDP_MIN_RESPONSE_PACKET_SIZE) { + throw new MessageValidationException("Invalid packet size!"); + } + + /** + * UDP response packets always start with the action (4 bytes), so + * we can extract it immediately. + */ + int action = data.getInt(0); + + if (action == Type.CONNECT_RESPONSE.getId()) { + return UDPConnectResponseMessage.parse(data); + } else if (action == Type.ANNOUNCE_RESPONSE.getId()) { + return UDPAnnounceResponseMessage.parse(data); + } else if (action == Type.ERROR.getId()) { + return UDPTrackerErrorMessage.parse(data); + } + + throw new MessageValidationException("Unknown UDP tracker " + + "response message!"); + } + }; +} diff --git a/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/PatternByteSource.java b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/PatternByteSource.java new file mode 100644 index 000000000..051d8628f --- /dev/null +++ b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/PatternByteSource.java @@ -0,0 +1,32 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.test; + +import com.google.common.io.ByteSource; +import java.io.IOException; +import java.io.InputStream; + +/** + * + * @author shevek + */ +public class PatternByteSource extends ByteSource { + + private final long size; + + public PatternByteSource(long size) { + this.size = size; + } + + @Override + public long size() throws IOException { + return size; + } + + @Override + public InputStream openStream() throws IOException { + return new PatternInputStream(size); + } +} diff --git a/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/PatternInputStream.java b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/PatternInputStream.java new file mode 100644 index 000000000..364c33f74 --- /dev/null +++ b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/PatternInputStream.java @@ -0,0 +1,37 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.test; + +import org.apache.commons.io.input.NullInputStream; + +/** + * + * @author shevek + */ +public class PatternInputStream extends NullInputStream { + + private long offset; + + public PatternInputStream(long size) { + super(size); + } + + @Override + protected int processByte() { + try { + long word = offset >> 3; + int index = (int) (offset & 0x7); + return (int) (word >>> (Long.SIZE - (Byte.SIZE * index))); + } finally { + offset++; + } + } + + @Override + protected void processBytes(byte[] bytes, int offset, int length) { + for (int i = 0; i < length; i++) + bytes[offset + i] = (byte) processByte(); + } +} diff --git a/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/RandomInputStream.java b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/RandomInputStream.java new file mode 100644 index 000000000..1f0e3ebc1 --- /dev/null +++ b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/RandomInputStream.java @@ -0,0 +1,37 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.test; + +import java.util.Random; +import org.apache.commons.io.input.NullInputStream; + +/** + * + * @author shevek + */ +public class RandomInputStream extends NullInputStream { + + private final Random r = new Random(1234); + + public RandomInputStream(long size) { + super(size); + } + + @Override + protected int processByte() { + return r.nextInt() & 0xFF; + } + + @Override + protected void processBytes(byte[] bytes, int offset, int length) { + if (offset == 0 && length == bytes.length) { + r.nextBytes(bytes); + } else { + byte[] tmp = new byte[length]; + r.nextBytes(tmp); + System.arraycopy(tmp, 0, bytes, offset, length); + } + } +} diff --git a/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/TestPeerIdentityProvider.java b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/TestPeerIdentityProvider.java new file mode 100644 index 000000000..4ed342c7b --- /dev/null +++ b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/TestPeerIdentityProvider.java @@ -0,0 +1,26 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.test; + +import com.google.common.base.Charsets; +import com.turn.ttorrent.protocol.PeerIdentityProvider; +import com.turn.ttorrent.protocol.TorrentUtils; + +/** + * + * @author shevek + */ +public class TestPeerIdentityProvider implements PeerIdentityProvider { + + @Override + public byte[] getLocalPeerId() { + return getClass().getSimpleName().getBytes(Charsets.ISO_8859_1); + } + + @Override + public String getLocalPeerName() { + return TorrentUtils.toText(getLocalPeerId()); + } +} diff --git a/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/TorrentTestUtils.java b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/TorrentTestUtils.java new file mode 100644 index 000000000..22954684c --- /dev/null +++ b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/test/TorrentTestUtils.java @@ -0,0 +1,72 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.test; + +import com.google.common.io.Files; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.torrent.TorrentCreator; +import io.netty.util.ResourceLeakDetector; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class TorrentTestUtils { + + private static final Logger LOG = LoggerFactory.getLogger(TorrentTestUtils.class); + + static { + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); + } + public static final String FILENAME = "torrent-data-file"; + private static File ROOT; + + @Nonnull + public static synchronized File newTorrentRoot() + throws IOException { + if (ROOT != null) + return ROOT; + File buildDir = new File("build/tmp"); + FileUtils.forceMkdir(buildDir); + File rootDir = File.createTempFile("ttorrent", ".tmp", buildDir); + FileUtils.forceDeleteOnExit(rootDir); + FileUtils.forceDelete(rootDir); + FileUtils.forceMkdir(rootDir); + ROOT = rootDir; + return rootDir; + } + + @Nonnull + public static File newTorrentDir(@Nonnull String name) + throws IOException { + File torrentDir = new File(newTorrentRoot(), name); + if (torrentDir.exists()) + FileUtils.forceDelete(torrentDir); + FileUtils.forceMkdir(torrentDir); + return torrentDir; + } + + @Nonnull + public static TorrentCreator newTorrentCreator(@Nonnull File dir, @Nonnegative final long size) + throws IOException, InterruptedException { + File file = new File(dir, FILENAME); + Files.asByteSink(file).writeFrom(new PatternInputStream(size)); + return new TorrentCreator(file); + } + + @Nonnull + public static Torrent newTorrent(@Nonnull File dir, @Nonnegative long size) + throws IOException, InterruptedException, URISyntaxException { + return newTorrentCreator(dir, size).create(); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/torrent/TorrentCreatorTest.java b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/torrent/TorrentCreatorTest.java new file mode 100644 index 000000000..37e66498e --- /dev/null +++ b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/torrent/TorrentCreatorTest.java @@ -0,0 +1,77 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.torrent; + +import com.google.common.io.Files; +import com.google.common.math.LongMath; +import com.turn.ttorrent.protocol.test.PatternInputStream; +import com.turn.ttorrent.protocol.test.TorrentTestUtils; +import java.io.File; +import java.math.RoundingMode; +import java.util.Random; +import org.junit.After; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class TorrentCreatorTest { + + private static final Logger LOG = LoggerFactory.getLogger(TorrentCreatorTest.class); + + @After + public void tearDown() throws Exception { + for (int i = 0; i < 4; i++) { + Thread.sleep(50); + System.gc(); + } + } + + @Test + public void testCreatorSingle() throws Exception { + File dir = TorrentTestUtils.newTorrentDir("single"); + TorrentCreator creator = TorrentTestUtils.newTorrentCreator(dir, 193723193); + creator.create(); + } + + @Test + public void testCreatorMultiple() throws Exception { + final long length = 109372319; + File dir = TorrentTestUtils.newTorrentDir("multiple"); + for (int i = 0; i < 4; i++) { + File file = new File(dir, "file-" + i); + Files.asByteSink(file).writeFrom(new PatternInputStream(length)); + } + TorrentCreator creator = new TorrentCreator(dir); + Torrent torrent = creator.create(); + + long pieceCount = LongMath.divide(length * 4, creator.getPieceLength(), RoundingMode.CEILING); + assertEquals(pieceCount, torrent.getPieceCount()); + + // TODO: Assert that the hash came out right. + } + + @Test + public void testFuzz() throws Exception { + Random r = new Random(); + File dir = TorrentTestUtils.newTorrentDir("fuzz"); + long total = 0L; + for (int i = 0; i < 10; i++) { + File file = new File(dir, "file-" + i); + Files.asByteSink(file).writeFrom(new PatternInputStream(r.nextInt(17 + i * 187))); + total += file.length(); + + TorrentCreator creator = new TorrentCreator(dir); + creator.setPieceLength(64); + Torrent torrent = creator.create(); + assertEquals(LongMath.divide(total, creator.getPieceLength(), RoundingMode.CEILING), + torrent.getPieceCount()); + } + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/tracker/InetAddressComparatorTest.java b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/tracker/InetAddressComparatorTest.java new file mode 100644 index 000000000..36b44d67c --- /dev/null +++ b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/tracker/InetAddressComparatorTest.java @@ -0,0 +1,38 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.tracker; + +import com.turn.ttorrent.protocol.tracker.InetAddressComparator; +import com.google.common.net.InetAddresses; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +// import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class InetAddressComparatorTest { + + private static final Logger LOG = LoggerFactory.getLogger(InetAddressComparatorTest.class); + + @Test + public void testComparator() { + List addresses = new ArrayList(); + addresses.add(InetAddresses.forString("1.2.3.4")); + addresses.add(InetAddresses.forString("0.0.0.0")); + addresses.add(InetAddresses.forString("::1")); + addresses.add(InetAddresses.forString("fe80::a6")); + Collections.shuffle(addresses); + LOG.info("In: " + addresses); + Collections.sort(addresses, new InetAddressComparator()); + LOG.info("Out: " + addresses); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceRequestMessageTest.java b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceRequestMessageTest.java new file mode 100644 index 000000000..26a2011e4 --- /dev/null +++ b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceRequestMessageTest.java @@ -0,0 +1,69 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.tracker.http; + +import com.google.common.collect.Multimap; +import com.turn.ttorrent.protocol.tracker.Peer; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import com.turn.ttorrent.tracker.TrackerUtils; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import javax.annotation.Nonnull; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class HTTPAnnounceRequestMessageTest { + + private static final Logger LOG = LoggerFactory.getLogger(HTTPAnnounceRequestMessageTest.class); + + private HTTPAnnounceRequestMessage test(@Nonnull HTTPAnnounceRequestMessage in) throws Exception { + LOG.info("In: " + in); + URI tracker = URI.create("http://localhost:3128/announce"); + URI request = in.toURI(tracker); + LOG.info("Request: " + request); + Multimap params = TrackerUtils.parseQuery(request.getRawQuery()); + // for (Map.Entry e : parser.entrySet()) LOG.info(e.getKey() + " -> " + e.getValue()); + HTTPAnnounceRequestMessage out = HTTPAnnounceRequestMessage.fromParams(params); + LOG.info("Out: " + out); + return out; + } + private Random random = new Random(); + + @Nonnull + private HTTPAnnounceRequestMessage newRequest(List addresses) { + byte[] infoHash = new byte[20]; + random.nextBytes(infoHash); + byte[] peerId = new byte[Peer.PEER_ID_LENGTH]; + random.nextBytes(peerId); + return new HTTPAnnounceRequestMessage(infoHash, peerId, addresses, + 1, 2, 3, true, true, TrackerMessage.AnnounceEvent.NONE, 50); + } + + private void test(@Nonnull InetSocketAddress... addresses) throws Exception { + HTTPAnnounceRequestMessage message = test(newRequest(Arrays.asList(addresses))); + assertEquals(Arrays.asList(addresses), message.getPeerAddresses()); + } + + @Test + public void testRequest() throws Exception { + test(new InetSocketAddress(123)); + test(new InetSocketAddress("1.2.3.4", 123)); + test(new InetSocketAddress("1.2.3.4", 123), new InetSocketAddress("2.3.4.5", 123)); + test( + new InetSocketAddress("fe80::3e97:eff:fe67:5809", 6882), + new InetSocketAddress("fe80::3e97:eff:fe67:5808", 6882), + new InetSocketAddress("fe80::3e97:eff:fe67:5807", 6882) + ); + } +} \ No newline at end of file diff --git a/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceResponseMessageTest.java b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceResponseMessageTest.java new file mode 100644 index 000000000..c102e3bef --- /dev/null +++ b/ttorrent-protocol/src/test/java/com/turn/ttorrent/protocol/tracker/http/HTTPAnnounceResponseMessageTest.java @@ -0,0 +1,128 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.protocol.tracker.http; + +import com.turn.ttorrent.protocol.tracker.http.HTTPAnnounceResponseMessage; +import com.turn.ttorrent.protocol.bcodec.BEValue; +import com.turn.ttorrent.protocol.tracker.Peer; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import javax.annotation.Nonnull; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class HTTPAnnounceResponseMessageTest { + + private static final Logger LOG = LoggerFactory.getLogger(HTTPAnnounceResponseMessageTest.class); + + private void test(HTTPAnnounceResponseMessage in, boolean compact, boolean noPeerIds) throws Exception { + LOG.info("in=" + in + ", compact=" + compact + ", noPeerIds=" + noPeerIds); + Map value = in.toBEValue(compact, noPeerIds); + HTTPAnnounceResponseMessage out = HTTPAnnounceResponseMessage.fromBEValue(value); + LOG.info("out=" + out); + } + + private void test(HTTPAnnounceResponseMessage message) throws Exception { + test(message, true, true); + test(message, true, false); + test(message, false, true); + test(message, false, false); + } + private Random random = new Random(); + + @Nonnull + private Peer newPeer(int len, boolean hasPeerId) throws UnknownHostException { + byte[] address = new byte[len]; + random.nextBytes(address); + int port = random.nextInt() & 0xFFFF; + byte[] peerId = null; + if (hasPeerId) { + peerId = new byte[Peer.PEER_ID_LENGTH]; + random.nextBytes(peerId); + } + InetAddress inaddr = InetAddress.getByAddress(address); + return new Peer(new InetSocketAddress(inaddr, port), peerId); + } + + @Test + public void testSerialization() throws Exception { + InetAddress clientAddress = InetAddress.getLoopbackAddress(); + + EMPTY: + { + List peers = new ArrayList(); + test(new HTTPAnnounceResponseMessage( + clientAddress, + 1, 2, 3, + peers)); + } + + IP4_WITHOUT: + { + List peers = new ArrayList(); + peers.add(newPeer(4, false)); + test(new HTTPAnnounceResponseMessage( + clientAddress, + 1, 2, 3, + peers)); + } + + IP4_WITH: + { + List peers = new ArrayList(); + peers.add(newPeer(4, true)); + test(new HTTPAnnounceResponseMessage( + clientAddress, + 1, 2, 3, + peers)); + } + + IP6_WITHOUT: + { + List peers = new ArrayList(); + peers.add(newPeer(16, false)); + test(new HTTPAnnounceResponseMessage( + clientAddress, + 1, 2, 3, + peers)); + } + + IP6_WITH: + { + List peers = new ArrayList(); + peers.add(newPeer(16, true)); + test(new HTTPAnnounceResponseMessage( + clientAddress, + 1, 2, 3, + peers)); + } + + ALL: + { + List peers = new ArrayList(); + for (int i = 0; i < 3; i++) { + peers.add(newPeer(4, false)); + peers.add(newPeer(4, true)); + peers.add(newPeer(16, false)); + peers.add(newPeer(16, true)); + } + test(new HTTPAnnounceResponseMessage( + clientAddress, + 1, 2, 3, + peers)); + } + + } +} \ No newline at end of file diff --git a/src/main/java/com/turn/ttorrent/client/announce/AnnounceException.java b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/AnnounceException.java similarity index 95% rename from src/main/java/com/turn/ttorrent/client/announce/AnnounceException.java rename to ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/AnnounceException.java index e075cb1fd..6ca2de547 100644 --- a/src/main/java/com/turn/ttorrent/client/announce/AnnounceException.java +++ b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/AnnounceException.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.turn.ttorrent.client.announce; +package com.turn.ttorrent.tracker.client; /** diff --git a/src/main/java/com/turn/ttorrent/client/announce/AnnounceResponseListener.java b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/AnnounceResponseListener.java similarity index 55% rename from src/main/java/com/turn/ttorrent/client/announce/AnnounceResponseListener.java rename to ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/AnnounceResponseListener.java index 850819920..e8dbe9131 100644 --- a/src/main/java/com/turn/ttorrent/client/announce/AnnounceResponseListener.java +++ b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/AnnounceResponseListener.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.turn.ttorrent.client.announce; +package com.turn.ttorrent.tracker.client; -import com.turn.ttorrent.common.Peer; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import java.net.URI; import java.util.EventListener; -import java.util.List; - +import javax.annotation.Nonnull; /** * EventListener interface for objects that want to receive tracker responses. @@ -28,21 +28,10 @@ */ public interface AnnounceResponseListener extends EventListener { - /** - * Handle an announce response event. - * - * @param interval The announce interval requested by the tracker. - * @param complete The number of seeders on this torrent. - * @param incomplete The number of leechers on this torrent. - */ - public void handleAnnounceResponse(int interval, int complete, - int incomplete); + /** + * Handle an announce response event. + */ + public void handleAnnounceResponse(@Nonnull URI tracker, @Nonnull TrackerMessage.AnnounceEvent event, @Nonnull TrackerMessage.AnnounceResponseMessage response); - /** - * Handle the discovery of new peers. - * - * @param peers The list of peers discovered (from the announce response or - * any other means like DHT/PEX, etc.). - */ - public void handleDiscoveredPeers(List peers); -} + public void handleAnnounceFailed(@Nonnull URI tracker, @Nonnull TrackerMessage.AnnounceEvent event, @Nonnull String reason); +} \ No newline at end of file diff --git a/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/HTTPTrackerClient.java b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/HTTPTrackerClient.java new file mode 100644 index 000000000..a9b406bb1 --- /dev/null +++ b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/HTTPTrackerClient.java @@ -0,0 +1,229 @@ +/* + * Copyright 2014 shevek. + * + * 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.tracker.client; + +import com.google.common.io.Closeables; +import com.turn.ttorrent.protocol.bcodec.BEValue; +import com.turn.ttorrent.protocol.bcodec.InvalidBEncodingException; +import com.turn.ttorrent.protocol.bcodec.StreamBDecoder; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import com.turn.ttorrent.protocol.tracker.TrackerMessage.AnnounceRequestMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPAnnounceRequestMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPAnnounceResponseMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPTrackerErrorMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPTrackerMessage; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.CheckForSigned; +import javax.annotation.Nonnull; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Announcer for HTTP trackers. + * + * @author shevek + */ +public class HTTPTrackerClient extends TrackerClient { + + protected static final Logger LOG = LoggerFactory.getLogger(HTTPTrackerClient.class); + private CloseableHttpAsyncClient httpclient; + + public HTTPTrackerClient(@Nonnull PeerAddressProvider peerAddressProvider) { + super(peerAddressProvider); + } + + @Override + public void start() throws Exception { + super.start(); + RequestConfig requestConfig = RequestConfig.custom() + .setSocketTimeout(3000) + .setConnectTimeout(3000) + .build(); + httpclient = HttpAsyncClients.custom() + .setDefaultRequestConfig(requestConfig) + .build(); + httpclient.start(); + } + + /** + * This method is not thread safe. + * + * However, it is guarded by the lock in com.turn.ttorrent.client.Client, so + * it's never called in a manner which would be unsafe. + * + * @throws Exception + */ + @Override + public void stop() throws Exception { + if (httpclient != null) + httpclient.close(); + httpclient = null; + super.stop(); + } + + private class HttpResponseCallback implements FutureCallback { + + private final AnnounceResponseListener listener; + private final HttpUriRequest request; + private final URI tracker; + private final TrackerMessage.AnnounceEvent event; + + public HttpResponseCallback(AnnounceResponseListener listener, HttpUriRequest request, URI tracker, TrackerMessage.AnnounceEvent event) { + this.listener = listener; + this.request = request; + this.tracker = tracker; + this.event = event; + } + + @Override + public void completed(HttpResponse response) { + if (LOG.isTraceEnabled()) + LOG.trace("Completed: {} -> {}", request.getRequestLine(), response.getStatusLine()); + try { + HTTPTrackerMessage message = toMessage(response, -1); + if (message != null) + handleTrackerAnnounceResponse(listener, tracker, event, message, false); + } catch (Exception e) { + if (LOG.isDebugEnabled()) + LOG.debug("Failed to handle announce response", e); + failed(e); + } + } + + @Override + public void failed(Exception e) { + // This error wasn't necessarily reported elsewhere. + if (LOG.isDebugEnabled()) + LOG.debug("Failed: {} -> {}", request.getRequestLine(), e); + // TODO: Pass failure back to TrackerHandler. + // LOG.trace("Failed: " + request.getRequestLine(), e); + listener.handleAnnounceFailed(tracker, event, "HTTP failed: " + e); + } + + @Override + public void cancelled() { + LOG.trace("Cancelled: {}", request.getRequestLine()); + } + } + + /** + * Build, send and process a tracker announce request. + * + *

+ * This function first builds an announce request for the specified event + * with all the required parameters. Then, the request is made to the + * tracker and the response analyzed. + *

+ * + *

+ * All registered {@link AnnounceResponseListener} objects are then fired + * with the decoded payload. + *

+ * + * @param event The announce event type (can be AnnounceEvent.NONE for + * periodic updates). + * @param inhibitEvents Prevent event listeners from being notified. + */ + @Override + public void announce( + AnnounceResponseListener listener, + TorrentMetadataProvider torrent, + URI tracker, + TrackerMessage.AnnounceEvent event, + boolean inhibitEvents) throws AnnounceException { + LOG.info("Announcing{} to tracker {} with {}U/{}D/{}L bytes...", + new Object[]{ + TrackerClient.formatAnnounceEvent(event), + tracker, + torrent.getUploaded(), + torrent.getDownloaded(), + torrent.getLeft() + }); + + try { + HTTPAnnounceRequestMessage message = + new HTTPAnnounceRequestMessage( + torrent.getInfoHash(), + getLocalPeerId(), getLocalPeerAddresses(), + torrent.getUploaded(), torrent.getDownloaded(), torrent.getLeft(), + true, false, event, AnnounceRequestMessage.DEFAULT_NUM_WANT); + URI target = message.toURI(tracker); + HttpGet request = new HttpGet(target); + HttpResponseCallback callback = new HttpResponseCallback(listener, request, tracker, event); + httpclient.execute(request, callback); + } catch (URISyntaxException mue) { + throw new AnnounceException("Invalid announce URI (" + + mue.getMessage() + ")", mue); + } catch (IOException ioe) { + throw new AnnounceException("Error building announce request (" + + ioe.getMessage() + ")", ioe); + } + } + + // The tracker may return valid BEncoded data even if the status code + // was not a 2xx code. On the other hand, it may return garbage. + @CheckForNull + public static HTTPTrackerMessage toMessage(@Nonnull HttpResponse response, @CheckForSigned long maxContentLength) + throws IOException { + HttpEntity entity = response.getEntity(); + if (entity == null) // Usually 204-no-content, etc. + return null; + try { + if (maxContentLength >= 0) { + long contentLength = entity.getContentLength(); + if (contentLength >= 0) + if (contentLength > maxContentLength) + throw new IllegalArgumentException("ContentLength was too big: " + contentLength + ": " + response); + } + + InputStream in = entity.getContent(); + if (in == null) + return null; + try { + StreamBDecoder decoder = new StreamBDecoder(in); + BEValue value = decoder.bdecodeMap(); + Map params = value.getMap(); + // TODO: "warning message" + if (params.containsKey("failure reason")) + return HTTPTrackerErrorMessage.fromBEValue(params); + else + return HTTPAnnounceResponseMessage.fromBEValue(params); + } finally { + Closeables.close(in, true); + } + } catch (InvalidBEncodingException e) { + throw new IOException("Failed to parse response " + response, e); + } catch (TrackerMessage.MessageValidationException e) { + throw new IOException("Failed to parse response " + response, e); + } finally { + EntityUtils.consumeQuietly(entity); + } + } +} diff --git a/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/PeerAddressProvider.java b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/PeerAddressProvider.java new file mode 100644 index 000000000..16e03c604 --- /dev/null +++ b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/PeerAddressProvider.java @@ -0,0 +1,20 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker.client; + +import com.turn.ttorrent.protocol.PeerIdentityProvider; +import java.net.SocketAddress; +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public interface PeerAddressProvider extends PeerIdentityProvider { + + @Nonnull + public Set getLocalAddresses(); +} diff --git a/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/TorrentMetadataProvider.java b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/TorrentMetadataProvider.java new file mode 100644 index 000000000..c36f918c7 --- /dev/null +++ b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/TorrentMetadataProvider.java @@ -0,0 +1,45 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker.client; + +import java.net.URI; +import java.util.List; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public interface TorrentMetadataProvider { + + public enum State { + + WAITING, + VALIDATING, + SHARING, + SEEDING, + ERROR, + DONE; + }; + + @Nonnull + public byte[] getInfoHash(); + + @Nonnull + public State getState(); + + @Nonnull + public List> getAnnounceList(); + + @Nonnegative + public long getUploaded(); + + @Nonnegative + public long getDownloaded(); + + @Nonnegative + public long getLeft(); +} diff --git a/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/TrackerClient.java b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/TrackerClient.java new file mode 100644 index 000000000..2bbe940bc --- /dev/null +++ b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/TrackerClient.java @@ -0,0 +1,138 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.tracker.client; + +import com.turn.ttorrent.protocol.tracker.InetSocketAddressComparator; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import com.turn.ttorrent.protocol.tracker.TrackerMessage.*; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; + +public abstract class TrackerClient { + + private final PeerAddressProvider peerAddressProvider; + + public TrackerClient(@Nonnull PeerAddressProvider peerAddressProvider) { + this.peerAddressProvider = peerAddressProvider; + } + + @Nonnull + protected byte[] getLocalPeerId() { + return peerAddressProvider.getLocalPeerId(); + } + + /** Returns a fresh, concrete list. */ + @Nonnull + protected List getLocalPeerAddresses() { + List out = new ArrayList(); + for (SocketAddress address : peerAddressProvider.getLocalAddresses()) + if (address instanceof InetSocketAddress) + out.add((InetSocketAddress) address); + Collections.sort(out, InetSocketAddressComparator.INSTANCE); + return out; + } + + /** + * Build, send and process a tracker announce request. + * + *

+ * This function first builds an announce request for the specified event + * with all the required parameters. Then, the request is made to the + * tracker and the response analyzed. + *

+ * + *

+ * All registered {@link AnnounceResponseListener} objects are then fired + * with the decoded payload. + *

+ * + * @param event The announce event type (can be AnnounceEvent.NONE for + * periodic updates). + * @param inhibitEvent Prevent event listeners from being notified. + */ + public abstract void announce( + AnnounceResponseListener listener, + TorrentMetadataProvider torrent, + URI tracker, + TrackerMessage.AnnounceEvent event, + boolean inhibitEvents) throws AnnounceException; + + public void start() throws Exception { + } + + /** + * Close any opened announce connection. + * + *

+ * This method is called by Client.stop() to make sure all connections + * are correctly closed when the announce thread is asked to stop. + *

+ */ + public void stop() throws Exception { + // Do nothing by default, but can be overloaded. + } + + /** + * Formats an announce event into a usable string. + */ + public static String formatAnnounceEvent(TrackerMessage.AnnounceEvent event) { + return TrackerMessage.AnnounceEvent.NONE.equals(event) + ? "" + : String.format(" %s", event.name()); + } + + /** + * Handle the announce response from the tracker. + * + *

+ * Analyzes the response from the tracker and acts on it. If the response + * is an error, it is logged. Otherwise, the announce response is used + * to fire the corresponding announce and peer events to all announce + * listeners. + *

+ * + * @param message The incoming {@link TrackerMessage}. + * @param inhibitEvents Whether or not to prevent events from being fired. + */ + protected void handleTrackerAnnounceResponse( + @Nonnull AnnounceResponseListener listener, + @Nonnull URI tracker, + @Nonnull TrackerMessage.AnnounceEvent event, + @Nonnull TrackerMessage message, // AnnounceResponse or Error + boolean inhibitEvents) throws AnnounceException { + if (message instanceof ErrorMessage) { + ErrorMessage error = (ErrorMessage) message; + throw new AnnounceException(tracker + "(" + event + "): " + error.getReason()); + } + + if (!(message instanceof AnnounceResponseMessage)) { + throw new AnnounceException("Unexpected tracker message " + message); + } + + if (inhibitEvents) { + return; + } + + AnnounceResponseMessage response = (AnnounceResponseMessage) message; + listener.handleAnnounceResponse(tracker, event, response); + } +} \ No newline at end of file diff --git a/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/UDPTrackerClient.removed b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/UDPTrackerClient.removed new file mode 100644 index 000000000..88346a396 --- /dev/null +++ b/ttorrent-tracker-client/src/main/java/com/turn/ttorrent/tracker/client/UDPTrackerClient.removed @@ -0,0 +1,334 @@ +/** + * 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. + */ +package com.turn.ttorrent.client.announce; + +import com.turn.ttorrent.client.ClientEnvironment; +import com.turn.ttorrent.client.SharedTorrent; +import com.turn.ttorrent.common.Peer; +import com.turn.ttorrent.common.Torrent; +import com.turn.ttorrent.common.protocol.TrackerMessage; +import com.turn.ttorrent.common.protocol.TrackerMessage.*; +import com.turn.ttorrent.common.protocol.udp.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.util.Arrays; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Announcer for UDP trackers. + * + *

+ * The UDP tracker protocol requires a two-step announce request/response + * exchange where the peer is first required to establish a "connection" + * with the tracker by sending a connection request message and retreiving + * a connection ID from the tracker to use in the following announce + * request messages (valid for 2 minutes). + *

+ * + *

+ * It also contains a backing-off retry mechanism (on a 15*2^n seconds + * scheme), in which if the announce request times-out for more than the + * connection ID validity period, another connection request/response + * exchange must be made before attempting to retransmit the announce + * request. + *

+ * + * @author mpetazzoni + */ +public class UDPTrackerClient extends TrackerClient { + + protected static final Logger logger = + LoggerFactory.getLogger(UDPTrackerClient.class); + /** + * Back-off timeout uses 15 * 2 ^ n formula. + */ + private static final int UDP_BASE_TIMEOUT_SECONDS = 15; + /** + * We don't try more than 8 times (3840 seconds, as per the formula defined + * for the backing-off timeout. + * + * @see #UDP_BASE_TIMEOUT_SECONDS + */ + private static final int UDP_MAX_TRIES = 8; + /** + * For STOPPED announce event, we don't want to be bothered with waiting + * that long. We'll try once and bail-out early. + */ + private static final int UDP_MAX_TRIES_ON_STOPPED = 1; + /** + * Maximum UDP packet size expected, in bytes. + * + * The biggest packet in the exchange is the announce response, which in 20 + * bytes + 6 bytes per peer. Common numWant is 50, so 20 + 6 * 50 = 320. + * With headroom, we'll ask for 512 bytes. + */ + private static final int UDP_PACKET_LENGTH = 512; + + private enum State { + + CONNECT_REQUEST, + ANNOUNCE_REQUEST; + // TODO: Failed (or similar) state. + }; + + private static class UDPTorrentId { + + private final byte[] infoHash; + + public UDPTorrentId(@Nonnull byte[] infoHash) { + this.infoHash = infoHash; + } + + @Override + public int hashCode() { + return Arrays.hashCode(infoHash); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (null == obj) + return false; + if (!getClass().equals(obj.getClass())) + return false; + UDPTorrentId other = (UDPTorrentId) obj; + return Arrays.equals(infoHash, other.infoHash); + } + + @Override + public String toString() { + return Torrent.byteArrayToHexString(infoHash); + } + } + + private static class UDPTorrentState { + + private AnnounceRequestMessage.RequestEvent event; + } + + private class UDPTrackerState { + + private final InetSocketAddress address; + private final SharedTorrent torrent; + private State state = State.CONNECT_REQUEST; + private long connectionId; + private long connectionExpiration; + private int attempt = 0; + private int transactionId; + private final Map torrents = new HashMap(); + + public UDPTrackerState(InetSocketAddress address, SharedTorrent torrent) { + this.address = address; + this.torrent = torrent; + } + + public void setEvent(@Nonnull AnnounceRequestMessage.RequestEvent event) { + this.event = event; + this.attempt = 0; + } + + public int getTimeout() { + return UDP_BASE_TIMEOUT_SECONDS * (int) Math.pow(2, attempt); + } + + public void run() { + if (attempt++ > getMaxAttempts(event)) { + logger.error("Timeout while announcing" + + formatAnnounceEvent(event) + " to tracker!"); + announcements.remove(address); + return; + } + + transactionId = environment.getRandom().nextInt(); + announcements.put(address, this); + + // Immediately decide if we can send the announce request + // directly or not. For this, we need a valid, non-expired + // connection ID. + if (connectionExpiration > System.currentTimeMillis()) + state = State.ANNOUNCE_REQUEST; + else + logger.debug("Announce connection ID expired, " + + "reconnecting with tracker..."); + + switch (state) { + case CONNECT_REQUEST: + send(address, new UDPConnectRequestMessage(transactionId)); + break; + + case ANNOUNCE_REQUEST: + send(address, new UDPAnnounceRequestMessage( + connectionId, + transactionId, + torrent.getInfoHash(), + peer.getPeerId(), + torrent.getDownloaded(), + torrent.getUploaded(), + torrent.getLeft(), + event, + peer.getAddress().getAddress(), + 0, + TrackerMessage.AnnounceRequestMessage.DEFAULT_NUM_WANT, + peer.getAddress().getPort())); + break; + + default: + throw new IllegalStateException("Invalid announce state!"); + } + + // Mark for retry. + } + + private void recv(UDPTrackerMessage.UDPTrackerResponseMessage message) { + if (message.getTransactionId() != transactionId) { + // Probably ignore silently: It's a delayed message after a timeout. + logger.warn("Transaction id mismatch: " + message + " for " + this); + return; + } + + if (message instanceof UDPConnectResponseMessage) { + UDPConnectResponseMessage response = (UDPConnectResponseMessage) message; + connectionId = response.getConnectionId(); + connectionExpiration = System.currentTimeMillis() + 60; + run(); + } else if (message instanceof UDPAnnounceResponseMessage) { + handleTrackerAnnounceResponse(null, message, false); + } else if (message instanceof UDPTrackerErrorMessage) { + UDPTrackerErrorMessage response = (UDPTrackerErrorMessage) message; + logger.warn("Announce failed: " + response.getReason()); + } else { + logger.error("Unknown UDP message " + message); + } + } + } + private final ConcurrentMap announcements = new ConcurrentHashMap(); + private DatagramChannel channel; + + /** + * + * @param torrent + */ + public UDPTrackerClient(ClientEnvironment environment, Peer peer) { + super(environment, peer); + + InetSocketAddress address = peer.getAddress(); + if (!(address.getAddress() instanceof Inet4Address)) + throw new UnsupportedOperationException("UDP announce only supports IPv4, see http://bittorrent.org/beps/bep_0015.html#ipv6"); + } + + @Override + public void start() throws Exception { + super.start(); + channel = DatagramChannel.open(); + channel.configureBlocking(false); + channel.bind(peer.getAddress()); + } + + /** + * Close this announce connection. + */ + @Override + public void stop() throws Exception { + if (channel != null && channel.isOpen()) + channel.close(); + channel = null; + super.stop(); + } + + private static int getMaxAttempts(AnnounceRequestMessage.RequestEvent event) { + return AnnounceRequestMessage.RequestEvent.STOPPED.equals(event) + ? UDP_MAX_TRIES_ON_STOPPED + : UDP_MAX_TRIES; + } + + @Override + public void announce( + AnnounceResponseListener listener, + SharedTorrent torrent, URI tracker, + AnnounceRequestMessage.RequestEvent event, boolean inhibitEvents) throws AnnounceException { + logger.info("Announcing{} to tracker with {}U/{}D/{}L bytes...", + new Object[]{ + formatAnnounceEvent(event), + torrent.getUploaded(), + torrent.getDownloaded(), + torrent.getLeft() + }); + + InetSocketAddress address = new InetSocketAddress(tracker.getHost(), tracker.getPort()); + UDPAnnounceName name = new UDPAnnounceName(address, torrent.getInfoHash()); + UDPTrackerState state = new UDPTrackerState(address, torrent); + { + UDPTrackerState state0 = announcements.putIfAbsent(name, state); + if (state0 != null) + state = state0; + } + + state.setEvent(event); + state.run(); + } + + /** + * Send a UDP packet to the tracker. + * + * @param data The {@link ByteBuffer} to send in a datagram packet to the + * tracker. + */ + private void send(InetSocketAddress destination, UDPTrackerMessage message) { + try { + ByteBuf buf = Unpooled.buffer(UDP_PACKET_LENGTH); + message.toWire(buf); + if (channel.send(buf.nioBuffer(), destination) < buf.readableBytes()) + logger.warn("Sent short datagram to tracker at {}", destination); + } catch (IOException ioe) { + logger.warn("Error sending datagram packet to tracker at {}: {}.", + destination, ioe.getMessage()); + } + } + + /** + * Receive a UDP packet from the tracker. + * + * @param attempt The attempt number, used to calculate the timeout for the + * receive operation. + * @return Returns a {@link ByteBuffer} containing the packet data. + */ + private void recv() + throws IOException, MessageValidationException { + ByteBuffer buffer = ByteBuffer.allocate(UDP_PACKET_LENGTH); + SocketAddress address = channel.receive(buffer); + ByteBuf buf = Unpooled.wrappedBuffer(buffer); + UDPTrackerMessage.UDPTrackerResponseMessage message = UDPTrackerMessage.UDPTrackerResponseMessage.parse(buf); + UDPAnnounceId id = new UDPAnnounceId(address, message.getTransactionId()); + UDPTrackerState state = announcements.remove(id); + state.recv(message); + } +} diff --git a/ttorrent-tracker-client/src/test/java/com/turn/ttorrent/tracker/client/HTTPTrackerClientTest.java b/ttorrent-tracker-client/src/test/java/com/turn/ttorrent/tracker/client/HTTPTrackerClientTest.java new file mode 100644 index 000000000..47aad6c1d --- /dev/null +++ b/ttorrent-tracker-client/src/test/java/com/turn/ttorrent/tracker/client/HTTPTrackerClientTest.java @@ -0,0 +1,79 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker.client; + +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import com.turn.ttorrent.tracker.client.test.TestPeerAddressProvider; +import com.turn.ttorrent.tracker.client.test.TestTorrentMetadataProvider; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.*; + +/** + * + * @author shevek + */ +public class HTTPTrackerClientTest { + + private static final Logger LOG = LoggerFactory.getLogger(HTTPTrackerClientTest.class); + private final byte[] infoHash = new byte[]{1, 2, 3, 4, 5, 6, 7, 8}; + private final List> uris = new ArrayList>(); + private final TorrentMetadataProvider metadataProvider = new TestTorrentMetadataProvider(infoHash, uris); + + private static class ResponseListener implements AnnounceResponseListener { + + private final CountDownLatch latch = new CountDownLatch(1); + private final AtomicInteger failed = new AtomicInteger(0); + + @Override + public void handleAnnounceResponse(URI tracker, TrackerMessage.AnnounceEvent event, TrackerMessage.AnnounceResponseMessage response) { + LOG.info("Response: " + tracker + ": " + event + " -> " + response); + latch.countDown(); + } + + @Override + public void handleAnnounceFailed(URI tracker, TrackerMessage.AnnounceEvent event, String reason) { + LOG.info("Failed: " + tracker + ": " + event + " -> " + reason); + failed.getAndIncrement(); + latch.countDown(); + } + } + + @Test + public void testConnectionRefused() throws Exception { + HTTPTrackerClient client = new HTTPTrackerClient(new TestPeerAddressProvider()); + client.start(); + try { + ResponseListener listener = new ResponseListener(); + URI uri = new URI("http://localhost:12/announce"); // Connection refused. + client.announce(listener, metadataProvider, uri, TrackerMessage.AnnounceEvent.STARTED, true); + listener.latch.await(); + assertEquals(1, listener.failed.get()); + } finally { + client.stop(); + } + } + + @Test + public void testConnectionTimeout() throws Exception { + HTTPTrackerClient client = new HTTPTrackerClient(new TestPeerAddressProvider()); + client.start(); + try { + ResponseListener listener = new ResponseListener(); + URI uri = new URI("http://1.1.1.1:80/announce"); // Connection timeout, I hope. + client.announce(listener, metadataProvider, uri, TrackerMessage.AnnounceEvent.STARTED, true); + listener.latch.await(); + assertEquals(1, listener.failed.get()); + } finally { + client.stop(); + } + } +} \ No newline at end of file diff --git a/ttorrent-tracker-client/src/test/java/com/turn/ttorrent/tracker/client/test/TestPeerAddressProvider.java b/ttorrent-tracker-client/src/test/java/com/turn/ttorrent/tracker/client/test/TestPeerAddressProvider.java new file mode 100644 index 000000000..dfe2b5d91 --- /dev/null +++ b/ttorrent-tracker-client/src/test/java/com/turn/ttorrent/tracker/client/test/TestPeerAddressProvider.java @@ -0,0 +1,35 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker.client.test; + +import com.google.common.base.Charsets; +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.tracker.client.PeerAddressProvider; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Collections; +import java.util.Set; + +/** + * + * @author shevek + */ +public class TestPeerAddressProvider /* extends TestPeerIdentityProvider */ implements PeerAddressProvider { + + @Override + public byte[] getLocalPeerId() { + return getClass().getSimpleName().getBytes(Charsets.ISO_8859_1); + } + + @Override + public String getLocalPeerName() { + return TorrentUtils.toText(getLocalPeerId()); + } + + @Override + public Set getLocalAddresses() { + return Collections.singleton(new InetSocketAddress(17)); + } +} diff --git a/ttorrent-tracker-client/src/test/java/com/turn/ttorrent/tracker/client/test/TestTorrentMetadataProvider.java b/ttorrent-tracker-client/src/test/java/com/turn/ttorrent/tracker/client/test/TestTorrentMetadataProvider.java new file mode 100644 index 000000000..8f68a6c3d --- /dev/null +++ b/ttorrent-tracker-client/src/test/java/com/turn/ttorrent/tracker/client/test/TestTorrentMetadataProvider.java @@ -0,0 +1,61 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker.client.test; + +import com.turn.ttorrent.tracker.client.TorrentMetadataProvider; +import com.turn.ttorrent.protocol.TorrentUtils; +import java.net.URI; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public class TestTorrentMetadataProvider implements TorrentMetadataProvider { + + private final byte[] infoHash; + private final List> uris; + + public TestTorrentMetadataProvider(@Nonnull byte[] infoHash, @Nonnull List> uris) { + this.infoHash = infoHash; + this.uris = uris; + } + + @Override + public byte[] getInfoHash() { + return infoHash; + } + + @Override + public State getState() { + return State.SHARING; + } + + @Override + public List> getAnnounceList() { + return uris; + } + + @Override + public long getUploaded() { + return 0L; + } + + @Override + public long getDownloaded() { + return 0L; + } + + @Override + public long getLeft() { + return 0L; + } + + @Override + public String toString() { + return "TestTorrentMetadataProvider(" + TorrentUtils.toHex(getInfoHash()) + ")"; + } +} diff --git a/ttorrent-tracker-servlet/src/main/java/com/turn/ttorrent/tracker/servlet/ServletTrackerService.java b/ttorrent-tracker-servlet/src/main/java/com/turn/ttorrent/tracker/servlet/ServletTrackerService.java new file mode 100644 index 000000000..4e549eeb6 --- /dev/null +++ b/ttorrent-tracker-servlet/src/main/java/com/turn/ttorrent/tracker/servlet/ServletTrackerService.java @@ -0,0 +1,34 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker.servlet; + +import com.google.common.collect.Multimap; +import com.google.common.net.InetAddresses; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPAnnounceRequestMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPTrackerMessage; +import com.turn.ttorrent.tracker.TrackerService; +import com.turn.ttorrent.tracker.TrackerUtils; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import javax.annotation.Nonnull; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author shevek + */ +public class ServletTrackerService extends TrackerService { + + public void process(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response) throws IOException, TrackerMessage.MessageValidationException { + Multimap params = TrackerUtils.parseQuery(request.getParameterMap()); + HTTPAnnounceRequestMessage announceRequest = HTTPAnnounceRequestMessage.fromParams(params); + InetAddress clientAddress = InetAddresses.forString(request.getRemoteAddr()); + HTTPTrackerMessage announceResponse = super.process(new InetSocketAddress(clientAddress, request.getRemotePort()), announceRequest); + } +} diff --git a/ttorrent-tracker-servlet/src/main/java/com/turn/ttorrent/tracker/servlet/TrackerServlet.java b/ttorrent-tracker-servlet/src/main/java/com/turn/ttorrent/tracker/servlet/TrackerServlet.java new file mode 100644 index 000000000..a902ac421 --- /dev/null +++ b/ttorrent-tracker-servlet/src/main/java/com/turn/ttorrent/tracker/servlet/TrackerServlet.java @@ -0,0 +1,35 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker.servlet; + +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import java.io.IOException; +import javax.annotation.Nonnull; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author shevek + */ +public class TrackerServlet extends HttpServlet { + + private final ServletTrackerService service; + + public TrackerServlet(@Nonnull ServletTrackerService service) { + this.service = service; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + try { + service.process(request, response); + } catch (TrackerMessage.MessageValidationException e) { + throw new ServletException(e); + } + } +} diff --git a/ttorrent-tracker-simple/build.gradle b/ttorrent-tracker-simple/build.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/ttorrent-tracker-simple/src/main/java/com/turn/ttorrent/tracker/simple/SimpleTracker.java b/ttorrent-tracker-simple/src/main/java/com/turn/ttorrent/tracker/simple/SimpleTracker.java new file mode 100644 index 000000000..c44666b45 --- /dev/null +++ b/ttorrent-tracker-simple/src/main/java/com/turn/ttorrent/tracker/simple/SimpleTracker.java @@ -0,0 +1,266 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.tracker.simple; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.net.InetAddresses; + +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.tracker.TrackedTorrent; +import com.turn.ttorrent.tracker.TrackedTorrentRegistry; +import com.turn.ttorrent.tracker.TrackerMetrics; +import com.turn.ttorrent.tracker.TrackerUtils; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.annotation.CheckForSigned; +import javax.annotation.Nonnull; +import org.simpleframework.http.core.ContainerServer; +import org.simpleframework.transport.Server; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * BitTorrent tracker. + * + *

+ * The tracker usually listens on port 6969 (the standard BitTorrent tracker + * port). Torrents must be registered directly to this tracker with the + * {@link #addTorrent(TrackedTorrent torrent)} method. + *

+ * + * @author mpetazzoni + */ +public class SimpleTracker { + + private static final Logger LOG = LoggerFactory.getLogger(SimpleTracker.class); + + public static class Listener { + + private Connection connection; + private SocketAddress connectionAddress; + } + private final SimpleTrackerService service; + private MetricRegistry metricRegistry = new MetricRegistry(); + private TrackerMetrics metrics; + private final ConcurrentMap listeners = new ConcurrentHashMap(); + private final Object lock = new Object(); + + /** + * Create a new BitTorrent tracker listening at the given address. + * + * You will want to call {@link #addAddress(InetSocketAddress)} + * to add listen addresses. + */ + public SimpleTracker(@Nonnull String version) { + this.service = new SimpleTrackerService(version); + } + + public SimpleTracker() { + this(TrackerUtils.DEFAULT_VERSION_STRING); + } + + /** + * Create a new BitTorrent tracker listening at the given address. + * + * @param address The address to bind to. + * @throws IOException Throws an IOException if the tracker + * cannot be initialized. + */ + public SimpleTracker(@Nonnull InetSocketAddress address) throws IOException { + this(); + addListenAddress(address); + } + + /** + * Create a new BitTorrent tracker listening at the given address on the + * default port. + * + * @param address The address to bind to. + * @throws IOException Throws an IOException if the tracker + * cannot be initialized. + */ + public SimpleTracker(@Nonnull InetAddress address) throws IOException { + this(new InetSocketAddress(address, TrackerUtils.DEFAULT_TRACKER_PORT)); + } + + /** Call this BEFORE you start the tracker. */ + // TODO: As per Client, allow adding after tracker is started. + public void addListenAddress(@Nonnull InetSocketAddress address) { + listeners.put(address, new Listener()); + } + + public void addListenInterface(@Nonnull NetworkInterface iface, @CheckForSigned int port) { + for (InetAddress ifaddr : Collections.list(iface.getInetAddresses())) { + addListenAddress(new InetSocketAddress(ifaddr, port)); + } + } + + private void add(@Nonnull Set out, @Nonnull InetSocketAddress in) throws SocketException { + // LOG.info("Looking for addresses from " + in); + InetAddress inaddr = in.getAddress(); + for (InetAddress address : TorrentUtils.getSpecificAddresses(inaddr)) + out.add(new InetSocketAddress(address, in.getPort())); + } + + @Nonnull + public Iterable getListenAddresses() throws SocketException { + Set out = new HashSet(); + for (Map.Entry e : listeners.entrySet()) { + SocketAddress a = e.getValue().connectionAddress; // In case we bound ephemerally. + if (a instanceof InetSocketAddress) // Also ensures != null. + add(out, (InetSocketAddress) a); + else + add(out, e.getKey()); + } + return out; + } + + @Nonnull + public TrackedTorrentRegistry getTorrents() { + return service.getTorrents(); + } + + @Nonnull + public TrackedTorrent addTorrent(@Nonnull TrackedTorrent torrent) { + return getTorrents().announce(torrent); + } + + @Nonnull + public TrackedTorrent addTorrent(@Nonnull Torrent torrent) { + return getTorrents().announce(torrent); + } + + /** + * Returns the full announce URL served by this tracker. + * + *

+ * This has the form http://ip:port/announce. + *

+ */ + @Nonnull + public List getAnnounceUrls() throws SocketException { + List out = new ArrayList(); + for (InetSocketAddress address : getListenAddresses()) { + try { + out.add(new URL("http", + InetAddresses.toUriString(address.getAddress()), + address.getPort(), + TrackerUtils.DEFAULT_ANNOUNCE_URL)); + } catch (MalformedURLException e) { + LOG.error("Could not build tracker URL from " + address, e); + } + } + return out; + } + + @Nonnull + public List getAnnounceUris() throws SocketException { + List out = new ArrayList(); + for (URL url : getAnnounceUrls()) { + try { + out.add(url.toURI()); + } catch (URISyntaxException e) { + LOG.error("Could not build tracker URI from " + url, e); + } + } + return out; + } + + @Nonnull + public MetricRegistry getMetricRegistry() { + return metricRegistry; + } + + public void setMetricRegistry(@Nonnull MetricRegistry metricRegistry) { + this.metricRegistry = metricRegistry; + } + + /** + * Start the tracker thread. + */ + public void start() throws IOException { + LOG.info("Starting tracker service on {}", getAnnounceUris()); + synchronized (lock) { + if (this.metrics == null) + this.metrics = new TrackerMetrics(getMetricRegistry(), String.valueOf(System.identityHashCode(this))); + this.service.start(metrics); + + for (Map.Entry e : listeners.entrySet()) { + Listener listener = e.getValue(); + if (listener.connection == null) { + // Creates a thread via: SocketConnection + // -> ListenerManager -> Listener + // -> DirectReactor -> ActionDistributor -> Daemon + Server server = new ContainerServer(service); + listener.connection = new SocketConnection(server); + listener.connectionAddress = listener.connection.connect(e.getKey()); + } + } + + } + LOG.info("Started tracker service on {}", getAnnounceUris()); + } + + /** + * Stop the tracker. + * + *

+ * This effectively closes the listening HTTP connection to terminate + * the service, and interrupts the peer collector thread as well. + *

+ */ + public void stop() throws IOException { + LOG.info("Stopping tracker service on {}", getAnnounceUris()); + synchronized (lock) { + + for (Map.Entry e : listeners.entrySet()) { + Listener listener = e.getValue(); + if (listener.connection != null) { + listener.connection.close(); + listener.connection = null; + } + listener.connectionAddress = null; + } + + this.service.stop(); + + if (this.metrics != null) { + this.metrics.shutdown(); + this.metrics = null; + } + } + LOG.info("Stopped tracker service on {}", getAnnounceUris()); + } +} \ No newline at end of file diff --git a/ttorrent-tracker-simple/src/main/java/com/turn/ttorrent/tracker/simple/SimpleTrackerMain.java b/ttorrent-tracker-simple/src/main/java/com/turn/ttorrent/tracker/simple/SimpleTrackerMain.java new file mode 100644 index 000000000..eef10e924 --- /dev/null +++ b/ttorrent-tracker-simple/src/main/java/com/turn/ttorrent/tracker/simple/SimpleTrackerMain.java @@ -0,0 +1,100 @@ +/** + * 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.tracker.simple; + +import com.codahale.metrics.JmxReporter; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.tracker.TrackerUtils; +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; + +import java.net.URISyntaxException; +import java.util.Arrays; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Command-line entry-point for starting a {@link SimpleTracker} + */ +public class SimpleTrackerMain { + + private static final Logger logger = LoggerFactory.getLogger(SimpleTrackerMain.class); + + private static void addAnnounce(SimpleTracker tracker, File file, int depth) throws IOException, URISyntaxException { + if (file.isFile()) { + logger.info("Loading torrent from " + file.getName()); + if (file.getName().endsWith(".torrent")) + tracker.addTorrent(new Torrent(file)); + return; + } + if (depth > 3) + return; + for (File child : file.listFiles()) + addAnnounce(tracker, child, depth + 1); + } + + /** + * Main function to start a tracker. + */ + public static void main(String[] args) throws Exception { + // BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-25t] %-5p: %m%n"))); + + OptionParser parser = new OptionParser(); + OptionSpec helpOption = parser.accepts("help") + .forHelp(); + OptionSpec fileOption = parser.acceptsAll(Arrays.asList("file", "directory")) + .withRequiredArg().ofType(File.class) + .required() + .describedAs("The list of torrent directories or files to announce."); + OptionSpec portOption = parser.accepts("port") + .withRequiredArg().ofType(Integer.class) + .defaultsTo(TrackerUtils.DEFAULT_TRACKER_PORT) + .required() + .describedAs("The port to listen on."); + parser.nonOptions().ofType(File.class); + + OptionSet options = parser.parse(args); + // List otherArgs = options.nonOptionArguments(); + + // Display help and exit if requested + if (options.has(helpOption)) { + System.out.println("Usage: " + SimpleTrackerMain.class.getSimpleName() + " []"); + parser.printHelpOn(System.err); + System.exit(0); + } + + InetSocketAddress address = new InetSocketAddress(options.valueOf(portOption)); + SimpleTracker t = new SimpleTracker(address); + JmxReporter reporter = JmxReporter.forRegistry(t.getMetricRegistry()).build(); + + try { + for (File file : options.valuesOf(fileOption)) + addAnnounce(t, file, 0); + logger.info("Starting tracker with {} announced torrents...", t.getTorrents().size()); + t.start(); + reporter.start(); + } catch (Exception e) { + logger.error("{}", e.getMessage(), e); + System.exit(2); + } finally { + t.stop(); + } + } +} diff --git a/ttorrent-tracker-simple/src/main/java/com/turn/ttorrent/tracker/simple/SimpleTrackerService.java b/ttorrent-tracker-simple/src/main/java/com/turn/ttorrent/tracker/simple/SimpleTrackerService.java new file mode 100644 index 000000000..5ba91247e --- /dev/null +++ b/ttorrent-tracker-simple/src/main/java/com/turn/ttorrent/tracker/simple/SimpleTrackerService.java @@ -0,0 +1,240 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.tracker.simple; + +import com.turn.ttorrent.protocol.tracker.http.HTTPTrackerErrorMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPAnnounceRequestMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPAnnounceResponseMessage; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.io.Closeables; +import com.turn.ttorrent.protocol.bcodec.BytesBEncoder; +import com.turn.ttorrent.protocol.tracker.TrackerMessage.AnnounceRequestMessage; +import com.turn.ttorrent.protocol.tracker.TrackerMessage.MessageValidationException; +import com.turn.ttorrent.protocol.tracker.http.HTTPTrackerMessage; +import com.turn.ttorrent.tracker.TrackedTorrentRegistry; +import com.turn.ttorrent.tracker.TrackerService; +import com.turn.ttorrent.tracker.TrackerUtils; +import java.io.IOException; +import java.io.OutputStream; +import javax.annotation.Nonnull; +import org.simpleframework.http.core.Container; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tracker service to serve the tracker's announce requests. + * + *

+ * It only serves announce requests on /announce, and only serves torrents the + * {@link SimpleTracker} it serves knows about. + *

+ * + *

+ * The list of torrents {@link #torrents} is a map of torrent hashes to their + * corresponding Torrent objects, and is maintained by the {@link SimpleTracker} this + * service is part of. The TrackerService only has a reference to this map, and + * does not modify it. + *

+ * + * @author mpetazzoni + * @see BitTorrent protocol specification + */ +public class SimpleTrackerService extends TrackerService implements Container { + + private static final Logger LOG = LoggerFactory.getLogger(SimpleTrackerService.class); + private final String version; + + /** + * Create a new TrackerService serving the given torrents. + * + * @param torrents The torrents this TrackerService should serve requests + * for. + */ + public SimpleTrackerService(@Nonnull String version, @Nonnull TrackedTorrentRegistry torrents) { + super(torrents); + this.version = version; + } + + public SimpleTrackerService(@Nonnull String version) { + this.version = version; + } + + /** + * Handle the incoming request on the tracker service. + * + *

+ * This makes sure the request is made to the tracker's announce URL, and + * delegates handling of the request to the process() method after + * preparing the response object. + *

+ * + * @param request The incoming HTTP request. + * @param response The response object. + */ + @Override + public void handle(Request request, Response response) { + // LOG.info("Request: " + request); + try { + // Reject non-announce requests + if (!TrackerUtils.DEFAULT_ANNOUNCE_URL.equals(request.getPath().toString())) { + metrics.requestRejected.mark(); + response.setStatus(Status.NOT_FOUND); + response.close(); + return; + } + + OutputStream body = null; + try { + body = response.getOutputStream(); + this.process(request, response, body); + body.flush(); + } finally { + Closeables.close(body, true); + } + + } catch (Exception e) { + LOG.warn("Error while handling request", e); + return; + } + } + + /** + * Process the announce request. + * + *

+ * This method attempts to read and parse the incoming announce request into + * an announce request message, then creates the appropriate announce + * response message and sends it back to the client. + *

+ * + * @param request The incoming announce request. + * @param response The response object. + * @param body The validated response body output stream. + */ + private void process(Request request, Response response, OutputStream body) + throws IOException { + // Prepare the response headers. + response.setContentType("text/plain"); + response.setValue("Server", this.version); + response.setDate("Date", System.currentTimeMillis()); + + /** + * Parse the query parameters into an announce request message. + * + * We need to rely on our own query parsing function because + * SimpleHTTP's Query map will contain UTF-8 decoded parameters, which + * doesn't work well for the byte-encoded strings we expect. + */ + HTTPAnnounceRequestMessage announceRequest; + try { + announceRequest = parseRequest(request); + } catch (MessageValidationException e) { + metrics.requestParseFailed.mark(); + LOG.error("Failed to parse request", e); + this.serveError(response, Status.BAD_REQUEST, e.getMessage()); + return; + } + + if (LOG.isTraceEnabled()) + LOG.trace("Announce request is {}", announceRequest); + + HTTPTrackerMessage announceResponse = super.process(request.getClientAddress(), announceRequest); + if (announceResponse instanceof HTTPTrackerErrorMessage) { + this.serveError(response, Status.BAD_REQUEST, (HTTPTrackerErrorMessage) announceResponse); + return; + } + + // Output the answer + try { + BytesBEncoder encoder = new BytesBEncoder(); + encoder.bencode(((HTTPAnnounceResponseMessage) announceResponse).toBEValue(announceRequest)); + body.write(encoder.toByteArray()); // This is the raw network stream. + } catch (Exception e) { + LOG.error("Failed to send response", e); + this.serveError(response, Status.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + /** + * Parse the query parameters using our defined BYTE_ENCODING. + * + *

+ * Because we're expecting byte-encoded strings as query parameters, we + * can't rely on SimpleHTTP's QueryParser which uses the wrong encoding for + * the job and returns us unparsable byte data. We thus have to implement + * our own little parsing method that uses BYTE_ENCODING to decode + * parameters from the URI. + *

+ * + *

+ * Note: array parameters are not supported. If a key is present + * multiple times in the URI, the latest value prevails. We don't really + * need to implement this functionality as this never happens in the + * Tracker HTTP protocol. + *

+ * + * @param request The Request object. + * @return The {@link AnnounceRequestMessage} representing the client's + * announce request. + */ + @Nonnull + @VisibleForTesting + /* pp */ static HTTPAnnounceRequestMessage parseRequest(Request request) + throws MessageValidationException { + String uri = request.getAddress().toString(); + Iterable it = Splitter.on('?').limit(2).split(uri); + String query = Iterables.get(it, 1, null); + if (query == null) + throw new MessageValidationException("No query string."); + Multimap params = TrackerUtils.parseQuery(query); + return HTTPAnnounceRequestMessage.fromParams(params); + } + + /** + * Write a {@link HTTPTrackerErrorMessage} to the response with the given + * HTTP status code. + * + * @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 error reported by the tracker. + */ + private void serveError(Response response, Status status, HTTPTrackerErrorMessage error) throws IOException { + LOG.warn("Could not process announce request ({}) !", error.getReason()); + response.setStatus(status); + BytesBEncoder encoder = new BytesBEncoder(); + encoder.bencode(error.toBEValue()); + response.getOutputStream().write(encoder.toByteArray()); // This is the raw network stream. + } + + /** + * Write an error message to the response with the given HTTP status code. + * + * @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 error message reported by the tracker. + */ + private void serveError(Response response, Status status, String error) throws IOException { + this.serveError(response, status, new HTTPTrackerErrorMessage(error)); + } +} \ No newline at end of file diff --git a/ttorrent-tracker-simple/src/test/java/com/turn/ttorrent/tracker/simple/SimpleTrackerTest.java b/ttorrent-tracker-simple/src/test/java/com/turn/ttorrent/tracker/simple/SimpleTrackerTest.java new file mode 100644 index 000000000..02e0074dc --- /dev/null +++ b/ttorrent-tracker-simple/src/test/java/com/turn/ttorrent/tracker/simple/SimpleTrackerTest.java @@ -0,0 +1,79 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker.simple; + +import com.turn.ttorrent.tracker.TrackerUtils; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import javax.annotation.Nonnull; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class SimpleTrackerTest { + + private static final Logger LOG = LoggerFactory.getLogger(SimpleTrackerTest.class); + private static final String[] PATHS = { + "/", + "/foo", + "/announce", + "/announce?foo" + }; + + private void test(@Nonnull SimpleTracker tracker) throws Exception { + LOG.info("Before start: " + tracker.getAnnounceUris()); + tracker.start(); + try { + LOG.info("Running: " + tracker.getAnnounceUris()); + CloseableHttpClient client = HttpClientBuilder.create().build(); + for (URI uri : tracker.getAnnounceUris()) { + for (String path : PATHS) { + HttpGet request = new HttpGet(uri.resolve(path)); + CloseableHttpResponse response = client.execute(request); + LOG.info(request + " -> " + response); + response.close(); + } + } + } finally { + tracker.stop(); + } + LOG.info("Done."); + } + + private void testTracker(@Nonnull InetSocketAddress address) throws Exception { + SimpleTracker tracker = new SimpleTracker(); + tracker.addListenAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + test(tracker); + } + + @Test + public void testLoopback() throws Exception { + testTracker(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + } + + @Test + public void testPort() throws Exception { + testTracker(new InetSocketAddress(TrackerUtils.DEFAULT_TRACKER_PORT)); + } + + @Test + public void testInaddrLoopback() throws Exception { + testTracker(new InetSocketAddress(InetAddress.getLoopbackAddress(), TrackerUtils.DEFAULT_TRACKER_PORT)); + } + + @Test + public void testInaddrAny() throws Exception { + testTracker(new InetSocketAddress("0.0.0.0", TrackerUtils.DEFAULT_TRACKER_PORT)); + } +} \ No newline at end of file diff --git a/ttorrent-tracker-spring/build.gradle b/ttorrent-tracker-spring/build.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/ttorrent-tracker-spring/src/main/java/com/turn/ttorrent/tracker/spring/TrackerController.java b/ttorrent-tracker-spring/src/main/java/com/turn/ttorrent/tracker/spring/TrackerController.java new file mode 100644 index 000000000..8bba464a8 --- /dev/null +++ b/ttorrent-tracker-spring/src/main/java/com/turn/ttorrent/tracker/spring/TrackerController.java @@ -0,0 +1,37 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker.spring; + +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import com.turn.ttorrent.tracker.servlet.ServletTrackerService; +import java.io.IOException; +import javax.annotation.Nonnull; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * + * @author shevek + */ +@RequestMapping("/announce") +public class TrackerController { + + private final ServletTrackerService service; + + @Autowired + public TrackerController(@Nonnull ServletTrackerService service) { + this.service = service; + } + + @RequestMapping(value = "/") + public void request( + @Nonnull HttpServletRequest request, + @Nonnull HttpServletResponse response) throws ServletException, IOException, TrackerMessage.MessageValidationException { + service.process(request, response); + } +} diff --git a/ttorrent-tracker/build.gradle b/ttorrent-tracker/build.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedPeer.java b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedPeer.java new file mode 100644 index 000000000..92c2527bf --- /dev/null +++ b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedPeer.java @@ -0,0 +1,177 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.tracker; + +import com.google.common.base.Objects; +import com.google.common.collect.Iterables; + +import com.turn.ttorrent.protocol.TorrentUtils; +import java.net.InetSocketAddress; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A BitTorrent tracker peer. + * + *

+ * Represents a peer exchanging on a given torrent. In this implementation, + * we don't really care about the status of the peers and how much they + * have downloaded / exchanged because we are not a torrent exchange and + * don't need to keep track of what peers are doing while they're + * downloading. We only care about when they start, and when they are done. + *

+ * + *

+ * We also never expire peers automatically. Unless peers send a STOPPED + * announce request, they remain as long as the torrent object they are a + * part of. + *

+ */ +public class TrackedPeer { + + private static final Logger LOG = LoggerFactory.getLogger(TrackedPeer.class); + private final byte[] peerId; + private final Set peerAddresses = new HashSet(); + private TrackedPeerState state = TrackedPeerState.UNKNOWN; + private long uploaded = 0; + private long downloaded = 0; + private long left = 0; + private long lastAnnounce = System.currentTimeMillis(); + // We need a happen-before relationship for multiple threads. + private final Object lock = new Object(); + + /** + * Instantiate a new tracked peer for the given torrent. + * + * @param peer The remote peer. + * @param torrent The torrent this peer exchanges on. + */ + public TrackedPeer(@Nonnull byte[] peerId) { + this.peerId = peerId; + } + + @Nonnull + public byte[] getPeerId() { + return peerId; + } + + @Nonnull + public String getPeerName() { + return TorrentUtils.toText(getPeerId()); + } + + @Nonnull + public Set getPeerAddresses() { + return peerAddresses; + } + + /** + * Update this peer's state and information. + * + *

+ * Note: if the peer reports 0 bytes left to download, its state will + * be automatically be set to COMPLETED. + *

+ * + * @param torrent The torrent. This should be the same on every call. + * @param state The peer's state. + * @param uploaded Uploaded byte count, as reported by the peer. + * @param downloaded Downloaded byte count, as reported by the peer. + * @param left Left-to-download byte count, as reported by the peer. + */ + public void update(@Nonnull TrackedTorrent torrent, TrackedPeerState state, Iterable peerAddresses, long uploaded, long downloaded, long left) { + if (TrackedPeerState.STARTED.equals(state) && left == 0) + state = TrackedPeerState.COMPLETED; + + if (!state.equals(this.state)) { + if (LOG.isDebugEnabled()) + LOG.debug("Peer {} {} download of {}.", + new Object[]{ + this, + state.name().toLowerCase(), + torrent + }); + } + + synchronized (lock) { + Iterables.addAll(this.peerAddresses, peerAddresses); + this.state = state; + this.uploaded = uploaded; + this.downloaded = downloaded; + this.left = left; + this.lastAnnounce = System.currentTimeMillis(); + } + } + + /** + * Tells whether this peer has completed its download and can thus be + * considered a seeder. + */ + public boolean isCompleted() { + return TrackedPeerState.COMPLETED.equals(this.state); + } + + /** + * Returns how many bytes the peer reported it has uploaded so far. + */ + public long getUploaded() { + return this.uploaded; + } + + /** + * Returns how many bytes the peer reported it has downloaded so far. + */ + public long getDownloaded() { + return this.downloaded; + } + + /** + * Returns how many bytes the peer reported it needs to retrieve before + * its download is complete. + */ + public long getLeft() { + return this.left; + } + + /** + * Tells whether this peer has checked in with the tracker recently. + * + *

+ * Non-fresh peers are automatically terminated and collected by the + * Tracker. + *

+ * + * @param now The current time. + * @param refresh The interval after which a peer is considered stale. + */ + public boolean isFresh(long now, long refresh) { + synchronized (lock) { + return (this.lastAnnounce > 0 + && (this.lastAnnounce + refresh > now)); + } + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("name", getPeerName()) + .add("addresses", getPeerAddresses()) + .toString(); + } +} \ No newline at end of file diff --git a/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedPeerState.java b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedPeerState.java new file mode 100644 index 000000000..1ad3455b6 --- /dev/null +++ b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedPeerState.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 shevek. + * + * 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.tracker; + +/** + * Represents the state of a peer exchanging on this torrent. + * + *

+ * Peers can be in the STARTED state, meaning they have announced + * themselves to us and are eventually exchanging data with other peers. + * Note that a peer starting with a completed file will also be in the + * started state and will never notify as being in the completed state. + * This information can be inferred from the fact that the peer reports 0 + * bytes left to download. + *

+ * + *

+ * Peers enter the COMPLETED state when they announce they have entirely + * downloaded the file. As stated above, we may also elect them for this + * state if they report 0 bytes left to download. + *

+ * + *

+ * Peers enter the STOPPED state very briefly before being removed. We + * still pass them to the STOPPED state in case someone else kept a + * reference on them. + *

+ */ +public enum TrackedPeerState { + + UNKNOWN, STARTED, COMPLETED, STOPPED +} diff --git a/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java new file mode 100644 index 000000000..a668f46aa --- /dev/null +++ b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java @@ -0,0 +1,303 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.tracker; + +import com.turn.ttorrent.protocol.tracker.Peer; + +import com.turn.ttorrent.protocol.TorrentUtils; +import com.turn.ttorrent.protocol.torrent.Torrent; +import com.turn.ttorrent.protocol.tracker.TrackerMessage.AnnounceEvent; +import io.netty.util.internal.PlatformDependent; +import java.io.UnsupportedEncodingException; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentMap; + +import java.util.concurrent.TimeUnit; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tracked torrents are torrent for which we don't expect to have data files + * for. + * + *

+ * {@link TrackedTorrent} objects are used by the BitTorrent tracker to + * represent a torrent that is announced by the tracker. As such, it is not + * expected to point to any valid local data like. It also contains some + * additional information used by the tracker to keep track of which peers + * exchange on it, etc. + *

+ * + * @author mpetazzoni + */ +public class TrackedTorrent { + + private static final Logger LOG = LoggerFactory.getLogger(TrackedTorrent.class); + /** Minimum announce interval requested from peers, in seconds. */ + public static final int MIN_ANNOUNCE_INTERVAL_SECONDS = 5; + /** Default number of peers included in a tracker response. */ + private static final int DEFAULT_ANSWER_NUM_PEERS = 30; + /** Default announce interval requested from peers, in seconds. */ + private static final int DEFAULT_ANNOUNCE_INTERVAL_SECONDS = 10; + @CheckForNull + private final String name; + @Nonnull + private final String infoHash; + private long announceInterval; + /** Peers currently exchanging on this torrent. */ + private final ConcurrentMap peers = PlatformDependent.newConcurrentHashMap(); + + public TrackedTorrent(@CheckForNull String name, @Nonnull byte[] infoHash) { + this.name = name; + this.infoHash = TorrentUtils.toHex(infoHash); + setAnnounceInterval(DEFAULT_ANNOUNCE_INTERVAL_SECONDS, TimeUnit.SECONDS); + } + + public TrackedTorrent(@Nonnull Torrent torrent) { + this(torrent.getName(), torrent.getInfoHash()); + } + + @CheckForNull + public String getName() { + return name; + } + + @Nonnull + public String getHexInfoHash() { + return infoHash; + } + + /** + * Returns the map of all peers currently exchanging on this torrent. + */ + @Nonnull + public Iterable getPeers() { + return peers.values(); + } + + /** + * Retrieve a peer exchanging on this torrent. + * + * @param peerId The hexadecimal representation of the peer's ID. + */ + @CheckForNull + public TrackedPeer getPeer(@Nonnull byte[] peerId) { + return peers.get(TorrentUtils.toHex(peerId)); + } + + /** + * Add a peer exchanging on this torrent. + * + * @param peer The new Peer involved with this torrent. + */ + public void addPeer(@Nonnull TrackedPeer peer) { + this.peers.put(TorrentUtils.toHex(peer.getPeerId()), peer); + } + + /** + * Remove a peer from this torrent's swarm. + * + * @param peerId The hexadecimal representation of the peer's ID. + */ + public TrackedPeer removePeer(@Nonnull byte[] peerId) { + return peers.remove(TorrentUtils.toHex(peerId)); + } + + /** + * Count the number of seeders (peers in the COMPLETED state) on this + * torrent. + */ + public int seeders() { + int count = 0; + for (TrackedPeer peer : this.peers.values()) { + if (peer.isCompleted()) { + count++; + } + } + return count; + } + + /** + * Count the number of leechers (non-COMPLETED peers) on this torrent. + */ + public int leechers() { + int count = 0; + for (TrackedPeer peer : this.peers.values()) { + if (!peer.isCompleted()) { + count++; + } + } + return count; + } + + /** + * Returns the announce interval for this torrent, in milliseconds. + */ + @Nonnegative + public long getAnnounceInterval() { + return this.announceInterval; + } + + public long getPeerExpiryInterval() { + return getAnnounceInterval() * 2; + } + + /** + * Set the announce interval for this torrent. + * + * @param interval New announce interval, in seconds. + */ + public void setAnnounceInterval(int interval, @Nonnull TimeUnit unit) { + if (interval <= 0) { + throw new IllegalArgumentException("Invalid announce interval"); + } + + long announceInterval = unit.toMillis(interval); + if (announceInterval < 0 || announceInterval > Integer.MAX_VALUE) + throw new IllegalArgumentException("Illegal (overflow) timeunit " + announceInterval); + this.announceInterval = announceInterval; + } + + /** + * Update this torrent's swarm from an announce event. + * + *

+ * This will automatically create a new peer on a 'started' announce event, + * and remove the peer on a 'stopped' announce event. + *

+ * + * @param event The reported event. If null, means a regular + * interval announce event, as defined in the BitTorrent specification. + * @param peerId The byte-encoded peer ID. + * @param hexPeerId The hexadecimal representation of the peer's ID. + * @param ip The peer's IP address. + * @param port The peer's inbound port. + * @param uploaded The peer's reported uploaded byte count. + * @param downloaded The peer's reported downloaded byte count. + * @param left The peer's reported left to download byte count. + * @return The peer that sent us the announce request. + */ + @CheckForNull + public TrackedPeer update(AnnounceEvent event, + byte[] peerId, List peerAddresses, + long uploaded, long downloaded, long left) throws UnsupportedEncodingException { + TrackedPeerState state = TrackedPeerState.UNKNOWN; + + TrackedPeer trackedPeer; + if (AnnounceEvent.STARTED.equals(event)) { + trackedPeer = new TrackedPeer(peerId); + state = TrackedPeerState.STARTED; + this.addPeer(trackedPeer); + } else if (AnnounceEvent.STOPPED.equals(event)) { + trackedPeer = removePeer(peerId); + state = TrackedPeerState.STOPPED; + } else if (AnnounceEvent.COMPLETED.equals(event)) { + trackedPeer = getPeer(peerId); + state = TrackedPeerState.COMPLETED; + } else if (AnnounceEvent.NONE.equals(event)) { + trackedPeer = getPeer(peerId); + // TODO: There is a chance this will change COMPLETED -> STARTED + state = TrackedPeerState.STARTED; + } else { + throw new IllegalArgumentException("Unexpected announce event type!"); + } + + // This can be null if we STOPPED an unknown peer. + if (trackedPeer != null) + trackedPeer.update(this, state, peerAddresses, uploaded, downloaded, left); + return trackedPeer; + } + + /** + * Get a list of peers we can return in an announce response for this + * torrent. + * + * @param peer The peer making the request, so we can exclude it from the + * list of returned peers. + * @return A list of peers we can include in an announce response. + */ + public List getSomePeers(TrackedPeer client, int numWant) { + numWant = Math.min(numWant, DEFAULT_ANSWER_NUM_PEERS); + + // Extract answerPeers random peers + List candidates = new ArrayList(peers.values()); + Collections.shuffle(candidates); + + List out = new ArrayList(numWant); + long now = System.currentTimeMillis(); + // LOG.info("Client PeerAddress is " + client.getPeerAddress()); + for (TrackedPeer candidate : candidates) { + // LOG.info("Candidate PeerAddress is " + candidate.getPeerAddress()); + // Collect unfresh peers, and obviously don't serve them as well. + if (!candidate.isFresh(now, getPeerExpiryInterval())) { + LOG.debug("Collecting stale peer {}...", candidate.getPeerAddresses()); + peers.remove(TorrentUtils.toHex(candidate.getPeerId()), candidate); + continue; + } + + // Don't include the requesting peer in the answer. + if (Arrays.equals(client.getPeerId(), candidate.getPeerId())) { + if (!client.equals(candidate)) { + LOG.debug("Collecting superceded peer {}...", candidate); + removePeer(candidate.getPeerId()); + } + continue; + } + + for (InetSocketAddress peerAddress : candidate.getPeerAddresses()) + out.add(new Peer(peerAddress, candidate.getPeerId())); + if (out.size() >= numWant) + break; + } + + LOG.trace("Some peers are {}", out); + return out; + } + + /** + * Remove unfresh peers from this torrent. + * + *

+ * Collect and remove all non-fresh peers from this torrent. This is + * usually called by the periodic peer collector of the BitTorrent tracker. + *

+ */ + @Nonnegative + public int collectUnfreshPeers() { + long now = System.currentTimeMillis(); + int count = 0; + for (TrackedPeer peer : peers.values()) { + if (!peer.isFresh(now, getPeerExpiryInterval())) { + peers.remove(TorrentUtils.toHex(peer.getPeerId()), peer); + count++; + } + } + return count; + } + + @Override + public String toString() { + return getName() + " (" + peers.size() + " peers, interval=" + getAnnounceInterval() + ")"; + } +} \ No newline at end of file diff --git a/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedTorrentRegistry.java b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedTorrentRegistry.java new file mode 100644 index 000000000..28c859bb5 --- /dev/null +++ b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedTorrentRegistry.java @@ -0,0 +1,198 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker; + +import com.codahale.metrics.Gauge; +import com.turn.ttorrent.protocol.torrent.Torrent; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class TrackedTorrentRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(TrackedTorrentRegistry.class); + private static final int PEER_COLLECTION_FREQUENCY_SECONDS = 15; + /** The in-memory repository of torrents tracked. */ + private final ConcurrentMap torrents = new ConcurrentHashMap(); + @GuardedBy("lock") + private ScheduledExecutorService scheduler; + private final Object lock = new Object(); + + /** + * Returns the list of tracker's torrents + */ + @Nonnull + public Collection getTorrents() { + return torrents.values(); + } + + @CheckForNull + public TrackedTorrent getTorrent(String infoHash) { + return torrents.get(infoHash); + } + + @Nonnegative + public int size() { + return getTorrents().size(); + } + + /** + * Start the maintenance threads. + */ + public void start(TrackerMetrics metrics) { + + synchronized (lock) { + if (this.scheduler == null || this.scheduler.isShutdown()) { + // TODO: Set a thread timeout, nothing is time critical. + this.scheduler = new ScheduledThreadPoolExecutor(1); + this.scheduler.scheduleWithFixedDelay(new PeerCollector(), + PEER_COLLECTION_FREQUENCY_SECONDS, + PEER_COLLECTION_FREQUENCY_SECONDS, + TimeUnit.SECONDS); + } + + metrics.addGauge("torrentCount", new Gauge() { + @Override + public Integer getValue() { + return torrents.size(); + } + }); + + } + } + + /** + * Stops the maintenance threads and cleans up. + */ + public void stop() { + synchronized (lock) { + if (this.scheduler != null) { + this.scheduler.shutdownNow(); + this.scheduler = null; + } + } + } + + /** + * Announce a new torrent on this tracker. + * + *

+ * The fact that torrents must be announced here first makes this tracker a + * closed BitTorrent tracker: it will only accept clients for torrents it + * knows about, and this list of torrents is managed by the program + * instrumenting this Tracker class. + *

+ * + * @param torrent The Torrent object to start tracking. + * @return The torrent object for this torrent on this tracker. This may be + * different from the supplied Torrent object if the tracker already + * contained a torrent with the same hash. + */ + @Nonnull + public synchronized TrackedTorrent announce(@Nonnull TrackedTorrent torrent) { + TrackedTorrent existing = this.torrents.get(torrent.getHexInfoHash()); + + if (existing != null) { + LOG.warn("Tracker already announced torrent for '{}' " + + "with hash {}.", existing.getName(), existing.getHexInfoHash()); + return existing; + } + + this.torrents.put(torrent.getHexInfoHash(), torrent); + LOG.info("Registered new torrent for '{}' with hash {}.", + torrent.getName(), torrent.getHexInfoHash()); + return torrent; + } + + @Nonnull + public TrackedTorrent announce(@Nonnull Torrent torrent) { + return announce(new TrackedTorrent(torrent)); + } + + /** + * Stop announcing the given torrent. + * + * @param torrent The Torrent object to stop tracking. + */ + public void remove(@CheckForNull Torrent torrent) { + if (torrent == null) + return; + + this.torrents.remove(torrent.getHexInfoHash()); + } + + /** + * Stop announcing the given torrent after a delay. + * + * @param torrent The Torrent object to stop tracking. + * @param delay The delay, in milliseconds, before removing the torrent. + */ + public void remove(Torrent torrent, long delay, TimeUnit unit) { + if (torrent == null) + return; + + synchronized (lock) { + if (scheduler != null) + scheduler.schedule(new TorrentRemover(torrent), delay, unit); + else + remove(torrent); + } + } + + /** + * Runnable for removing a torrent from a tracker. + * + *

+ * This task can be used to stop announcing a torrent after a certain delay. + *

+ */ + private class TorrentRemover implements Runnable { + + private final Torrent torrent; + + TorrentRemover(@Nonnull Torrent torrent) { + this.torrent = torrent; + } + + @Override + public void run() { + remove(torrent); + } + } + + /** + * The unfresh peer collector. + * + *

+ * Every PEER_COLLECTION_FREQUENCY_SECONDS, this runnable will collect + * unfresh peers from all announced torrents. + *

+ */ + private class PeerCollector implements Runnable { + + @Override + public void run() { + int count = 0; + for (TrackedTorrent torrent : torrents.values()) { + count += torrent.collectUnfreshPeers(); + } + if (count > 0) + LOG.debug("Collected {} stale peers.", count); + } + } +} diff --git a/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerMetrics.java b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerMetrics.java new file mode 100644 index 000000000..3ea2ebc0f --- /dev/null +++ b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerMetrics.java @@ -0,0 +1,60 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; + +/** + * + * @author shevek + */ +public class TrackerMetrics { + + private final MetricRegistry metricRegistry; + private final Object trackerId; + private final List names = new ArrayList(); + + public TrackerMetrics(@Nonnull MetricRegistry metricRegistry, Object trackerId) { + this.metricRegistry = metricRegistry; + this.trackerId = trackerId; + } + + @Nonnull + private String newMetricName(@Nonnull String name) { + String n = MetricRegistry.name(getClass().getName(), "Tracker-" + trackerId, name); + synchronized (names) { + names.add(n); + } + return n; + } + + @Nonnull + public Counter addCounter(@Nonnull String name) { + return metricRegistry.counter(newMetricName(name)); + } + + @Nonnull + public Gauge addGauge(@Nonnull String name, @Nonnull Gauge gauge) { + return metricRegistry.register(newMetricName(name), gauge); + } + + @Nonnull + public Meter addMeter(@Nonnull String name, @Nonnull String item, @Nonnull TimeUnit unit) { + return metricRegistry.meter(newMetricName(name)); + } + + public void shutdown() { + for (String n : names) { + metricRegistry.remove(n); + } + } +} diff --git a/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerService.java b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerService.java new file mode 100644 index 000000000..0a7350284 --- /dev/null +++ b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerService.java @@ -0,0 +1,217 @@ +/** + * Copyright (C) 2011-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. + */ +package com.turn.ttorrent.tracker; + +import com.turn.ttorrent.protocol.tracker.http.HTTPAnnounceRequestMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPAnnounceResponseMessage; +import com.codahale.metrics.Meter; +import com.turn.ttorrent.protocol.tracker.Peer; +import com.turn.ttorrent.protocol.tracker.TrackerMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPTrackerErrorMessage; +import com.turn.ttorrent.protocol.tracker.http.HTTPTrackerMessage; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract tracker service to serve the tracker's announce requests. + * @see BitTorrent protocol specification + */ +public class TrackerService { + + private static final Logger LOG = LoggerFactory.getLogger(TrackerService.class); + + protected static class Metrics { + + public final Meter requestRejected; + public final Meter requestParseFailed; + public final Meter requestReceived; + public final Meter requestNoTorrent; + public final Meter requestInvalidEvent; + public final Meter requestUpdateFailed; + public final Meter requestResponseFailed; + public final Meter requestSucceeded; + + private Metrics(@Nonnull TrackerMetrics metrics) { + this.requestRejected = metrics.addMeter("requestRejected", "requests", TimeUnit.SECONDS); + this.requestParseFailed = metrics.addMeter("requestParseFailed", "requests", TimeUnit.SECONDS); + this.requestReceived = metrics.addMeter("requestReceived", "requests", TimeUnit.SECONDS); + this.requestNoTorrent = metrics.addMeter("requestNoTorrent", "requests", TimeUnit.SECONDS); + this.requestInvalidEvent = metrics.addMeter("requestInvalidEvent", "requests", TimeUnit.SECONDS); + this.requestUpdateFailed = metrics.addMeter("requestUpdateFailed", "requests", TimeUnit.SECONDS); + this.requestResponseFailed = metrics.addMeter("requestResponseFailed", "requests", TimeUnit.SECONDS); + this.requestSucceeded = metrics.addMeter("requestSucceeded", "requests", TimeUnit.SECONDS); + } + } + private final TrackedTorrentRegistry torrents; + protected Metrics metrics; + private final Object lock = new Object(); + + /** + * Create a new TrackerService serving the given torrents. + * + * @param torrents The torrents this TrackerService should serve requests + * for. + */ + public TrackerService(@Nonnull TrackedTorrentRegistry torrents) { + this.torrents = torrents; + } + + public TrackerService() { + this(new TrackedTorrentRegistry()); + } + + @Nonnull + public TrackedTorrentRegistry getTorrents() { + return torrents; + } + + /** + * Process the announce request. + * + *

+ * This method attempts to read and parse the incoming announce request into + * an announce request message, then creates the appropriate announce + * response message and sends it back to the client. + *

+ * + * @param request The incoming announce request. + * @param response The response object. + * @param body The validated response body output stream. + */ + @Nonnull + public HTTPTrackerMessage process(@Nonnull InetSocketAddress clientAddress, @Nonnull HTTPAnnounceRequestMessage request) { + metrics.requestReceived.mark(); + + if (LOG.isTraceEnabled()) + LOG.trace("Announce request is {}", request); + + // The requested torrent must be announced by the tracker. + TrackedTorrent torrent = this.torrents.getTorrent(request.getHexInfoHash()); + if (torrent == null) { + metrics.requestNoTorrent.mark(); + LOG.warn("No such torrent: {}", request.getHexInfoHash()); + return new HTTPTrackerErrorMessage(TrackerMessage.ErrorMessage.FailureReason.UNKNOWN_TORRENT); + } + + int peerPort = -1; + List peerAddresses = request.getPeerAddresses(); + Iterator peerAddressIterator = peerAddresses.iterator(); + while (peerAddressIterator.hasNext()) { + InetSocketAddress peerAddress = peerAddressIterator.next(); + if (peerPort == -1) + peerPort = peerAddress.getPort(); + if (!Peer.isValidIpAddress(peerAddress)) { + LOG.debug("Peer specified invalid address {}", peerAddress); + peerAddressIterator.remove(); + } + } + if (peerPort == -1) { + LOG.debug("Peer specified no valid address."); + return new HTTPTrackerErrorMessage(TrackerMessage.ErrorMessage.FailureReason.MISSING_PEER_ADDRESS); + } + if (peerAddresses.isEmpty()) { + InetSocketAddress peerAddress = new InetSocketAddress(clientAddress.getAddress(), peerPort); + LOG.debug("Peer specified no valid address; using {} instead.", peerAddress); + peerAddresses.add(peerAddress); + } + + TrackedPeer client = torrent.getPeer(request.getPeerId()); + + TrackerMessage.AnnounceEvent event = request.getEvent(); + // When no event is specified, it's a periodic update while the client + // is operating. If we don't have a peer for this announce, it means + // the tracker restarted while the client was running. Consider this + // announce request as a 'started' event. + if ((event == null || TrackerMessage.AnnounceEvent.NONE.equals(event)) + && client == null) { + event = TrackerMessage.AnnounceEvent.STARTED; + } + + // If an event other than 'started' is specified and we also haven't + // seen the peer on this torrent before, something went wrong. A + // previous 'started' announce request should have been made by the + // client that would have had us register that peer on the torrent this + // request refers to. + if (event != null && client == null && !TrackerMessage.AnnounceEvent.STARTED.equals(event)) { + metrics.requestInvalidEvent.mark(); + return new HTTPTrackerErrorMessage(TrackerMessage.ErrorMessage.FailureReason.INVALID_EVENT); + } + + // Update the torrent according to the announce event + try { + client = torrent.update(event, + request.getPeerId(), + peerAddresses, + request.getUploaded(), + request.getDownloaded(), + request.getLeft()); + } catch (Exception e) { + metrics.requestUpdateFailed.mark(); + LOG.error("Failed to update torrent", e); + return new HTTPTrackerErrorMessage(TrackerMessage.ErrorMessage.FailureReason.UPDATE_FAILED); + } + + // Craft and output the answer + try { + HTTPAnnounceResponseMessage response = new HTTPAnnounceResponseMessage( + clientAddress.getAddress(), + (int) TimeUnit.MILLISECONDS.toSeconds(torrent.getAnnounceInterval()), + // TrackedTorrent.MIN_ANNOUNCE_INTERVAL_SECONDS, + torrent.seeders(), + torrent.leechers(), + torrent.getSomePeers(client, request.getNumWant())); + metrics.requestSucceeded.mark(); + return response; + } catch (Exception e) { + metrics.requestResponseFailed.mark(); + LOG.error("Failed to send response", e); + return new HTTPTrackerErrorMessage(TrackerMessage.ErrorMessage.FailureReason.SERVER_ERROR); + } + } + + /** + * Start the tracker thread. + */ + public void start(@Nonnull TrackerMetrics metrics) throws IOException { + synchronized (lock) { + if (this.metrics == null) { + this.metrics = new Metrics(metrics); + } + this.torrents.start(metrics); + } + } + + /** + * Stop the tracker. + * + *

+ * This effectively closes the listening HTTP connection to terminate + * the service, and interrupts the peer collector thread as well. + *

+ */ + public void stop() throws IOException { + synchronized (lock) { + this.torrents.stop(); + this.metrics = null; + } + } +} diff --git a/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerUtils.java b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerUtils.java new file mode 100644 index 000000000..9acdbc88a --- /dev/null +++ b/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerUtils.java @@ -0,0 +1,78 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.turn.ttorrent.tracker; + +import com.google.common.base.Splitter; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.turn.ttorrent.protocol.bcodec.BEUtils; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author shevek + */ +public class TrackerUtils { + + private static final Logger LOG = LoggerFactory.getLogger(TrackerUtils.class); + /** Default server name and version announced by the tracker. */ + public static final String DEFAULT_VERSION_STRING = "BitTorrent Tracker (ttorrent)"; + /** Request path handled by the tracker announce request handler. */ + public static final String DEFAULT_ANNOUNCE_URL = "/announce"; + /** Default tracker listening port (BitTorrent's default is 6969). */ + public static final int DEFAULT_TRACKER_PORT = 6969; + + @Nonnull + public static Multimap parseQuery(String query) { + Multimap params = ArrayListMultimap.create(); + Splitter ampersand = Splitter.on('&').omitEmptyStrings(); + // Splitter equals = Splitter.on('=').limit(2); + + try { + for (String pair : ampersand.split(query)) { + String[] keyval = pair.split("[=]", 2); + if (keyval.length == 1) { + parseParam(params, keyval[0], null); + } else { + parseParam(params, keyval[0], keyval[1]); + } + } + } catch (ArrayIndexOutOfBoundsException e) { + params.clear(); + } + return params; + } + + private static void parseParam(@Nonnull Multimap params, @Nonnull String key, @CheckForNull String value) { + try { + if (value != null) + value = URLDecoder.decode(value, BEUtils.BYTE_ENCODING_NAME); + else + value = ""; + params.put(key, value); + } catch (UnsupportedEncodingException uee) { + // Ignore, act like parameter was not there + if (LOG.isDebugEnabled()) + LOG.debug("Could not decode {}", value); + } + } + + @Nonnull + public static Multimap parseQuery(Map in) { + Multimap out = HashMultimap.create(); + for (Map.Entry e : in.entrySet()) { + for (String value : e.getValue()) + out.put(e.getKey(), value); + } + return out; + } +}