tracker
Change-Id: I8f8ac81f9c4d7c7650cd64d2dade701dc6c11dce
diff --git a/ttorrent-master/common/pom.xml b/ttorrent-master/common/pom.xml
new file mode 100644
index 0000000..909c3b8
--- /dev/null
+++ b/ttorrent-master/common/pom.xml
@@ -0,0 +1,26 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.turn</groupId>
+ <artifactId>ttorrent</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <name>ttorrent/common</name>
+ <url>http://turn.github.com/ttorrent/</url>
+ <artifactId>ttorrent-common</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <packaging>jar</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.turn</groupId>
+ <artifactId>ttorrent-bencoding</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/Constants.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/Constants.java
new file mode 100644
index 0000000..001bda2
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/Constants.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2013 JetBrains s.r.o.
+ *
+ * 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;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author Sergey.Pak
+ * Date: 9/19/13
+ * Time: 2:57 PM
+ */
+public class Constants {
+ public static final int DEFAULT_ANNOUNCE_INTERVAL_SEC = 15;
+
+ public final static int DEFAULT_SOCKET_CONNECTION_TIMEOUT_MILLIS = 100000;
+ public static final int DEFAULT_CONNECTION_TIMEOUT_MILLIS = 10000;
+
+ public static final int DEFAULT_MAX_CONNECTION_COUNT = 100;
+
+ public static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
+
+ public static final int DEFAULT_SELECTOR_SELECT_TIMEOUT_MILLIS = 10000;
+ public static final int DEFAULT_CLEANUP_RUN_TIMEOUT_MILLIS = 120000;
+
+ public static final String BYTE_ENCODING = "ISO-8859-1";
+
+ public static final int PIECE_HASH_SIZE = 20;
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/AnnounceableInformation.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/AnnounceableInformation.java
new file mode 100644
index 0000000..234a4d3
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/AnnounceableInformation.java
@@ -0,0 +1,33 @@
+package com.turn.ttorrent.common;
+
+import java.util.List;
+
+public interface AnnounceableInformation extends TorrentHash {
+
+ /**
+ * @return number of bytes uploaded by the client for this torrent
+ */
+ long getUploaded();
+
+ /**
+ * @return number of bytes downloaded by the client for this torrent
+ */
+ long getDownloaded();
+
+ /**
+ * @return number of bytes left to download by the client for this torrent
+ */
+ long getLeft();
+
+ /**
+ * @return all tracker for announce
+ * @see <a href="http://bittorrent.org/beps/bep_0012.html"></a>
+ */
+ List<List<String>> getAnnounceList();
+
+ /**
+ * @return main announce url for tracker
+ */
+ String getAnnounce();
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/ImmutableTorrentHash.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/ImmutableTorrentHash.java
new file mode 100644
index 0000000..ee4098c
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/ImmutableTorrentHash.java
@@ -0,0 +1,24 @@
+package com.turn.ttorrent.common;
+
+import java.util.Arrays;
+
+public class ImmutableTorrentHash implements TorrentHash {
+
+ private final byte[] hash;
+ private final String hexHash;
+
+ public ImmutableTorrentHash(byte[] hash) {
+ this.hash = hash;
+ this.hexHash = TorrentUtils.byteArrayToHexString(hash);
+ }
+
+ @Override
+ public byte[] getInfoHash() {
+ return Arrays.copyOf(hash, hash.length);
+ }
+
+ @Override
+ public String getHexInfoHash() {
+ return hexHash;
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/LoggerUtils.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/LoggerUtils.java
new file mode 100644
index 0000000..5a2e3f4
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/LoggerUtils.java
@@ -0,0 +1,32 @@
+package com.turn.ttorrent.common;
+
+import org.slf4j.Logger;
+
+public final class LoggerUtils {
+
+ public static void warnAndDebugDetails(Logger logger, String message, Throwable t) {
+ logger.warn(message);
+ logger.debug("", t);
+ }
+
+ public static void warnAndDebugDetails(Logger logger, String message, Object arg, Throwable t) {
+ logger.warn(message, arg);
+ logger.debug("", t);
+ }
+
+ public static void warnWithMessageAndDebugDetails(Logger logger, String message, Object arg, Throwable t) {
+ logger.warn(message + ": " + (t.getMessage() != null ? t.getMessage() : t.getClass().getName()), arg);
+ logger.debug("", t);
+ }
+
+ public static void errorAndDebugDetails(Logger logger, String message, Object arg, Throwable t) {
+ logger.error(message, arg);
+ logger.debug("", t);
+ }
+
+ public static void errorAndDebugDetails(Logger logger, String message, Throwable t) {
+ logger.error(message);
+ logger.debug("", t);
+ }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Optional.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Optional.java
new file mode 100644
index 0000000..5e9b84b
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Optional.java
@@ -0,0 +1,46 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.NoSuchElementException;
+
+public final class Optional<T> {
+
+ private static final Optional<?> EMPTY = new Optional();
+
+ @Nullable
+ private final T value;
+
+ public Optional(@NotNull T value) {
+ this.value = value;
+ }
+
+ private Optional() {
+ this.value = null;
+ }
+
+ @NotNull
+ @SuppressWarnings("unchecked")
+ public static <T> Optional<T> of(@Nullable T value) {
+ return value == null ? (Optional<T>) EMPTY : new Optional<T>(value);
+ }
+
+ @NotNull
+ public T get() throws NoSuchElementException {
+ if (value == null) {
+ throw new NoSuchElementException("No value present");
+ }
+ return value;
+ }
+
+ public boolean isPresent() {
+ return value != null;
+ }
+
+ @NotNull
+ public T orElse(@NotNull T defaultValue) {
+ return value != null ? value : defaultValue;
+ }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Pair.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Pair.java
new file mode 100644
index 0000000..48325f1
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Pair.java
@@ -0,0 +1,46 @@
+package com.turn.ttorrent.common;
+
+public class Pair<A, B> {
+
+ private final A myFirst;
+ private final B mySecond;
+
+ public Pair(A first, B second) {
+ myFirst = first;
+ mySecond = second;
+ }
+
+ public A first() {
+ return myFirst;
+ }
+
+ public B second() {
+ return mySecond;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Pair<?, ?> pair = (Pair<?, ?>) o;
+
+ if (!myFirst.equals(pair.myFirst)) return false;
+ return mySecond.equals(pair.mySecond);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = myFirst.hashCode();
+ result = 31 * result + mySecond.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Pair{" +
+ "myFirst=" + myFirst +
+ ", mySecond=" + mySecond +
+ '}';
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Peer.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Peer.java
new file mode 100644
index 0000000..72f00c0
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Peer.java
@@ -0,0 +1,242 @@
+/**
+ * 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.Constants;
+import org.slf4j.Logger;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+
+
+/**
+ * A basic BitTorrent peer.
+ *
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+
+// Peer(对等节点)类,用于表示参与文件共享的单个客户端或服务器节点
+// 基础peer类,被tracked peer继承
+// 实现功能: 获取各个类型的peerid
+public class Peer {
+
+ private static final Logger logger = TorrentLoggerFactory.getLogger(Peer.class);
+
+ private final InetSocketAddress address;//地址和端口
+ private final String hostId;// 格式化为 "IP:端口" 的字符串
+
+ private ByteBuffer peerId;// 主键,唯一标识
+ private volatile String hexPeerId;
+ private volatile String hexInfoHash;
+
+ /**
+ * 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;
+ }
+
+ public byte[] getPeerIdArray() {
+ return peerId == null ? null : peerId.array();
+ }
+
+ /**
+ * 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 = TorrentUtils.byteArrayToHexString(peerId.array());
+ } else {
+ this.peerId = null;
+ this.hexPeerId = null;
+ }
+ }
+
+ public String getStringPeerId() {
+ try {
+ return new String(peerId.array(), Constants.BYTE_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ LoggerUtils.warnAndDebugDetails(logger, "can not get peer id as string", e);
+ }
+ return 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 InetSocketAddress getAddress() {
+ return this.address;
+ }
+
+ /**
+ * 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() {
+ final InetAddress address = this.address.getAddress();
+ if (address == null) return null;
+ return address.getAddress();
+ }
+
+
+ /**
+ * 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.getPort() == other.getPort();
+ }
+
+ public void setTorrentHash(String hexInfoHash) {
+ this.hexInfoHash = hexInfoHash;
+ }
+
+ public String getHexInfoHash() {
+ return hexInfoHash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Peer peer = (Peer) o;
+
+ if (hexPeerId == null && peer.hexPeerId == null) return super.equals(o);
+
+ if (hexPeerId != null ? !hexPeerId.equals(peer.hexPeerId) : peer.hexPeerId != null) return false;
+ return hexInfoHash != null ? hexInfoHash.equals(peer.hexInfoHash) : peer.hexInfoHash == null;
+ }
+
+ @Override
+ public int hashCode() {
+
+ if (hexPeerId == null) return super.hashCode();
+
+ int result = hexPeerId != null ? hexPeerId.hashCode() : 0;
+ result = 31 * result + (hexInfoHash != null ? hexInfoHash.hashCode() : 0);
+ return result;
+ }
+
+ /**
+ * Returns a human-readable representation of this peer.
+ */
+ @Override
+ public String toString() {
+ return "Peer " + address + " for torrent " + hexInfoHash;
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/PeerUID.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/PeerUID.java
new file mode 100644
index 0000000..bac1b02
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/PeerUID.java
@@ -0,0 +1,51 @@
+package com.turn.ttorrent.common;
+
+import java.net.InetSocketAddress;
+
+/* 保存用户的地址相关信息
+* 标识唯一一个用户和用户相关的torrent地址
+* */
+public class PeerUID {
+
+ private final InetSocketAddress myAddress;// 保存ip地址,端口号,主机名
+ private final String myTorrentHash;// 标识唯一的torrent文件
+
+ public PeerUID(InetSocketAddress address, String torrentHash) {
+ myAddress = address;
+ myTorrentHash = torrentHash;
+ }
+
+ public String getTorrentHash() {
+ return myTorrentHash;
+ }
+
+ public InetSocketAddress getAddress() {
+ return myAddress;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PeerUID peerUID = (PeerUID) o;
+
+ if (!myAddress.equals(peerUID.myAddress)) return false;
+ return myTorrentHash.equals(peerUID.myTorrentHash);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = myAddress.hashCode();
+ result = 31 * result + myTorrentHash.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "PeerUID{" +
+ "address=" + myAddress +
+ ", torrent hash='" + myTorrentHash + '\'' +
+ '}';
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/SystemTimeService.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/SystemTimeService.java
new file mode 100644
index 0000000..6a55cd1
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/SystemTimeService.java
@@ -0,0 +1,10 @@
+package com.turn.ttorrent.common;
+
+public class SystemTimeService implements TimeService {
+
+ @Override
+ public long now() {
+ return System.currentTimeMillis();
+ }
+ // 返回自 Unix 纪元(1970-01-01 00:00:00 UTC) 以来的毫秒数(long 类型)
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TimeService.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TimeService.java
new file mode 100644
index 0000000..ac41ffd
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TimeService.java
@@ -0,0 +1,14 @@
+package com.turn.ttorrent.common;
+
+/**
+ * Abstract time service. Provides current time millis.
+ */
+public interface TimeService {
+ /**
+ * Provides current time millis.
+ *
+ * @return current time.
+ */
+ long now();
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentCreator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentCreator.java
new file mode 100644
index 0000000..c5de1ef
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentCreator.java
@@ -0,0 +1,376 @@
+package com.turn.ttorrent.common;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.common.creation.MetadataBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+import java.io.*;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+
+/**
+ * Old API for creating .torrent files, use {@link MetadataBuilder}
+ * @deprecated
+ */
+@Deprecated
+public class TorrentCreator {
+
+ private final static Logger logger = TorrentLoggerFactory.getLogger(TorrentCreator.class);
+
+ /**
+ * Torrent file piece length (in bytes), we use 512 kB.
+ */
+ public static final int DEFAULT_PIECE_LENGTH = 512 * 1024;
+ private static final int HASHING_TIMEOUT_SEC = 15;
+ public static int HASHING_THREADS_COUNT = Runtime.getRuntime().availableProcessors();
+
+ static {
+ String threads = System.getenv("TTORRENT_HASHING_THREADS");
+
+ if (threads != null) {
+ try {
+ int count = Integer.parseInt(threads);
+ if (count > 0) {
+ TorrentCreator.HASHING_THREADS_COUNT = count;
+ }
+ } catch (NumberFormatException nfe) {
+ // Pass
+ }
+ }
+ }
+
+ private static final ExecutorService HASHING_EXECUTOR = Executors.newFixedThreadPool(HASHING_THREADS_COUNT, new ThreadFactory() {
+ @Override
+ public Thread newThread(@NotNull final Runnable r) {
+ final Thread thread = new Thread(r);
+ thread.setDaemon(true);
+ return thread;
+ }
+ });
+
+ /**
+ * Create a {@link TorrentMetadata} object for a file.
+ *
+ * <p>
+ * Hash the given file to create the {@link TorrentMetadata} object representing
+ * the Torrent meta info about this file, needed for announcing and/or
+ * sharing said file.
+ * </p>
+ *
+ * @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 TorrentMetadata create(File source, URI announce, String createdBy)
+ throws InterruptedException, IOException {
+ return create(source, null, announce, createdBy);
+ }
+
+ /**
+ * Create a {@link TorrentMetadata} object for a set of files.
+ *
+ * <p>
+ * Hash the given files to create the multi-file {@link TorrentMetadata} 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.
+ * </p>
+ *
+ * @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 TorrentMetadata create(File parent, List<File> files, URI announce,
+ String createdBy) throws InterruptedException, IOException {
+ return create(parent, files, announce, null, createdBy);
+ }
+
+ /**
+ * Create a {@link TorrentMetadata} object for a file.
+ *
+ * <p>
+ * Hash the given file to create the {@link TorrentMetadata} object representing
+ * the Torrent metainfo about this file, needed for announcing and/or
+ * sharing said file.
+ * </p>
+ *
+ * @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 TorrentMetadata create(File source, List<List<URI>> announceList,
+ String createdBy) throws InterruptedException, IOException {
+ return create(source, null, null, announceList, createdBy);
+ }
+
+ /**
+ * Create a {@link TorrentMetadata} object for a set of files.
+ *
+ * <p>
+ * Hash the given files to create the multi-file {@link TorrentMetadata} 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.
+ * </p>
+ *
+ * @param source The parent directory or location of the torrent files,
+ * also used as the torrent's name.
+ * @param files The files to add into this torrent.
+ * @param announceList The announce URIs organized as tiers that will
+ * be used for this torrent
+ * @param createdBy The creator's name, or any string identifying the
+ * torrent's creator.
+ */
+ public static TorrentMetadata create(File source, List<File> files,
+ List<List<URI>> announceList, String createdBy)
+ throws InterruptedException, IOException {
+ return create(source, files, null, announceList, createdBy);
+ }
+
+ /**
+ * Helper method to create a {@link TorrentMetadata} object for a set of files.
+ *
+ * <p>
+ * Hash the given files to create the multi-file {@link TorrentMetadata} 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.
+ * </p>
+ *
+ * @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.
+ */
+ public static TorrentMetadata create(File parent, List<File> files, URI announce, List<List<URI>> announceList, String createdBy)
+ throws InterruptedException, IOException {
+ return create(parent, files, announce, announceList, createdBy, DEFAULT_PIECE_LENGTH);
+ }
+
+ public static TorrentMetadata create(File parent, List<File> files, URI announce,
+ List<List<URI>> announceList, String createdBy, final int pieceSize)
+ throws InterruptedException, IOException {
+ return create(parent, files, announce, announceList, createdBy, System.currentTimeMillis() / 1000, pieceSize);
+ }
+
+ //for tests
+ /*package local*/
+ static TorrentMetadata create(File parent, List<File> files, URI announce,
+ List<List<URI>> announceList, String createdBy, long creationTimeSecs, final int pieceSize)
+ throws InterruptedException, IOException {
+ Map<String, BEValue> torrent = new HashMap<String, BEValue>();
+
+ if (announce != null) {
+ torrent.put(ANNOUNCE, new BEValue(announce.toString()));
+ }
+ if (announceList != null) {
+ List<BEValue> tiers = new LinkedList<BEValue>();
+ for (List<URI> trackers : announceList) {
+ List<BEValue> tierInfo = new LinkedList<BEValue>();
+ 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_SEC, new BEValue(creationTimeSecs));
+ torrent.put(CREATED_BY, new BEValue(createdBy));
+
+ Map<String, BEValue> info = new TreeMap<String, BEValue>();
+ info.put(NAME, new BEValue(parent.getName()));
+ info.put(PIECE_LENGTH, new BEValue(pieceSize));
+
+ if (files == null || files.isEmpty()) {
+ info.put(FILE_LENGTH, new BEValue(parent.length()));
+ info.put(PIECES, new BEValue(hashFile(parent, pieceSize),
+ Constants.BYTE_ENCODING));
+ } else {
+ List<BEValue> fileInfo = new LinkedList<BEValue>();
+ for (File file : files) {
+ Map<String, BEValue> fileMap = new HashMap<String, BEValue>();
+ fileMap.put(FILE_LENGTH, new BEValue(file.length()));
+
+ LinkedList<BEValue> filePath = new LinkedList<BEValue>();
+ while (file != null) {
+ if (file.equals(parent)) {
+ break;
+ }
+
+ filePath.addFirst(new BEValue(file.getName()));
+ file = file.getParentFile();
+ }
+
+ fileMap.put(FILE_PATH, new BEValue(filePath));
+ fileInfo.add(new BEValue(fileMap));
+ }
+ info.put(FILES, new BEValue(fileInfo));
+ info.put(PIECES, new BEValue(hashFiles(files, pieceSize),
+ Constants.BYTE_ENCODING));
+ }
+ torrent.put(INFO_TABLE, new BEValue(info));
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ BEncoder.bencode(new BEValue(torrent), baos);
+ return new TorrentParser().parse(baos.toByteArray());
+ }
+
+ /**
+ * Return the concatenation of the SHA-1 hashes of a file's pieces.
+ *
+ * <p>
+ * Hashes the given file piece by piece using the default Torrent piece
+ * length (see {@link #DEFAULT_PIECE_LENGTH}) and returns the concatenation of
+ * these hashes, as a string.
+ * </p>
+ *
+ * <p>
+ * This is used for creating Torrent meta-info structures from a file.
+ * </p>
+ *
+ * @param file The file to hash.
+ */
+ private static String hashFile(final File file, final int pieceSize)
+ throws InterruptedException, IOException {
+ return hashFiles(Collections.singletonList(file), pieceSize);
+ }
+
+ private static String hashFiles(final List<File> files, final int pieceSize)
+ throws InterruptedException, IOException {
+ if (files.size() == 0) {
+ return "";
+ }
+ List<Future<String>> results = new LinkedList<Future<String>>();
+ long length = 0L;
+
+ final ByteBuffer buffer = ByteBuffer.allocate(pieceSize);
+
+
+ final AtomicInteger threadIdx = new AtomicInteger(0);
+ final String firstFileName = files.get(0).getName();
+
+ StringBuilder hashes = new StringBuilder();
+
+ long start = System.nanoTime();
+ for (File file : files) {
+ logger.debug("Analyzing local data for {} with {} threads...",
+ file.getName(), HASHING_THREADS_COUNT);
+
+ length += file.length();
+
+ FileInputStream fis = new FileInputStream(file);
+ FileChannel channel = fis.getChannel();
+
+ try {
+ while (channel.read(buffer) > 0) {
+ if (buffer.remaining() == 0) {
+ buffer.clear();
+ final ByteBuffer data = prepareDataFromBuffer(buffer);
+
+ results.add(HASHING_EXECUTOR.submit(new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+ Thread.currentThread().setName(String.format("%s hasher #%d", firstFileName, threadIdx.incrementAndGet()));
+ return new CallableChunkHasher(data).call();
+ }
+ }));
+ }
+
+ if (results.size() >= HASHING_THREADS_COUNT) {
+ // process hashers, otherwise they will spend too much memory
+ waitForHashesToCalculate(results, hashes);
+ results.clear();
+ }
+ }
+ } finally {
+ channel.close();
+ fis.close();
+ }
+ }
+
+ // Hash the last bit, if any
+ if (buffer.position() > 0) {
+ buffer.limit(buffer.position());
+ buffer.position(0);
+ final ByteBuffer data = prepareDataFromBuffer(buffer);
+ results.add(HASHING_EXECUTOR.submit(new CallableChunkHasher(data)));
+ }
+ // here we have only a few hashes to wait for calculation
+ waitForHashesToCalculate(results, hashes);
+
+ long elapsed = System.nanoTime() - start;
+
+ int expectedPieces = (int) (Math.ceil(
+ (double) length / pieceSize));
+ logger.debug("Hashed {} file(s) ({} bytes) in {} pieces ({} expected) in {}ms.",
+ new Object[]{
+ files.size(),
+ length,
+ results.size(),
+ expectedPieces,
+ String.format("%.1f", elapsed / 1e6),
+ });
+
+ return hashes.toString();
+ }
+
+ private static ByteBuffer prepareDataFromBuffer(ByteBuffer buffer) {
+ final ByteBuffer data = ByteBuffer.allocate(buffer.remaining());
+ buffer.mark();
+ data.put(buffer);
+ data.clear();
+ buffer.reset();
+ return data;
+ }
+
+ private static void waitForHashesToCalculate(List<Future<String>> results, StringBuilder hashes) throws InterruptedException, IOException {
+ try {
+ for (Future<String> chunk : results) {
+ hashes.append(chunk.get(HASHING_TIMEOUT_SEC, TimeUnit.SECONDS));
+ }
+ } catch (ExecutionException ee) {
+ throw new IOException("Error while hashing the torrent data!", ee);
+ } catch (TimeoutException e) {
+ throw new RuntimeException(String.format("very slow hashing: took more than %d seconds to calculate several pieces. Cancelling", HASHING_TIMEOUT_SEC));
+ }
+ }
+
+ /**
+ * A {@link Callable} to hash a data chunk.
+ *
+ * @author mpetazzoni
+ */
+ private static class CallableChunkHasher implements Callable<String> {
+
+ private final ByteBuffer data;
+
+ CallableChunkHasher(final ByteBuffer data) {
+ this.data = data;
+ }
+
+ @Override
+ public String call() throws UnsupportedEncodingException {
+ byte[] sha1Hash = TorrentUtils.calculateSha1Hash(this.data.array());
+ return new String(sha1Hash, Constants.BYTE_ENCODING);
+ }
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentFile.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentFile.java
new file mode 100644
index 0000000..2443b54
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentFile.java
@@ -0,0 +1,42 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * @author dgiffin
+ * @author mpetazzoni
+ */
+public class TorrentFile {
+
+ @NotNull
+ public final List<String> relativePath;
+ public final long size;
+ @NotNull
+ public final Optional<String> md5Hash;
+
+ public TorrentFile(@NotNull List<String> relativePath, long size, @Nullable String md5Hash) {
+ this.relativePath = new ArrayList<String>(relativePath);
+ this.size = size;
+ this.md5Hash = Optional.of(md5Hash);
+ }
+
+ public String getRelativePathAsString() {
+ String delimiter = File.separator;
+ final Iterator<String> iterator = relativePath.iterator();
+ StringBuilder sb = new StringBuilder();
+ if (iterator.hasNext()) {
+ sb.append(iterator.next());
+ while (iterator.hasNext()) {
+ sb.append(delimiter).append(iterator.next());
+ }
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentHash.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentHash.java
new file mode 100644
index 0000000..7fae345
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentHash.java
@@ -0,0 +1,13 @@
+package com.turn.ttorrent.common;
+
+public interface TorrentHash {
+ /**
+ * Return the hash of the B-encoded meta-info structure of a torrent.
+ */
+ byte[] getInfoHash();
+
+ /**
+ * Get torrent's info hash (as an hexadecimal-coded string).
+ */
+ String getHexInfoHash();
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentInfo.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentInfo.java
new file mode 100644
index 0000000..8bc5fc1
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentInfo.java
@@ -0,0 +1,28 @@
+package com.turn.ttorrent.common;
+
+/**
+ * @author Sergey.Pak
+ * Date: 8/9/13
+ * Time: 6:00 PM
+ */
+public interface TorrentInfo extends TorrentHash {
+
+ /*
+ * Number of bytes uploaded by the client for this torrent
+ * */
+ long getUploaded();
+
+ /*
+ * Number of bytes downloaded by the client for this torrent
+ * */
+ long getDownloaded();
+
+ /*
+ * Number of bytes left to download by the client for this torrent
+ * */
+ long getLeft();
+
+ int getPieceCount();
+
+ long getPieceSize(int pieceIdx);
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentLoggerFactory.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentLoggerFactory.java
new file mode 100644
index 0000000..1b5a545
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentLoggerFactory.java
@@ -0,0 +1,24 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class TorrentLoggerFactory {
+
+ @Nullable
+ private static volatile String staticLoggersName = null;
+
+ public static Logger getLogger(Class<?> clazz) {
+ String name = staticLoggersName;
+ if (name == null) {
+ name = clazz.getName();
+ }
+ return LoggerFactory.getLogger(name);
+ }
+
+ public static void setStaticLoggersName(@Nullable String staticLoggersName) {
+ TorrentLoggerFactory.staticLoggersName = staticLoggersName;
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadata.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadata.java
new file mode 100644
index 0000000..22d6d1e
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadata.java
@@ -0,0 +1,76 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Provided access to all stored info in .torrent file
+ *
+ * @see <a href="https://wiki.theory.org/index.php/BitTorrentSpecification#Metainfo_File_Structure"></a>
+ */
+public interface TorrentMetadata extends TorrentHash {
+
+ /**
+ * @return all tracker for announce
+ * @see <a href="http://bittorrent.org/beps/bep_0012.html"></a>
+ */
+ @Nullable
+ List<List<String>> getAnnounceList();
+
+ /**
+ * @return main announce url for tracker or <code>null</code> if main announce is not specified
+ */
+ @Nullable
+ String getAnnounce();
+
+ /**
+ * @return creation date of the torrent in unix format
+ */
+ Optional<Long> getCreationDate();
+
+ /**
+ * @return free-form text comment of the author
+ */
+ Optional<String> getComment();
+
+ /**
+ * @return name and version of the program used to create .torrent
+ */
+ Optional<String> getCreatedBy();
+
+ /**
+ * @return number of bytes in each piece
+ */
+ int getPieceLength();
+
+ /**
+ * @return concatenation of all 20-byte SHA1 hash values, one per piece.
+ * So the length of this array must be a multiple of 20
+ */
+ byte[] getPiecesHashes();
+
+ /**
+ * @return true if it's private torrent. In this case client must get peers only from tracker and
+ * must initiate connections to peers returned from the tracker.
+ * @see <a href="http://bittorrent.org/beps/bep_0027.html"></a>
+ */
+ boolean isPrivate();
+
+ /**
+ * @return count of pieces in torrent
+ */
+ int getPiecesCount();
+
+ /**
+ * @return The filename of the directory in which to store all the files
+ */
+ String getDirectoryName();
+
+ /**
+ * @return list of files, stored in this torrent
+ */
+ List<TorrentFile> getFiles();
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataImpl.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataImpl.java
new file mode 100644
index 0000000..8b831d0
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataImpl.java
@@ -0,0 +1,115 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class TorrentMetadataImpl implements TorrentMetadata {
+
+ private final byte[] myInfoHash;
+ @Nullable
+ private final List<List<String>> myAnnounceList;
+ private final String myMainAnnounce;
+ private final long myCreationDate;
+ private final String myComment;
+ private final String myCreatedBy;
+ private final String myName;
+ private final List<TorrentFile> myFiles;
+ private final int myPieceCount;
+ private final int myPieceLength;
+ private final byte[] myPiecesHashes;
+ private final String myHexString;
+
+ TorrentMetadataImpl(byte[] infoHash,
+ @Nullable List<List<String>> announceList,
+ String mainAnnounce,
+ long creationDate,
+ String comment,
+ String createdBy,
+ String name,
+ List<TorrentFile> files,
+ int pieceCount,
+ int pieceLength,
+ byte[] piecesHashes) {
+ myInfoHash = infoHash;
+ myAnnounceList = announceList;
+ myMainAnnounce = mainAnnounce;
+ myCreationDate = creationDate;
+ myComment = comment;
+ myCreatedBy = createdBy;
+ myName = name;
+ myFiles = files;
+ myPieceCount = pieceCount;
+ myPieceLength = pieceLength;
+ myPiecesHashes = piecesHashes;
+ myHexString = TorrentUtils.byteArrayToHexString(myInfoHash);
+ }
+
+ @Override
+ public String getDirectoryName() {
+ return myName;
+ }
+
+ @Override
+ public List<TorrentFile> getFiles() {
+ return myFiles;
+ }
+
+ @Nullable
+ @Override
+ public List<List<String>> getAnnounceList() {
+ return myAnnounceList;
+ }
+
+ @Nullable
+ @Override
+ public String getAnnounce() {
+ return myMainAnnounce;
+ }
+
+ @Override
+ public Optional<Long> getCreationDate() {
+ return Optional.of(myCreationDate == -1 ? null : myCreationDate);
+ }
+
+ @Override
+ public Optional<String> getComment() {
+ return Optional.of(myComment);
+ }
+
+ @Override
+ public Optional<String> getCreatedBy() {
+ return Optional.of(myCreatedBy);
+ }
+
+ @Override
+ public int getPieceLength() {
+ return myPieceLength;
+ }
+
+ @Override
+ public byte[] getPiecesHashes() {
+ return myPiecesHashes;
+ }
+
+ @Override
+ public boolean isPrivate() {
+ return false;
+ }
+
+ @Override
+ public int getPiecesCount() {
+ return myPieceCount;
+ }
+
+ @Override
+ public byte[] getInfoHash() {
+ return myInfoHash;
+ }
+
+ @Override
+ public String getHexInfoHash() {
+ return myHexString;
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataKeys.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataKeys.java
new file mode 100644
index 0000000..a74a33f
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataKeys.java
@@ -0,0 +1,23 @@
+package com.turn.ttorrent.common;
+
+@SuppressWarnings("WeakerAccess")
+public final class TorrentMetadataKeys {
+
+ public final static String MD5_SUM = "md5sum";
+ public final static String FILE_LENGTH = "length";
+ public final static String FILES = "files";
+ public final static String FILE_PATH = "path";
+ public final static String FILE_PATH_UTF8 = "path.utf-8";
+ public final static String COMMENT = "comment";
+ public final static String CREATED_BY = "created by";
+ public final static String ANNOUNCE = "announce";
+ public final static String PIECE_LENGTH = "piece length";
+ public final static String PIECES = "pieces";
+ public final static String CREATION_DATE_SEC = "creation date";
+ public final static String PRIVATE = "private";
+ public final static String NAME = "name";
+ public final static String INFO_TABLE = "info";
+ public final static String ANNOUNCE_LIST = "announce-list";
+ public final static String URL_LIST = "url-list";
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentParser.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentParser.java
new file mode 100644
index 0000000..81ea519
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentParser.java
@@ -0,0 +1,162 @@
+package com.turn.ttorrent.common;
+
+import com.turn.ttorrent.Constants;
+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 org.apache.commons.io.FileUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+
+public class TorrentParser {
+
+ public TorrentMetadata parseFromFile(File torrentFile) throws IOException {
+ byte[] fileContent = FileUtils.readFileToByteArray(torrentFile);
+ return parse(fileContent);
+ }
+
+ /**
+ * @param metadata binary .torrent content
+ * @return parsed metadata object. This parser also wraps single torrent as multi torrent with one file
+ * @throws InvalidBEncodingException if metadata has incorrect BEP format or missing required fields
+ * @throws RuntimeException It's wrapped io exception from bep decoder.
+ * This exception doesn't must to throw io exception because reading from
+ * byte array input stream cannot throw the exception
+ */
+ public TorrentMetadata parse(byte[] metadata) throws InvalidBEncodingException, RuntimeException {
+ final Map<String, BEValue> dictionaryMetadata;
+ try {
+ dictionaryMetadata = BDecoder.bdecode(new ByteArrayInputStream(metadata)).getMap();
+ } catch (InvalidBEncodingException e) {
+ throw e;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ final Map<String, BEValue> infoTable = getRequiredValueOrThrowException(dictionaryMetadata, INFO_TABLE).getMap();
+
+ final BEValue creationDateValue = dictionaryMetadata.get(CREATION_DATE_SEC);
+ final long creationDate = creationDateValue == null ? -1 : creationDateValue.getLong();
+
+ final String comment = getStringOrNull(dictionaryMetadata, COMMENT);
+ final String createdBy = getStringOrNull(dictionaryMetadata, CREATED_BY);
+ final String announceUrl = getStringOrNull(dictionaryMetadata, ANNOUNCE);
+ final List<List<String>> trackers = getTrackers(dictionaryMetadata);
+ final int pieceLength = getRequiredValueOrThrowException(infoTable, PIECE_LENGTH).getInt();
+ final byte[] piecesHashes = getRequiredValueOrThrowException(infoTable, PIECES).getBytes();
+
+ final boolean torrentContainsManyFiles = infoTable.get(FILES) != null;
+
+ final String dirName = getRequiredValueOrThrowException(infoTable, NAME).getString();
+
+ final List<TorrentFile> files = parseFiles(infoTable, torrentContainsManyFiles, dirName);
+
+ if (piecesHashes.length % Constants.PIECE_HASH_SIZE != 0)
+ throw new InvalidBEncodingException("Incorrect size of pieces hashes");
+
+ final int piecesCount = piecesHashes.length / Constants.PIECE_HASH_SIZE;
+
+ byte[] infoTableBytes;
+ try {
+ infoTableBytes = BEncoder.bencode(infoTable).array();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ return new TorrentMetadataImpl(
+ TorrentUtils.calculateSha1Hash(infoTableBytes),
+ trackers,
+ announceUrl,
+ creationDate,
+ comment,
+ createdBy,
+ dirName,
+ files,
+ piecesCount,
+ pieceLength,
+ piecesHashes
+ );
+ }
+
+ private List<TorrentFile> parseFiles(Map<String, BEValue> infoTable, boolean torrentContainsManyFiles, String name) throws InvalidBEncodingException {
+ if (!torrentContainsManyFiles) {
+ final BEValue md5Sum = infoTable.get(MD5_SUM);
+ return Collections.singletonList(new TorrentFile(
+ Collections.singletonList(name),
+ getRequiredValueOrThrowException(infoTable, FILE_LENGTH).getLong(),
+ md5Sum == null ? null : md5Sum.getString()
+ ));
+ }
+
+ List<TorrentFile> result = new ArrayList<TorrentFile>();
+ for (BEValue file : infoTable.get(FILES).getList()) {
+ Map<String, BEValue> fileInfo = file.getMap();
+ List<String> path = new ArrayList<String>();
+ BEValue filePathList = fileInfo.get(FILE_PATH_UTF8);
+ if (filePathList == null) {
+ filePathList = fileInfo.get(FILE_PATH);
+ }
+ for (BEValue pathElement : filePathList.getList()) {
+ path.add(pathElement.getString());
+ }
+ final BEValue md5Sum = infoTable.get(MD5_SUM);
+ result.add(new TorrentFile(
+ path,
+ fileInfo.get(FILE_LENGTH).getLong(),
+ md5Sum == null ? null : md5Sum.getString()));
+ }
+ return result;
+ }
+
+ @Nullable
+ private String getStringOrNull(Map<String, BEValue> dictionaryMetadata, String key) throws InvalidBEncodingException {
+ final BEValue value = dictionaryMetadata.get(key);
+ if (value == null) return null;
+ return value.getString();
+ }
+
+ @Nullable
+ private List<List<String>> getTrackers(Map<String, BEValue> dictionaryMetadata) throws InvalidBEncodingException {
+ final BEValue announceListValue = dictionaryMetadata.get(ANNOUNCE_LIST);
+ if (announceListValue == null) return null;
+ List<BEValue> announceList = announceListValue.getList();
+ List<List<String>> result = new ArrayList<List<String>>();
+ Set<String> allTrackers = new HashSet<String>();
+ for (BEValue tv : announceList) {
+ List<BEValue> trackers = tv.getList();
+ if (trackers.isEmpty()) {
+ continue;
+ }
+
+ List<String> tier = new ArrayList<String>();
+ for (BEValue tracker : trackers) {
+ final String url = tracker.getString();
+ if (!allTrackers.contains(url)) {
+ tier.add(url);
+ allTrackers.add(url);
+ }
+ }
+
+ if (!tier.isEmpty()) {
+ result.add(tier);
+ }
+ }
+ return result;
+ }
+
+ @NotNull
+ private BEValue getRequiredValueOrThrowException(Map<String, BEValue> map, String key) throws InvalidBEncodingException {
+ final BEValue value = map.get(key);
+ if (value == null)
+ throw new InvalidBEncodingException("Invalid metadata format. Map doesn't contain required field " + key);
+ return value;
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentSerializer.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentSerializer.java
new file mode 100644
index 0000000..1aa287a
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentSerializer.java
@@ -0,0 +1,93 @@
+package com.turn.ttorrent.common;
+
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+
+public class TorrentSerializer {
+
+ public byte[] serialize(TorrentMetadata metadata) throws IOException {
+ Map<String, BEValue> mapMetadata = new HashMap<String, BEValue>();
+ Map<String, BEValue> infoTable = new HashMap<String, BEValue>();
+
+ String announce = metadata.getAnnounce();
+ if (announce != null) mapMetadata.put(ANNOUNCE, new BEValue(announce));
+
+ putOptionalIfPresent(mapMetadata, COMMENT, metadata.getComment());
+ putOptionalIfPresent(mapMetadata, CREATED_BY, metadata.getCreatedBy());
+
+ if (metadata.getCreationDate().isPresent())
+ mapMetadata.put(CREATION_DATE_SEC, new BEValue(metadata.getCreationDate().get()));
+
+ List<BEValue> announceList = getAnnounceListAsBEValues(metadata.getAnnounceList());
+ if (announceList != null) {
+ mapMetadata.put(ANNOUNCE_LIST, new BEValue(announceList));
+ }
+ infoTable.put(PIECE_LENGTH, new BEValue(metadata.getPieceLength()));
+ infoTable.put(PIECES, new BEValue(metadata.getPiecesHashes()));
+ if (metadata.isPrivate()) {
+ infoTable.put(PRIVATE, new BEValue(1));
+ }
+
+ infoTable.put(NAME, new BEValue(metadata.getDirectoryName()));
+ if (metadata.getFiles().size() == 1) {
+ final TorrentFile torrentFile = metadata.getFiles().get(0);
+ infoTable.put(FILE_LENGTH, new BEValue(torrentFile.size));
+ putOptionalIfPresent(infoTable, MD5_SUM, torrentFile.md5Hash);
+ } else {
+ List<BEValue> files = new ArrayList<BEValue>();
+ for (TorrentFile torrentFile : metadata.getFiles()) {
+ Map<String, BEValue> entry = new HashMap<String, BEValue>();
+ entry.put(FILE_LENGTH, new BEValue(torrentFile.size));
+ putOptionalIfPresent(entry, MD5_SUM, torrentFile.md5Hash);
+ entry.put(FILE_PATH, new BEValue(mapStringListToBEValueList(torrentFile.relativePath)));
+ files.add(new BEValue(entry));
+ }
+ infoTable.put(FILES, new BEValue(files));
+ }
+
+ mapMetadata.put(INFO_TABLE, new BEValue(infoTable));
+
+ final ByteBuffer buffer = BEncoder.bencode(mapMetadata);
+ return buffer.array();
+ }
+
+ @Nullable
+ private List<BEValue> getAnnounceListAsBEValues(@Nullable List<List<String>> announceList) throws UnsupportedEncodingException {
+ if (announceList == null) return null;
+ List<BEValue> result = new ArrayList<BEValue>();
+
+ for (List<String> announceTier : announceList) {
+ List<BEValue> tier = mapStringListToBEValueList(announceTier);
+ if (!tier.isEmpty()) result.add(new BEValue(tier));
+ }
+
+ if (result.isEmpty()) return null;
+
+ return result;
+ }
+
+ private List<BEValue> mapStringListToBEValueList(List<String> list) throws UnsupportedEncodingException {
+ List<BEValue> result = new ArrayList<BEValue>();
+ for (String s : list) {
+ result.add(new BEValue(s));
+ }
+ return result;
+ }
+
+ private void putOptionalIfPresent(Map<String, BEValue> map, String key, Optional<String> optional) throws UnsupportedEncodingException {
+ if (!optional.isPresent()) return;
+ map.put(key, new BEValue(optional.get()));
+ }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentStatistic.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentStatistic.java
new file mode 100644
index 0000000..8465493
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentStatistic.java
@@ -0,0 +1,72 @@
+package com.turn.ttorrent.common;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Class store statistic for downloaded, uploaded and left bytes count.
+ */
+public class TorrentStatistic {
+
+ private final AtomicLong myUploadedBytes;
+ private final AtomicLong myDownloadedBytes;
+ private final AtomicLong myLeftBytes;
+
+ public TorrentStatistic() {
+ myDownloadedBytes = new AtomicLong();
+ myUploadedBytes = new AtomicLong();
+ myLeftBytes = new AtomicLong();
+ }
+
+ public TorrentStatistic(TorrentStatistic torrentStatistic){
+ myDownloadedBytes = new AtomicLong(torrentStatistic.getDownloadedBytes());
+ myUploadedBytes = new AtomicLong(torrentStatistic.getUploadedBytes());
+ myLeftBytes = new AtomicLong(torrentStatistic.getLeftBytes());
+ }
+
+ public long getUploadedBytes() {
+ return myUploadedBytes.get();
+ }
+
+ public long getDownloadedBytes() {
+ return myDownloadedBytes.get();
+ }
+
+ public long getLeftBytes() {
+ return myLeftBytes.get();
+ }
+
+ public void addUploaded(long delta) {
+ myUploadedBytes.addAndGet(delta);
+ }
+
+ public void addDownloaded(long delta) {
+ myDownloadedBytes.addAndGet(delta);
+ }
+
+ public void addLeft(long delta) {
+ myLeftBytes.addAndGet(delta);
+ }
+
+ public void setLeft(long value) {
+ myLeftBytes.set(value);
+ }
+
+ public void setUploaded(long value) {
+ myUploadedBytes.set(value);
+ }
+
+ public void setDownloaded(long value) {
+ myDownloadedBytes.set(value);
+ }
+
+ public long getPercentageDownloaded(){
+ long downloadedBytes = getDownloadedBytes();
+ long totalBytes = getTotalBytes();
+ return (downloadedBytes * 100) / totalBytes;
+ }
+
+ public long getTotalBytes(){
+ return getDownloadedBytes() + getLeftBytes();
+ }
+
+}
\ No newline at end of file
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentUtils.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentUtils.java
new file mode 100644
index 0000000..c2d6d83
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentUtils.java
@@ -0,0 +1,50 @@
+package com.turn.ttorrent.common;
+
+import org.apache.commons.codec.digest.DigestUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class TorrentUtils {
+
+ private final static char[] HEX_SYMBOLS = "0123456789ABCDEF".toCharArray();
+
+ /**
+ * @param data for hashing
+ * @return sha 1 hash of specified data
+ */
+ public static byte[] calculateSha1Hash(byte[] data) {
+ return DigestUtils.sha1(data);
+ }
+
+ /**
+ * 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) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = HEX_SYMBOLS[v >>> 4];
+ hexChars[j * 2 + 1] = HEX_SYMBOLS[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ public static boolean isTrackerLessInfo(AnnounceableInformation information) {
+ return information.getAnnounce() == null && information.getAnnounceList() == null;
+ }
+
+ public static List<String> getTorrentFileNames(TorrentMetadata metadata) {
+ List<String> result = new ArrayList<String>();
+
+ for (TorrentFile torrentFile : metadata.getFiles()) {
+ result.add(torrentFile.getRelativePathAsString());
+ }
+
+ return result;
+ }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/CommonHashingCalculator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/CommonHashingCalculator.java
new file mode 100644
index 0000000..006cd17
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/CommonHashingCalculator.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+class CommonHashingCalculator {
+
+ static final CommonHashingCalculator INSTANCE = new CommonHashingCalculator();
+
+ List<Long> processDataSources(List<DataSourceHolder> sources,
+ int pieceSize,
+ Processor processor) throws IOException {
+ List<Long> sourcesSizes = new ArrayList<Long>();
+ byte[] buffer = new byte[pieceSize];
+ int read = 0;
+ for (DataSourceHolder source : sources) {
+ long streamSize = 0;
+ InputStream stream = source.getStream();
+ try {
+ while (true) {
+ int readFromStream = stream.read(buffer, read, buffer.length - read);
+ if (readFromStream < 0) {
+ break;
+ }
+ streamSize += readFromStream;
+ read += readFromStream;
+ if (read == buffer.length) {
+ processor.process(buffer);
+ read = 0;
+ }
+ }
+ } finally {
+ source.close();
+ sourcesSizes.add(streamSize);
+ }
+ }
+ if (read > 0) {
+ processor.process(Arrays.copyOf(buffer, read));
+ }
+
+ return sourcesSizes;
+ }
+
+ interface Processor {
+
+ /**
+ * Invoked when next piece is received from data source. Array will be overwritten
+ * after invocation this method (next piece will be read in same array). So multi-threading
+ * implementations must create copy of array and work with the copy.
+ *
+ * @param buffer byte array which contains bytes from data sources.
+ * length of array equals piece size excluding last piece
+ */
+ void process(byte[] buffer);
+
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/DataSourceHolder.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/DataSourceHolder.java
new file mode 100644
index 0000000..7fb12d8
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/DataSourceHolder.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface DataSourceHolder extends Closeable {
+
+ /**
+ * provides {@link InputStream} associated with the holder. Holder can just store reference to stream or create
+ * new stream from some source (e.g. {@link java.io.FileInputStream} from {@link java.io.File}) on first invocation.
+ *
+ * @return {@link InputStream} associated with the holder.
+ * @throws IOException if io error occurs in creating new stream from source.
+ * IO exception can be thrown only on first invocation
+ */
+ InputStream getStream() throws IOException;
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/HashingResult.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/HashingResult.java
new file mode 100644
index 0000000..c97e3b2
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/HashingResult.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import java.util.List;
+
+public class HashingResult {
+
+ private final List<byte[]> hashes;
+ private final List<Long> sourceSizes;
+
+ public HashingResult(List<byte[]> hashes, List<Long> sourceSizes) {
+ this.hashes = hashes;
+ this.sourceSizes = sourceSizes;
+ }
+
+ public List<byte[]> getHashes() {
+ return hashes;
+ }
+
+ public List<Long> getSourceSizes() {
+ return sourceSizes;
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MetadataBuilder.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MetadataBuilder.java
new file mode 100644
index 0000000..71d07da
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MetadataBuilder.java
@@ -0,0 +1,497 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentParser;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import java.io.*;
+import java.util.*;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+
+@SuppressWarnings({"unused", "WeakerAccess"})
+public class MetadataBuilder {
+
+ private final static Logger logger = TorrentLoggerFactory.getLogger(MetadataBuilder.class);
+ private final static String DEFAULT_CREATED_BY = "ttorrent library";
+
+ //root dictionary
+ @NotNull
+ private String announce = "";
+ @NotNull
+ private List<List<String>> announceList = new ArrayList<List<String>>();
+ private long creationDate = -1;
+ @NotNull
+ private String comment = "";
+ @NotNull
+ private String createdBy = DEFAULT_CREATED_BY;
+ @NotNull
+ private List<String> webSeedUrlList = new ArrayList<String>();
+ //end root dictionary
+
+ //info dictionary
+ private int pieceLength = 512 * 1024;//512kb by default
+ private boolean isPrivate = false;
+ @NotNull
+ private List<String> filesPaths = new ArrayList<String>();
+ @Nullable
+ private HashingResult hashingResult = null;
+ @NotNull
+ private List<DataSourceHolder> dataSources = new ArrayList<DataSourceHolder>();
+ @NotNull
+ private String directoryName = "";
+ //end info dictionary
+
+ //fields which store some internal information
+ @NotNull
+ private PiecesHashesCalculator piecesHashesCalculator = new SingleThreadHashesCalculator();
+ //end
+
+ /**
+ * set main announce tracker URL if you use single tracker.
+ * In case with many trackers use {@link #addTracker(String)}
+ * and {@link #newTier()}. Then as main announce will be selected first tracker.
+ * You can specify main announce using this method for override this behaviour
+ * Torrent clients which support BEP12 extension will ignore main announce.
+ *
+ * @param announce announce URL for the tracker
+ */
+ public MetadataBuilder setTracker(String announce) {
+ this.announce = announce;
+ return this;
+ }
+
+
+ /**
+ * Multi-tracker Metadata Extension. Add new tracker URL to current tier.
+ * This method will create first tier automatically if it doesn't exist
+ * You can find more information about this extension in documentation
+ * <a href="http://bittorrent.org/beps/bep_0012.html">http://bittorrent.org/beps/bep_0012.html</a>
+ *
+ * @param url tracker url
+ */
+ public MetadataBuilder addTracker(String url) {
+ initFirstTier();
+ announceList.get(announceList.size() - 1).add(url);
+ return this;
+ }
+
+ /**
+ * Multi-tracker Metadata Extension. Add all trackers to current tier.
+ * This method will create first tier automatically if it doesn't exist
+ * You can find more information about this extension in documentation
+ * <a href="http://bittorrent.org/beps/bep_0012.html">http://bittorrent.org/beps/bep_0012.html</a>
+ *
+ * @param trackers collections of trackers URLs
+ */
+ public MetadataBuilder addTrackers(Collection<String> trackers) {
+ initFirstTier();
+ announceList.get(announceList.size() - 1).addAll(trackers);
+ return this;
+ }
+
+ /**
+ * Multi-tracker Metadata Extension. Create new tier for adding tracker using {@link #addTracker(String)} method
+ * If you don't add at least one tracker on the tier this tier will be removed in building metadata
+ * You can find more information about this extension in documentation
+ * <a href="http://bittorrent.org/beps/bep_0012.html">http://bittorrent.org/beps/bep_0012.html</a>
+ */
+ public MetadataBuilder newTier() {
+ announceList.add(new ArrayList<String>());
+ return this;
+ }
+
+ /**
+ * Web Seeding Metadata.
+ * Web seeding url as defined by <a href='http://bittorrent.org/beps/bep_0019.html'>bep 0019</a>
+ * @param url URL to add for web seeding
+ */
+ public MetadataBuilder addWebSeedUrl(String url) {
+ webSeedUrlList.add(url);
+ return this;
+ }
+
+ /**
+ * Set the creation time of the torrent in standard UNIX epoch format.
+ *
+ * @param creationTime the seconds since January 1, 1970, 00:00:00 UTC.
+ */
+ public MetadataBuilder setCreationTime(int creationTime) {
+ this.creationDate = creationTime;
+ return this;
+ }
+
+ /**
+ * Set free-form textual comment of the author
+ */
+ public MetadataBuilder setComment(String comment) {
+ this.comment = comment;
+ return this;
+ }
+
+ /**
+ * Set program name which is used for creating torrent file.
+ */
+ public MetadataBuilder setCreatedBy(String createdBy) {
+ this.createdBy = createdBy;
+ return this;
+ }
+
+ /**
+ * Set {@link PiecesHashesCalculator} instance for calculating hashes. In rare cases user's
+ * implementation can be used for increasing hashing performance
+ */
+ public MetadataBuilder setPiecesHashesCalculator(@NotNull PiecesHashesCalculator piecesHashesCalculator) {
+ this.piecesHashesCalculator = piecesHashesCalculator;
+ return this;
+ }
+
+ /**
+ * Set length int bytes of one piece. By default is used 512KB.
+ * Larger piece size reduces size of .torrent file but cause inefficiency
+ * (torrent-client need to download full piece from peer for validating)
+ * and too-small piece sizes cause large .torrent metadata file.
+ * Recommended size is between 256KB and 1MB.
+ */
+ public MetadataBuilder setPieceLength(int pieceLength) {
+ this.pieceLength = pieceLength;
+ return this;
+ }
+
+ /**
+ * Set the name of the directory in which to store all the files.
+ * If {@link #directoryName} isn't empty then multi-file torrent will be created, otherwise single-file
+ */
+ public MetadataBuilder setDirectoryName(@NotNull String directoryName) {
+ this.directoryName = directoryName;
+ return this;
+ }
+
+ /**
+ * add custom source in torrent with custom path. Path can be separated with any slash.
+ *
+ * @param closeAfterBuild if true then source stream will be closed after {@link #build()} invocation
+ */
+ public MetadataBuilder addDataSource(@NotNull InputStream dataSource, String path, boolean closeAfterBuild) {
+ checkHashingResultIsNotSet();
+ filesPaths.add(path);
+ dataSources.add(new StreamBasedHolderImpl(dataSource, closeAfterBuild));
+ return this;
+ }
+
+ /**
+ * add custom source in torrent with custom path. Path can be separated with any slash.
+ */
+ public MetadataBuilder addDataSource(@NotNull InputStream dataSource, String path) {
+ addDataSource(dataSource, path, true);
+ return this;
+ }
+
+ /**
+ * add specified file in torrent with custom path. The file will be stored in .torrent
+ * by specified path. Path can be separated with any slash. In case of single-file torrent
+ * this path will be used as name of source file
+ */
+ public MetadataBuilder addFile(@NotNull File source, @NotNull String path) {
+ if (!source.isFile()) {
+ throw new IllegalArgumentException(source + " is not exist");
+ }
+ checkHashingResultIsNotSet();
+ filesPaths.add(path);
+ dataSources.add(new FileSourceHolder(source));
+ return this;
+ }
+
+ private void checkHashingResultIsNotSet() {
+ if (hashingResult != null) {
+ throw new IllegalStateException("Unable to add new source when hashes are set manually");
+ }
+ }
+
+ /**
+ * add specified file in torrent. In case of multi-torrent this file will be downloaded to
+ * {@link #directoryName}. In single-file torrent this file will be downloaded in download folder
+ */
+ public MetadataBuilder addFile(@NotNull File source) {
+ return addFile(source, source.getName());
+ }
+
+ /**
+ * allow to create information about files via speicified hashes, files paths and files lengths.
+ * Using of this method is not compatible with using source-based methods
+ * ({@link #addFile(File)}, {@link #addDataSource(InputStream, String, boolean)}, etc
+ * because it's not possible to calculate concat this hashes and calculated hashes.
+ * each byte array in hashes list should have {{@link Constants#PIECE_HASH_SIZE}} length
+ *
+ * @param hashes list of files hashes in same order as files in files paths list
+ * @param filesPaths list of files paths
+ * @param filesLengths list of files lengths in same order as files in files paths list
+ */
+ public MetadataBuilder setFilesInfo(@NotNull List<byte[]> hashes,
+ @NotNull List<String> filesPaths,
+ @NotNull List<Long> filesLengths) {
+ if (dataSources.size() != 0) {
+ throw new IllegalStateException("Unable to add hashes-based files info. Some data sources already added");
+ }
+ this.filesPaths.clear();
+ this.filesPaths.addAll(filesPaths);
+ this.hashingResult = new HashingResult(hashes, filesLengths);
+ return this;
+ }
+
+ /**
+ * marks torrent as private
+ *
+ * @see <a href="http://bittorrent.org/beps/bep_0027.html">http://bittorrent.org/beps/bep_0027.html</a>
+ */
+ public void doPrivate() {
+ isPrivate = true;
+ }
+
+ /**
+ * marks torrent as public
+ *
+ * @see <a href="http://bittorrent.org/beps/bep_0027.html">http://bittorrent.org/beps/bep_0027.html</a>
+ */
+ public void doPublic() {
+ isPrivate = false;
+ }
+
+ /**
+ * @return new {@link TorrentMetadata} instance with builder's fields
+ * @throws IOException if IO error occurs on reading from source streams and files
+ * @throws IllegalStateException if builder's state is incorrect (e.g. missing required fields)
+ */
+ public TorrentMetadata build() throws IOException {
+ return new TorrentParser().parse(buildBinary());
+ }
+
+ /**
+ * @return binary representation of metadata
+ * @throws IOException if IO error occurs on reading from source streams and files
+ * @throws IllegalStateException if builder's state is incorrect (e.g. missing required fields)
+ */
+ public byte[] buildBinary() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ BEncoder.bencode(buildBEP(), out);
+ return out.toByteArray();
+ }
+
+ /**
+ * @return BEP-encoded dictionary of metadata
+ * @throws IOException if IO error occurs on reading from source streams and files
+ * @throws IllegalStateException if builder's state is incorrect (e.g. missing required fields)
+ */
+ public BEValue buildBEP() throws IOException {
+ return buildAndCloseResources();
+ }
+
+ private BEValue buildAndCloseResources() throws IOException {
+ try {
+ return doBuild();
+ } finally {
+ closeAllSources();
+ }
+ }
+
+ private BEValue doBuild() throws IOException {
+ dropEmptyTiersFromAnnounce();
+
+ if (announce.isEmpty() && !announceList.isEmpty()) {
+ announce = announceList.get(0).get(0);
+ }
+ if (filesPaths.size() == 0) {
+ throw new IllegalStateException("Unable to create metadata without sources. Use addSource() method for adding sources");
+ }
+ final boolean isSingleMode = filesPaths.size() == 1 && directoryName.isEmpty();
+ final String name;
+ if (!directoryName.isEmpty()) {
+ name = directoryName;
+ } else {
+ if (isSingleMode) {
+ name = filesPaths.get(0);
+ } else {
+ throw new IllegalStateException("Missing required field 'name'. Use setDirectoryName() method for specifying name of torrent");
+ }
+ }
+
+ Map<String, BEValue> torrent = new HashMap<String, BEValue>();
+ if (!announce.isEmpty()) torrent.put(ANNOUNCE, new BEValue(announce));
+ if (!announceList.isEmpty()) torrent.put(ANNOUNCE_LIST, wrapAnnounceList());
+ if (creationDate > 0) {
+ torrent.put(CREATION_DATE_SEC, new BEValue(creationDate));
+ }
+
+ if (!comment.isEmpty()) torrent.put(COMMENT, new BEValue(comment));
+ if (!createdBy.isEmpty()) torrent.put(CREATED_BY, new BEValue(createdBy));
+ if (!webSeedUrlList.isEmpty()) torrent.put(URL_LIST, wrapStringList(webSeedUrlList));
+
+ HashingResult hashingResult = this.hashingResult == null ?
+ piecesHashesCalculator.calculateHashes(dataSources, pieceLength) :
+ this.hashingResult;
+
+ Map<String, BEValue> info = new HashMap<String, BEValue>();
+ info.put(PIECE_LENGTH, new BEValue(pieceLength));
+ info.put(PIECES, concatHashes(hashingResult.getHashes()));
+ info.put(PRIVATE, new BEValue(isPrivate ? 1 : 0));
+ info.put(NAME, new BEValue(name));
+ if (isSingleMode) {
+ Long sourceSize = hashingResult.getSourceSizes().get(0);
+ info.put(FILE_LENGTH, new BEValue(sourceSize));
+ } else {
+ List<BEValue> files = getFilesList(hashingResult);
+ info.put(FILES, new BEValue(files));
+ }
+ torrent.put(INFO_TABLE, new BEValue(info));
+
+ return new BEValue(torrent);
+ }
+
+ private List<BEValue> getFilesList(HashingResult hashingResult) throws UnsupportedEncodingException {
+ ArrayList<BEValue> result = new ArrayList<BEValue>();
+ for (int i = 0; i < filesPaths.size(); i++) {
+ Map<String, BEValue> file = new HashMap<String, BEValue>();
+ Long sourceSize = hashingResult.getSourceSizes().get(i);
+ String fullPath = filesPaths.get(i);
+ List<BEValue> filePath = new ArrayList<BEValue>();
+ for (String path : fullPath.replace("\\", "/").split("/")) {
+ filePath.add(new BEValue(path));
+ }
+ file.put(FILE_PATH, new BEValue(filePath));
+ file.put(FILE_LENGTH, new BEValue(sourceSize));
+ result.add(new BEValue(file));
+ }
+ return result;
+ }
+
+ private BEValue concatHashes(List<byte[]> hashes) throws UnsupportedEncodingException {
+ StringBuilder sb = new StringBuilder();
+ for (byte[] hash : hashes) {
+ sb.append(new String(hash, Constants.BYTE_ENCODING));
+ }
+ return new BEValue(sb.toString(), Constants.BYTE_ENCODING);
+ }
+
+ private BEValue wrapStringList(List<String> lst) throws UnsupportedEncodingException {
+ List<BEValue> result = new LinkedList<BEValue>();
+ for(String s : lst) {
+ result.add(new BEValue(s));
+ }
+ return new BEValue(result);
+ }
+
+ private BEValue wrapAnnounceList() throws UnsupportedEncodingException {
+ List<BEValue> result = new LinkedList<BEValue>();
+ for (List<String> tier : announceList) {
+ result.add(wrapStringList(tier));
+ }
+ return new BEValue(result);
+ }
+
+ private void dropEmptyTiersFromAnnounce() {
+ Iterator<List<String>> iterator = announceList.iterator();
+ while (iterator.hasNext()) {
+ List<String> tier = iterator.next();
+ if (tier.isEmpty()) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private void closeAllSources() {
+ for (DataSourceHolder sourceHolder : dataSources) {
+ try {
+ sourceHolder.close();
+ } catch (Throwable e) {
+ logger.error("Error in closing data source " + sourceHolder, e);
+ }
+ }
+ }
+
+ private void initFirstTier() {
+ if (announceList.isEmpty()) {
+ newTier();
+ }
+ }
+
+ private static class FileSourceHolder implements DataSourceHolder {
+ @Nullable
+ private FileInputStream fis;
+ @NotNull
+ private final File source;
+
+ public FileSourceHolder(@NotNull File source) {
+ this.source = source;
+ }
+
+ @Override
+ public InputStream getStream() throws IOException {
+ if (fis == null) {
+ fis = new FileInputStream(source);
+ }
+ return fis;
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (fis != null) {
+ fis.close();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Data source for file stream " + fis;
+ }
+ }
+
+ private static class StreamBasedHolderImpl implements DataSourceHolder {
+ private final InputStream source;
+ private final boolean closeAfterBuild;
+
+ public StreamBasedHolderImpl(InputStream source, boolean closeAfterBuild) {
+ this.source = source;
+ this.closeAfterBuild = closeAfterBuild;
+ }
+
+ @Override
+ public InputStream getStream() {
+ return source;
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (closeAfterBuild) {
+ source.close();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Data source for user's stream " + source;
+ }
+ }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MultiThreadHashesCalculator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MultiThreadHashesCalculator.java
new file mode 100644
index 0000000..cd6b4b2
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MultiThreadHashesCalculator.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import com.turn.ttorrent.common.TorrentUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+public class MultiThreadHashesCalculator implements PiecesHashesCalculator {
+
+ private final ExecutorService executor;
+ private final int maxInMemoryPieces;
+
+ public MultiThreadHashesCalculator(ExecutorService executor, int maxInMemoryPieces) {
+ this.executor = executor;
+ this.maxInMemoryPieces = maxInMemoryPieces;
+ }
+
+ @Override
+ public HashingResult calculateHashes(List<DataSourceHolder> sources, int pieceSize) throws IOException {
+ final List<byte[]> hashes = new ArrayList<byte[]>();
+ final List<Future<byte[]>> futures = new ArrayList<Future<byte[]>>();
+ List<Long> sourcesSizes = CommonHashingCalculator.INSTANCE.processDataSources(
+ sources,
+ pieceSize,
+ new CommonHashingCalculator.Processor() {
+ @Override
+ public void process(final byte[] buffer) {
+ awaitHashesCalculationAndStore(futures, hashes, maxInMemoryPieces);
+ final byte[] bufferCopy = Arrays.copyOf(buffer, buffer.length);
+ futures.add(executor.submit(new Callable<byte[]>() {
+ @Override
+ public byte[] call() {
+ return TorrentUtils.calculateSha1Hash(bufferCopy);
+ }
+ }));
+ }
+ }
+ );
+ awaitHashesCalculationAndStore(futures, hashes, 0);
+
+ return new HashingResult(hashes, sourcesSizes);
+ }
+
+ private void awaitHashesCalculationAndStore(List<Future<byte[]>> futures, List<byte[]> hashes, int count) {
+ while (futures.size() > count) {
+ byte[] hash;
+ try {
+ Future<byte[]> future = futures.remove(0);
+ hash = future.get();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ hashes.add(hash);
+ }
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/PiecesHashesCalculator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/PiecesHashesCalculator.java
new file mode 100644
index 0000000..dda7396
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/PiecesHashesCalculator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface PiecesHashesCalculator {
+
+ /**
+ * calculates sha1 hashes of each chunk with specified piece size
+ * and returns list of hashes and stream's sizes. If one stream is ended and piece size threshold is not reached
+ * implementation must read bytes from next stream
+ * For example if source list is 3 streams with next bytes:
+ * first stream: [1,2,3]
+ * second stream: [4,5,6,7]
+ * third stream: [8,9]
+ * and pieceSize = 4
+ * result must contain source size [3,4,2] and hashes: [sha1(1,2,3,4), sha1(5,6,7,8), sha1(9)]
+ *
+ * @param sources list of input stream's providers
+ * @param pieceSize size of one piece
+ * @return see above
+ * @throws IOException if IO error occurs in reading from streams
+ */
+ HashingResult calculateHashes(List<DataSourceHolder> sources, int pieceSize) throws IOException;
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/SingleThreadHashesCalculator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/SingleThreadHashesCalculator.java
new file mode 100644
index 0000000..0446372
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/SingleThreadHashesCalculator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import com.turn.ttorrent.common.TorrentUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SingleThreadHashesCalculator implements PiecesHashesCalculator {
+
+ @Override
+ public HashingResult calculateHashes(List<DataSourceHolder> sources, int pieceSize) throws IOException {
+ final List<byte[]> hashes = new ArrayList<byte[]>();
+ List<Long> sourcesSizes = CommonHashingCalculator.INSTANCE.processDataSources(
+ sources,
+ pieceSize,
+ new CommonHashingCalculator.Processor() {
+ @Override
+ public void process(byte[] buffer) {
+ byte[] hash = TorrentUtils.calculateSha1Hash(buffer);
+ hashes.add(hash);
+ }
+ }
+ );
+
+ return new HashingResult(hashes, sourcesSizes);
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/StringUtils.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/StringUtils.java
new file mode 100644
index 0000000..dfe8c8c
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/StringUtils.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import java.util.Iterator;
+
+public final class StringUtils {
+
+ public static String join(String delimiter, Iterable<? extends CharSequence> iterable) {
+ Iterator<? extends CharSequence> iterator = iterable.iterator();
+ StringBuilder sb = new StringBuilder();
+ if (iterator.hasNext()) {
+ sb.append(iterator.next());
+ }
+ while (iterator.hasNext()) {
+ sb.append(delimiter).append(iterator.next());
+ }
+ return sb.toString();
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceRequestMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceRequestMessage.java
new file mode 100644
index 0000000..92a9110
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceRequestMessage.java
@@ -0,0 +1,171 @@
+package com.turn.ttorrent.common.protocol;
+
+/**
+ * Base interface for announce request messages.
+ * 公告请求消息的基础接口。
+ *
+ * <p>
+ * This interface must be implemented by all subtypes of announce request
+ * messages for the various tracker protocols.
+ * </p>
+ *
+ * For details information see <a href="https://wiki.theory.org/index.php/BitTorrentSpecification#Tracker_Request_Parameters"></a>
+ *
+ * @author mpetazzoni
+ */
+public interface AnnounceRequestMessage {
+
+ int DEFAULT_NUM_WANT = 50;
+
+ /**
+ * Announce request event types.
+ *
+ * <p>
+ * 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).
+ * </p>
+ *
+ * <p>
+ * 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.
+ * </p>
+ */
+ 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;
+ }
+ }
+
+ /**
+ * @return SHA1 hash of value associated with "info" key in .torrent file
+ */
+ byte[] getInfoHash();
+
+ /**
+ * String representation of {@link #getInfoHash} where each byte replaced by hex-string value with lead zero
+ * for example for byte array [1, 2, 15, -2] this method must return 01020FFE
+ *
+ * @return String representation of {@link #getInfoHash}
+ */
+ String getHexInfoHash();
+
+ /**
+ * @return peer id generated by current client
+ */
+ byte[] getPeerId();
+
+ /**
+ * @return String representation of {@link #getPeerId}. It's similarly {@link #getHexInfoHash()} method
+ */
+ String getHexPeerId();
+
+ /**
+ * @return current client port on which it listens for new connections
+ */
+ int getPort();
+
+ /**
+ * @return count of uploaded bytes for current torrent by current peer after sending STARTED event to the tracker
+ */
+ long getUploaded();
+
+ /**
+ * @return count of downloaded bytes for current torrent by current peer after sending STARTED event to the tracker
+ */
+ long getDownloaded();
+
+ /**
+ * @return count of bytes which client must be download
+ */
+ long getLeft();
+
+ /**
+ * Tells that it's compact request.
+ * In this case tracker return compact peers list which contains only address and port without peer id,
+ * but according to specification some trackers can ignore this parameter
+ *
+ * @return true if it's compact request.
+ */
+ boolean isCompact();
+
+ /**
+ * Tells that tracker can omit peer id field in response. This parameter is ignored if {@link #isCompact()} method
+ * return true
+ *
+ * @return true if tracker can omit peer id
+ */
+ boolean canOmitPeerId();
+
+ /**
+ * @return event of current request
+ */
+ RequestEvent getEvent();
+
+ /**
+ * Optional. If it's not specified thet tracker get ip address from request
+ *
+ * @return current client address on which it listens for new connections
+ */
+ String getIp();
+
+ /**
+ * Optional. If it's not specified (value is zero or negative) tracker return default peers count. As a rule this count is 50
+ *
+ * @return count of peers which client want to get from tracker
+ */
+ int getNumWant();
+
+ /**
+ * Optional. Contains key of current client. Client can use this key for confirm for tracker that it's old client
+ * after change IP address
+ *
+ * @return key of current client.
+ */
+ String getKey();
+
+ /**
+ * Optional. If previous response from tracker contains tracker id field, then client must send this value here
+ *
+ * @return previous tracker id
+ */
+ String getTrackerId();
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceResponseMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceResponseMessage.java
new file mode 100644
index 0000000..89bf212
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceResponseMessage.java
@@ -0,0 +1,26 @@
+package com.turn.ttorrent.common.protocol;
+
+import com.turn.ttorrent.common.Peer;
+
+import java.util.List;
+
+/**
+ * Base interface for announce response messages.
+ *
+ * <p>
+ * This interface must be implemented by all subtypes of announce response
+ * messages for the various tracker protocols.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+public interface AnnounceResponseMessage {
+
+ int getInterval();
+
+ int getComplete();
+
+ int getIncomplete();
+
+ List<Peer> getPeers();
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/PeerMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/PeerMessage.java
new file mode 100644
index 0000000..6ffba62
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/PeerMessage.java
@@ -0,0 +1,692 @@
+/**
+ * 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.Constants;
+import com.turn.ttorrent.common.TorrentInfo;
+
+import java.nio.ByteBuffer;
+import java.text.ParseException;
+import java.util.BitSet;
+
+/**
+ * BitTorrent peer protocol messages representations.
+ *
+ * <p>
+ * This class and its <em>*Messages</em> 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.
+ * </p>
+ *
+ * @author mpetazzoni
+ * @see <a href="http://wiki.theory.org/BitTorrentSpecification#Peer_wire_protocol_.28TCP.29">BitTorrent peer wire protocol</a>
+ */
+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.
+ *
+ * <p>
+ * Note that the keep-alive messages don't actually have an type ID defined
+ * in the protocol as they are of length 0.
+ * </p>
+ */
+ 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.
+ *
+ * <p>
+ * 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.
+ * </p>
+ */
+ public ByteBuffer getData() {
+ return this.data.duplicate();
+ }
+
+ /**
+ * Validate that this message makes sense for the torrent it's related to.
+ *
+ * <p>
+ * This method is meant to be overloaded by distinct message types, where
+ * it makes sense. Otherwise, it defaults to true.
+ * </p>
+ *
+ * @param torrent The torrent this message is about.
+ */
+ public PeerMessage validate(TorrentInfo torrent)
+ throws MessageValidationException {
+ return this;
+ }
+
+ public String toString() {
+ return this.getType().name();
+ }
+
+ /**
+ * Parse the given buffer into a peer protocol message.
+ *
+ * <p>
+ * Parses the provided byte array and builds the corresponding PeerMessage
+ * subclass object.
+ * </p>
+ *
+ * @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, TorrentInfo 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.
+ *
+ * <len=0000>
+ */
+ 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,
+ TorrentInfo torrent) throws MessageValidationException {
+ return (KeepAliveMessage) new KeepAliveMessage(buffer)
+ .validate(torrent);
+ }
+
+ public static KeepAliveMessage craft() {
+ ByteBuffer buffer = ByteBuffer.allocate(
+ MESSAGE_LENGTH_FIELD_SIZE + KeepAliveMessage.BASE_SIZE);
+ buffer.putInt(KeepAliveMessage.BASE_SIZE);
+ return new KeepAliveMessage(buffer);
+ }
+ }
+
+ /**
+ * Choke message.
+ *
+ * <len=0001><id=0>
+ */
+ 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,
+ TorrentInfo torrent) throws MessageValidationException {
+ return (ChokeMessage) new ChokeMessage(buffer)
+ .validate(torrent);
+ }
+
+ public static ChokeMessage craft() {
+ ByteBuffer buffer = ByteBuffer.allocate(
+ MESSAGE_LENGTH_FIELD_SIZE + ChokeMessage.BASE_SIZE);
+ buffer.putInt(ChokeMessage.BASE_SIZE);
+ buffer.put(PeerMessage.Type.CHOKE.getTypeByte());
+ return new ChokeMessage(buffer);
+ }
+ }
+
+ /**
+ * Unchoke message.
+ *
+ * <len=0001><id=1>
+ */
+ 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,
+ TorrentInfo torrent) throws MessageValidationException {
+ return (UnchokeMessage) new UnchokeMessage(buffer)
+ .validate(torrent);
+ }
+
+ public static UnchokeMessage craft() {
+ ByteBuffer buffer = ByteBuffer.allocate(
+ MESSAGE_LENGTH_FIELD_SIZE + UnchokeMessage.BASE_SIZE);
+ buffer.putInt(UnchokeMessage.BASE_SIZE);
+ buffer.put(PeerMessage.Type.UNCHOKE.getTypeByte());
+ return new UnchokeMessage(buffer);
+ }
+ }
+
+ /**
+ * Interested message.
+ *
+ * <len=0001><id=2>
+ */
+ 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,
+ TorrentInfo torrent) throws MessageValidationException {
+ return (InterestedMessage) new InterestedMessage(buffer)
+ .validate(torrent);
+ }
+
+ public static InterestedMessage craft() {
+ ByteBuffer buffer = ByteBuffer.allocate(
+ 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.
+ *
+ * <len=0001><id=3>
+ */
+ 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,
+ TorrentInfo torrent) throws MessageValidationException {
+ return (NotInterestedMessage) new NotInterestedMessage(buffer)
+ .validate(torrent);
+ }
+
+ public static NotInterestedMessage craft() {
+ ByteBuffer buffer = ByteBuffer.allocate(
+ 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.
+ *
+ * <len=0005><id=4><piece index=xxxx>
+ */
+ 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(TorrentInfo torrent)
+ throws MessageValidationException {
+ if (this.piece >= 0 && this.piece < torrent.getPieceCount()) {
+ return this;
+ }
+
+ throw new MessageValidationException(this);
+ }
+
+ public static HaveMessage parse(ByteBuffer buffer,
+ TorrentInfo torrent) throws MessageValidationException {
+ return new HaveMessage(buffer, buffer.getInt())
+ .validate(torrent);
+ }
+
+ public static HaveMessage craft(int piece) {
+ ByteBuffer buffer = ByteBuffer.allocate(
+ 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.
+ *
+ * <len=0001+X><id=5><bitfield>
+ */
+ 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(TorrentInfo torrent)
+ throws MessageValidationException {
+ if (this.bitfield.length() <= torrent.getPieceCount()) {
+ return this;
+ }
+
+ throw new MessageValidationException(this);
+ }
+
+ public static BitfieldMessage parse(ByteBuffer buffer,
+ TorrentInfo 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) {
+ int len = availablePieces.length() / 8;
+ if (availablePieces.length() % 8 > 0) len++;
+ byte[] bitfield = new byte[len];
+ for (int i = availablePieces.nextSetBit(0); i >= 0;
+ i = availablePieces.nextSetBit(i + 1)) {
+ bitfield[i / 8] |= 1 << (7 - (i % 8));
+ }
+
+ ByteBuffer buffer = ByteBuffer.allocate(
+ 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.
+ *
+ * <len=00013><id=6><piece index><block offset><block length>
+ */
+ 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 long mySendTime;
+
+ private RequestMessage(ByteBuffer buffer, int piece,
+ int offset, int length) {
+ super(Type.REQUEST, buffer);
+ this.piece = piece;
+ this.offset = offset;
+ this.length = length;
+ mySendTime = System.currentTimeMillis();
+ }
+
+ public int getPiece() {
+ return this.piece;
+ }
+
+ public int getOffset() {
+ return this.offset;
+ }
+
+ public int getLength() {
+ return this.length;
+ }
+
+ public long getSendTime() {
+ return mySendTime;
+ }
+
+ public void renew() {
+ mySendTime = System.currentTimeMillis();
+ }
+
+ @Override
+ public RequestMessage validate(TorrentInfo torrent)
+ throws MessageValidationException {
+ if (this.piece >= 0 && this.piece < torrent.getPieceCount() &&
+ this.offset + this.length <=
+ torrent.getPieceSize(this.piece)) {
+ return this;
+ }
+
+ throw new MessageValidationException(this);
+ }
+
+ public static RequestMessage parse(ByteBuffer buffer,
+ TorrentInfo 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.allocate(
+ 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.
+ *
+ * <len=0009+X><id=7><piece index><block offset><block data>
+ */
+ 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(TorrentInfo torrent)
+ throws MessageValidationException {
+ if (this.piece >= 0 && this.piece < torrent.getPieceCount() &&
+ this.offset + this.block.limit() <=
+ torrent.getPieceSize(this.piece)) {
+ return this;
+ }
+
+ throw new MessageValidationException(this);
+ }
+
+ public static PieceMessage parse(ByteBuffer buffer,
+ TorrentInfo 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 buffer) {
+ return new PieceMessage(buffer, piece, offset, Constants.EMPTY_BUFFER);
+ }
+
+ public static ByteBuffer createBufferWithHeaderForMessage(int piece, int offset, int blockSize) {
+ ByteBuffer result = ByteBuffer.allocate(
+ MESSAGE_LENGTH_FIELD_SIZE + PieceMessage.BASE_SIZE + blockSize);
+ result.putInt(PieceMessage.BASE_SIZE + blockSize);
+ result.put(PeerMessage.Type.PIECE.getTypeByte());
+ result.putInt(piece);
+ result.putInt(offset);
+ return result;
+ }
+
+ public String toString() {
+ return super.toString() + " #" + this.getPiece() +
+ " (" + this.getBlock().capacity() + "@" + this.getOffset() + ")";
+ }
+ }
+
+ /**
+ * Cancel message.
+ *
+ * <len=00013><id=8><piece index><block offset><block length>
+ */
+ 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(TorrentInfo torrent)
+ throws MessageValidationException {
+ if (this.piece >= 0 && this.piece < torrent.getPieceCount() &&
+ this.offset + this.length <=
+ torrent.getPieceSize(this.piece)) {
+ return this;
+ }
+
+ throw new MessageValidationException(this);
+ }
+
+ public static CancelMessage parse(ByteBuffer buffer,
+ TorrentInfo 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.allocate(
+ 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/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/TrackerMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/TrackerMessage.java
new file mode 100644
index 0000000..bd25837
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/TrackerMessage.java
@@ -0,0 +1,185 @@
+/**
+ * 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.
+ *
+ * <p>
+ * This class and its <em>*TrackerMessage</em> 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.
+ * </p>
+ *
+ * @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.
+ *
+ * <p>
+ * This interface must be implemented by all subtypes of connection request
+ * messages for the various tracker protocols.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+ public interface ConnectionRequestMessage {
+
+ }
+
+
+ /**
+ * Base interface for connection response messages.
+ *
+ * <p>
+ * This interface must be implemented by all subtypes of connection
+ * response messages for the various tracker protocols.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+ public interface ConnectionResponseMessage {
+
+ }
+
+
+ /**
+ * Base interface for tracker error messages.
+ *
+ * <p>
+ * This interface must be implemented by all subtypes of tracker error
+ * messages for the various tracker protocols.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+ public interface ErrorMessage {
+
+ /**
+ * The various tracker error states.
+ *
+ * <p>
+ * 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.
+ * </p>
+ */
+ 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;
+ }
+ }
+
+ String getReason();
+ }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java
new file mode 100644
index 0000000..8e25185
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java
@@ -0,0 +1,313 @@
+/**
+ * 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.Constants;
+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.TorrentUtils;
+import com.turn.ttorrent.common.protocol.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.
+ * announce请求包含的信息
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @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 TorrentUtils.byteArrayToHexString(this.infoHash);
+ }
+
+ @Override
+ public byte[] getPeerId() {
+ return this.peer.getPeerIdArray();
+ }
+
+ @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 isCompact() {
+ return this.compact;
+ }
+
+ @Override
+ public boolean canOmitPeerId() {
+ 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;
+ }
+
+ @Override
+ public String getKey() {
+ return "";
+ }
+
+ @Override
+ public String getTrackerId() {
+ return "";
+ }
+
+ /**
+ * 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.
+ */
+ // 构造 announce请求的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(), Constants.BYTE_ENCODING),
+ Constants.BYTE_ENCODING))
+ .append("&peer_id=")
+ .append(URLEncoder.encode(
+ new String(this.getPeerId(), Constants.BYTE_ENCODING),
+ Constants.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.isCompact() ? 1 : 0)
+ .append("&no_peer_id=").append(this.canOmitPeerId() ? 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());
+ }
+
+ // 解析(parse)tracker请求
+ // 将Tracker收到的B编码请求数据解析为结构化消息对象。
+ public static HTTPAnnounceRequestMessage parse(BEValue decoded)
+ throws IOException, MessageValidationException {
+ if (decoded == null) {
+ throw new MessageValidationException(
+ "Could not decode tracker message (not B-encoded?)!");
+ }
+
+ Map<String, BEValue> 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(Constants.BYTE_ENCODING);
+ }
+
+ RequestEvent event = RequestEvent.NONE;
+ if (params.containsKey("event")) {
+ event = RequestEvent.getByName(params.get("event")
+ .getString(Constants.BYTE_ENCODING));
+ }
+
+ return new HTTPAnnounceRequestMessage(Constants.EMPTY_BUFFER, infoHash,
+ new Peer(ip, port, ByteBuffer.wrap(peerId)),
+ uploaded, downloaded, left, compact, noPeerId,
+ event, numWant);
+ } catch (InvalidBEncodingException ibee) {
+ throw new MessageValidationException(
+ "Invalid HTTP tracker request!", ibee);
+ }
+ }
+
+ // 创建信息
+ //将Java类型参数转换为B编码字典
+ 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 {
+ Map<String, BEValue> params = new HashMap<String, BEValue>();
+ 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(), Constants.BYTE_ENCODING));
+ }
+
+ if (ip != null) {
+ params.put("ip",
+ new BEValue(ip, Constants.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/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java
new file mode 100644
index 0000000..e8b5efc
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java
@@ -0,0 +1,224 @@
+/**
+ * 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.Constants;
+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.protocol.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.*;
+
+
+/**
+ * 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<Peer> peers;
+ private String hexInfoHash;
+
+ private HTTPAnnounceResponseMessage(ByteBuffer data,
+ int interval, int complete, int incomplete, List<Peer> peers) {
+ super(Type.ANNOUNCE_RESPONSE, data);
+ this.interval = interval;
+ this.complete = complete;
+ this.incomplete = incomplete;
+ this.peers = peers;
+ }
+
+ private HTTPAnnounceResponseMessage(ByteBuffer data,
+ int interval, int complete, int incomplete, List<Peer> peers, String hexInfoHash) {
+ this(data, interval, complete, incomplete, peers);
+ this.hexInfoHash = hexInfoHash;
+ }
+
+ @Override
+ public int getInterval() {
+ return this.interval;
+ }
+
+ @Override
+ public int getComplete() {
+ return this.complete;
+ }
+
+ @Override
+ public int getIncomplete() {
+ return this.incomplete;
+ }
+
+ @Override
+ public List<Peer> getPeers() {
+ return this.peers;
+ }
+
+ public String getHexInfoHash() {
+ return this.hexInfoHash;
+ }
+
+ public static HTTPAnnounceResponseMessage parse(BEValue decoded)
+ throws IOException, MessageValidationException {
+ if (decoded == null) {
+ throw new MessageValidationException(
+ "Could not decode tracker message (not B-encoded?)!");
+ }
+
+ Map<String, BEValue> params = decoded.getMap();
+
+ if (params.get("interval") == null) {
+ throw new MessageValidationException("Tracker message missing mandatory field 'interval'!");
+ }
+
+ try {
+ List<Peer> 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());
+ }
+
+ if (params.get("torrentIdentifier") != null) {
+ return new HTTPAnnounceResponseMessage(Constants.EMPTY_BUFFER,
+ params.get("interval").getInt(),
+ params.get("complete") != null ? params.get("complete").getInt() : 0,
+ params.get("incomplete") != null ? params.get("incomplete").getInt() : 0,
+ peers, params.get("torrentIdentifier").getString());
+ } else {
+ return new HTTPAnnounceResponseMessage(Constants.EMPTY_BUFFER,
+ params.get("interval").getInt(),
+ params.get("complete") != null ? params.get("complete").getInt() : 0,
+ params.get("incomplete") != null ? params.get("incomplete").getInt() : 0,
+ peers);
+ }
+ } catch (InvalidBEncodingException ibee) {
+ throw new MessageValidationException("Invalid response " +
+ "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<Peer> toPeerList(List<BEValue> peers)
+ throws InvalidBEncodingException {
+ List<Peer> result = new LinkedList<Peer>();
+
+ for (BEValue peer : peers) {
+ Map<String, BEValue> peerInfo = peer.getMap();
+ result.add(new Peer(
+ peerInfo.get("ip").getString(Constants.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<Peer> toPeerList(byte[] data)
+ throws InvalidBEncodingException, UnknownHostException {
+ if (data.length % 6 != 0) {
+ throw new InvalidBEncodingException("Invalid peers " +
+ "binary information string!");
+ }
+
+ List<Peer> result = new LinkedList<Peer>();
+ 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 with a torrent identifier.
+ *
+ * @param interval
+ * @param complete
+ * @param incomplete
+ * @param peers
+ */
+ public static HTTPAnnounceResponseMessage craft(int interval,
+ int complete, int incomplete,
+ List<Peer> peers, String hexInfoHash) throws IOException, UnsupportedEncodingException {
+ Map<String, BEValue> response = new HashMap<String, BEValue>();
+ response.put("interval", new BEValue(interval));
+ response.put("complete", new BEValue(complete));
+ response.put("incomplete", new BEValue(incomplete));
+ if (hexInfoHash != null) {
+ response.put("torrentIdentifier", new BEValue(hexInfoHash));
+ }
+
+ 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(Arrays.copyOf(data.array(), data.position())));
+
+ return new HTTPAnnounceResponseMessage(
+ BEncoder.bencode(response),
+ interval, complete, incomplete, peers, hexInfoHash);
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerErrorMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerErrorMessage.java
new file mode 100644
index 0000000..bb3d1a7
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerErrorMessage.java
@@ -0,0 +1,84 @@
+/**
+ * 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.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+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(BEValue decoded)
+ throws IOException, MessageValidationException {
+ if (decoded == null) {
+ throw new MessageValidationException(
+ "Could not decode tracker message (not B-encoded?)!");
+ }
+
+ Map<String, BEValue> params = decoded.getMap();
+
+ try {
+ return new HTTPTrackerErrorMessage(
+ Constants.EMPTY_BUFFER,
+ params.get("failure reason")
+ .getString(Constants.BYTE_ENCODING));
+ } catch (InvalidBEncodingException ibee) {
+ throw new MessageValidationException("Invalid tracker error " +
+ "message!", ibee);
+ }
+ }
+
+ public static HTTPTrackerErrorMessage craft(
+ ErrorMessage.FailureReason reason) throws IOException {
+ return HTTPTrackerErrorMessage.craft(reason.getMessage());
+ }
+
+ public static HTTPTrackerErrorMessage craft(String reason)
+ throws IOException {
+ Map<String, BEValue> params = new HashMap<String, BEValue>();
+ params.put("failure reason",
+ new BEValue(reason, Constants.BYTE_ENCODING));
+ return new HTTPTrackerErrorMessage(
+ BEncoder.bencode(params),
+ reason);
+ }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerMessage.java
new file mode 100644
index 0000000..239c2bc
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerMessage.java
@@ -0,0 +1,62 @@
+/**
+ * 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.io.InputStream;
+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(InputStream data)
+ throws IOException, MessageValidationException {
+ BEValue decoded = BDecoder.bdecode(data);
+ if (decoded == null) {
+ throw new MessageValidationException("Could not decode tracker message (not B-encoded?)!: ");
+ }
+ return parse(decoded);
+ }
+
+ public static HTTPTrackerMessage parse(BEValue decoded) throws IOException, MessageValidationException {
+ Map<String, BEValue> params = decoded.getMap();
+
+ if (params.containsKey("info_hash")) {
+ return HTTPAnnounceRequestMessage.parse(decoded);
+ } else if (params.containsKey("peers")) {
+ return HTTPAnnounceResponseMessage.parse(decoded);
+ } else if (params.containsKey("failure reason")) {
+ return HTTPTrackerErrorMessage.parse(decoded);
+ }
+
+ throw new MessageValidationException("Unknown HTTP tracker message!");
+ }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceRequestMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceRequestMessage.java
new file mode 100644
index 0000000..7c53b02
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceRequestMessage.java
@@ -0,0 +1,259 @@
+/**
+ * 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.TorrentUtils;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+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 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 TorrentUtils.byteArrayToHexString(this.infoHash);
+ }
+
+ @Override
+ public byte[] getPeerId() {
+ return this.peerId;
+ }
+
+ @Override
+ public String getHexPeerId() {
+ return TorrentUtils.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 isCompact() {
+ return true;
+ }
+
+ @Override
+ public boolean canOmitPeerId() {
+ return true;
+ }
+
+ @Override
+ public RequestEvent getEvent() {
+ return this.event;
+ }
+
+ @Override
+ public String getIp() {
+ return this.ip.toString();
+ }
+
+ @Override
+ public int getNumWant() {
+ return this.numWant;
+ }
+
+ @Override
+ public String getKey() {
+ return "";
+ }
+
+ @Override
+ public String getTrackerId() {
+ return "";
+ }
+
+ 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(left);
+ data.putLong(uploaded);
+ 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/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessage.java
new file mode 100644
index 0000000..0f5c34b
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessage.java
@@ -0,0 +1,165 @@
+/**
+ * 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.AnnounceResponseMessage;
+
+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 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<Peer> peers;
+
+ private UDPAnnounceResponseMessage(ByteBuffer data, int transactionId,
+ int interval, int complete, int incomplete, List<Peer> 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<Peer> getPeers() {
+ return this.peers;
+ }
+
+ public String getHexInfoHash() {
+ return "";
+ }
+
+ 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<Peer> peers = new LinkedList<Peer>();
+ int totalPeersInResponse = data.remaining() / 6;
+ for (int i = 0; i < totalPeersInResponse; 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<Peer> 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/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectRequestMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectRequestMessage.java
new file mode 100644
index 0000000..3d9b981
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectRequestMessage.java
@@ -0,0 +1,89 @@
+/**
+ * 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/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectResponseMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectResponseMessage.java
new file mode 100644
index 0000000..396b3b7
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectResponseMessage.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.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/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerErrorMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerErrorMessage.java
new file mode 100644
index 0000000..4966177
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerErrorMessage.java
@@ -0,0 +1,102 @@
+/**
+ * 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.Constants;
+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, Constants.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(Constants.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/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerMessage.java
new file mode 100644
index 0000000..608014b
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerMessage.java
@@ -0,0 +1,109 @@
+/**
+ * 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/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentParserTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentParserTest.java
new file mode 100644
index 0000000..dee5969
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentParserTest.java
@@ -0,0 +1,97 @@
+package com.turn.ttorrent.common;
+
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.*;
+
+import static org.testng.Assert.*;
+
+@Test
+public class TorrentParserTest {
+
+ private TorrentParser myTorrentParser;
+
+ @BeforeMethod
+ public void setUp() {
+ myTorrentParser = new TorrentParser();
+ }
+
+ public void testParseNullAnnounce() throws IOException {
+ final Map<String, BEValue> metadataMap = new HashMap<String, BEValue>();
+ final HashMap<String, BEValue> infoTable = new HashMap<String, BEValue>();
+ infoTable.put(TorrentMetadataKeys.PIECES, new BEValue(new byte[20]));
+ infoTable.put(TorrentMetadataKeys.PIECE_LENGTH, new BEValue(512));
+
+ infoTable.put(TorrentMetadataKeys.FILE_LENGTH, new BEValue(10));
+ infoTable.put(TorrentMetadataKeys.NAME, new BEValue("file.txt"));
+
+ metadataMap.put(TorrentMetadataKeys.INFO_TABLE, new BEValue(infoTable));
+
+ TorrentMetadata metadata = new TorrentParser().parse(BEncoder.bencode(metadataMap).array());
+
+ assertNull(metadata.getAnnounce());
+ }
+
+ public void parseTest() throws IOException {
+ final Map<String, BEValue> metadata = new HashMap<String, BEValue>();
+ final HashMap<String, BEValue> infoTable = new HashMap<String, BEValue>();
+
+ metadata.put("announce", new BEValue("http://localhost/announce"));
+
+ infoTable.put("piece length", new BEValue(4));
+
+ infoTable.put("pieces", new BEValue(new byte[100]));
+ infoTable.put("name", new BEValue("test.file"));
+ infoTable.put("length", new BEValue(19));
+
+ metadata.put("info", new BEValue(infoTable));
+
+ final TorrentMetadata torrentMetadata = myTorrentParser.parse(BEncoder.bencode(metadata).array());
+
+ assertEquals(torrentMetadata.getPieceLength(), 4);
+ assertEquals(torrentMetadata.getAnnounce(), "http://localhost/announce");
+ assertEquals(torrentMetadata.getDirectoryName(), "test.file");
+ assertNull(torrentMetadata.getAnnounceList());
+
+ List<BEValue> announceList = new ArrayList<BEValue>();
+ announceList.add(new BEValue(Collections.singletonList(new BEValue("http://localhost/announce"))));
+ announceList.add(new BEValue(Collections.singletonList(new BEValue("http://second/announce"))));
+ metadata.put("announce-list", new BEValue(announceList));
+
+ final TorrentMetadata torrentMetadataWithAnnounceList = myTorrentParser.parse(BEncoder.bencode(metadata).array());
+
+ final List<List<String>> actualAnnounceList = torrentMetadataWithAnnounceList.getAnnounceList();
+ assertNotNull(actualAnnounceList);
+ assertEquals(actualAnnounceList.get(0).get(0), "http://localhost/announce");
+ assertEquals(actualAnnounceList.get(1).get(0), "http://second/announce");
+
+ }
+
+ public void badBEPFormatTest() {
+ try {
+ myTorrentParser.parse("abcd".getBytes());
+ fail("This method must throw invalid bencoding exception");
+ } catch (InvalidBEncodingException e) {
+ //it's okay
+ }
+ }
+
+ public void missingRequiredFieldTest() {
+ Map<String, BEValue> map = new HashMap<String, BEValue>();
+ map.put("info", new BEValue(new HashMap<String, BEValue>()));
+
+ try {
+ myTorrentParser.parse(BEncoder.bencode(map).array());
+ fail("This method must throw invalid bencoding exception");
+ } catch (InvalidBEncodingException e) {
+ //it's okay
+ } catch (IOException e) {
+ fail("", e);
+ }
+ }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentUtilsTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentUtilsTest.java
new file mode 100644
index 0000000..f43622d
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentUtilsTest.java
@@ -0,0 +1,68 @@
+/*
+ Copyright (C) 2016 Philipp Henkel
+ <p>
+ 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
+ <p>
+ http://www.apache.org/licenses/LICENSE-2.0
+ <p>
+ 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 org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+public class TorrentUtilsTest {
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void testBytesToHexWithNull() {
+ //noinspection ResultOfMethodCallIgnored,ConstantConditions
+ TorrentUtils.byteArrayToHexString(null);
+ }
+
+ @Test
+ public void testBytesToHexWithEmptyByteArray() {
+ assertEquals("", TorrentUtils.byteArrayToHexString(new byte[0]));
+ }
+
+ @Test
+ public void testBytesToHexWithSingleByte() {
+ assertEquals("BC", TorrentUtils.byteArrayToHexString(new byte[]{
+ (byte) 0xBC
+ }));
+ }
+
+ @Test
+ public void testBytesToHexWithZeroByte() {
+ assertEquals("00", TorrentUtils.byteArrayToHexString(new byte[1]));
+ }
+
+ @Test
+ public void testBytesToHexWithLeadingZero() {
+ assertEquals("0053FF", TorrentUtils.byteArrayToHexString(new byte[]{
+ (byte) 0x00, (byte) 0x53, (byte) 0xFF
+ }));
+ }
+
+ @Test
+ public void testBytesToHexTrailingZero() {
+ assertEquals("AA004500", TorrentUtils.byteArrayToHexString(new byte[]{
+ (byte) 0xAA, (byte) 0x00, (byte) 0x45, (byte) 0x00
+ }));
+ }
+
+ @Test
+ public void testBytesToHexAllSymbols() {
+ assertEquals("0123456789ABCDEF", TorrentUtils.byteArrayToHexString(new byte[]{
+ (byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67,
+ (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF
+ }));
+ }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/HashesCalculatorsTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/HashesCalculatorsTest.java
new file mode 100644
index 0000000..ef6ca77
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/HashesCalculatorsTest.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import com.turn.ttorrent.common.TorrentUtils;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static java.util.Arrays.asList;
+import static org.testng.Assert.assertEquals;
+
+@Test
+public class HashesCalculatorsTest {
+
+ private List<? extends PiecesHashesCalculator> implementations;
+ private ExecutorService executor;
+
+ @BeforeMethod
+ public void setUp() {
+ executor = Executors.newFixedThreadPool(4);
+ implementations = Arrays.asList(
+ new SingleThreadHashesCalculator(),
+ new MultiThreadHashesCalculator(executor, 3),
+ new MultiThreadHashesCalculator(executor, 20),
+ new MultiThreadHashesCalculator(executor, 1)
+ );
+ }
+
+ @AfterMethod
+ public void tearDown() throws InterruptedException {
+ executor.shutdown();
+ executor.awaitTermination(10, TimeUnit.SECONDS);
+ }
+
+ public void testEmptySource() throws IOException {
+ List<byte[]> sourceBytes = new ArrayList<byte[]>();
+ sourceBytes.add(new byte[]{1, 2});
+ sourceBytes.add(new byte[]{});
+ sourceBytes.add(new byte[]{3, 4});
+
+ HashingResult expected = new HashingResult(Collections.singletonList(
+ TorrentUtils.calculateSha1Hash(new byte[]{1, 2, 3, 4})),
+ asList(2L, 0L, 2L)
+ );
+ verifyImplementationsResults(sourceBytes, 512, expected);
+ }
+
+ public void testStreamsAsPiece() throws IOException {
+ List<byte[]> sourceBytes = new ArrayList<byte[]>();
+ sourceBytes.add(new byte[]{1, 2, 3, 4});
+ sourceBytes.add(new byte[]{5, 6, 7, 8});
+
+ HashingResult expected = new HashingResult(asList(
+ TorrentUtils.calculateSha1Hash(new byte[]{1, 2, 3, 4}),
+ TorrentUtils.calculateSha1Hash(new byte[]{5, 6, 7, 8})),
+ asList(4L, 4L)
+ );
+ verifyImplementationsResults(sourceBytes, 4, expected);
+ }
+
+ public void testReadingNotFullyBuffer() throws IOException {
+ List<byte[]> sourceBytes = new ArrayList<byte[]>();
+ sourceBytes.add(new byte[]{1, 2, 3, 4, 5, 6, 7});
+ HashingResult expected = new HashingResult(asList(
+ TorrentUtils.calculateSha1Hash(new byte[]{1, 2, 3, 4, 5}),
+ TorrentUtils.calculateSha1Hash(new byte[]{6, 7})),
+ Collections.singletonList(7L)
+ );
+
+ final int maxToRead = 2;
+ List<HashingResult> hashingResults = new ArrayList<HashingResult>();
+ for (PiecesHashesCalculator implementation : implementations) {
+ List<DataSourceHolder> sources = new ArrayList<DataSourceHolder>();
+ for (byte[] sourceByte : sourceBytes) {
+ final InputStream is = new ByteArrayInputStream(sourceByte) {
+ @Override
+ public synchronized int read(byte[] b, int off, int len) {
+ if (len <= maxToRead) {
+ return super.read(b, off, len);
+ }
+ if (pos >= count) {
+ return -1;
+ }
+
+ int avail = count - pos;
+ if (len > avail) {
+ len = avail;
+ }
+ if (len <= 0) {
+ return 0;
+ }
+ System.arraycopy(buf, pos, b, off, maxToRead);
+ pos += maxToRead;
+ return maxToRead;
+ }
+ };
+ sources.add(new DataSourceHolder() {
+ @Override
+ public InputStream getStream() {
+ return is;
+ }
+
+ @Override
+ public void close() throws IOException {
+ is.close();
+ }
+ });
+ }
+ hashingResults.add(implementation.calculateHashes(sources, 5));
+ }
+ for (HashingResult actual : hashingResults) {
+ assertHashingResult(actual, expected);
+ }
+ }
+
+ public void testWithSmallSource() throws IOException {
+ List<byte[]> sourceBytes = new ArrayList<byte[]>();
+ sourceBytes.add(new byte[]{0, 1, 2, 3, 4, 5, 4});
+ sourceBytes.add(new byte[]{-1, -2});
+ sourceBytes.add(new byte[]{6, 7, 8, 9, 10});
+ sourceBytes.add(new byte[]{1, 2, 3, 4});
+
+ HashingResult expected = new HashingResult(asList(
+ TorrentUtils.calculateSha1Hash(new byte[]{0, 1, 2, 3, 4, 5}),
+ TorrentUtils.calculateSha1Hash(new byte[]{4, -1, -2, 6, 7, 8}),
+ TorrentUtils.calculateSha1Hash(new byte[]{9, 10, 1, 2, 3, 4})),
+ asList(7L, 2L, 5L, 4L)
+ );
+ verifyImplementationsResults(sourceBytes, 6, expected);
+ }
+
+ public void testOneLargeSource() throws IOException {
+
+ int size = 1024 * 1024 * 100;//100mb
+ byte[] sourceBytes = new byte[size];
+ List<byte[]> hashes = new ArrayList<byte[]>();
+ final int pieceSize = 128 * 1024;//128kb
+ for (int i = 0; i < sourceBytes.length; i++) {
+ sourceBytes[i] = (byte) (i * i);
+ if (i % pieceSize == 0 && i > 0) {
+ byte[] forHashing = Arrays.copyOfRange(sourceBytes, i - pieceSize, i);
+ hashes.add(TorrentUtils.calculateSha1Hash(forHashing));
+ }
+ }
+ hashes.add(TorrentUtils.calculateSha1Hash(
+ Arrays.copyOfRange(sourceBytes, hashes.size() * pieceSize, size)
+ ));
+
+ HashingResult expected = new HashingResult(hashes, Collections.singletonList((long) size));
+
+ verifyImplementationsResults(Collections.singletonList(sourceBytes), pieceSize, expected);
+ }
+
+ private void verifyImplementationsResults(List<byte[]> sourceBytes,
+ int pieceSize,
+ HashingResult expected) throws IOException {
+ List<HashingResult> hashingResults = new ArrayList<HashingResult>();
+ for (PiecesHashesCalculator implementation : implementations) {
+ List<DataSourceHolder> sources = new ArrayList<DataSourceHolder>();
+ for (byte[] sourceByte : sourceBytes) {
+ addSource(sourceByte, sources);
+ }
+ hashingResults.add(implementation.calculateHashes(sources, pieceSize));
+ }
+ for (HashingResult actual : hashingResults) {
+ assertHashingResult(actual, expected);
+ }
+ }
+
+ private void assertHashingResult(HashingResult actual, HashingResult expected) {
+
+ assertEquals(actual.getHashes().size(), expected.getHashes().size());
+ for (int i = 0; i < actual.getHashes().size(); i++) {
+ assertEquals(actual.getHashes().get(i), expected.getHashes().get(i));
+ }
+ assertEquals(actual.getSourceSizes(), expected.getSourceSizes());
+ }
+
+ private void addSource(byte[] bytes, List<DataSourceHolder> sources) {
+ final ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
+ sources.add(new DataSourceHolder() {
+ @Override
+ public InputStream getStream() {
+ return stream;
+ }
+
+ @Override
+ public void close() {
+ }
+ });
+ }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/MetadataBuilderTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/MetadataBuilderTest.java
new file mode 100644
index 0000000..193b25a
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/MetadataBuilderTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.creation;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.common.TorrentUtils;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.*;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+@Test
+public class MetadataBuilderTest {
+
+ public void testMultiFileModeWithOneFile() throws IOException {
+ Map<String, BEValue> map = new MetadataBuilder()
+ .setDirectoryName("root")
+ .addDataSource(new ByteArrayInputStream(new byte[]{1, 2}), "path/some_file", true)
+ .buildBEP().getMap();
+ Map<String, BEValue> info = map.get(INFO_TABLE).getMap();
+ assertEquals(info.get(NAME).getString(), "root");
+ List<BEValue> files = info.get(FILES).getList();
+ assertEquals(files.size(), 1);
+ Map<String, BEValue> file = files.get(0).getMap();
+ assertEquals(file.get(FILE_LENGTH).getInt(), 2);
+
+ StringBuilder path = new StringBuilder();
+ Iterator<BEValue> iterator = file.get(FILE_PATH).getList().iterator();
+ if (iterator.hasNext()) {
+ path = new StringBuilder(iterator.next().getString());
+ }
+ while (iterator.hasNext()) {
+ path.append("/").append(iterator.next().getString());
+ }
+ assertEquals(path.toString(), "path/some_file");
+ }
+
+ public void testBuildWithSpecifiedHashes() throws IOException {
+ byte[] expectedHash = TorrentUtils.calculateSha1Hash(new byte[]{1, 2, 3});
+ Map<String, BEValue> metadata = new MetadataBuilder()
+ .setPiecesHashesCalculator(new PiecesHashesCalculator() {
+ @Override
+ public HashingResult calculateHashes(List<DataSourceHolder> sources, int pieceSize) {
+ throw new RuntimeException("should not be invoked");
+ }
+ })
+ .setFilesInfo(
+ Collections.singletonList(expectedHash),
+ Collections.singletonList("file"),
+ Collections.singletonList(42L))
+ .setPieceLength(512)
+ .setTracker("http://localhost:12346")
+ .buildBEP().getMap();
+
+ assertEquals(metadata.get(ANNOUNCE).getString(), "http://localhost:12346");
+ Map<String, BEValue> info = metadata.get(INFO_TABLE).getMap();
+ assertEquals(info.get(PIECES).getBytes(), expectedHash);
+ assertEquals(info.get(NAME).getString(), "file");
+ assertEquals(info.get(FILE_LENGTH).getLong(), 42);
+ }
+
+ public void testSingleFile() throws IOException {
+
+ byte[] data = {1, 2, 12, 4, 5};
+ Map<String, BEValue> metadata = new MetadataBuilder()
+ .addDataSource(new ByteArrayInputStream(data), "singleFile.txt", true)
+ .setTracker("http://localhost:12346")
+ .buildBEP().getMap();
+ assertEquals(metadata.get(ANNOUNCE).getString(), "http://localhost:12346");
+ assertNull(metadata.get(CREATION_DATE_SEC));
+ assertNull(metadata.get(COMMENT));
+ assertEquals(metadata.get(CREATED_BY).getString(), "ttorrent library");
+
+ Map<String, BEValue> info = metadata.get(INFO_TABLE).getMap();
+ assertEquals(info.get(PIECES).getBytes().length / Constants.PIECE_HASH_SIZE, 1);
+ assertEquals(info.get(PIECE_LENGTH).getInt(), 512 * 1024);
+
+ assertEquals(info.get(FILE_LENGTH).getInt(), data.length);
+ assertEquals(info.get(NAME).getString(), "singleFile.txt");
+
+ }
+
+ public void testMultiFileWithOneFileValues() throws IOException {
+
+ byte[] data = {34, 2, 12, 4, 5};
+ List<String> paths = Arrays.asList("unix/path", "win\\path");
+ Map<String, BEValue> metadata = new MetadataBuilder()
+ .addDataSource(new ByteArrayInputStream(data), paths.get(0), true)
+ .addDataSource(new ByteArrayInputStream(data), paths.get(1), true)
+ .setDirectoryName("downloadDirName")
+ .buildBEP().getMap();
+
+ Map<String, BEValue> info = metadata.get(INFO_TABLE).getMap();
+ assertEquals(info.get(PIECES).getBytes().length, Constants.PIECE_HASH_SIZE);
+ assertEquals(info.get(PIECE_LENGTH).getInt(), 512 * 1024);
+ assertEquals(info.get(NAME).getString(), "downloadDirName");
+
+ int idx = 0;
+ for (BEValue value : info.get(FILES).getList()) {
+ Map<String, BEValue> fileInfo = value.getMap();
+ String path = paths.get(idx);
+ idx++;
+ String[] split = path.split("[/\\\\]");
+ List<BEValue> list = fileInfo.get(FILE_PATH).getList();
+
+ assertEquals(fileInfo.get(FILE_LENGTH).getInt(), data.length);
+ assertEquals(list.size(), split.length);
+
+ for (int i = 0; i < list.size(); i++) {
+ assertEquals(list.get(i).getString(), split[i]);
+ }
+ }
+
+ }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessageTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessageTest.java
new file mode 100644
index 0000000..42e2a13
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessageTest.java
@@ -0,0 +1,49 @@
+package com.turn.ttorrent.common.protocol.http;
+
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.testng.Assert.assertEquals;
+
+public class HTTPAnnounceResponseMessageTest {
+
+ @Test
+ public void parseTest() throws IOException, TrackerMessage.MessageValidationException {
+
+ Map<String, BEValue> trackerResponse = new HashMap<String, BEValue>();
+ trackerResponse.put("interval", new BEValue(5));
+ trackerResponse.put("complete", new BEValue(1));
+ trackerResponse.put("incomplete", new BEValue(0));
+
+ String ip = "192.168.1.1";
+ int port = 6881;
+ InetSocketAddress peerAddress = new InetSocketAddress(ip, port);
+ ByteBuffer binaryPeerAddress = ByteBuffer.allocate(6);
+ binaryPeerAddress.put(peerAddress.getAddress().getAddress());
+ binaryPeerAddress.putShort((short) port);
+ trackerResponse.put("peers", new BEValue(binaryPeerAddress.array()));
+
+ HTTPAnnounceResponseMessage parsedResponse = (HTTPAnnounceResponseMessage) HTTPAnnounceResponseMessage.parse(
+ new ByteArrayInputStream(BEncoder.bencode(trackerResponse).array()));
+
+ assertEquals(parsedResponse.getInterval(), 5);
+ assertEquals(parsedResponse.getComplete(), 1);
+ assertEquals(parsedResponse.getIncomplete(), 0);
+ List<Peer> peers = parsedResponse.getPeers();
+ assertEquals(peers.size(), 1);
+ Peer peer = peers.get(0);
+ assertEquals(peer.getIp(), ip);
+ assertEquals(peer.getPort(), port);
+ }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessageTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessageTest.java
new file mode 100644
index 0000000..b19f81c
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessageTest.java
@@ -0,0 +1,55 @@
+package com.turn.ttorrent.common.protocol.udp;
+
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+import org.testng.annotations.Test;
+
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import static org.testng.Assert.assertEquals;
+
+/**
+ * Specification of UDP announce protocol:
+ * <a href="http://www.bittorrent.org/beps/bep_0015.html">http://www.bittorrent.org/beps/bep_0015.html</a>
+ */
+public class UDPAnnounceResponseMessageTest {
+
+ @Test
+ public void parseTest() throws TrackerMessage.MessageValidationException {
+
+ final int peersCount = 3;
+ ByteBuffer response = ByteBuffer.allocate(20 + peersCount * 6);
+ response.putInt(1);//announce response message identifier
+ response.putInt(3);//transaction_id
+ response.putInt(5);//interval
+ response.putInt(1);//incomplete
+ response.putInt(2);//complete
+
+
+ String ipPrefix = "192.168.1.1";
+ final int firstPort = 6881;
+ for (int i = 0; i < peersCount; i++) {
+ String ip = ipPrefix + i;
+ InetSocketAddress peerAddress = new InetSocketAddress(ip, firstPort);
+ response.put(peerAddress.getAddress().getAddress());
+ response.putShort((short) (firstPort + i));
+ }
+
+ response.rewind();
+ UDPAnnounceResponseMessage parsedResponse = UDPAnnounceResponseMessage.parse(response);
+ assertEquals(parsedResponse.getActionId(), 1);
+ assertEquals(parsedResponse.getTransactionId(), 3);
+ assertEquals(parsedResponse.getInterval(), 5);
+ assertEquals(parsedResponse.getComplete(), 2);
+ assertEquals(parsedResponse.getIncomplete(), 1);
+ List<Peer> peers = parsedResponse.getPeers();
+ assertEquals(peers.size(), peersCount);
+ for (int i = 0; i < peersCount; i++) {
+ Peer peer = peers.get(i);
+ assertEquals(peer.getIp(), ipPrefix + i);
+ assertEquals(peer.getPort(), firstPort + i);
+ }
+ }
+}