From 5cc4b215d1b22c0a63e9135e97c10c60ba927af0 Mon Sep 17 00:00:00 2001 From: Rogiel Date: Sat, 14 Jan 2012 17:14:53 -0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + pom.xml | 263 +++++++++++++ src/main/assembly/distribution-bin.xml | 25 ++ src/main/assembly/distribution-src.xml | 26 ++ .../java/com/torrent4j/TorrentController.java | 184 +++++++++ .../TorrentControllerConfiguration.java | 15 + .../model/AbstractTorrentPiecesContainer.java | 99 +++++ .../java/com/torrent4j/model/Torrent.java | 359 ++++++++++++++++++ .../model/TorrentCompletePieces.java | 37 ++ .../java/com/torrent4j/model/TorrentFile.java | 162 ++++++++ .../com/torrent4j/model/TorrentFileHash.java | 21 + .../java/com/torrent4j/model/TorrentHash.java | 18 + .../java/com/torrent4j/model/TorrentPeer.java | 233 ++++++++++++ .../torrent4j/model/TorrentPeerPieces.java | 24 ++ .../com/torrent4j/model/TorrentPeerState.java | 266 +++++++++++++ .../model/TorrentPeerTrafficControl.java | 17 + .../com/torrent4j/model/TorrentPiece.java | 128 +++++++ .../torrent4j/model/TorrentPieceBlock.java | 81 ++++ .../com/torrent4j/model/TorrentPieceHash.java | 21 + .../com/torrent4j/model/TorrentSwarm.java | 69 ++++ .../com/torrent4j/model/TorrentTracker.java | 13 + .../model/TorrentTrafficControl.java | 91 +++++ .../model/metadata/MetadataFile.java | 51 +++ .../model/metadata/MetadataInfo.java | 83 ++++ .../model/metadata/MetadataTracker.java | 48 +++ .../model/metadata/TorrentMetadata.java | 73 ++++ .../com/torrent4j/net/TorrentProtocol.java | 12 + .../torrent4j/net/TorrentProtocolPeer.java | 43 +++ .../torrent4j/net/TorrentTrafficShaper.java | 8 + .../net/peerwire/AbstractPeerWireMessage.java | 28 ++ .../net/peerwire/PeerWireHandler.java | 270 +++++++++++++ .../net/peerwire/PeerWireMessage.java | 9 + .../net/peerwire/PeerWirePipelineFactory.java | 46 +++ .../net/peerwire/PeerWireProtocol.java | 65 ++++ .../net/peerwire/PeerWireProtocolPeer.java | 129 +++++++ .../peerwire/codec/PeerWireFrameDecoder.java | 40 ++ .../peerwire/codec/PeerWireFrameEncoder.java | 30 ++ .../codec/PeerWireMessageDecoder.java | 85 +++++ .../codec/PeerWireMessageEncoder.java | 33 ++ .../peerwire/messages/BitFieldMessage.java | 58 +++ .../net/peerwire/messages/BlockMessage.java | 51 +++ .../net/peerwire/messages/CancelMessage.java | 49 +++ .../net/peerwire/messages/ChokeMessage.java | 19 + .../peerwire/messages/HandshakeMessage.java | 54 +++ .../net/peerwire/messages/HaveMessage.java | 40 ++ .../peerwire/messages/InterestedMessage.java | 21 + .../peerwire/messages/KeepAliveMessage.java | 28 ++ .../messages/NotInterestedMessage.java | 21 + .../net/peerwire/messages/PortMessage.java | 40 ++ .../net/peerwire/messages/RequestMessage.java | 44 +++ .../net/peerwire/messages/UnchokeMessage.java | 21 + .../traffic/PeerTrafficShapingHandler.java | 60 +++ .../traffic/TorrentTrafficShapingHandler.java | 60 +++ .../storage/AbstractTorrentStorage.java | 25 ++ .../storage/FileAwareTorrentStorage.java | 101 +++++ .../storage/InMemoryTorrentStorage.java | 66 ++++ .../torrent4j/storage/NIOTorrentStorage.java | 98 +++++ .../com/torrent4j/storage/TorrentStorage.java | 61 +++ .../torrent4j/storage/VoidTorrentStorage.java | 39 ++ .../strategy/TorrentDownloadStrategy.java | 51 +++ .../strategy/TorrentPeerStrategy.java | 136 +++++++ .../torrent4j/strategy/TorrentStrategy.java | 23 ++ .../strategy/TorrentUploadStrategy.java | 38 ++ .../strategy/standard/PieceSelector.java | 22 ++ .../standard/RandomPieceSelector.java | 22 ++ .../StandardTorrentDownloadStrategy.java | 63 +++ .../standard/StandardTorrentPeerStrategy.java | 99 +++++ .../standard/StandardTorrentStrategy.java | 61 +++ .../StandardTorrentUploadStrategy.java | 37 ++ .../java/com/torrent4j/util/BitField.java | 102 +++++ src/main/java/com/torrent4j/util/Hash.java | 55 +++ .../java/com/torrent4j/util/HashType.java | 105 +++++ .../com/torrent4j/util/PeerIDGenerator.java | 11 + src/main/java/com/torrent4j/util/Range.java | 178 +++++++++ .../torrent4j/util/bencoding/BDecoder.java | 325 ++++++++++++++++ .../util/bencoding/BDecodingException.java | 21 + .../torrent4j/util/bencoding/BEncoder.java | 180 +++++++++ .../com/torrent4j/util/bencoding/BList.java | 21 + .../com/torrent4j/util/bencoding/BMap.java | 21 + .../util/bencoding/BTypeException.java | 11 + .../torrent4j/util/bencoding/HashBMap.java | 32 ++ 81 files changed, 5778 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/assembly/distribution-bin.xml create mode 100644 src/main/assembly/distribution-src.xml create mode 100644 src/main/java/com/torrent4j/TorrentController.java create mode 100644 src/main/java/com/torrent4j/TorrentControllerConfiguration.java create mode 100644 src/main/java/com/torrent4j/model/AbstractTorrentPiecesContainer.java create mode 100644 src/main/java/com/torrent4j/model/Torrent.java create mode 100644 src/main/java/com/torrent4j/model/TorrentCompletePieces.java create mode 100644 src/main/java/com/torrent4j/model/TorrentFile.java create mode 100644 src/main/java/com/torrent4j/model/TorrentFileHash.java create mode 100644 src/main/java/com/torrent4j/model/TorrentHash.java create mode 100644 src/main/java/com/torrent4j/model/TorrentPeer.java create mode 100644 src/main/java/com/torrent4j/model/TorrentPeerPieces.java create mode 100644 src/main/java/com/torrent4j/model/TorrentPeerState.java create mode 100644 src/main/java/com/torrent4j/model/TorrentPeerTrafficControl.java create mode 100644 src/main/java/com/torrent4j/model/TorrentPiece.java create mode 100644 src/main/java/com/torrent4j/model/TorrentPieceBlock.java create mode 100644 src/main/java/com/torrent4j/model/TorrentPieceHash.java create mode 100644 src/main/java/com/torrent4j/model/TorrentSwarm.java create mode 100644 src/main/java/com/torrent4j/model/TorrentTracker.java create mode 100644 src/main/java/com/torrent4j/model/TorrentTrafficControl.java create mode 100644 src/main/java/com/torrent4j/model/metadata/MetadataFile.java create mode 100644 src/main/java/com/torrent4j/model/metadata/MetadataInfo.java create mode 100644 src/main/java/com/torrent4j/model/metadata/MetadataTracker.java create mode 100644 src/main/java/com/torrent4j/model/metadata/TorrentMetadata.java create mode 100644 src/main/java/com/torrent4j/net/TorrentProtocol.java create mode 100644 src/main/java/com/torrent4j/net/TorrentProtocolPeer.java create mode 100644 src/main/java/com/torrent4j/net/TorrentTrafficShaper.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/AbstractPeerWireMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/PeerWireHandler.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/PeerWireMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/PeerWirePipelineFactory.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/PeerWireProtocol.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/PeerWireProtocolPeer.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/codec/PeerWireFrameDecoder.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/codec/PeerWireFrameEncoder.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/codec/PeerWireMessageDecoder.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/codec/PeerWireMessageEncoder.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/BitFieldMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/BlockMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/CancelMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/ChokeMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/HandshakeMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/HaveMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/InterestedMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/KeepAliveMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/NotInterestedMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/PortMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/RequestMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/messages/UnchokeMessage.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/traffic/PeerTrafficShapingHandler.java create mode 100644 src/main/java/com/torrent4j/net/peerwire/traffic/TorrentTrafficShapingHandler.java create mode 100644 src/main/java/com/torrent4j/storage/AbstractTorrentStorage.java create mode 100644 src/main/java/com/torrent4j/storage/FileAwareTorrentStorage.java create mode 100644 src/main/java/com/torrent4j/storage/InMemoryTorrentStorage.java create mode 100644 src/main/java/com/torrent4j/storage/NIOTorrentStorage.java create mode 100644 src/main/java/com/torrent4j/storage/TorrentStorage.java create mode 100644 src/main/java/com/torrent4j/storage/VoidTorrentStorage.java create mode 100644 src/main/java/com/torrent4j/strategy/TorrentDownloadStrategy.java create mode 100644 src/main/java/com/torrent4j/strategy/TorrentPeerStrategy.java create mode 100644 src/main/java/com/torrent4j/strategy/TorrentStrategy.java create mode 100644 src/main/java/com/torrent4j/strategy/TorrentUploadStrategy.java create mode 100644 src/main/java/com/torrent4j/strategy/standard/PieceSelector.java create mode 100644 src/main/java/com/torrent4j/strategy/standard/RandomPieceSelector.java create mode 100644 src/main/java/com/torrent4j/strategy/standard/StandardTorrentDownloadStrategy.java create mode 100644 src/main/java/com/torrent4j/strategy/standard/StandardTorrentPeerStrategy.java create mode 100644 src/main/java/com/torrent4j/strategy/standard/StandardTorrentStrategy.java create mode 100644 src/main/java/com/torrent4j/strategy/standard/StandardTorrentUploadStrategy.java create mode 100644 src/main/java/com/torrent4j/util/BitField.java create mode 100644 src/main/java/com/torrent4j/util/Hash.java create mode 100644 src/main/java/com/torrent4j/util/HashType.java create mode 100644 src/main/java/com/torrent4j/util/PeerIDGenerator.java create mode 100644 src/main/java/com/torrent4j/util/Range.java create mode 100644 src/main/java/com/torrent4j/util/bencoding/BDecoder.java create mode 100644 src/main/java/com/torrent4j/util/bencoding/BDecodingException.java create mode 100644 src/main/java/com/torrent4j/util/bencoding/BEncoder.java create mode 100644 src/main/java/com/torrent4j/util/bencoding/BList.java create mode 100644 src/main/java/com/torrent4j/util/bencoding/BMap.java create mode 100644 src/main/java/com/torrent4j/util/bencoding/BTypeException.java create mode 100644 src/main/java/com/torrent4j/util/bencoding/HashBMap.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3a9b66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.project +/.classpath +/.settings diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b1955dd --- /dev/null +++ b/pom.xml @@ -0,0 +1,263 @@ + + 4.0.0 + com.rogiel + torrent4j + 0.0.1-SNAPSHOT + torrent4j + Bittorrent java library + + 2012 + + Rogiel + http://www.rogiel.com/ + + http://www.rogiel.com/ + + + scm:git:git@github.com:torrent4j/torrent4j.git + scm:git:github.com:torrent4j/torrent4j.git + + + https://github.com/torrent4j/torrent4j/downloads + + torrent4j-repository + torrent4j-repository + scp://rogiels@rogiel.com/home/rogiels/maven.rogiel.com + default + false + + + torrent4j-site + torrent4j-site + scp://rogiels@rogiel.com/home/rogiels/torrent4j.rogiel.com + + + + + GitHub + https://github.com/torrent4j/torrent4j/issues + + + + GitHub + https://github.com/torrent4j/torrent4j + + + + + Rogiel + Rogiel + rogiel@rogiel.com + http://www.rogiel.com/ + -3 + + Creator + API Designer + + + + + + + GNU General Public License version 3 + http://www.gnu.org/licenses/gpl.txt + repo + + + + + package + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.7 + 1.7 + UTF-8 + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2.2 + + + package + + attached + + + + + + src/main/assembly/distribution-bin.xml + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + false + + + + org.apache.maven.wagon + wagon-ssh + 2.1 + + + + + com.github.github + downloads-maven-plugin + 0.4 + + + deploy + + upload + + + + + ${project.version} release of ${project.name} + true + true + torrent4j + torrent4j + + + + + org.apache.maven.plugins + maven-release-plugin + 2.2.2 + + + + org.apache.maven.plugins + maven-site-plugin + 3.0 + + UTF-8 + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.8 + + UTF-8 + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.4 + + scm:git@github.com:torrent4j/torrent4j.git + scm:git://github.com/torrent4j/torrent4j.git + https://github.com/torrent4j/torrent4j/tree/master/ + + + + org.apache.maven.plugins + maven-changelog-plugin + 2.2 + + UTF-8 + + + + org.apache.maven.plugins + maven-jxr-plugin + 2.3 + + UTF-8 + UTF-8 + + + + + + + org.apache.maven.wagon + wagon-ssh + 2.1 + + + + + com.github.github + site-maven-plugin + 0.4 + + Creating site for ${project.version} + torrent4j + torrent4j.github.com + + + + site + + site + + + + + + + + + + junit + junit + 4.10 + test + + + io.netty + netty + 4.0.0.Alpha1-SNAPSHOT + compile + + + commons-codec + commons-codec + 20041127.091804 + + + + + + + jboss-snapshots + https://repository.jboss.org/nexus/content/repositories/snapshots/ + + true + + + + oss-sonatype-snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + true + + + + + + + oss-sonatype-snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + true + + + + \ No newline at end of file diff --git a/src/main/assembly/distribution-bin.xml b/src/main/assembly/distribution-bin.xml new file mode 100644 index 0000000..f83f27d --- /dev/null +++ b/src/main/assembly/distribution-bin.xml @@ -0,0 +1,25 @@ + + bin + + zip + + + + ${project.build.directory}/${project.artifactId}-${project.version}.jar + / + torrent4j-${project.version}.jar + 0755 + + + + + /libs + false + false + runtime + + + \ No newline at end of file diff --git a/src/main/assembly/distribution-src.xml b/src/main/assembly/distribution-src.xml new file mode 100644 index 0000000..05336e1 --- /dev/null +++ b/src/main/assembly/distribution-src.xml @@ -0,0 +1,26 @@ + + src + + zip + + + + ${project.basedir} + / + + src/** + + + + + + /libs + false + false + runtime + + + \ No newline at end of file diff --git a/src/main/java/com/torrent4j/TorrentController.java b/src/main/java/com/torrent4j/TorrentController.java new file mode 100644 index 0000000..66ea4c6 --- /dev/null +++ b/src/main/java/com/torrent4j/TorrentController.java @@ -0,0 +1,184 @@ +package com.torrent4j; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPiece; +import com.torrent4j.net.TorrentProtocol; +import com.torrent4j.net.peerwire.PeerWireProtocol; +import com.torrent4j.storage.NIOTorrentStorage; +import com.torrent4j.storage.TorrentStorage; +import com.torrent4j.util.Hash; + +/** + * The main class for starting torrent transfers. Each torrent instance need to + * be registered into an controller. Once registered, connections can be + * established to start downloading the file (or files) from other peers in the + * BitTorrent network. + * + * @author Rogiel + */ +public class TorrentController { + /** + * The controller configuration + */ + private final TorrentControllerConfiguration config = new TorrentControllerConfiguration(); + /** + * The torrent protocol + */ + private final TorrentProtocol protocol; + /** + * The torrent storage + */ + private final TorrentStorage storage; + /** + * The list of torrents controlled on this controller + */ + private final List torrents = new ArrayList<>(); + + /** + * Creates a new controller + * + * @param protocol + * the protocol to use + * @param storage + * the storage to use + */ + public TorrentController(TorrentProtocol protocol, TorrentStorage storage) { + this.protocol = protocol; + this.storage = storage; + } + + /** + * Creates a new controller with {@link NIOTorrentStorage} as default + * storage. + * + * @param protocol + * thw protocol to use + */ + public TorrentController(TorrentProtocol protocol) { + this(protocol, new NIOTorrentStorage()); + } + + /** + * Creates a new controller with {@link PeerWireProtocol} as default + * protocol. + * + * @param storage + * the storage to use + */ + public TorrentController(TorrentStorage storage) { + this(new PeerWireProtocol(), storage); + } + + /** + * Creates a new controller with {@link PeerWireProtocol} as default + * protocol and {@link NIOTorrentStorage} as default storage. + */ + public TorrentController() { + this(new PeerWireProtocol(), new NIOTorrentStorage()); + } + + /** + * Retrieves data from the storage and validate all piece information + * already downloaded, if any. Automatically updates the torrent internal + * state to match the real download progress. + * + * @param torrent + * the torrent to check existing data + * @throws IOException + * if any IO error occur + */ + public void checkExistingData(Torrent torrent) throws IOException { + for (final TorrentPiece piece : torrent.getPieces()) { + final Hash hash = storage.checksum(piece); + if (!piece.getHash().equals(hash)) + continue; + torrent.getCompletedPieces().addPiece(piece); + } + } + + /** + * Registers a new torrent on this controller + * + * @param torrent + * the torrent to be registered + */ + public void registerTorrent(Torrent torrent) { + torrents.add(torrent); + torrent.setController(this); + } + + /** + * Removes an already registered torrent from this controller + * + * @param torrent + * the torrent to be removed + */ + public void removeTorrent(Torrent torrent) { + torrents.remove(torrent); + torrent.setController(null); + } + + /** + * Tries to locate the torrent represented by hash. + * + * @param hash + * the torrent hash to look for + * @return the torrent with the given hash, if any + */ + public Torrent findTorrent(Hash hash) { + for (final Torrent torrent : torrents) { + if (torrent.getHash().equals(hash)) + return torrent; + } + return null; + } + + /** + * Stars the controller + * + * @param port + * the listen port + */ + public void start(int port) { + protocol.start(this, port); + } + + /** + * Stops the controller + */ + public void stop() { + protocol.stop(); + } + + /** + * @return this controller configuration object + */ + public TorrentControllerConfiguration getConfig() { + return config; + } + + /** + * @return this controller protocol + */ + public TorrentProtocol getProtocol() { + return protocol; + } + + /** + * @return this controller storage + */ + public TorrentStorage getStorage() { + return storage; + } + + /** + * @return this controller torrent list + */ + public List getTorrents() { + return torrents; + } +} diff --git a/src/main/java/com/torrent4j/TorrentControllerConfiguration.java b/src/main/java/com/torrent4j/TorrentControllerConfiguration.java new file mode 100644 index 0000000..0af7a9b --- /dev/null +++ b/src/main/java/com/torrent4j/TorrentControllerConfiguration.java @@ -0,0 +1,15 @@ +package com.torrent4j; + +import com.torrent4j.util.PeerIDGenerator; + +public class TorrentControllerConfiguration { + private String peerID = PeerIDGenerator.generateRandomPeerID(); + + public String getPeerID() { + return peerID; + } + + public void setPeerID(String peerID) { + this.peerID = peerID; + } +} diff --git a/src/main/java/com/torrent4j/model/AbstractTorrentPiecesContainer.java b/src/main/java/com/torrent4j/model/AbstractTorrentPiecesContainer.java new file mode 100644 index 0000000..c3ecbb1 --- /dev/null +++ b/src/main/java/com/torrent4j/model/AbstractTorrentPiecesContainer.java @@ -0,0 +1,99 @@ +package com.torrent4j.model; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Iterator; +import java.util.List; + +import com.torrent4j.util.BitField; + +public abstract class AbstractTorrentPiecesContainer implements Iterable { + protected final Torrent torrent; + protected final BitField bitSet; + + public AbstractTorrentPiecesContainer(Torrent torrent) { + this.torrent = torrent; + this.bitSet = new BitField(torrent.getPieces().size()); + } + + public boolean hasPiece(TorrentPiece piece) { + return bitSet.get(piece.getIndex()); + } + + public void addPiece(TorrentPiece piece) { + bitSet.set(piece.getIndex(), true); + } + + public void removePiece(TorrentPiece piece) { + bitSet.set(piece.getIndex(), false); + } + + public boolean isSeeder() { + return bitSet.cardinality() == torrent.getPieces().size(); + } + + public boolean isLeecher() { + return !isSeeder(); + } + + public void clear() { + bitSet.clear(); + } + + public List getPieces() { + return createList(bitSet); + } + + public boolean hasMissingPieces(AbstractTorrentPiecesContainer other) { + final BitSet difference = (BitSet) bitSet.clone(); + difference.andNot(other.bitSet); + return difference.cardinality() != 0; + } + + public List getMissingPieces(AbstractTorrentPiecesContainer other) { + final BitSet difference = (BitSet) bitSet.clone(); + difference.andNot(other.bitSet); + return createList(difference); + } + + public List createList(BitSet bits) { + final List list = new ArrayList<>(); + for (int i = bits.nextSetBit(0); i >= 0; i = bits.nextSetBit(i + 1)) { + list.add(getTorrent().getPiece(i)); + } + return list; + } + + @Override + public Iterator iterator() { + return new Iterator() { + private int lastIndex = 0; + private int index = 0; + + @Override + public boolean hasNext() { + return bitSet.nextSetBit(lastIndex) >= 0; + } + + @Override + public TorrentPiece next() { + lastIndex = index; + return getTorrent().getPiece( + (index = bitSet.nextSetBit(lastIndex))); + } + + @Override + public void remove() { + removePiece(getTorrent().getPiece(lastIndex)); + } + }; + } + + public BitField getBitSet() { + return bitSet; + } + + public Torrent getTorrent() { + return torrent; + } +} diff --git a/src/main/java/com/torrent4j/model/Torrent.java b/src/main/java/com/torrent4j/model/Torrent.java new file mode 100644 index 0000000..ffe0b1c --- /dev/null +++ b/src/main/java/com/torrent4j/model/Torrent.java @@ -0,0 +1,359 @@ +package com.torrent4j.model; + +import static com.torrent4j.util.HashType.MD5; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.torrent4j.TorrentController; +import com.torrent4j.model.metadata.MetadataFile; +import com.torrent4j.model.metadata.MetadataInfo; +import com.torrent4j.model.metadata.TorrentMetadata; +import com.torrent4j.net.TorrentProtocol; +import com.torrent4j.strategy.TorrentStrategy; +import com.torrent4j.strategy.standard.StandardTorrentStrategy; +import com.torrent4j.util.Range; +import com.torrent4j.util.bencoding.BDecoder; +import com.torrent4j.util.bencoding.BMap; + +/** + * The main Torrent instance class. With this class, all torrent state is stored + * and external interaction can be performed. + * + * @author Rogiel + */ +public class Torrent { + /** + * The torrent strategy + */ + private final TorrentStrategy strategy; + + /** + * The torrent meta data + */ + private final TorrentMetadata metadata; + /** + * The torrent hash + */ + private final TorrentHash hash; + + /** + * The torrent pieces + */ + private final List pieces = new ArrayList<>(); + /** + * The torrent files + */ + private final List files = new ArrayList<>(); + + /** + * The torrent traffic control + */ + private final TorrentTrafficControl trafficControl = new TorrentTrafficControl(this); + /** + * The torrent swarm (list of all peers) + */ + private final TorrentSwarm swarm = new TorrentSwarm(this); + /** + * The torrent already downloaded pieces + */ + private final TorrentCompletePieces completedPieces; + + /** + * The current controller for this torrent object. If not attached to any + * controller, its value is null. + */ + private TorrentController controller; + + /** + * Creates a new torrent instance + * + * @param strategy + * the torrent strategy + * @param metadata + * the torrent meta data + */ + public Torrent(TorrentStrategy strategy, TorrentMetadata metadata) { + this.strategy = strategy; + this.metadata = metadata; + this.hash = new TorrentHash(this, metadata.getInfoHash()); + + // parse file list + final MetadataInfo info = metadata.getInfo(); + final byte[] pieceHashes = info.getPieceHashes(); + final int pieces = pieceHashes.length / 20; + long piecesLength = 0; + for (int i = 0; i < pieces; i++) { + final byte[] hash = Arrays.copyOfRange(pieceHashes, i * 20, + (i + 1) * 20); + final int length = (int) (i + 1 == pieces ? (info.getLength() - piecesLength) + : info.getPieceLength()); + + this.pieces.add(new TorrentPiece(this, hash, i, info + .getPieceLength() * i, length)); + piecesLength += length; + } + + final Path torrentPath = Paths.get(info.getName()); + long offset = 0; + for (final MetadataFile metaFile : info.getFiles()) { + byte[] hash = null; + if (metaFile.getHash() != null) + hash = MD5.fromString(metaFile.getHash()); + final List filePieces = getPieces(Range + .getRangeByLength(offset, metaFile.getLength())); + final TorrentFile file = new TorrentFile(this, offset, + metaFile.getLength(), filePieces, + torrentPath.resolve(metaFile.getFileName()), hash); + files.add(file); + for (final TorrentPiece piece : filePieces) { + piece.addFile(file); + } + offset += metaFile.getLength(); + } + + completedPieces = new TorrentCompletePieces(this); + + // try { + // this.localPeer.setPeerID(new String(Hex + // .decodeHex("851054102530302d9c640cd409c769266ad3a04f" + // .toCharArray()))); + // } catch (DecoderException e) { + // } + } + + /** + * Creates a new torrent instance with {@link StandardTorrentStrategy} as + * default strategy + * + * @param metadata + * the torrent meta data + */ + public Torrent(TorrentMetadata metadata) { + this(new StandardTorrentStrategy(), metadata); + } + + /** + * @return this torrent's strategy + */ + public TorrentStrategy getStrategy() { + return strategy; + } + + /** + * @return this torrent's meta data + */ + public TorrentMetadata getMetadata() { + return metadata; + } + + /** + * @return this torrent's hash + */ + public TorrentHash getHash() { + return hash; + } + + /** + * @return this torrent's pieces + */ + public List getPieces() { + return pieces; + } + + /** + * Determines all the pieces that contains at least one byte inside the + * requested range. + * + * @param range + * the range to look for pieces + * @return the pieces inside the requested range + */ + public List getPieces(Range range) { + final List pieces = new ArrayList<>(); + for (final TorrentPiece piece : this.pieces) { + if (piece.getTorrentRange().intersects(range)) + pieces.add(piece); + } + return pieces; + } + + /** + * @param index + * the piece index + * @return the piece at the requested index, if any. + */ + public TorrentPiece getPiece(int index) { + if ((index + 1) > pieces.size()) + return null; + return pieces.get(index); + } + + /** + * @return all the files for this torrent + */ + public List getFiles() { + return files; + } + + /** + * Determines all the files that contains at least one byte inside the + * requested range. + * + * @param range + * the range to search for files + * @return the list of files inside the requested range + */ + public List getFiles(Range range) { + final List files = new ArrayList<>(); + for (final TorrentFile file : this.files) { + if (file.getTorrentRange().intersects(range)) + files.add(file); + } + return files; + } + + /** + * @return the sum of all file sizes + */ + public long getTorrentSize() { + long size = 0; + for (final TorrentFile file : files) { + size += file.getLength(); + } + return size; + } + + /** + * @return the trafficControl + */ + public TorrentTrafficControl getTrafficControl() { + return trafficControl; + } + + /** + * @return the torrent swarm + */ + public TorrentSwarm getSwarm() { + return swarm; + } + + /** + * @return the downloaded pieces + */ + public TorrentCompletePieces getCompletedPieces() { + return completedPieces; + } + + /** + * @return this torrent's controller, if any. + */ + public TorrentController getController() { + return controller; + } + + /** + * Sets the current controller. This method should not be invoked + * manually! + * + * @param controller + * the controller to set + */ + public void setController(TorrentController controller) { + this.controller = controller; + } + + /** + * @return the torrent protocol + */ + public TorrentProtocol getProtocol() { + return controller.getProtocol(); + } + + @Override + public String toString() { + return "Torrent [hash=" + hash + "]"; + } + + /** + * Load an torrent from an {@link InputStream} + * + * @param in + * the {@link InputStream} + * @return the loaded {@link Torrent} instance + * @throws IOException + * if any error occur while reading the torrent file + */ + public static Torrent load(InputStream in) throws IOException { + final Object node = new BDecoder(in).readElement(); + final TorrentMetadata metadata = new TorrentMetadata((BMap) node); + return new Torrent(metadata); + } + + /** + * Load an torrent from an {@link InputStream} + * + * @param file + * the file {@link Path} + * @return the loaded {@link Torrent} instance + * @throws IOException + * if any error occur while reading the torrent file + */ + public static Torrent load(Path file) throws IOException { + return load(Files.newInputStream(file)); + } + + /** + * Load an torrent from an {@link File} + * + * @param file + * the {@link File} + * @return the loaded {@link Torrent} instance + * @throws IOException + * if any error occur while reading the torrent file + */ + public static Torrent load(File file) throws IOException { + if (file == null) + throw new InvalidParameterException("File cannot be null"); + return load(new FileInputStream(file)); + } + + /** + * Load an torrent from an {@link Byte} array + * + * @param content + * the {@link Byte} array + * @return the loaded {@link Torrent} instance + * @throws IOException + * if any error occur while reading the torrent file + */ + public static Torrent load(byte[] content) throws IOException { + return load(new ByteArrayInputStream(content)); + } + + /** + * Load an torrent from an {@link File} + * + * @param url + * the {@link URL} + * @return the loaded {@link Torrent} instance + * @throws IOException + * if any error occur while reading the torrent file + */ + public static Torrent load(URL url) throws IOException { + if (url == null) + throw new InvalidParameterException("File cannot be null"); + return load(url.openStream()); + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentCompletePieces.java b/src/main/java/com/torrent4j/model/TorrentCompletePieces.java new file mode 100644 index 0000000..7e5de95 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentCompletePieces.java @@ -0,0 +1,37 @@ +package com.torrent4j.model; + +/** + * An {@link AbstractTorrentPiecesContainer} dedicated to storing an + * {@link Torrent} already downloaded pieces. + *

+ * This instance overrides the {@link #addPiece(TorrentPiece)} and + * {@link #removePiece(TorrentPiece)} in order to set + * {@link TorrentPiece#setDownloaded(boolean)} accordingly, after that, the call + * is redirected to the super implementation. + * + * @author Rogiel + */ +public class TorrentCompletePieces extends AbstractTorrentPiecesContainer { + /** + * Creates a new instance + * + * @param torrent + * the torrent + */ + public TorrentCompletePieces(Torrent torrent) { + super(torrent); + } + + @Override + public void addPiece(TorrentPiece piece) { + super.addPiece(piece); + piece.setDownloaded(true); + } + + @Override + public void removePiece(TorrentPiece piece) { + super.removePiece(piece); + piece.setDownloaded(false); + } + +} diff --git a/src/main/java/com/torrent4j/model/TorrentFile.java b/src/main/java/com/torrent4j/model/TorrentFile.java new file mode 100644 index 0000000..4b974a6 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentFile.java @@ -0,0 +1,162 @@ +package com.torrent4j.model; + +import java.nio.file.Path; +import java.util.List; + +import com.torrent4j.storage.TorrentStorage; +import com.torrent4j.util.Range; + +/** + * An file that can be downloaded from a torrent peer. + *

+ * Though protocol implementations does not know about files, files themselves + * are composed from several pieces (multiple files can be represented by a + * single piece and (normally) a file is represented by several pieces -- both + * are valid, even simultaneously) that are downloaded and can later be + * assembled into a file by an {@link TorrentStorage}. + * + * @author Rogiel + */ +public class TorrentFile { + /** + * The torrent containing this file + */ + private final Torrent torrent; + /** + * The file hash, if present at the metadata file + */ + private TorrentFileHash hash; + + /** + * The file offset (relative to the torrent) + */ + private final long offset; + /** + * The file length + */ + private final long length; + + /** + * The list of pieces that contains at least one byte of the file data + */ + private final List pieces; + + /** + * The file path relative to the torrent + */ + private Path path; + + /** + * Creates a new instance + * + * @param torrent + * the torrent + * @param offset + * the file offset (relative to the torrent) + * @param length + * the file length + * @param pieces + * the list of pieces that contains at least one byte of the file + * data + * @param path + * the file path relative to the torrent + * @param hash + * the file hash, if present at the metadata file + */ + public TorrentFile(Torrent torrent, long offset, long length, + List pieces, Path path, byte[] hash) { + this.torrent = torrent; + this.offset = offset; + this.length = length; + this.pieces = pieces; + this.path = path; + if (hash != null) + this.hash = new TorrentFileHash(this, hash); + } + + /** + * @return the file hash, if present at the metadata file + */ + public TorrentFileHash getHash() { + return hash; + } + + /** + * @param hash + * the file hash + */ + public void setHash(TorrentFileHash hash) { + this.hash = hash; + } + + /** + * @return the file offset (relative to the torrent) + */ + public long getOffset() { + return offset; + } + + /** + * @return the file length + */ + public long getLength() { + return length; + } + + /** + * @return the range of this file inside the torrent + */ + public Range getTorrentRange() { + return Range.getRangeByLength(offset, length); + } + + /** + * @return the range of this file as a single file + */ + public Range getFileRange() { + return Range.getRangeByLength(0, length); + } + + /** + * @return the list of pieces that contains at least one byte of the file + * data + */ + public List getPieces() { + return pieces; + } + + /** + * @return the file path relative to the torrent + */ + public Path getPath() { + return path; + } + + /** + * @param path + * the file path relative to the torrent + */ + public void setPath(Path path) { + this.path = path; + } + + /** + * @return the file name + */ + public String getFileName() { + return path.getFileName().toString(); + } + + /** + * @return the parent torrent + */ + public Torrent getTorrent() { + return torrent; + } + + @Override + public String toString() { + return "TorrentFile [torrent=" + torrent + ", hash=" + hash + + ", length=" + length + ", path=" + path + "]"; + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentFileHash.java b/src/main/java/com/torrent4j/model/TorrentFileHash.java new file mode 100644 index 0000000..0cc50a3 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentFileHash.java @@ -0,0 +1,21 @@ +package com.torrent4j.model; + +import com.torrent4j.util.Hash; +import com.torrent4j.util.HashType; + +public class TorrentFileHash extends Hash { + private final TorrentFile file; + + public TorrentFileHash(TorrentFile file, byte[] hash) { + super(HashType.MD5, hash); + this.file = file; + } + + public TorrentFile getFile() { + return file; + } + + public Torrent getTorrent() { + return file.getTorrent(); + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentHash.java b/src/main/java/com/torrent4j/model/TorrentHash.java new file mode 100644 index 0000000..f7483ff --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentHash.java @@ -0,0 +1,18 @@ +package com.torrent4j.model; + +import com.torrent4j.util.Hash; +import com.torrent4j.util.HashType; + +public class TorrentHash extends Hash { + private final Torrent torrent; + + public TorrentHash(Torrent torrent, byte[] hash) { + super(HashType.SHA1, hash); + this.torrent = torrent; + } + + public Torrent getTorrent() { + return torrent; + } + +} diff --git a/src/main/java/com/torrent4j/model/TorrentPeer.java b/src/main/java/com/torrent4j/model/TorrentPeer.java new file mode 100644 index 0000000..9e1f2c9 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentPeer.java @@ -0,0 +1,233 @@ +package com.torrent4j.model; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +import com.torrent4j.model.TorrentPeerState.TorrentPeerChoking; +import com.torrent4j.model.TorrentPeerState.TorrentPeerInterest; +import com.torrent4j.net.TorrentProtocolPeer; + +/** + * Represents a peer on the BitTorrent network. This object contains information + * necessary to locate and connect to a peer. + * + * @author Rogiel + */ +public class TorrentPeer { + /** + * The parent torrent for this peer + */ + private final Torrent torrent; + /** + * The pieces this peer has complete + */ + private final TorrentPeerPieces pieces; + + /** + * The peer ID + */ + private String peerID; + + /** + * The protocol peer used to communicate with it + */ + private TorrentProtocolPeer protocolPeer; + /** + * The socket address + */ + private InetSocketAddress address; + /** + * Whether the peer can be contacted directly (has public accessible IP) + */ + private boolean accessible; + + /** + * This peer's traffic control + */ + private final TorrentPeerTrafficControl trafficControl = new TorrentPeerTrafficControl( + this); + /** + * The peer state + */ + private TorrentPeerState state = new TorrentPeerState(this); + + /** + * Creates a new instance + * + * @param torrent + * the torrent + */ + public TorrentPeer(Torrent torrent) { + this.torrent = torrent; + this.pieces = new TorrentPeerPieces(this); + } + + public TorrentPeerPieces getPieces() { + return pieces; + } + + public String getPeerID() { + return peerID; + } + + public void setPeerID(String peerID) { + this.peerID = peerID; + } + + public TorrentProtocolPeer getProtocolPeer() { + return protocolPeer; + } + + public void setProtocolPeer(TorrentProtocolPeer protocolPeer) { + this.protocolPeer = protocolPeer; + } + + public boolean connect() { + if (protocolPeer == null) { + if (torrent.getProtocol() == null) + return false; + return torrent.getProtocol().connect(this); + } else { + return protocolPeer.connect(); + } + } + + public boolean isConnectable() { + return address != null; + } + + public boolean isConnected() { + if (protocolPeer == null) + return false; + return protocolPeer.isConnected(); + } + + public boolean disconnect() { + if (protocolPeer == null) + return false; + return protocolPeer.disconnect(); + } + + public InetSocketAddress getAddress() { + return address; + } + + public void setAddress(InetSocketAddress address) { + this.address = address; + } + + public final InetAddress getIP() { + return address.getAddress(); + } + + public final int getPort() { + return address.getPort(); + } + + public final String getHostName(boolean resolve) { + return resolve ? address.getHostName() : address.getHostString(); + } + + public final String getHostName() { + return getHostName(false); + } + + public boolean isAccessible() { + return accessible; + } + + public void setAccessible(boolean accessible) { + this.accessible = accessible; + } + + /** + * @return the traffic shaper + */ + public TorrentPeerTrafficControl getTrafficControl() { + return trafficControl; + } + + public TorrentPeerState getState() { + return state; + } + + public void resetState() { + state = new TorrentPeerState(this); + } + + // NETWORK RELATED THINGS! + public void handshake() { + // FIXME sent another peer id + protocolPeer.handshake(torrent.getHash().getHash(), torrent + .getController().getConfig().getPeerID()); + } + + public void declareInterest() { + if (state.isLocalllyInterested()) + return; + protocolPeer.interested(); + state.setLocalInterest(TorrentPeerInterest.INTERESTED); + } + + public void withdrawInterest() { + if (!state.isLocalllyInterested()) + return; + protocolPeer.notInterested(); + state.setLocalInterest(TorrentPeerInterest.NOT_INTERESTED); + } + + public void choke() { + if (state.isLocallyChoked()) + return; + protocolPeer.choke(); + state.setLocallyChoked(TorrentPeerChoking.CHOKED); + } + + public void unchoke() { + if (!state.isLocallyChoked()) + return; + protocolPeer.unchoke(); + state.setLocallyChoked(TorrentPeerChoking.UNCHOKED); + } + + public void have(TorrentPiece piece) { + protocolPeer.have(piece.getIndex()); + } + + public void bitField() { + protocolPeer.bitField(torrent.getCompletedPieces().getBitSet()); + } + + public void requestBlock(TorrentPieceBlock block) { + protocolPeer.requestBlock(block.getPiece().getIndex(), + block.getOffset(), block.getLength()); + } + + public void cancelRequestedBlock(TorrentPieceBlock block) { + protocolPeer.cancelRequestedBlock(block.getPiece().getIndex(), + block.getOffset(), block.getLength()); + } + + public void sendBlock(TorrentPieceBlock block, ByteBuffer data) { + protocolPeer.sendBlock(block.getPiece().getIndex(), block.getOffset(), + data); + } + + public void sendPort(int port) { + protocolPeer.port(port); + } + + public void keepAlive() { + protocolPeer.keepAlive(); + } + + public Torrent getTorrent() { + return torrent; + } + + @Override + public String toString() { + return "TorrentPeer [torrent=" + torrent + ", address=" + address + "]"; + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentPeerPieces.java b/src/main/java/com/torrent4j/model/TorrentPeerPieces.java new file mode 100644 index 0000000..28cff6c --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentPeerPieces.java @@ -0,0 +1,24 @@ +package com.torrent4j.model; + +import java.util.BitSet; + + +public class TorrentPeerPieces extends AbstractTorrentPiecesContainer { + private final TorrentPeer peer; + + public void load(BitSet bitSet) { + this.bitSet.set(0, this.bitSet.size(), false); + for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) { + this.bitSet.set(i, true); + } + } + + public TorrentPeerPieces(TorrentPeer peer) { + super(peer.getTorrent()); + this.peer = peer; + } + + public TorrentPeer getPeer() { + return peer; + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentPeerState.java b/src/main/java/com/torrent4j/model/TorrentPeerState.java new file mode 100644 index 0000000..90f0a29 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentPeerState.java @@ -0,0 +1,266 @@ +package com.torrent4j.model; + +import java.util.Date; + +public class TorrentPeerState { + private final TorrentPeer peer; + + private TorrentPeerInterest remoteInterest = TorrentPeerInterest.NOT_INTERESTED; + private TorrentPeerInterest localInterest = TorrentPeerInterest.NOT_INTERESTED; + + public enum TorrentPeerInterest { + INTERESTED, NOT_INTERESTED; + } + + private TorrentPeerChoking remoteChoked = TorrentPeerChoking.CHOKED; + private TorrentPeerChoking locallyChoked = TorrentPeerChoking.CHOKED; + + public enum TorrentPeerChoking { + CHOKED, UNCHOKED; + } + + private TorrentPieceBlock downloadRequestedBlock; + private Date downloadRequestedDate; + private TorrentPieceBlock lastDownloadedBlock; + private Date lastDownloadedBlockDate; + + private TorrentPieceBlock uploadRequestedBlock; + private Date uploadRequestedDate; + private TorrentPieceBlock lastUploadedBlock; + private Date lastUploadedBlockDate; + + public TorrentPeerState(TorrentPeer peer) { + this.peer = peer; + } + + /** + * @return the remoteInterest + */ + public TorrentPeerInterest getRemoteInterest() { + return remoteInterest; + } + + /** + * @return the remoteInterest + */ + public boolean isRemotellyInterested() { + return remoteInterest == TorrentPeerInterest.INTERESTED; + } + + /** + * @param remoteInterest + * the remoteInterest to set + */ + public void setRemoteInterest(TorrentPeerInterest remoteInterest) { + this.remoteInterest = remoteInterest; + } + + /** + * @return the localInterest + */ + public TorrentPeerInterest getLocalInterest() { + return localInterest; + } + + /** + * @return the localInterest + */ + public boolean isLocalllyInterested() { + return localInterest == TorrentPeerInterest.INTERESTED; + } + + /** + * @param localInterest + * the localInterest to set + */ + public void setLocalInterest(TorrentPeerInterest localInterest) { + this.localInterest = localInterest; + } + + /** + * @return the remoteChoked + */ + public TorrentPeerChoking getRemoteChoked() { + return remoteChoked; + } + + /** + * @return the remoteChoked + */ + public boolean isRemotellyChoked() { + return remoteChoked == TorrentPeerChoking.CHOKED; + } + + /** + * @param remoteChoked + * the remoteChoked to set + */ + public void setRemoteChoked(TorrentPeerChoking remoteChoked) { + this.remoteChoked = remoteChoked; + } + + /** + * @return the locallyChoked + */ + public TorrentPeerChoking getLocallyChoked() { + return locallyChoked; + } + + /** + * @return the locallyChoked + */ + public boolean isLocallyChoked() { + return locallyChoked == TorrentPeerChoking.CHOKED; + } + + /** + * @param locallyChoked + * the locallyChoked to set + */ + public void setLocallyChoked(TorrentPeerChoking locallyChoked) { + this.locallyChoked = locallyChoked; + } + + /** + * @return the downloadRequestedBlock + */ + public TorrentPieceBlock getDownloadRequestedBlock() { + return downloadRequestedBlock; + } + + /** + * @return the downloadRequestedBlock + */ + public boolean hasDownloadRequestedBlock() { + return downloadRequestedBlock != null; + } + + /** + * @param downloadRequestedBlock + * the downloadRequestedBlock to set + */ + public void setDownloadRequestedBlock( + TorrentPieceBlock downloadRequestedBlock) { + this.downloadRequestedBlock = downloadRequestedBlock; + } + + /** + * @return the downloadRequestedDate + */ + public Date getDownloadRequestedDate() { + return downloadRequestedDate; + } + + /** + * @param downloadRequestedDate + * the downloadRequestedDate to set + */ + public void setDownloadRequestedDate(Date downloadRequestedDate) { + this.downloadRequestedDate = downloadRequestedDate; + } + + /** + * @return the lastDownloadedBlock + */ + public TorrentPieceBlock getLastDownloadedBlock() { + return lastDownloadedBlock; + } + + /** + * @param lastDownloadedBlock + * the lastDownloadedBlock to set + */ + public void setLastDownloadedBlock(TorrentPieceBlock lastDownloadedBlock) { + this.lastDownloadedBlock = lastDownloadedBlock; + } + + /** + * @return the lastDownloadedBlockDate + */ + public Date getLastDownloadedBlockDate() { + return lastDownloadedBlockDate; + } + + /** + * @param lastDownloadedBlockDate + * the lastDownloadedBlockDate to set + */ + public void setLastDownloadedBlockDate(Date lastDownloadedBlockDate) { + this.lastDownloadedBlockDate = lastDownloadedBlockDate; + } + + /** + * @return the uploadRequestedBlock + */ + public TorrentPieceBlock getUploadRequestedBlock() { + return uploadRequestedBlock; + } + + /** + * @return the uploadRequestedBlock + */ + public boolean hasUploadRequestedBlock() { + return uploadRequestedBlock != null; + } + + /** + * @param uploadRequestedBlock + * the uploadRequestedBlock to set + */ + public void setUploadRequestedBlock(TorrentPieceBlock uploadRequestedBlock) { + this.uploadRequestedBlock = uploadRequestedBlock; + } + + /** + * @return the uploadRequestedDate + */ + public Date getUploadRequestedDate() { + return uploadRequestedDate; + } + + /** + * @param uploadRequestedDate + * the uploadRequestedDate to set + */ + public void setUploadRequestedDate(Date uploadRequestedDate) { + this.uploadRequestedDate = uploadRequestedDate; + } + + /** + * @return the lastUploadedBlock + */ + public TorrentPieceBlock getLastUploadedBlock() { + return lastUploadedBlock; + } + + /** + * @param lastUploadedBlock + * the lastUploadedBlock to set + */ + public void setLastUploadedBlock(TorrentPieceBlock lastUploadedBlock) { + this.lastUploadedBlock = lastUploadedBlock; + } + + /** + * @return the lastUploadedBlockDate + */ + public Date getLastUploadedBlockDate() { + return lastUploadedBlockDate; + } + + /** + * @param lastUploadedBlockDate + * the lastUploadedBlockDate to set + */ + public void setLastUploadedBlockDate(Date lastUploadedBlockDate) { + this.lastUploadedBlockDate = lastUploadedBlockDate; + } + + public TorrentPeer getPeer() { + return peer; + } + + public Torrent getTorrent() { + return peer.getTorrent(); + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentPeerTrafficControl.java b/src/main/java/com/torrent4j/model/TorrentPeerTrafficControl.java new file mode 100644 index 0000000..46c89d4 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentPeerTrafficControl.java @@ -0,0 +1,17 @@ +package com.torrent4j.model; + +public class TorrentPeerTrafficControl extends TorrentTrafficControl { + private final TorrentPeer peer; + + public TorrentPeerTrafficControl(TorrentPeer peer) { + super(peer.getTorrent()); + this.peer = peer; + } + + /** + * @return the peer + */ + public TorrentPeer getPeer() { + return peer; + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentPiece.java b/src/main/java/com/torrent4j/model/TorrentPiece.java new file mode 100644 index 0000000..e26755f --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentPiece.java @@ -0,0 +1,128 @@ +package com.torrent4j.model; + +import java.util.ArrayList; +import java.util.List; + +import com.torrent4j.util.Range; + +public class TorrentPiece { + private final Torrent torrent; + private final TorrentPieceHash hash; + + private final int index; + private final int offset; + private final int length; + + private final List blocks = new ArrayList<>(); + private final List files = new ArrayList<>(); + + public TorrentPiece(Torrent torrent, byte[] hash, int index, int offset, + int length) { + this.torrent = torrent; + this.hash = new TorrentPieceHash(this, hash); + this.index = index; + this.offset = offset; + this.length = length; + + final int blocks = (int) Math.ceil((double) length + / TorrentPieceBlock.BLOCK_LENGTH); + for (int i = 0; i < blocks; i++) { + final int blockOffset = TorrentPieceBlock.BLOCK_LENGTH * i; + int len = TorrentPieceBlock.BLOCK_LENGTH; + if (i == blocks - 1 && length % TorrentPieceBlock.BLOCK_LENGTH != 0) + len = length - blockOffset; + this.blocks.add(new TorrentPieceBlock(this, blockOffset, len)); + } + } + + public TorrentPieceHash getHash() { + return hash; + } + + public int getIndex() { + return index; + } + + public long getOffset() { + return offset; + } + + public int getLength() { + return length; + } + + public Range getTorrentRange() { + return Range.getRangeByLength(offset, length); + } + + public boolean isFirst() { + return index == 0; + } + + public boolean isLast() { + return torrent.getPieces().size() == index + 1; + } + + public List getBlocks() { + return blocks; + } + + public TorrentPieceBlock getBlock(int offset, int length) { + for (final TorrentPieceBlock block : blocks) { + if (block.getOffset() == offset && block.getLength() == length) + return block; + } + return null; + } + + public TorrentPieceBlock getFirstBlock() { + return blocks.get(0); + } + + public TorrentPieceBlock getLastBlock() { + return blocks.get(blocks.size() - 1); + } + + public TorrentPieceBlock getNextBlock(TorrentPieceBlock block) { + int next = blocks.indexOf(block) + 1; + if (blocks.size() == next) + return null; + return blocks.get(next); + } + + public boolean isDownloaded() { + for (final TorrentPieceBlock block : blocks) { + if (!block.isDownloaded()) + return false; + } + return true; + } + + public void setDownloaded(boolean downloaded) { + for (final TorrentPieceBlock block : blocks) { + block.setDownloaded(downloaded); + } + } + + public List getFiles() { + return files; + } + + /* package protected! */void addFile(TorrentFile file) { + files.add(file); + } + + public TorrentPiece getNextPiece() { + return torrent.getPiece(index + 1); + } + + public Torrent getTorrent() { + return torrent; + } + + @Override + public String toString() { + return "TorrentPiece [torrent=" + torrent + ", hash=" + hash + + ", index=" + index + ", length=" + length + "]"; + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentPieceBlock.java b/src/main/java/com/torrent4j/model/TorrentPieceBlock.java new file mode 100644 index 0000000..2660984 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentPieceBlock.java @@ -0,0 +1,81 @@ +package com.torrent4j.model; + +import com.torrent4j.util.Range; + +public class TorrentPieceBlock { + public static final int BLOCK_LENGTH = 16 * 1024; + + private final TorrentPiece piece; + private final int offset; + private final int length; + + private boolean downloaded = false; + + public TorrentPieceBlock(TorrentPiece piece, int offset, int length) { + this.piece = piece; + this.offset = offset; + this.length = length; + } + + public int getOffset() { + return offset; + } + + public int getLength() { + return length; + } + + public Range getTorrentRange() { + return Range.getRangeByLength(piece.getOffset() + offset, length); + } + + /** + * Get the range inside the piece. + * + * @return the range in piece + */ + public Range getPieceRange() { + return Range.getRangeByLength(offset, length); + } + + /** + * Get the range inside the file + * + * @param file + * the file + * @return the range in file + */ + public Range getFileRange(TorrentFile file) { + if (piece.getOffset() - file.getOffset() + offset < 0) + return null; + return Range.getRangeByLength( + piece.getOffset() - file.getOffset() + offset, length) + .intersection(file.getFileRange()); + } + + public boolean isDownloaded() { + return downloaded; + } + + public void setDownloaded(boolean downloaded) { + this.downloaded = downloaded; + } + + public TorrentPieceBlock getNextBlock() { + return piece.getNextBlock(this); + } + + public TorrentPiece getPiece() { + return piece; + } + + public Torrent getTorrent() { + return piece.getTorrent(); + } + + @Override + public String toString() { + return "TorrentPieceBlock [piece=" + piece + ", offset=" + offset + + ", length=" + length + "]"; + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentPieceHash.java b/src/main/java/com/torrent4j/model/TorrentPieceHash.java new file mode 100644 index 0000000..c2a2ee7 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentPieceHash.java @@ -0,0 +1,21 @@ +package com.torrent4j.model; + +import com.torrent4j.util.Hash; +import com.torrent4j.util.HashType; + +public class TorrentPieceHash extends Hash { + private final TorrentPiece piece; + + public TorrentPieceHash(TorrentPiece piece, byte[] hash) { + super(HashType.SHA1, hash); + this.piece = piece; + } + + public TorrentPiece getPiece() { + return piece; + } + + public Torrent getTorrent() { + return piece.getTorrent(); + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentSwarm.java b/src/main/java/com/torrent4j/model/TorrentSwarm.java new file mode 100644 index 0000000..15ea0bb --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentSwarm.java @@ -0,0 +1,69 @@ +package com.torrent4j.model; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TorrentSwarm { + private final Torrent torrent; + private final List peers = new ArrayList<>(); + + public TorrentSwarm(Torrent torrent) { + this.torrent = torrent; + } + + public void broadcast(SwarmBroadcastHandler handler) { + for (final TorrentPeer peer : getConnectedPeers()) { + try { + handler.broadcast(peer); + } catch (Exception e) { + if (handler.exception(e)) + throw new RuntimeException(e); + } + } + } + + public interface SwarmBroadcastHandler { + void broadcast(TorrentPeer peer); + + boolean exception(Exception e); + } + + public List getPeers() { + return Collections.unmodifiableList(peers); + } + + public List getConnectedPeers() { + final List list = new ArrayList<>(); + for (final TorrentPeer peer : peers) { + if (peer.isConnected()) + list.add(peer); + } + return Collections.unmodifiableList(list); + } + + public void addPeer(TorrentPeer peer) { + peers.add(peer); + torrent.getStrategy().getPeerStrategy().peerDiscovered(torrent, peer); + } + + public void removePeer(TorrentPeer peer) { + peers.remove(peer); + torrent.getStrategy().getPeerStrategy().peerRemoved(torrent, peer); + } + + public TorrentPeer findPeer(InetSocketAddress address, String peerID) { + for (final TorrentPeer peer : peers) { + if (peerID != null && peerID.equals(peer.getPeerID())) + return peer; + if (address != null && address.equals(peer.getAddress())) + return peer; + } + return null; + } + + public Torrent getTorrent() { + return torrent; + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentTracker.java b/src/main/java/com/torrent4j/model/TorrentTracker.java new file mode 100644 index 0000000..4a376b3 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentTracker.java @@ -0,0 +1,13 @@ +package com.torrent4j.model; + +public class TorrentTracker { + private final Torrent torrent; + + public TorrentTracker(Torrent torrent) { + this.torrent = torrent; + } + + public Torrent getTorrent() { + return torrent; + } +} diff --git a/src/main/java/com/torrent4j/model/TorrentTrafficControl.java b/src/main/java/com/torrent4j/model/TorrentTrafficControl.java new file mode 100644 index 0000000..293c203 --- /dev/null +++ b/src/main/java/com/torrent4j/model/TorrentTrafficControl.java @@ -0,0 +1,91 @@ +package com.torrent4j.model; + +public class TorrentTrafficControl { + private final Torrent torrent; + + private long downloadSpeedLimit = 0; + private long uploadSpeedLimit = 0; + + private long currentDownloadSpeed = 0; + private long currentUploadSpeed = 0; + + public TorrentTrafficControl(Torrent torrent) { + this.torrent = torrent; + } + + /** + * @return the downloadSpeedLimit + */ + public long getDownloadSpeedLimit() { + return downloadSpeedLimit; + } + + /** + * @param downloadSpeedLimit + * the downloadSpeedLimit to set + */ + public void setDownloadSpeedLimit(long downloadSpeedLimit) { + this.downloadSpeedLimit = downloadSpeedLimit; + } + + /** + * @return the uploadSpeedLimit + */ + public long getUploadSpeedLimit() { + return uploadSpeedLimit; + } + + /** + * @param uploadSpeedLimit + * the uploadSpeedLimit to set + */ + public void setUploadSpeedLimit(long uploadSpeedLimit) { + this.uploadSpeedLimit = uploadSpeedLimit; + } + + /** + * @return the currentDownloadSpeed + */ + public long getCurrentDownloadSpeed() { + return currentDownloadSpeed; + } + + /** + * @param currentDownloadSpeed + * the currentDownloadSpeed to set + */ + public void setCurrentDownloadSpeed(long currentDownloadSpeed) { + this.currentDownloadSpeed = currentDownloadSpeed; + } + + /** + * @return the currentUploadSpeed + */ + public long getCurrentUploadSpeed() { + return currentUploadSpeed; + } + + /** + * @param currentUploadSpeed + * the currentUploadSpeed to set + */ + public void setCurrentUploadSpeed(long currentUploadSpeed) { + this.currentUploadSpeed = currentUploadSpeed; + } + + @Override + public String toString() { + return "TorrentTrafficControl [torrent=" + torrent + + ", downloadSpeedLimit=" + downloadSpeedLimit + + ", uploadSpeedLimit=" + uploadSpeedLimit + + ", currentDownloadSpeed=" + currentDownloadSpeed + + ", currentUploadSpeed=" + currentUploadSpeed + "]"; + } + + /** + * @return the torrent + */ + public Torrent getTorrent() { + return torrent; + } +} diff --git a/src/main/java/com/torrent4j/model/metadata/MetadataFile.java b/src/main/java/com/torrent4j/model/metadata/MetadataFile.java new file mode 100644 index 0000000..a1508f3 --- /dev/null +++ b/src/main/java/com/torrent4j/model/metadata/MetadataFile.java @@ -0,0 +1,51 @@ +package com.torrent4j.model.metadata; + +import java.io.IOException; + +import com.torrent4j.util.bencoding.BList; +import com.torrent4j.util.bencoding.BMap; + +public class MetadataFile { + private String fileName; + private long length; + private String hash; + + public MetadataFile(BMap file) throws IOException { + if (file.get("path") != null) { + final BList path = file.getList("path"); + final StringBuilder builder = new StringBuilder(); + for (final Object pathPart : path) { + builder.append(new String((byte[]) pathPart)).append("/"); + } + this.fileName = builder.substring(0, builder.length() - 1); + } else { + this.fileName = file.getString("name"); + } + this.length = file.getLong("length"); + this.hash = file.getString("md5sum"); + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public long getLength() { + return length; + } + + public void setLength(long length) { + this.length = length; + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } +} diff --git a/src/main/java/com/torrent4j/model/metadata/MetadataInfo.java b/src/main/java/com/torrent4j/model/metadata/MetadataInfo.java new file mode 100644 index 0000000..37a088f --- /dev/null +++ b/src/main/java/com/torrent4j/model/metadata/MetadataInfo.java @@ -0,0 +1,83 @@ +package com.torrent4j.model.metadata; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.torrent4j.util.bencoding.BList; +import com.torrent4j.util.bencoding.BMap; + +public class MetadataInfo { + private int pieceLength; + private byte[] pieceHashes; + private boolean privateTorrent; + + private String name; + private List files = new ArrayList<>(); + + public MetadataInfo(BMap dictionary) throws IOException { + this.pieceLength = dictionary.getInteger("piece length"); + this.pieceHashes = (byte[]) dictionary.get("pieces"); + if (dictionary.get("private") != null) + this.privateTorrent = dictionary.getInteger("private") == 1; + this.name = dictionary.getString("name"); + + if (dictionary.get("files") != null) { + final BList files = dictionary + .getList("files"); + for (final Object file : files) { + this.files.add(new MetadataFile((BMap) file)); + } + } else { + this.files.add(new MetadataFile(dictionary)); + } + } + + public int getPieceLength() { + return pieceLength; + } + + public void setPieceLength(int pieceLength) { + this.pieceLength = pieceLength; + } + + public byte[] getPieceHashes() { + return pieceHashes; + } + + public void setPieceHashes(byte[] pieceHashes) { + this.pieceHashes = pieceHashes; + } + + public boolean isPrivateTorrent() { + return privateTorrent; + } + + public void setPrivateTorrent(boolean privateTorrent) { + this.privateTorrent = privateTorrent; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getFiles() { + return files; + } + + public void setFiles(List files) { + this.files = files; + } + + public long getLength() { + long length = 0; + for (final MetadataFile file : files) { + length += file.getLength(); + } + return length; + } +} diff --git a/src/main/java/com/torrent4j/model/metadata/MetadataTracker.java b/src/main/java/com/torrent4j/model/metadata/MetadataTracker.java new file mode 100644 index 0000000..b87ceb5 --- /dev/null +++ b/src/main/java/com/torrent4j/model/metadata/MetadataTracker.java @@ -0,0 +1,48 @@ +package com.torrent4j.model.metadata; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import com.torrent4j.util.bencoding.BList; + +public class MetadataTracker { + private String url; + private List backupUrls; + + public MetadataTracker(String url, List backupUrls) { + this.url = url; + this.backupUrls = backupUrls; + } + + public MetadataTracker(String url, String... backupUrls) { + this(url, Arrays.asList(backupUrls)); + } + + public MetadataTracker(String url) { + this.url = url; + } + + public MetadataTracker(BList urls) throws IOException { + this.url = urls.getString(0); + if(urls.size() > 1) { + //TODO + } + } + + public String getURL() { + return url; + } + + public void setURL(String url) { + this.url = url; + } + + public List getBackupURLs() { + return backupUrls; + } + + public void setBackupURLs(List backupUrls) { + this.backupUrls = backupUrls; + } +} diff --git a/src/main/java/com/torrent4j/model/metadata/TorrentMetadata.java b/src/main/java/com/torrent4j/model/metadata/TorrentMetadata.java new file mode 100644 index 0000000..01e7d9c --- /dev/null +++ b/src/main/java/com/torrent4j/model/metadata/TorrentMetadata.java @@ -0,0 +1,73 @@ +package com.torrent4j.model.metadata; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.torrent4j.util.HashType; +import com.torrent4j.util.bencoding.BEncoder; +import com.torrent4j.util.bencoding.BList; +import com.torrent4j.util.bencoding.BMap; + +public class TorrentMetadata { + private MetadataInfo info; + private byte[] infoHash; + + private List trackers = new ArrayList<>(); + private Date creationDate; + + public TorrentMetadata(BMap dictionary) throws IOException { + this.trackers + .add(new MetadataTracker(dictionary.getString("announce"))); + final BList announceListNode = dictionary.getList("announce-list"); + if (announceListNode != null) { + for (final Object trackerGroup : announceListNode) { + trackers.add(new MetadataTracker((BList) trackerGroup)); + } + } + if (dictionary.get("creation date") != null) + this.creationDate = new Date( + dictionary.getInteger("creation date") * 1000); + + this.infoHash = HashType.SHA1.hash(BEncoder.bencode(dictionary + .get("info"))); + this.info = new MetadataInfo(dictionary.getMap("info")); + } + + public byte[] getInfoHash() { + return infoHash; + } + + public void setInfoHash(byte[] infoHash) { + this.infoHash = infoHash; + } + + public MetadataInfo getInfo() { + return info; + } + + public void setInfo(MetadataInfo info) { + this.info = info; + } + + public List getTrackers() { + return trackers; + } + + public MetadataTracker getMainTracker() { + return trackers.get(0); + } + + public void setTrackers(List trackers) { + this.trackers = trackers; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } +} diff --git a/src/main/java/com/torrent4j/net/TorrentProtocol.java b/src/main/java/com/torrent4j/net/TorrentProtocol.java new file mode 100644 index 0000000..92e01ba --- /dev/null +++ b/src/main/java/com/torrent4j/net/TorrentProtocol.java @@ -0,0 +1,12 @@ +package com.torrent4j.net; + +import com.torrent4j.TorrentController; +import com.torrent4j.model.TorrentPeer; + +public interface TorrentProtocol { + void start(TorrentController controller, int listenPort); + + void stop(); + + boolean connect(TorrentPeer peer); +} diff --git a/src/main/java/com/torrent4j/net/TorrentProtocolPeer.java b/src/main/java/com/torrent4j/net/TorrentProtocolPeer.java new file mode 100644 index 0000000..3e2b1c2 --- /dev/null +++ b/src/main/java/com/torrent4j/net/TorrentProtocolPeer.java @@ -0,0 +1,43 @@ +package com.torrent4j.net; + +import java.nio.ByteBuffer; +import java.util.BitSet; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPeer; + +public interface TorrentProtocolPeer { + public TorrentPeer getTorrentPeer(); + + public Torrent getTorrent(); + + boolean connect(); + + boolean isConnected(); + + boolean disconnect(); + + void handshake(byte[] torrentHash, String peerID); + + void requestBlock(int pieceIndex, int start, int length); + + void cancelRequestedBlock(int pieceIndex, int start, int length); + + void sendBlock(int pieceIndex, int start, ByteBuffer data); + + void bitField(BitSet bitSet); + + void have(int pieceIndex); + + void choke(); + + void unchoke(); + + void interested(); + + void notInterested(); + + void port(int dhtPort); + + void keepAlive(); +} diff --git a/src/main/java/com/torrent4j/net/TorrentTrafficShaper.java b/src/main/java/com/torrent4j/net/TorrentTrafficShaper.java new file mode 100644 index 0000000..e337676 --- /dev/null +++ b/src/main/java/com/torrent4j/net/TorrentTrafficShaper.java @@ -0,0 +1,8 @@ +package com.torrent4j.net; + +public interface TorrentTrafficShaper { + void update(long writeLimit, long readLimit); + + long getDownloadSpeed(); + long getUploadSpeed(); +} diff --git a/src/main/java/com/torrent4j/net/peerwire/AbstractPeerWireMessage.java b/src/main/java/com/torrent4j/net/peerwire/AbstractPeerWireMessage.java new file mode 100644 index 0000000..0b5cff3 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/AbstractPeerWireMessage.java @@ -0,0 +1,28 @@ +package com.torrent4j.net.peerwire; + +import io.netty.buffer.ChannelBuffer; + +public abstract class AbstractPeerWireMessage implements PeerWireMessage { + public final int messageID; + + public AbstractPeerWireMessage(int messageID) { + this.messageID = messageID; + } + + @Override + public final void write(ChannelBuffer buffer) { + buffer.writeByte(messageID); + writeImpl(buffer); + } + + public void writeImpl(ChannelBuffer buffer) { + } + + @Override + public final void read(ChannelBuffer buffer) { + readImpl(buffer); + } + + public void readImpl(ChannelBuffer buffer) { + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/PeerWireHandler.java b/src/main/java/com/torrent4j/net/peerwire/PeerWireHandler.java new file mode 100644 index 0000000..70a373f --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/PeerWireHandler.java @@ -0,0 +1,270 @@ +package com.torrent4j.net.peerwire; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.MessageEvent; +import io.netty.channel.SimpleChannelHandler; + +import java.net.InetSocketAddress; +import java.util.Date; + +import com.torrent4j.TorrentController; +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPeerState.TorrentPeerChoking; +import com.torrent4j.model.TorrentPeerState.TorrentPeerInterest; +import com.torrent4j.model.TorrentPiece; +import com.torrent4j.model.TorrentPieceBlock; +import com.torrent4j.net.peerwire.codec.PeerWireFrameDecoder; +import com.torrent4j.net.peerwire.codec.PeerWireFrameEncoder; +import com.torrent4j.net.peerwire.codec.PeerWireMessageDecoder; +import com.torrent4j.net.peerwire.codec.PeerWireMessageEncoder; +import com.torrent4j.net.peerwire.messages.BitFieldMessage; +import com.torrent4j.net.peerwire.messages.BlockMessage; +import com.torrent4j.net.peerwire.messages.CancelMessage; +import com.torrent4j.net.peerwire.messages.ChokeMessage; +import com.torrent4j.net.peerwire.messages.HandshakeMessage; +import com.torrent4j.net.peerwire.messages.HaveMessage; +import com.torrent4j.net.peerwire.messages.InterestedMessage; +import com.torrent4j.net.peerwire.messages.NotInterestedMessage; +import com.torrent4j.net.peerwire.messages.RequestMessage; +import com.torrent4j.net.peerwire.messages.UnchokeMessage; +import com.torrent4j.net.peerwire.traffic.PeerTrafficShapingHandler; +import com.torrent4j.net.peerwire.traffic.TorrentTrafficShapingHandler; +import com.torrent4j.util.Hash; +import com.torrent4j.util.HashType; + +public class PeerWireHandler extends SimpleChannelHandler { + private final TorrentController controller; + private PeerWireProtocolPeer peer; + + public PeerWireHandler(TorrentController controller) { + this.controller = controller; + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + try { + final Object msg = e.getMessage(); + if (!(msg instanceof PeerWireMessage)) + return; + if (msg instanceof HandshakeMessage) { + final HandshakeMessage message = (HandshakeMessage) msg; + + ((PeerWireFrameDecoder) e.getChannel().getPipeline() + .get("frame-decoder")).setHandshaked(true); + ((PeerWireMessageDecoder) e.getChannel().getPipeline() + .get("message-decoder")).setHandshaked(true); + + final Hash hash = new Hash(HashType.SHA1, message.torrentHash); + final Torrent torrent = controller.findTorrent(hash); + + if (torrent == null) { + e.getChannel().disconnect(); + return; + } + + TorrentPeer peer = torrent.getSwarm().findPeer( + (InetSocketAddress) e.getChannel().getRemoteAddress(), + message.peerID); + if (peer == null) { + peer = new TorrentPeer(torrent); + peer.setPeerID(message.peerID); + peer.setAddress((InetSocketAddress) e.getChannel() + .getRemoteAddress()); + } + this.peer = (PeerWireProtocolPeer) peer.getProtocolPeer(); + + e.getChannel().getPipeline().get(PeerTrafficShapingHandler.class) + .setPeer(peer); + e.getChannel().getPipeline() + .get(TorrentTrafficShapingHandler.class) + .setTorrent(torrent); + + peer.resetState(); + this.peer.getStrategy().getPeerStrategy() + .peerConnected(torrent, peer); + } else if (msg instanceof HaveMessage) { + peer.getStrategy() + .getPeerStrategy() + .havePiece( + peer.getTorrent(), + peer.getTorrentPeer(), + peer.getTorrent().getPiece( + ((HaveMessage) msg).pieceIndex)); + } else if (msg instanceof UnchokeMessage) { + peer.getTorrentPeer().getState() + .setRemoteChoked(TorrentPeerChoking.UNCHOKED); + peer.getStrategy().getPeerStrategy() + .unchoked(peer.getTorrent(), peer.getTorrentPeer()); + } else if (msg instanceof ChokeMessage) { + peer.getTorrentPeer().getState() + .setRemoteChoked(TorrentPeerChoking.CHOKED); + peer.getStrategy().getPeerStrategy() + .choked(peer.getTorrent(), peer.getTorrentPeer()); + } else if (msg instanceof InterestedMessage) { + peer.getTorrentPeer().getState() + .setRemoteInterest(TorrentPeerInterest.INTERESTED); + peer.getStrategy().getPeerStrategy() + .interested(peer.getTorrent(), peer.getTorrentPeer()); + } else if (msg instanceof NotInterestedMessage) { + peer.getTorrentPeer().getState() + .setRemoteInterest(TorrentPeerInterest.NOT_INTERESTED); + peer.getStrategy() + .getPeerStrategy() + .notInterested(peer.getTorrent(), peer.getTorrentPeer()); + } else if (msg instanceof BitFieldMessage) { + peer.getTorrentPeer().getPieces() + .load(((BitFieldMessage) msg).bitSet); + peer.getStrategy().getPeerStrategy() + .bitField(peer.getTorrent(), peer.getTorrentPeer()); + } else if (msg instanceof RequestMessage) { + final RequestMessage message = (RequestMessage) msg; + + final TorrentPiece piece = peer.getTorrent().getPiece( + message.pieceIndex); + final TorrentPieceBlock block = piece.getBlock(message.begin, + message.length); + + peer.getTorrentPeer().getState().setUploadRequestedBlock(block); + peer.getTorrentPeer().getState() + .setUploadRequestedDate(new Date()); + + peer.getStrategy() + .getUploadStrategy() + .blockRequested(peer.getTorrent(), block, + peer.getTorrentPeer()); + } else if (msg instanceof CancelMessage) { + final CancelMessage message = (CancelMessage) msg; + + final TorrentPiece piece = peer.getTorrent().getPiece( + message.pieceIndex); + final TorrentPieceBlock block = piece.getBlock(message.begin, + message.length); + + peer.getTorrentPeer().getState().setUploadRequestedBlock(null); + peer.getTorrentPeer().getState().setUploadRequestedDate(null); + + peer.getStrategy() + .getUploadStrategy() + .blockRequestCancelled(peer.getTorrent(), block, + peer.getTorrentPeer()); + } else if (msg instanceof BlockMessage) { + final BlockMessage message = (BlockMessage) msg; + + final TorrentPiece piece = peer.getTorrent().getPiece( + message.pieceIndex); + final TorrentPieceBlock block = piece.getBlock(message.begin, + message.data.remaining()); + + peer.getTorrentPeer().getState().setLastDownloadedBlock(block); + peer.getTorrentPeer().getState() + .setLastDownloadedBlockDate(new Date()); + + peer.getTorrentPeer().getState() + .setDownloadRequestedBlock(null); + peer.getTorrentPeer().getState().setDownloadRequestedDate(null); + + controller.getStorage().write(piece.getTorrent(), + block.getTorrentRange(), message.data); + block.setDownloaded(true); + if (piece.isDownloaded()) { + final Hash pieceHash = controller.getStorage().checksum( + piece); + if (!piece.getHash().equals(pieceHash)) { + piece.getTorrent() + .getStrategy() + .getDownloadStrategy() + .pieceChecksumFailed(piece.getTorrent(), piece, + peer.getTorrentPeer()); + } else { + piece.getTorrent().getCompletedPieces().addPiece(piece); + piece.getTorrent() + .getStrategy() + .getDownloadStrategy() + .pieceComplete(piece.getTorrent(), piece, + peer.getTorrentPeer()); + } + } else { + piece.getTorrent() + .getStrategy() + .getDownloadStrategy() + .blockReceived(block.getTorrent(), block, + peer.getTorrentPeer()); + } + } else { + System.out.println(msg); + } + } finally { + ctx.sendUpstream(e); + } + } + + @Override + public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + try { + final Object msg = e.getMessage(); + if (!(msg instanceof PeerWireMessage)) + return; + if (msg instanceof HandshakeMessage) { + e.getFuture().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) + throws Exception { + ((PeerWireFrameEncoder) future.getChannel() + .getPipeline().get("frame-encoder")) + .setHandshaked(true); + ((PeerWireMessageEncoder) future.getChannel() + .getPipeline().get("message-encoder")) + .setHandshaked(true); + } + }); + } else if (msg instanceof BlockMessage) { + final BlockMessage message = (BlockMessage) msg; + + final TorrentPiece piece = peer.getTorrent().getPiece( + message.pieceIndex); + final TorrentPieceBlock block = piece.getBlock(message.begin, + message.data.remaining()); + + peer.getTorrentPeer().getState().setLastUploadedBlock(block); + peer.getTorrentPeer().getState() + .setLastUploadedBlockDate(new Date()); + peer.getTorrentPeer().getState().setUploadRequestedBlock(null); + peer.getTorrentPeer().getState().setUploadRequestedDate(null); + } else if (msg instanceof RequestMessage) { + final RequestMessage message = (RequestMessage) msg; + + final TorrentPiece piece = peer.getTorrent().getPiece( + message.pieceIndex); + final TorrentPieceBlock block = piece.getBlock(message.begin, + message.length); + + peer.getTorrentPeer().getState() + .setDownloadRequestedBlock(block); + peer.getTorrentPeer().getState() + .setDownloadRequestedDate(new Date()); + } else if (msg instanceof CancelMessage) { + peer.getTorrentPeer().getState() + .setDownloadRequestedBlock(null); + peer.getTorrentPeer().getState().setDownloadRequestedDate(null); + } + } finally { + ctx.sendDownstream(e); + } + } + + // @Override + // public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent + // e) + // throws Exception { + // try { + // peer = new PeerWireProtocolPeer(e.getChannel()); + // } finally { + // ctx.sendUpstream(e); + // } + // } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/PeerWireMessage.java b/src/main/java/com/torrent4j/net/peerwire/PeerWireMessage.java new file mode 100644 index 0000000..bcb98df --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/PeerWireMessage.java @@ -0,0 +1,9 @@ +package com.torrent4j.net.peerwire; + +import io.netty.buffer.ChannelBuffer; + +public interface PeerWireMessage { + void write(ChannelBuffer buffer); + + void read(ChannelBuffer buffer); +} diff --git a/src/main/java/com/torrent4j/net/peerwire/PeerWirePipelineFactory.java b/src/main/java/com/torrent4j/net/peerwire/PeerWirePipelineFactory.java new file mode 100644 index 0000000..06daa0e --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/PeerWirePipelineFactory.java @@ -0,0 +1,46 @@ +package com.torrent4j.net.peerwire; + +import static io.netty.channel.Channels.pipeline; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPipelineFactory; + +import java.util.concurrent.Executor; + +import com.torrent4j.TorrentController; +import com.torrent4j.net.peerwire.codec.PeerWireFrameDecoder; +import com.torrent4j.net.peerwire.codec.PeerWireFrameEncoder; +import com.torrent4j.net.peerwire.codec.PeerWireMessageDecoder; +import com.torrent4j.net.peerwire.codec.PeerWireMessageEncoder; +import com.torrent4j.net.peerwire.traffic.TorrentTrafficShapingHandler; +import com.torrent4j.net.peerwire.traffic.PeerTrafficShapingHandler; + +public class PeerWirePipelineFactory implements ChannelPipelineFactory { + private final TorrentController controller; + private final Executor executor; + + public PeerWirePipelineFactory(TorrentController controller, + Executor executor) { + this.controller = controller; + this.executor = executor; + } + + @Override + public ChannelPipeline getPipeline() throws Exception { + final ChannelPipeline p = pipeline(); + + p.addLast("torrent-shaper", new TorrentTrafficShapingHandler(executor)); + p.addLast("traffic-shaper", new PeerTrafficShapingHandler(executor)); + + p.addLast("frame-decoder", new PeerWireFrameDecoder()); + p.addLast("frame-encoder", new PeerWireFrameEncoder()); + + p.addLast("message-decoder", new PeerWireMessageDecoder()); + p.addLast("message-encoder", new PeerWireMessageEncoder()); + + // p.addLast("logging", new LoggingHandler(InternalLogLevel.WARN)); + + p.addLast("handler", new PeerWireHandler(controller)); + + return p; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/PeerWireProtocol.java b/src/main/java/com/torrent4j/net/peerwire/PeerWireProtocol.java new file mode 100644 index 0000000..65d5c34 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/PeerWireProtocol.java @@ -0,0 +1,65 @@ +package com.torrent4j.net.peerwire; + +import io.netty.bootstrap.ClientBootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.socket.nio.NioClientSocketChannelFactory; +import io.netty.channel.socket.nio.NioServerSocketChannelFactory; + +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import com.torrent4j.TorrentController; +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.net.TorrentProtocol; + +public class PeerWireProtocol implements TorrentProtocol { + private final Executor threadPool = Executors.newCachedThreadPool(); + + private final ServerBootstrap serverBootstrap = new ServerBootstrap( + new NioServerSocketChannelFactory(threadPool, threadPool)); + private final ClientBootstrap clientBootstrap = new ClientBootstrap( + new NioClientSocketChannelFactory(threadPool, threadPool)); + + private Channel serverChannel; + + @Override + public void start(TorrentController controller, int listenPort) { + serverBootstrap.setPipelineFactory(new PeerWirePipelineFactory( + controller, threadPool)); + clientBootstrap.setPipelineFactory(new PeerWirePipelineFactory( + controller, threadPool)); + + try { + serverChannel = serverBootstrap.bind(new InetSocketAddress( + Inet4Address.getByName("0.0.0.0"), listenPort)); + } catch (UnknownHostException e) { + } + } + + @Override + public void stop() { + clientBootstrap.releaseExternalResources(); + serverChannel.close(); + serverBootstrap.releaseExternalResources(); + } + + @Override + public boolean connect(TorrentPeer peer) { + final ChannelFuture future = clientBootstrap.connect(peer.getAddress()) + .awaitUninterruptibly(); + if (future.isSuccess()) { + final PeerWireProtocolPeer protocolPeer = new PeerWireProtocolPeer( + future.getChannel()); + protocolPeer.setTorrentPeer(peer); + peer.setProtocolPeer(protocolPeer); + return true; + } else { + return false; + } + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/PeerWireProtocolPeer.java b/src/main/java/com/torrent4j/net/peerwire/PeerWireProtocolPeer.java new file mode 100644 index 0000000..b7ce9cb --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/PeerWireProtocolPeer.java @@ -0,0 +1,129 @@ +package com.torrent4j.net.peerwire; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; + +import java.nio.ByteBuffer; +import java.util.BitSet; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.net.TorrentProtocolPeer; +import com.torrent4j.net.peerwire.messages.BitFieldMessage; +import com.torrent4j.net.peerwire.messages.BlockMessage; +import com.torrent4j.net.peerwire.messages.CancelMessage; +import com.torrent4j.net.peerwire.messages.ChokeMessage; +import com.torrent4j.net.peerwire.messages.HandshakeMessage; +import com.torrent4j.net.peerwire.messages.HaveMessage; +import com.torrent4j.net.peerwire.messages.InterestedMessage; +import com.torrent4j.net.peerwire.messages.KeepAliveMessage; +import com.torrent4j.net.peerwire.messages.NotInterestedMessage; +import com.torrent4j.net.peerwire.messages.PortMessage; +import com.torrent4j.net.peerwire.messages.RequestMessage; +import com.torrent4j.net.peerwire.messages.UnchokeMessage; +import com.torrent4j.strategy.TorrentStrategy; + +public class PeerWireProtocolPeer implements TorrentProtocolPeer { + private final Channel channel; + private TorrentPeer peer; + + public PeerWireProtocolPeer(Channel channel) { + this.channel = channel; + } + + @Override + public boolean connect() { + return channel.connect(channel.getRemoteAddress()) + .awaitUninterruptibly().isSuccess(); + } + + @Override + public boolean isConnected() { + return channel.isConnected(); + } + + @Override + public boolean disconnect() { + return channel.disconnect().awaitUninterruptibly().isSuccess(); + } + + @Override + public void handshake(byte[] torrentHash, String peerID) { + write(new HandshakeMessage(torrentHash, peerID)); + } + + @Override + public void requestBlock(int pieceIndex, int start, int length) { + write(new RequestMessage(pieceIndex, start, length)); + } + + @Override + public void cancelRequestedBlock(int pieceIndex, int start, int length) { + write(new CancelMessage(pieceIndex, start, length)); + } + + @Override + public void sendBlock(int pieceIndex, int start, ByteBuffer data) { + write(new BlockMessage(pieceIndex, start, data)); + } + + @Override + public void bitField(BitSet bitSet) { + write(new BitFieldMessage(bitSet)); + } + + @Override + public void have(int pieceIndex) { + write(new HaveMessage(pieceIndex)); + } + + @Override + public void choke() { + write(new ChokeMessage()); + } + + @Override + public void unchoke() { + write(new UnchokeMessage()); + } + + @Override + public void interested() { + write(new InterestedMessage()); + } + + @Override + public void notInterested() { + write(new NotInterestedMessage()); + } + + @Override + public void port(int dhtPort) { + write(new PortMessage(dhtPort)); + } + + @Override + public void keepAlive() { + write(new KeepAliveMessage()); + } + + public ChannelFuture write(PeerWireMessage message) { + return channel.write(message); + } + + public TorrentPeer getTorrentPeer() { + return peer; + } + + public void setTorrentPeer(TorrentPeer peer) { + this.peer = peer; + } + + public Torrent getTorrent() { + return peer.getTorrent(); + } + + public TorrentStrategy getStrategy() { + return peer.getTorrent().getStrategy(); + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireFrameDecoder.java b/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireFrameDecoder.java new file mode 100644 index 0000000..d31b46b --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireFrameDecoder.java @@ -0,0 +1,40 @@ +package com.torrent4j.net.peerwire.codec; + +import io.netty.buffer.ChannelBuffer; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.frame.FrameDecoder; + +public class PeerWireFrameDecoder extends FrameDecoder { + private boolean handshaked = false; + + @Override + protected Object decode(ChannelHandlerContext ctx, Channel channel, + ChannelBuffer buffer) throws Exception { + if (!handshaked) { + int pos = buffer.readerIndex(); + buffer.skipBytes(68); + return buffer.copy(pos, 68); + } + + if (buffer.readableBytes() < 4) + return null; + + int pos = buffer.readerIndex(); + final int len = buffer.readInt(); + if (buffer.readableBytes() >= len) { + try { + return buffer.slice(buffer.readerIndex(), len); + } finally { + buffer.skipBytes(len); + } + } else { + buffer.readerIndex(pos); + return null; + } + } + + public void setHandshaked(boolean handshaked) { + this.handshaked = handshaked; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireFrameEncoder.java b/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireFrameEncoder.java new file mode 100644 index 0000000..61a2abd --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireFrameEncoder.java @@ -0,0 +1,30 @@ +package com.torrent4j.net.peerwire.codec; + +import io.netty.buffer.ChannelBuffer; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.oneone.OneToOneEncoder; + +public class PeerWireFrameEncoder extends OneToOneEncoder { + private boolean handshaked = false; + + @Override + protected Object encode(ChannelHandlerContext ctx, Channel channel, + Object msg) throws Exception { + if (!(msg instanceof ChannelBuffer) || !handshaked) + return msg; + final ChannelBuffer buffer = (ChannelBuffer) msg; + + buffer.readerIndex(0); + if (handshaked) { + final int len = buffer.readableBytes() - 4; + buffer.setInt(0, len); + } + + return buffer; + } + + public void setHandshaked(boolean handshaked) { + this.handshaked = handshaked; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireMessageDecoder.java b/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireMessageDecoder.java new file mode 100644 index 0000000..75f9b2b --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireMessageDecoder.java @@ -0,0 +1,85 @@ +package com.torrent4j.net.peerwire.codec; + +import io.netty.buffer.ChannelBuffer; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.oneone.OneToOneDecoder; + +import com.torrent4j.net.peerwire.PeerWireMessage; +import com.torrent4j.net.peerwire.messages.BitFieldMessage; +import com.torrent4j.net.peerwire.messages.BlockMessage; +import com.torrent4j.net.peerwire.messages.CancelMessage; +import com.torrent4j.net.peerwire.messages.ChokeMessage; +import com.torrent4j.net.peerwire.messages.HandshakeMessage; +import com.torrent4j.net.peerwire.messages.HaveMessage; +import com.torrent4j.net.peerwire.messages.InterestedMessage; +import com.torrent4j.net.peerwire.messages.KeepAliveMessage; +import com.torrent4j.net.peerwire.messages.NotInterestedMessage; +import com.torrent4j.net.peerwire.messages.PortMessage; +import com.torrent4j.net.peerwire.messages.RequestMessage; +import com.torrent4j.net.peerwire.messages.UnchokeMessage; + +public class PeerWireMessageDecoder extends OneToOneDecoder { + private boolean handshaked = false; + + @Override + protected Object decode(ChannelHandlerContext ctx, Channel channel, + Object msg) throws Exception { + if (!(msg instanceof ChannelBuffer)) + return msg; + final ChannelBuffer buffer = (ChannelBuffer) msg; + + if (!handshaked) { + final HandshakeMessage message = new HandshakeMessage(); + message.read(buffer); + + return message; + } else { + if(buffer.readableBytes() == 0) + return new KeepAliveMessage(); + + final byte opcode = buffer.readByte(); + final PeerWireMessage message; + switch (opcode) { + case CancelMessage.MESSAGE_ID: + message = new CancelMessage(); + break; + case BitFieldMessage.MESSAGE_ID: + message = new BitFieldMessage(); + break; + case ChokeMessage.MESSAGE_ID: + message = new ChokeMessage(); + break; + case HaveMessage.MESSAGE_ID: + message = new HaveMessage(); + break; + case InterestedMessage.MESSAGE_ID: + message = new InterestedMessage(); + break; + case NotInterestedMessage.MESSAGE_ID: + message = new NotInterestedMessage(); + break; + case BlockMessage.MESSAGE_ID: + message = new BlockMessage(); + break; + case PortMessage.MESSAGE_ID: + message = new PortMessage(); + break; + case RequestMessage.MESSAGE_ID: + message = new RequestMessage(); + break; + case UnchokeMessage.MESSAGE_ID: + message = new UnchokeMessage(); + break; + default: + return null; + } + message.read(buffer); + return message; + } + } + + public void setHandshaked(boolean handshaked) { + this.handshaked = handshaked; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireMessageEncoder.java b/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireMessageEncoder.java new file mode 100644 index 0000000..9863ed8 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/codec/PeerWireMessageEncoder.java @@ -0,0 +1,33 @@ +package com.torrent4j.net.peerwire.codec; + +import io.netty.buffer.ChannelBuffer; +import io.netty.buffer.ChannelBuffers; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.oneone.OneToOneEncoder; + +import com.torrent4j.net.peerwire.PeerWireMessage; +import com.torrent4j.net.peerwire.messages.KeepAliveMessage; + +public class PeerWireMessageEncoder extends OneToOneEncoder { + private boolean handshaked = false; + + @Override + protected Object encode(ChannelHandlerContext ctx, Channel channel, + Object msg) throws Exception { + if (!(msg instanceof PeerWireMessage)) + return msg; + final PeerWireMessage message = (PeerWireMessage) msg; + final ChannelBuffer buffer = ChannelBuffers.dynamicBuffer(); + + if (handshaked && !(message instanceof KeepAliveMessage)) + buffer.writeInt(0x00); + message.write(buffer); + + return buffer; + } + + public void setHandshaked(boolean handshaked) { + this.handshaked = handshaked; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/BitFieldMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/BitFieldMessage.java new file mode 100644 index 0000000..565cd33 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/BitFieldMessage.java @@ -0,0 +1,58 @@ +package com.torrent4j.net.peerwire.messages; + +import io.netty.buffer.ChannelBuffer; + +import java.util.BitSet; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class BitFieldMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x05; + + public BitSet bitSet; + + public BitFieldMessage() { + super(MESSAGE_ID); + } + + public BitFieldMessage(BitSet bitSet) { + super(MESSAGE_ID); + this.bitSet = bitSet; + } + + @Override + public void writeImpl(ChannelBuffer buffer) { + for (int i = 0; i < bitSet.size();) { + byte data = 0; + for (int j = 128; i < bitSet.size() && j > 0; j >>= 1, i++) { + if (bitSet.get(i)) { + data |= j; + } + } + buffer.writeByte(data); + } + } + + @Override + public void readImpl(ChannelBuffer buffer) { + bitSet = new BitSet(); + int i = 0; + while (buffer.readable()) { + byte b = buffer.readByte(); + for (int j = 128; j > 0; j >>= 1) { + bitSet.set(i++, (b & j) != 0); + } + + } + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "BITFIELD [bitSet=" + bitSet + "]"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/BlockMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/BlockMessage.java new file mode 100644 index 0000000..a0f1fba --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/BlockMessage.java @@ -0,0 +1,51 @@ +package com.torrent4j.net.peerwire.messages; + +import io.netty.buffer.ChannelBuffer; + +import java.nio.ByteBuffer; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class BlockMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x07; + + public int pieceIndex; + public int begin; + public ByteBuffer data; + + public BlockMessage() { + super(MESSAGE_ID); + } + + public BlockMessage(int pieceIndex, int begin, ByteBuffer data) { + super(MESSAGE_ID); + this.pieceIndex = pieceIndex; + this.begin = begin; + this.data = data; + } + + @Override + public void writeImpl(ChannelBuffer buffer) { + buffer.writeInt(pieceIndex); + buffer.writeInt(begin); + buffer.writeBytes(data); + } + + @Override + public void readImpl(ChannelBuffer buffer) { + pieceIndex = buffer.readInt(); + begin = buffer.readInt(); + data = buffer.readBytes(buffer.readableBytes()).toByteBuffer(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "BLOCK [pieceIndex=" + pieceIndex + ", begin=" + begin + + ", data=" + data + "]"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/CancelMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/CancelMessage.java new file mode 100644 index 0000000..86bd7e0 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/CancelMessage.java @@ -0,0 +1,49 @@ +package com.torrent4j.net.peerwire.messages; + +import io.netty.buffer.ChannelBuffer; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class CancelMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x08; + + public int pieceIndex; + public int begin; + public int length; + + public CancelMessage() { + super(MESSAGE_ID); + } + + public CancelMessage(int pieceIndex, int begin, int length) { + super(MESSAGE_ID); + this.pieceIndex = pieceIndex; + this.begin = begin; + this.length = length; + } + + @Override + public void writeImpl(ChannelBuffer buffer) { + buffer.writeInt(pieceIndex); + buffer.writeInt(begin); + buffer.writeInt(length); + } + + @Override + public void readImpl(ChannelBuffer buffer) { + pieceIndex = buffer.readInt(); + begin = buffer.readInt(); + length = buffer.readInt(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "CANCEL [pieceIndex=" + pieceIndex + ", begin=" + begin + + ", length=" + length + "]"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/ChokeMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/ChokeMessage.java new file mode 100644 index 0000000..93d9cb2 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/ChokeMessage.java @@ -0,0 +1,19 @@ +package com.torrent4j.net.peerwire.messages; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class ChokeMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x00; + + public ChokeMessage() { + super(MESSAGE_ID); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "CHOKE []"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/HandshakeMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/HandshakeMessage.java new file mode 100644 index 0000000..90bc33e --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/HandshakeMessage.java @@ -0,0 +1,54 @@ +package com.torrent4j.net.peerwire.messages; + +import io.netty.buffer.ChannelBuffer; + +import java.util.Arrays; + +import com.torrent4j.net.peerwire.PeerWireMessage; + +public class HandshakeMessage implements PeerWireMessage { + public int protocolStringLength = 19; + public String protocolString = "BitTorrent protocol"; + public long reserved = 0; + public byte[] torrentHash; + public String peerID; + + public HandshakeMessage() { + } + + public HandshakeMessage(byte[] torrentHash, String peerID) { + this.torrentHash = torrentHash; + this.peerID = peerID; + } + + @Override + public void write(ChannelBuffer buffer) { + buffer.writeByte(protocolStringLength); + buffer.writeBytes(protocolString.getBytes()); + buffer.writeLong(reserved); + buffer.writeBytes(torrentHash, 0, 20); + buffer.writeBytes(peerID.getBytes(), 0, 20); + } + + @Override + public void read(ChannelBuffer buffer) { + protocolStringLength = buffer.readByte(); + protocolString = buffer.readBytes(protocolStringLength).toString(); + reserved = buffer.readLong(); + torrentHash = buffer.readBytes(20).array(); + peerID = buffer.readBytes(20).toString(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "HANDSHAKE [protocolStringLength=" + protocolStringLength + + ", protocolString=" + protocolString + ", reserved=" + + reserved + ", torrentHash=" + Arrays.toString(torrentHash) + + ", peerID=" + peerID + "]"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/HaveMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/HaveMessage.java new file mode 100644 index 0000000..ad369b3 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/HaveMessage.java @@ -0,0 +1,40 @@ +package com.torrent4j.net.peerwire.messages; + +import io.netty.buffer.ChannelBuffer; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class HaveMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x04; + + public int pieceIndex; + + public HaveMessage() { + super(MESSAGE_ID); + } + + public HaveMessage(int pieceIndex) { + super(MESSAGE_ID); + this.pieceIndex = pieceIndex; + } + + @Override + public void writeImpl(ChannelBuffer buffer) { + buffer.writeInt(pieceIndex); + } + + @Override + public void readImpl(ChannelBuffer buffer) { + pieceIndex = buffer.readInt(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "HAVE [pieceIndex=" + pieceIndex + "]"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/InterestedMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/InterestedMessage.java new file mode 100644 index 0000000..6f37f1b --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/InterestedMessage.java @@ -0,0 +1,21 @@ +package com.torrent4j.net.peerwire.messages; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class InterestedMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x02; + + public InterestedMessage() { + super(MESSAGE_ID); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "INTERESTED []"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/KeepAliveMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/KeepAliveMessage.java new file mode 100644 index 0000000..9e278b0 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/KeepAliveMessage.java @@ -0,0 +1,28 @@ +package com.torrent4j.net.peerwire.messages; + +import io.netty.buffer.ChannelBuffer; + +import com.torrent4j.net.peerwire.PeerWireMessage; + +public class KeepAliveMessage implements PeerWireMessage { + public KeepAliveMessage() { + } + + @Override + public void write(ChannelBuffer buffer) { + } + + @Override + public void read(ChannelBuffer buffer) { + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "KEEP_ALIVE []"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/NotInterestedMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/NotInterestedMessage.java new file mode 100644 index 0000000..cb11e78 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/NotInterestedMessage.java @@ -0,0 +1,21 @@ +package com.torrent4j.net.peerwire.messages; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class NotInterestedMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x03; + + public NotInterestedMessage() { + super(MESSAGE_ID); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "NOT_INTERESTED []"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/PortMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/PortMessage.java new file mode 100644 index 0000000..1560cd3 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/PortMessage.java @@ -0,0 +1,40 @@ +package com.torrent4j.net.peerwire.messages; + +import io.netty.buffer.ChannelBuffer; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class PortMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x09; + + public int listenPort; + + public PortMessage() { + super(MESSAGE_ID); + } + + public PortMessage(int listenPort) { + super(MESSAGE_ID); + this.listenPort = listenPort; + } + + @Override + public void writeImpl(ChannelBuffer buffer) { + buffer.writeInt(listenPort); + } + + @Override + public void readImpl(ChannelBuffer buffer) { + listenPort = buffer.readInt(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "PORT [listenPort=" + listenPort + "]"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/RequestMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/RequestMessage.java new file mode 100644 index 0000000..144954d --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/RequestMessage.java @@ -0,0 +1,44 @@ +package com.torrent4j.net.peerwire.messages; + +import io.netty.buffer.ChannelBuffer; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class RequestMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x06; + + public int pieceIndex; + public int begin; + public int length; + + public RequestMessage() { + super(MESSAGE_ID); + } + + public RequestMessage(int pieceIndex, int begin, int length) { + super(MESSAGE_ID); + this.pieceIndex = pieceIndex; + this.begin = begin; + this.length = length; + } + + @Override + public void writeImpl(ChannelBuffer buffer) { + buffer.writeInt(pieceIndex); + buffer.writeInt(begin); + buffer.writeInt(length); + } + + @Override + public void readImpl(ChannelBuffer buffer) { + pieceIndex = buffer.readInt(); + begin = buffer.readInt(); + length = buffer.readInt(); + } + + @Override + public String toString() { + return "REQUEST [pieceIndex=" + pieceIndex + ", begin=" + begin + + ", length=" + length + "]"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/messages/UnchokeMessage.java b/src/main/java/com/torrent4j/net/peerwire/messages/UnchokeMessage.java new file mode 100644 index 0000000..72871d3 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/messages/UnchokeMessage.java @@ -0,0 +1,21 @@ +package com.torrent4j.net.peerwire.messages; + +import com.torrent4j.net.peerwire.AbstractPeerWireMessage; + +public class UnchokeMessage extends AbstractPeerWireMessage { + public static final int MESSAGE_ID = 0x01; + + public UnchokeMessage() { + super(MESSAGE_ID); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "UNCHOKE []"; + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/traffic/PeerTrafficShapingHandler.java b/src/main/java/com/torrent4j/net/peerwire/traffic/PeerTrafficShapingHandler.java new file mode 100644 index 0000000..f23f8c6 --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/traffic/PeerTrafficShapingHandler.java @@ -0,0 +1,60 @@ +package com.torrent4j.net.peerwire.traffic; + +import io.netty.handler.traffic.ChannelTrafficShapingHandler; +import io.netty.handler.traffic.TrafficCounter; + +import java.util.concurrent.Executor; + +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPeerTrafficControl; + +public class PeerTrafficShapingHandler extends ChannelTrafficShapingHandler { + private long writeLimit; + private long readLimit; + + private TorrentPeer peer; + + public PeerTrafficShapingHandler(Executor executor) { + super(executor, 0, 0); + } + + private void reconfigure() { + if (peer == null) + return; + final TorrentPeerTrafficControl peerTraffic = peer.getTrafficControl(); + long readLimit = peerTraffic.getDownloadSpeedLimit(); + long writeLimit = peerTraffic.getUploadSpeedLimit(); + if (readLimit != this.readLimit || writeLimit != this.writeLimit) { + this.writeLimit = writeLimit; + this.readLimit = readLimit; + configure(writeLimit, readLimit); + } + } + + @Override + protected void doAccounting(TrafficCounter counter) { + if (peer == null) + return; + reconfigure(); + peer.getTrafficControl().setCurrentDownloadSpeed( + counter.getLastReadThroughput()); + peer.getTrafficControl().setCurrentUploadSpeed( + counter.getLastWriteThroughput()); + } + + /** + * @return the peer + */ + public TorrentPeer getPeer() { + return peer; + } + + /** + * @param peer + * the peer to set + */ + public void setPeer(TorrentPeer peer) { + this.peer = peer; + reconfigure(); + } +} diff --git a/src/main/java/com/torrent4j/net/peerwire/traffic/TorrentTrafficShapingHandler.java b/src/main/java/com/torrent4j/net/peerwire/traffic/TorrentTrafficShapingHandler.java new file mode 100644 index 0000000..4baec2c --- /dev/null +++ b/src/main/java/com/torrent4j/net/peerwire/traffic/TorrentTrafficShapingHandler.java @@ -0,0 +1,60 @@ +package com.torrent4j.net.peerwire.traffic; + +import io.netty.handler.traffic.GlobalTrafficShapingHandler; +import io.netty.handler.traffic.TrafficCounter; + +import java.util.concurrent.Executor; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentTrafficControl; + +public class TorrentTrafficShapingHandler extends GlobalTrafficShapingHandler { + private Torrent torrent; + + private long writeLimit; + private long readLimit; + + public TorrentTrafficShapingHandler(Executor executor) { + super(executor, 0, 0); + } + + private void reconfigure() { + if (torrent == null) + return; + final TorrentTrafficControl traffic = torrent.getTrafficControl(); + long readLimit = traffic.getDownloadSpeedLimit(); + long writeLimit = traffic.getUploadSpeedLimit(); + if (readLimit != this.readLimit || writeLimit != this.writeLimit) { + this.writeLimit = writeLimit; + this.readLimit = readLimit; + configure(writeLimit, readLimit); + } + } + + @Override + protected void doAccounting(TrafficCounter counter) { + if (torrent == null) + return; + reconfigure(); + torrent.getTrafficControl().setCurrentDownloadSpeed( + counter.getLastReadThroughput()); + torrent.getTrafficControl().setCurrentUploadSpeed( + counter.getLastWriteThroughput()); + } + + /** + * @return the torrent + */ + public Torrent getTorrent() { + return torrent; + } + + /** + * @param torrent + * the torrent to set + */ + public void setTorrent(Torrent torrent) { + this.torrent = torrent; + reconfigure(); + } +} diff --git a/src/main/java/com/torrent4j/storage/AbstractTorrentStorage.java b/src/main/java/com/torrent4j/storage/AbstractTorrentStorage.java new file mode 100644 index 0000000..94fd930 --- /dev/null +++ b/src/main/java/com/torrent4j/storage/AbstractTorrentStorage.java @@ -0,0 +1,25 @@ +package com.torrent4j.storage; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.torrent4j.model.TorrentPiece; +import com.torrent4j.util.Hash; + +/** + * Abstract {@link TorrentStorage} that implements universal that methods that + * very likely don't need to be override by implementations. + * + * @author Rogiel + */ +public abstract class AbstractTorrentStorage implements TorrentStorage { + @Override + public Hash checksum(TorrentPiece piece) throws IOException { + final ByteBuffer buffer = read(piece.getTorrent(), + piece.getTorrentRange()); + if (buffer == null) + return null; + return new Hash(piece.getHash().getType(), piece.getHash().getType() + .hash(buffer)); + } +} diff --git a/src/main/java/com/torrent4j/storage/FileAwareTorrentStorage.java b/src/main/java/com/torrent4j/storage/FileAwareTorrentStorage.java new file mode 100644 index 0000000..f3f5248 --- /dev/null +++ b/src/main/java/com/torrent4j/storage/FileAwareTorrentStorage.java @@ -0,0 +1,101 @@ +package com.torrent4j.storage; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentFile; +import com.torrent4j.util.Range; + +/** + * Provides an abstraction to storage implementations that allows data to be + * stored into multiple files, respecting data stored into the original .torrent + * meta data file. {@link #write(Torrent, Range, ByteBuffer)} and + * {@link #read(Torrent, Range)} detect the file (or files) contained in the + * requested range and for each file that needs to be written or redden, a call + * to {@link #write(TorrentFile, Range, ByteBuffer)} and + * {@link #read(TorrentFile, Range, ByteBuffer)} is made. On all cases, the same + * buffer is sent for several calls. + *

+ * If any error occur while reading any of the files, the whole reading process + * fails. + * + * @author Rogiel + * + */ +public abstract class FileAwareTorrentStorage extends AbstractTorrentStorage + implements TorrentStorage { + @Override + public final boolean write(Torrent torrent, Range dataRange, ByteBuffer data) + throws IOException { + final List files = torrent.getFiles(dataRange); + for (final TorrentFile file : files) { + final Range rangeOnFile = file.getTorrentRange().intersection( + dataRange); + final Range range = file.getFileRange().intersection( + Range.getRangeByLength( + rangeOnFile.getStart() - file.getOffset(), + rangeOnFile.getLength())); + data.limit((int) (data.position() + range.getLength())); + if (!this.write(file, range, data)) + return false; + } + return true; + } + + @Override + public final ByteBuffer read(Torrent torrent, Range dataRange) + throws IOException { + final List files = torrent.getFiles(dataRange); + final ByteBuffer data = ByteBuffer + .allocate((int) dataRange.getLength()); + for (final TorrentFile file : files) { + final Range rangeOnFile = file.getTorrentRange().intersection( + dataRange); + final Range range = file.getFileRange().intersection( + Range.getRangeByLength( + rangeOnFile.getStart() - file.getOffset(), + rangeOnFile.getLength())); + data.limit((int) (data.position() + range.getLength())); + if (!this.read(file, range, data)) + return null; + } + data.flip(); + return data; + } + + /** + * Writes data into the given file at the + * requested range. + * + * @param file + * the file to write the data to + * @param range + * the range to write the data to + * @param data + * the data to be written + * @return true if write was successful + * @throws IOException + * if any exception occur while writing data + */ + protected abstract boolean write(TorrentFile file, Range range, + ByteBuffer data) throws IOException; + + /** + * Reads data from the given file in the requested + * range. + * + * @param file + * the file to read the data from + * @param range + * the range to read the data from + * @param data + * the data to be redden + * @return true if read was successful + * @throws IOException + * if any exception occur while reading data + */ + protected abstract boolean read(TorrentFile file, Range range, + ByteBuffer data) throws IOException; +} diff --git a/src/main/java/com/torrent4j/storage/InMemoryTorrentStorage.java b/src/main/java/com/torrent4j/storage/InMemoryTorrentStorage.java new file mode 100644 index 0000000..c583094 --- /dev/null +++ b/src/main/java/com/torrent4j/storage/InMemoryTorrentStorage.java @@ -0,0 +1,66 @@ +package com.torrent4j.storage; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import com.torrent4j.model.Torrent; +import com.torrent4j.util.Range; + +/** + * An simple {@link TorrentStorage} implementation that stores data into huge + * in-memory direct buffers. Please note that though it provides really fast + * write and reading, it should not be used in real world implementations. + * Storing data in-memory consumes a lot of memory and may cause the JVM to be + * terminated by some OSes. + *

+ * This class is intended for benchmarking and testing! + * + * @author Rogiel + */ +public class InMemoryTorrentStorage extends AbstractTorrentStorage implements + TorrentStorage { + /** + * The map containing all buffers for all active torrents + */ + private final Map buffers = new HashMap<>(); + + @Override + public boolean write(Torrent torrent, Range range, ByteBuffer data) + throws IOException { + final ByteBuffer buffer = getBuffer(torrent); + synchronized (buffer) { + buffer.position((int) range.getStart()); + buffer.put(data); + } + return true; + } + + @Override + public ByteBuffer read(Torrent torrent, Range range) throws IOException { + final ByteBuffer store = getBuffer(torrent); + synchronized (store) { + return (ByteBuffer) ((ByteBuffer) store.position((int) range + .getStart())).slice().limit((int) range.getLength()); + } + } + + /** + * Tries to return an existing buffer for the torrent. If none + * is found, creates a new buffer and returns it. + * + * @param torrent + * the torrent instance + * @return an {@link ByteBuffer} allocated with enough size to store all the + * torrent data into it + */ + private ByteBuffer getBuffer(Torrent torrent) { + ByteBuffer buffer = buffers.get(torrent); + if (buffer == null) { + buffer = ByteBuffer.allocateDirect((int) torrent.getTorrentSize()); + buffers.put(torrent, buffer); + } + return buffer; + } +} diff --git a/src/main/java/com/torrent4j/storage/NIOTorrentStorage.java b/src/main/java/com/torrent4j/storage/NIOTorrentStorage.java new file mode 100644 index 0000000..94d92d2 --- /dev/null +++ b/src/main/java/com/torrent4j/storage/NIOTorrentStorage.java @@ -0,0 +1,98 @@ +package com.torrent4j.storage; + +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.READ; +import static java.nio.file.StandardOpenOption.WRITE; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; + +import com.torrent4j.model.TorrentFile; +import com.torrent4j.util.Range; + +/** + * This {@link TorrentStorage} instance implements + * {@link FileAwareTorrentStorage}, and thus, stores data into separated files + * backed by an NIO.2 {@link SeekableByteChannel} available on Java 7. The + * implementation tries to respect as much as possible from the original file + * and folder structure, however, if any invalid name is found, beware that this + * implementation might override it. + * + * @author Rogiel + */ +public class NIOTorrentStorage extends FileAwareTorrentStorage { + /** + * The place where data files are stored + */ + private final Path root; + + /** + * Creates a new instance + * + * @param root + * the place where torrent data is stored + */ + public NIOTorrentStorage(Path root) { + this.root = root; + } + + /** + * Creates a new instance which stores data into the current working + * directory + */ + public NIOTorrentStorage() { + this(Paths.get(".").toAbsolutePath()); + } + + @Override + protected synchronized boolean write(TorrentFile file, Range range, + ByteBuffer data) throws IOException { + try (final SeekableByteChannel channel = openChannel(file, CREATE, + WRITE)) { + if (channel == null) + return false; + channel.position(range.getStart()); + return channel.write(data) > 0; + } + } + + @Override + protected synchronized boolean read(TorrentFile file, Range range, + ByteBuffer data) throws IOException { + try (final SeekableByteChannel channel = openChannel(file, READ)) { + if (channel == null) + return false; + channel.position(range.getStart()); + return channel.read(data) > 0; + } + } + + /** + * Opens a new channel ready for writing or reading (defines by + * modes) data into the torrent file. + * + * @param file + * the torrent file + * @param modes + * the file open mode + * @return an {@link SeekableByteChannel} + * @throws IOException + * if any error is thrown by NIO.2 + */ + private SeekableByteChannel openChannel(TorrentFile file, + OpenOption... modes) throws IOException { + final Path filePath = root.resolve(file.getFileName()); + try { + SeekableByteChannel channel = Files.newByteChannel(filePath, modes); + return channel; + } catch (NoSuchFileException e) { + return null; + } + } +} diff --git a/src/main/java/com/torrent4j/storage/TorrentStorage.java b/src/main/java/com/torrent4j/storage/TorrentStorage.java new file mode 100644 index 0000000..b27d023 --- /dev/null +++ b/src/main/java/com/torrent4j/storage/TorrentStorage.java @@ -0,0 +1,61 @@ +package com.torrent4j.storage; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPiece; +import com.torrent4j.util.Hash; +import com.torrent4j.util.Range; + +/** + * Provides an easy interface to access torrent data in an linear way. All + * torrent data is accessed by an single addressing, starting from the first + * piece to that last one. Implementations can create abstractions to support + * files (see {@link FileAwareTorrentStorage}) or can store data into a huge + * in-memory buffer (see {@link InMemoryTorrentStorage}). + * + * @author Rogiel + */ +public interface TorrentStorage { + /** + * Writes data into the underlying storage + * + * @param torrent + * the torrent + * @param torrentRange + * the range in which the data is located + * @param data + * the data itself + * @return true if the write was successful + * @throws IOException + * if any error occur while writing + */ + boolean write(Torrent torrent, Range torrentRange, ByteBuffer data) + throws IOException; + + /** + * Reads data from the underlying storage + * + * @param torrent + * the torrent + * @param torrentRange + * the range in which the data should be read from + * @return an {@link ByteBuffer} + * @throws IOException + * if any error occur while reading + */ + ByteBuffer read(Torrent torrent, Range torrentRange) throws IOException; + + /** + * Calculates the checksum of an given piece + * + * @param piece + * the piece + * @return the {@link Hash} for the requested piece + * @throws IOException + * if any error occur while reading data or calculating the + * checksum + */ + Hash checksum(TorrentPiece piece) throws IOException; +} diff --git a/src/main/java/com/torrent4j/storage/VoidTorrentStorage.java b/src/main/java/com/torrent4j/storage/VoidTorrentStorage.java new file mode 100644 index 0000000..1f018c8 --- /dev/null +++ b/src/main/java/com/torrent4j/storage/VoidTorrentStorage.java @@ -0,0 +1,39 @@ +package com.torrent4j.storage; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPiece; +import com.torrent4j.util.Hash; +import com.torrent4j.util.Range; + +/** + * This {@link TorrentStorage} implementation is similar to a UNIX + * /dev/null for writing and /dev/zero for reading. + * Data on this storage are never persisted and are discarded immediately. All + * reads return a {@link ByteBuffer} composed entirely from 0. + *

+ * Also, in order for checksums to match, when checksumming an piece, the piece + * hash itself is returned. This means that the checksums will never + * fail, no matter what, independent from data contents. + * + * @author Rogiel + */ +public class VoidTorrentStorage implements TorrentStorage { + @Override + public boolean write(Torrent torrent, Range range, ByteBuffer data) + throws IOException { + return true; + } + + @Override + public ByteBuffer read(Torrent torrent, Range range) throws IOException { + return ByteBuffer.allocate((int) range.getLength()); + } + + @Override + public Hash checksum(TorrentPiece piece) { + return piece.getHash(); + } +} diff --git a/src/main/java/com/torrent4j/strategy/TorrentDownloadStrategy.java b/src/main/java/com/torrent4j/strategy/TorrentDownloadStrategy.java new file mode 100644 index 0000000..86e1c89 --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/TorrentDownloadStrategy.java @@ -0,0 +1,51 @@ +package com.torrent4j.strategy; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPiece; +import com.torrent4j.model.TorrentPieceBlock; + +/** + * Determines the actions that should be taken regarding downloads + * + * @author Rogiel + */ +public interface TorrentDownloadStrategy { + /** + * Notifies that an block has been received and stored in the storage. + * + * @param torrent + * the torrent + * @param block + * the block that has been downloaded + * @param peer + * the peer who uploaded the block + */ + void blockReceived(Torrent torrent, TorrentPieceBlock block, + TorrentPeer peer); + + /** + * Notifies that an piece is complete and its checksum its corrects + * + * @param torrent + * the torrent + * @param piece + * the completed piece + * @param peer + * the peer who uploaded the last block to complete this piece + */ + void pieceComplete(Torrent torrent, TorrentPiece piece, TorrentPeer peer); + + /** + * Notifies that an piece has completed but its checksum data is incorrect. + * + * @param torrent + * the torrent + * @param piece + * the piece which is complete but with wrong data + * @param peer + * the peer who uploaded the last block of the corrupted piece + */ + void pieceChecksumFailed(Torrent torrent, TorrentPiece piece, + TorrentPeer peer); +} diff --git a/src/main/java/com/torrent4j/strategy/TorrentPeerStrategy.java b/src/main/java/com/torrent4j/strategy/TorrentPeerStrategy.java new file mode 100644 index 0000000..eab3962 --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/TorrentPeerStrategy.java @@ -0,0 +1,136 @@ +package com.torrent4j.strategy; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPiece; + +/** + * Determine actions that the client should take regarding peers and their + * requests. + * + * @author Rogiel + */ +public interface TorrentPeerStrategy { + /** + * Performs an action with a peer that has been recently discovered. + * Discovery can happen through tracker announcements, DHT, peer exchange or + * by manually adding them to the torrent object. + * + * @param torrent + * the torrent + * @param peer + * the discovered peer + */ + void peerDiscovered(Torrent torrent, TorrentPeer peer); + + /** + * Performs an action once handshaked with a peer. + * + * @param torrent + * the torrent + * @param peer + * the connected peer + */ + void peerConnected(Torrent torrent, TorrentPeer peer); + + /** + * Notifies that the bitfield for the given peer has been received. + * + * @param torrent + * the torrent + * @param peer + * the peer which sent his bitfield + */ + void bitField(Torrent torrent, TorrentPeer peer); + + /** + * Notifies that the peer has gotten a new piece + * + * @param torrent + * the torrent + * @param peer + * the peer which downloaded a new piece + * @param piece + * the piece which the peer has received + */ + void havePiece(Torrent torrent, TorrentPeer peer, TorrentPiece piece); + + /** + * Notifies that the peer has unchoked the connection + * + * @param torrent + * the torrent + * @param peer + * the peer + */ + void unchoked(Torrent torrent, TorrentPeer peer); + + /** + * Notifies that the peer has choked the connection + * + * @param torrent + * the torrent + * @param peer + * the peer + */ + void choked(Torrent torrent, TorrentPeer peer); + + /** + * Notifies that the peer has interest in any of our pieces. Normally, when + * this message is received, the remote peer is asking for an unchoke. + * + * @param torrent + * the torrent + * @param peer + * the peer that is interested in our pieces + */ + void interested(Torrent torrent, TorrentPeer peer); + + /** + * Notifies that the peer no more has interest in any of our pieces. + * Normally, it is safe to choke the peer after this. + * + * @param torrent + * the torrent + * @param peer + * the peer that has no more intested in any of the pieces + */ + void notInterested(Torrent torrent, TorrentPeer peer); + + /** + * Notifies that the peer has been idle for some time. If required, an + * keepalive message should be sent. It is also possible to + * close the connection if the peer is no longer interesting. + * + * @param torrent + * the torrent + * @param peer + * the idle peer + * @param idleTime + * the peer's idle time in milliseconds + */ + void peerIdle(Torrent torrent, TorrentPeer peer, long idleTime); + + /** + * Notifies the the peer has been disconnected. Note that this method is + * called either if the client closed the connection or if we requested an + * disconnection. + * + * @param torrent + * the torrent + * @param peer + * the disconnected peer + */ + void peerDisconnected(Torrent torrent, TorrentPeer peer); + + /** + * Notifies that the peer has been removed from the swarm list. Although it + * is possible to re-add the peer to the list, this is not recommended! + * + * @param torrent + * the torrent + * @param peer + * the removed peer + */ + void peerRemoved(Torrent torrent, TorrentPeer peer); +} diff --git a/src/main/java/com/torrent4j/strategy/TorrentStrategy.java b/src/main/java/com/torrent4j/strategy/TorrentStrategy.java new file mode 100644 index 0000000..70f8d46 --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/TorrentStrategy.java @@ -0,0 +1,23 @@ +package com.torrent4j.strategy; + +/** + * Provides an easy method to access all torrent strategies + * + * @author Rogiel + */ +public interface TorrentStrategy { + /** + * @return the download strategy + */ + TorrentDownloadStrategy getDownloadStrategy(); + + /** + * @return the upload strategy + */ + TorrentUploadStrategy getUploadStrategy(); + + /** + * @return the peer strategy + */ + TorrentPeerStrategy getPeerStrategy(); +} diff --git a/src/main/java/com/torrent4j/strategy/TorrentUploadStrategy.java b/src/main/java/com/torrent4j/strategy/TorrentUploadStrategy.java new file mode 100644 index 0000000..183cf3c --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/TorrentUploadStrategy.java @@ -0,0 +1,38 @@ +package com.torrent4j.strategy; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPieceBlock; + +/** + * Determines the actions that should be taken regarding uploads + * + * @author Rogiel + */ +public interface TorrentUploadStrategy { + /** + * Notifies that an given peer is requesting an block upload + * + * @param torrent + * the torrent + * @param block + * the block requested for upload + * @param peer + * the peer requesting the block + */ + void blockRequested(Torrent torrent, TorrentPieceBlock block, + TorrentPeer peer); + + /** + * Notifies that the peer has cancelled its previous request for the block + * + * @param torrent + * the torrent + * @param block + * the previous requested block (now cancelled) + * @param peer + * the peer canceling the block request + */ + void blockRequestCancelled(Torrent torrent, TorrentPieceBlock block, + TorrentPeer peer); +} diff --git a/src/main/java/com/torrent4j/strategy/standard/PieceSelector.java b/src/main/java/com/torrent4j/strategy/standard/PieceSelector.java new file mode 100644 index 0000000..31670e4 --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/standard/PieceSelector.java @@ -0,0 +1,22 @@ +package com.torrent4j.strategy.standard; + +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPiece; + +/** + * Selects an suitable piece for download + * + * @author Rogiel + */ +public interface PieceSelector { + /** + * Applies an algorithm to determine the most suitable piece for download + * from the peer. If no pieces are available or none are worth to download + * from this peer, null should be returned. + * + * @param peer + * the peer to download the piece from + * @return the piece to start downloading, if any + */ + TorrentPiece selectPiece(TorrentPeer peer); +} diff --git a/src/main/java/com/torrent4j/strategy/standard/RandomPieceSelector.java b/src/main/java/com/torrent4j/strategy/standard/RandomPieceSelector.java new file mode 100644 index 0000000..556521a --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/standard/RandomPieceSelector.java @@ -0,0 +1,22 @@ +package com.torrent4j.strategy.standard; + +import java.util.List; + +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPiece; + +/** + * Randomly selects an piece from the available peer pieces + * + * @author Rogiel + */ +public class RandomPieceSelector implements PieceSelector { + @Override + public TorrentPiece selectPiece(TorrentPeer peer) { + final List pieces = peer.getPieces().getMissingPieces( + peer.getTorrent().getCompletedPieces()); + if (pieces.isEmpty()) + return null; + return pieces.get((int) (Math.random() * pieces.size())); + } +} diff --git a/src/main/java/com/torrent4j/strategy/standard/StandardTorrentDownloadStrategy.java b/src/main/java/com/torrent4j/strategy/standard/StandardTorrentDownloadStrategy.java new file mode 100644 index 0000000..f64de08 --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/standard/StandardTorrentDownloadStrategy.java @@ -0,0 +1,63 @@ +package com.torrent4j.strategy.standard; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPiece; +import com.torrent4j.model.TorrentPieceBlock; +import com.torrent4j.model.TorrentSwarm.SwarmBroadcastHandler; +import com.torrent4j.strategy.TorrentDownloadStrategy; + +public class StandardTorrentDownloadStrategy implements TorrentDownloadStrategy { + private final StandardTorrentStrategy strategy; + + public StandardTorrentDownloadStrategy(StandardTorrentStrategy strategy) { + this.strategy = strategy; + } + + @Override + public void blockReceived(Torrent torrent, TorrentPieceBlock block, + TorrentPeer peer) { + TorrentPieceBlock next = block.getNextBlock(); + if (next == null) { + final TorrentPiece piece = strategy.getPieceSelector().selectPiece( + peer); + if (piece == null) { + peer.withdrawInterest(); + return; + } + next = piece.getFirstBlock(); + } + peer.requestBlock(next); + } + + @Override + public void pieceComplete(Torrent torrent, + final TorrentPiece completePiece, TorrentPeer peer) { + torrent.getSwarm().broadcast(new SwarmBroadcastHandler() { + @Override + public void broadcast(TorrentPeer peer) { + peer.have(completePiece); + } + + @Override + public boolean exception(Exception e) { + return false; + } + }); + + final TorrentPiece piece = strategy.getPieceSelector() + .selectPiece(peer); + if (piece == null) { + peer.withdrawInterest(); + return; + } + peer.requestBlock(piece.getFirstBlock()); + } + + @Override + public void pieceChecksumFailed(Torrent torrent, TorrentPiece piece, + TorrentPeer peer) { + System.out.println("Checksum has failed!"); + strategy.banPeer(peer); + } +} diff --git a/src/main/java/com/torrent4j/strategy/standard/StandardTorrentPeerStrategy.java b/src/main/java/com/torrent4j/strategy/standard/StandardTorrentPeerStrategy.java new file mode 100644 index 0000000..8ba3b53 --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/standard/StandardTorrentPeerStrategy.java @@ -0,0 +1,99 @@ +package com.torrent4j.strategy.standard; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPiece; +import com.torrent4j.strategy.TorrentPeerStrategy; + +public class StandardTorrentPeerStrategy implements TorrentPeerStrategy { + private final StandardTorrentStrategy strategy; + + public StandardTorrentPeerStrategy(StandardTorrentStrategy strategy) { + this.strategy = strategy; + } + + @Override + public void peerDiscovered(Torrent torrent, TorrentPeer peer) { + peer.connect(); + peer.handshake(); + } + + @Override + public void peerConnected(Torrent torrent, TorrentPeer peer) { + peer.bitField(); + } + + @Override + public void bitField(Torrent torrent, TorrentPeer peer) { + if (peer.getPieces().isSeeder() + && torrent.getCompletedPieces().isSeeder()) { + peer.disconnect(); + return; + } + if (peer.getPieces().hasMissingPieces(torrent.getCompletedPieces())) { + peer.declareInterest(); + return; + } + } + + @Override + public void havePiece(Torrent torrent, TorrentPeer peer, TorrentPiece piece) { + if (torrent.getCompletedPieces().hasPiece(piece)) + return; + if (peer.getState().hasDownloadRequestedBlock()) + return; + if (peer.getState().isRemotellyChoked()) + return; + peer.requestBlock(piece.getFirstBlock()); + } + + @Override + public void unchoked(Torrent torrent, TorrentPeer peer) { + final TorrentPiece piece = strategy.getPieceSelector() + .selectPiece(peer); + if (piece == null) { + peer.withdrawInterest(); + return; + } + peer.requestBlock(piece.getFirstBlock()); + } + + @Override + public void choked(Torrent torrent, TorrentPeer peer) { + if (peer.getPieces().isSeeder() + && torrent.getCompletedPieces().isSeeder()) { + peer.disconnect(); + return; + } + } + + @Override + public void interested(Torrent torrent, TorrentPeer peer) { + peer.unchoke(); + } + + @Override + public void notInterested(Torrent torrent, TorrentPeer peer) { + peer.choke(); + if (peer.getPieces().isSeeder() + && torrent.getCompletedPieces().isSeeder()) { + peer.disconnect(); + return; + } + } + + @Override + public void peerIdle(Torrent torrent, TorrentPeer peer, long idleTime) { + peer.keepAlive(); + } + + @Override + public void peerDisconnected(Torrent torrent, TorrentPeer peer) { + } + + @Override + public void peerRemoved(Torrent torrent, TorrentPeer peer) { + if (peer.isConnected()) + peer.disconnect(); + } +} diff --git a/src/main/java/com/torrent4j/strategy/standard/StandardTorrentStrategy.java b/src/main/java/com/torrent4j/strategy/standard/StandardTorrentStrategy.java new file mode 100644 index 0000000..7dfdd16 --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/standard/StandardTorrentStrategy.java @@ -0,0 +1,61 @@ +package com.torrent4j.strategy.standard; + +import java.util.ArrayList; +import java.util.List; + +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.strategy.TorrentDownloadStrategy; +import com.torrent4j.strategy.TorrentPeerStrategy; +import com.torrent4j.strategy.TorrentStrategy; +import com.torrent4j.strategy.TorrentUploadStrategy; + +public class StandardTorrentStrategy implements TorrentStrategy { + private final TorrentDownloadStrategy downloadStrategy = new StandardTorrentDownloadStrategy( + this); + private final TorrentUploadStrategy uploadStrategy = new StandardTorrentUploadStrategy( + this); + private final TorrentPeerStrategy peerStrategy = new StandardTorrentPeerStrategy( + this); + private final PieceSelector pieceSelector; + + private final List bannedPeers = new ArrayList<>(); + + public StandardTorrentStrategy(PieceSelector pieceSelector) { + this.pieceSelector = pieceSelector; + } + + public StandardTorrentStrategy() { + this.pieceSelector = new RandomPieceSelector(); + } + + @Override + public TorrentDownloadStrategy getDownloadStrategy() { + return downloadStrategy; + } + + @Override + public TorrentUploadStrategy getUploadStrategy() { + return uploadStrategy; + } + + @Override + public TorrentPeerStrategy getPeerStrategy() { + return peerStrategy; + } + + public PieceSelector getPieceSelector() { + return pieceSelector; + } + + public void banPeer(TorrentPeer peer) { + bannedPeers.add(peer); + } + + public void unbanPeer(TorrentPeer peer) { + bannedPeers.remove(peer); + } + + public boolean isBanned(TorrentPeer peer) { + return bannedPeers.contains(peer); + } +} diff --git a/src/main/java/com/torrent4j/strategy/standard/StandardTorrentUploadStrategy.java b/src/main/java/com/torrent4j/strategy/standard/StandardTorrentUploadStrategy.java new file mode 100644 index 0000000..ed8aa61 --- /dev/null +++ b/src/main/java/com/torrent4j/strategy/standard/StandardTorrentUploadStrategy.java @@ -0,0 +1,37 @@ +package com.torrent4j.strategy.standard; + +import java.io.IOException; + +import com.torrent4j.model.Torrent; +import com.torrent4j.model.TorrentPeer; +import com.torrent4j.model.TorrentPieceBlock; +import com.torrent4j.storage.TorrentStorage; +import com.torrent4j.strategy.TorrentUploadStrategy; + +public class StandardTorrentUploadStrategy implements TorrentUploadStrategy { + @SuppressWarnings("unused") + private final StandardTorrentStrategy strategy; + + public StandardTorrentUploadStrategy(StandardTorrentStrategy strategy) { + this.strategy = strategy; + } + + @Override + public void blockRequested(Torrent torrent, TorrentPieceBlock block, + TorrentPeer peer) { + final TorrentStorage storage = torrent.getController().getStorage(); + try { + peer.sendBlock(block, + storage.read(torrent, block.getTorrentRange())); + } catch (IOException e) { + peer.disconnect(); + return; + } + } + + @Override + public void blockRequestCancelled(Torrent torrent, TorrentPieceBlock block, + TorrentPeer peer) { + // we don't queue uploads, so we don't need to worry about this + } +} diff --git a/src/main/java/com/torrent4j/util/BitField.java b/src/main/java/com/torrent4j/util/BitField.java new file mode 100644 index 0000000..8372a14 --- /dev/null +++ b/src/main/java/com/torrent4j/util/BitField.java @@ -0,0 +1,102 @@ +package com.torrent4j.util; + +import java.util.BitSet; + +public class BitField extends BitSet { + private static final long serialVersionUID = 1L; + + private final int length; + + public BitField(int length) { + super(length); + this.length = length; + } + + private void ensureBitFieldLength(int index) { + if (index > length) + throw new ArrayIndexOutOfBoundsException(index + " is bigger than " + + length); + } + + @Override + public int size() { + return length; + } + + @Override + public void flip(int bitIndex) { + ensureBitFieldLength(bitIndex); + super.flip(bitIndex); + } + + @Override + public void flip(int fromIndex, int toIndex) { + ensureBitFieldLength(fromIndex); + ensureBitFieldLength(toIndex); + super.flip(fromIndex, toIndex); + } + + @Override + public void set(int bitIndex) { + ensureBitFieldLength(bitIndex); + super.set(bitIndex); + } + + @Override + public void set(int bitIndex, boolean value) { + ensureBitFieldLength(bitIndex); + super.set(bitIndex, value); + } + + @Override + public void set(int fromIndex, int toIndex) { + ensureBitFieldLength(fromIndex); + ensureBitFieldLength(toIndex); + super.set(fromIndex, toIndex); + } + + @Override + public void set(int fromIndex, int toIndex, boolean value) { + ensureBitFieldLength(fromIndex); + ensureBitFieldLength(toIndex); + super.set(fromIndex, toIndex, value); + } + + @Override + public void clear(int bitIndex) { + ensureBitFieldLength(bitIndex); + super.clear(bitIndex); + } + + @Override + public void clear(int fromIndex, int toIndex) { + ensureBitFieldLength(fromIndex); + ensureBitFieldLength(toIndex); + super.clear(fromIndex, toIndex); + } + + @Override + public boolean get(int bitIndex) { + ensureBitFieldLength(bitIndex); + return super.get(bitIndex); + } + + @Override + public BitSet get(int fromIndex, int toIndex) { + ensureBitFieldLength(fromIndex); + ensureBitFieldLength(toIndex); + return super.get(fromIndex, toIndex); + } + + @Override + public int nextSetBit(int fromIndex) { + ensureBitFieldLength(fromIndex); + return super.nextSetBit(fromIndex); + } + + @Override + public int nextClearBit(int fromIndex) { + ensureBitFieldLength(fromIndex); + return super.nextClearBit(fromIndex); + } +} diff --git a/src/main/java/com/torrent4j/util/Hash.java b/src/main/java/com/torrent4j/util/Hash.java new file mode 100644 index 0000000..476d053 --- /dev/null +++ b/src/main/java/com/torrent4j/util/Hash.java @@ -0,0 +1,55 @@ +package com.torrent4j.util; + +import java.util.Arrays; + +public class Hash { + private final HashType type; + private final byte[] hash; + + public Hash(HashType type, byte[] hash) { + this.type = type; + this.hash = hash; + } + + public HashType getType() { + return type; + } + + public byte[] getHash() { + return hash; + } + + public String getString() { + return type.toString(hash); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(hash); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof Hash)) + return false; + Hash other = (Hash) obj; + if (!Arrays.equals(hash, other.hash)) + return false; + if (type != other.type) + return false; + return true; + } + + @Override + public String toString() { + return type.name() + ":" + getString(); + } +} diff --git a/src/main/java/com/torrent4j/util/HashType.java b/src/main/java/com/torrent4j/util/HashType.java new file mode 100644 index 0000000..546d1ef --- /dev/null +++ b/src/main/java/com/torrent4j/util/HashType.java @@ -0,0 +1,105 @@ +package com.torrent4j.util; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; + +public enum HashType { + SHA1(new SimpleHashDelegate("SHA1")), + + SHA256(new SimpleHashDelegate("SHA-256")), + + SHA384(new SimpleHashDelegate("SHA-384")), + + SHA512(new SimpleHashDelegate("SHA-512")), + + MD5(new SimpleHashDelegate("MD5")), + + MD2(new SimpleHashDelegate("MD2")); + + private final HashDelegate delegate; + + HashType(HashDelegate delegate) { + this.delegate = delegate; + } + + public byte[] hash(byte[] data) { + return delegate.hash(data); + } + + public byte[] hash(String string) { + return hash(string.getBytes()); + } + + public byte[] hash(ByteBuffer buffer) { + final byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return hash(data); + } + + public String hashAsString(byte[] data) { + return toString(hash(data)); + } + + public String hashAsString(String string) { + return hashAsString(string.getBytes()); + } + + public String hashAsString(ByteBuffer buffer) { + final byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return hashAsString(data); + } + + public String toString(byte[] hash) { + return delegate.toString(hash); + } + + public byte[] fromString(String hash) { + return delegate.fromString(hash); + } + + private interface HashDelegate { + byte[] hash(byte[] data); + + String toString(byte[] hash); + + byte[] fromString(String hash); + } + + private static class SimpleHashDelegate implements HashDelegate { + private final String name; + + protected SimpleHashDelegate(String name) { + this.name = name; + } + + @Override + public byte[] hash(byte[] data) { + try { + final MessageDigest hasher = MessageDigest.getInstance(name); + hasher.update(data); + return hasher.digest(); + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + @Override + public String toString(byte[] hash) { + return new String(Hex.encodeHex(hash)); + } + + @Override + public byte[] fromString(String hash) { + try { + return Hex.decodeHex(hash.toCharArray()); + } catch (DecoderException e) { + return null; + } + } + } +} diff --git a/src/main/java/com/torrent4j/util/PeerIDGenerator.java b/src/main/java/com/torrent4j/util/PeerIDGenerator.java new file mode 100644 index 0000000..810ab1e --- /dev/null +++ b/src/main/java/com/torrent4j/util/PeerIDGenerator.java @@ -0,0 +1,11 @@ +package com.torrent4j.util; + +public class PeerIDGenerator { + public static String generateRandomPeerID() { + byte[] id = new byte[20]; + for (int i = 0; i < id.length; i++) { + id[i] = (byte) (Math.random() * Byte.MAX_VALUE); + } + return new String(id); + } +} diff --git a/src/main/java/com/torrent4j/util/Range.java b/src/main/java/com/torrent4j/util/Range.java new file mode 100644 index 0000000..0c57bbe --- /dev/null +++ b/src/main/java/com/torrent4j/util/Range.java @@ -0,0 +1,178 @@ +/* + * Copyright 2011 Rogiel Josias Sulzbach + * + * 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.torrent4j.util; + +import java.io.Serializable; + +/** + * This class represents an interval. + * + * @author Rogiel + */ +public final class Range implements Serializable { + private static final long serialVersionUID = 1L; + + private final long start, length; + + /** + * Creates a range with the given parameters + * + * @param start + * the beginning of the range + * @param length + * the length of the range + * @return a range + */ + public static Range getRangeByLength(long start, long length) { + return new Range(start, length); + } + + /** + * Creates the smallest possible range containing the given numbers. + * + * @param number1 + * number to be within the range + * @param number2 + * number to be within the range + * @return + */ + public static Range getRangeByNumbers(long number1, long number2) { + long s = Math.min(number1, number2); + return new Range(s, Math.max(number1, number2) - s + 1); + } + + /** + * Creates a new range + * + * @param start + * @param length + */ + private Range(long start, long length) { + if (start < 0) + throw new IllegalArgumentException("start must be >= 0"); + if (length < 0) + throw new IllegalArgumentException("length must be >= 0"); + + this.start = start; + this.length = length; + } + + /** + * @param range + * @return true if the given range is contained within this range + */ + public boolean contains(Range range) { + return getStart() <= range.getStart() && getEnd() >= range.getEnd(); + } + + /** + * @param pos + * @return true if the given point is contained within this range + */ + public boolean contains(long pos) { + return getStart() <= pos && getEnd() >= pos; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Range) { + Range r = (Range) obj; + return getStart() == r.getStart() && getLength() == r.getLength(); + } + return false; + } + + /** + * @return the last index contained in this range + */ + public long getEnd() { + return start + length - 1; + } + + /** + * @return the length of this range + */ + public long getLength() { + return length; + } + + /** + * @return the first index contained in this range + */ + public long getStart() { + return start; + } + + /** + * Test if this range defines a zero-sized range. + * + * @return true if zero-sized. + */ + public boolean isEmpty() { + return length == 0; + } + + @Override + public int hashCode() { + return (int) ((getStart() * 13) & (getEnd() * 137)); + } + + /** + * Creates a range which contains only the indices contained in the + * intersection of this range and the given range. + * + * @param range + * the range to intersect with + * @return the intersected range or null if the ranges don't overlap + */ + public Range intersection(Range range) { + if (!intersects(range)) { + return null; + } + return getRangeByNumbers(Math.max(getStart(), range.getStart()), + Math.min(getEnd(), range.getEnd())); + } + + /** + * Returns the number of indices which are in this range and the given + * range. + * + * @param r + * @return 0 if the ranges don't overlap, the length of the intersection + * between them otherwise + */ + public long intersectionLength(Range r) { + if (!intersects(r)) { + return 0; + } + return Math.min(getEnd(), r.getEnd()) + - Math.max(getStart(), r.getStart()) + 1; + } + + /** + * @param range + * the range to intersect test with + * @return true if the ranges overlap + */ + public boolean intersects(Range range) { + return getStart() <= range.getEnd() && getEnd() >= range.getStart(); + } + + @Override + public String toString() { + return "[" + getStart() + " - " + getEnd() + "]"; + } +} diff --git a/src/main/java/com/torrent4j/util/bencoding/BDecoder.java b/src/main/java/com/torrent4j/util/bencoding/BDecoder.java new file mode 100644 index 0000000..62bec1d --- /dev/null +++ b/src/main/java/com/torrent4j/util/bencoding/BDecoder.java @@ -0,0 +1,325 @@ +package com.torrent4j.util.bencoding; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Helper class to decode "bencoded" data. + * + * @author Dennis "Bytekeeper" Waldherr + * + */ +public class BDecoder extends InputStream { + private static final int MAX_STRING_LENGTH = 1024 * 1024; + + private final PushbackInputStream in; + + public BDecoder(InputStream in) { + this.in = new PushbackInputStream(in); + } + + /** + * Utility method to decode exactly one bencoded element. + * + * @param bencode + * @return + * @throws BDecodingException + * @throws IOException + */ + public static Object bdecode(byte[] bencode) throws BDecodingException { + BDecoder in = new BDecoder(new ByteArrayInputStream(bencode)); + try { + return in.readElement(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Reads one bencoded element. + * + * @param in + * @return + * @throws IOException + * @throws BDecodingException + */ + public Object readElement() throws IOException, BDecodingException { + return bdecode(); + } + + /** + * Converts a string result from bencoding to a java String. If the data is + * already a String or null it is simply returned. + * + * @param data + * @return + * @throws BTypeException + */ + private static String stringOf(Object data) throws BTypeException { + if (data == null) { + return null; + } + try { + if (data instanceof byte[]) { + return new String((byte[]) data, "UTF-8"); + } else if (data instanceof String) { + return (String) data; + } else { + throw new BTypeException("Unsupported type: " + data.getClass()); + } + } catch (UnsupportedEncodingException e) { + throw new Error(e); + } + } + + /** + * Converts a constructed or decoded integer to a java Integer. If the data + * is already an Integer or null it is simply returned. + * + * @param data + * @return + * @throws BTypeException + */ + private static Integer intOf(Object data) throws BTypeException { + if (data == null) { + return null; + } + if (data instanceof BigInteger) { + return ((BigInteger) data).intValue(); + } else if (data instanceof Integer) { + return (Integer) data; + } else { + throw new BTypeException("Unsupported type: " + data.getClass()); + } + } + + /** + * Converts a constructed or decoded integer to a java Integer. If the data + * is already an Integer or null it is simply returned. + * + * @param data + * @return + * @throws BTypeException + */ + private static Long longOf(Object data) throws BTypeException { + if (data == null) { + return null; + } + if (data instanceof BigInteger) { + return ((BigInteger) data).longValue(); + } else if (data instanceof Integer) { + return Long.valueOf((Integer) data); + } else if (data instanceof Long) { + return (Long) data; + } else { + throw new BTypeException("Unsupported type: " + data.getClass()); + } + } + + private Object bdecode() throws IOException, BDecodingException { + int head = in.read(); + switch (head) { + case -1: + throw new EOFException(); + case 'l': + return bdecodeList(); + case 'i': + return bdecodeInteger(); + case 'd': + return bdecodeDictionary(); + default: + if (Character.isDigit(head)) { + in.unread(head); + return bdecodeString(); + } + } + throw new BDecodingException("Parameter is not bencoded data."); + } + + private BMap bdecodeDictionary() throws IOException, BDecodingException { + assert in != null; + + BMap map = new BMapImpl(); + int head; + while ((head = in.read()) != 'e') { + if (head < 0) { + throw new EOFException(); + } + in.unread(head); + String key; + try { + key = stringOf(bdecodeString()); + } catch (BTypeException e) { + throw new BDecodingException(e); + } + map.put(key, bdecode()); + } + return map; + } + + private byte[] bdecodeString() throws IOException, BDecodingException { + assert in != null; + int len = 0; + int head; + while ((head = in.read()) != ':') { + if (head < 0) { + throw new EOFException(); + } + len = len * 10 + (head - '0'); + if (len > MAX_STRING_LENGTH) { + throw new BDecodingException("Encoded string length exceeds " + + MAX_STRING_LENGTH + " bytes!"); + } + } + byte data[] = new byte[len]; + int off = 0; + while (len > 0) { + int read = in.read(data, off, len); + len -= read; + off += read; + } + return data; + } + + private Object bdecodeInteger() throws IOException, BDecodingException { + assert in != null; + + StringBuilder b = new StringBuilder(); + int head; + while ((head = in.read()) != 'e') { + if (head < 0) { + throw new EOFException(); + } + if (!Character.isDigit(head)) { + throw new BDecodingException("Expected digit but got: " + head); + } + b.append((char) head); + } + return new BigInteger(b.toString()); + } + + private BList bdecodeList() throws IOException, BDecodingException { + assert in != null; + + BList list = new BListImpl(); + int head; + while ((head = in.read()) != 'e') { + if (head < 0) { + throw new EOFException(); + } + in.unread(head); + list.add(bdecode()); + } + return list; + } + + @Override + public int read() throws IOException { + return in.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return in.read(b, off, len); + } + + private static class BListImpl extends ArrayList implements BList { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Override + public Integer getInteger(int index) throws BTypeException { + return intOf(get(index)); + } + + @Override + public Long getLong(int index) throws BTypeException { + return longOf(get(index)); + } + + @Override + public String getString(int index) throws BTypeException { + return stringOf(get(index)); + } + + @Override + public BList getList(int index) throws BTypeException { + Object tmp = get(index); + if (tmp instanceof BList) { + return (BList) tmp; + } + throw new BTypeException("Unexpected type: " + tmp.getClass()); + } + + @Override + public BMap getMap(int index) throws BTypeException { + Object tmp = get(index); + if (tmp instanceof BMap) { + return (BMap) tmp; + } + throw new BTypeException("Unexpected type: " + tmp.getClass()); + } + + } + + private static class BMapImpl extends HashMap implements + BMap { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Override + public Integer getInteger(String key) throws BTypeException { + return intOf(get(key)); + } + + @Override + public Long getLong(String key) throws BTypeException { + return longOf(get(key)); + } + + @Override + public String getString(String key) throws BTypeException { + return stringOf(get(key)); + } + + @Override + public BList getList(String key) throws BTypeException { + Object tmp = get(key); + if (tmp instanceof BList) { + return (BList) tmp; + } + if (tmp != null) + throw new BTypeException("Unexpected type: " + tmp.getClass()); + return null; + } + + @Override + public BMap getMap(String key) throws BTypeException { + Object tmp = get(key); + if (tmp instanceof BMap) { + return (BMap) tmp; + } + throw new BTypeException("Unexpected type: " + tmp.getClass()); + } + } + + public static final Object decode(String encoded) throws IOException { + try(final ByteArrayInputStream in = new ByteArrayInputStream( + encoded.getBytes())) { + return new BDecoder(in).readElement(); + } + } +} diff --git a/src/main/java/com/torrent4j/util/bencoding/BDecodingException.java b/src/main/java/com/torrent4j/util/bencoding/BDecodingException.java new file mode 100644 index 0000000..85cdf98 --- /dev/null +++ b/src/main/java/com/torrent4j/util/bencoding/BDecodingException.java @@ -0,0 +1,21 @@ +package com.torrent4j.util.bencoding; + +import java.io.IOException; + +/** + * BDecodingException: an error has occured when decoding bencoded data. + * + * @author Dennis "Bytekeeper" Waldherr + */ +public class BDecodingException extends IOException { + private static final long serialVersionUID = 1L; + + public BDecodingException(Exception e) { + super(e); + } + + public BDecodingException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/torrent4j/util/bencoding/BEncoder.java b/src/main/java/com/torrent4j/util/bencoding/BEncoder.java new file mode 100644 index 0000000..5aa2800 --- /dev/null +++ b/src/main/java/com/torrent4j/util/bencoding/BEncoder.java @@ -0,0 +1,180 @@ +package com.torrent4j.util.bencoding; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Helper class to "BEncode" data. + * + * @author Dennis "Bytekeeper" Waldherr + * + */ +public final class BEncoder extends OutputStream { + static final Comparator BYTE_COMPARATOR = new Comparator() { + @Override + public int compare(String o1, String o2) { + byte b1[] = bytesOf(o1); + byte b2[] = bytesOf(o2); + int len = Math.min(b1.length, b2.length); + for (int i = 0; i < len; i++) { + if (b1[i] > b2[i]) { + return 1; + } + if (b1[i] < b2[i]) { + return -1; + } + } + return b1.length - b2.length; + } + }; + + private final OutputStream out; + + /** + * Converts a string into the raw byte array representation used to store + * into bencoded data. + * + * @param s + * @return + */ + public static byte[] bytesOf(String s) { + try { + return s.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error(e); + } + } + + private BEncoder(OutputStream out) { + if (out == null) + throw new InvalidParameterException("output stream is null"); + this.out = out; + } + + /** + * @param obj + * @return + */ + public static byte[] bencode(Object obj) { + if (obj == null) + throw new InvalidParameterException("Object to encode is null!"); + try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { + try (final BEncoder bout = new BEncoder(out)) { + bout.writeElement(obj); + } + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * BEncodes data and outputs it to an OutputStream. + * + * @param out + * @param obj + * @throws IOException + */ + @SuppressWarnings("unchecked") + public void writeElement(Object obj) throws IOException { + if (obj == null) + throw new InvalidParameterException("Object to encode is null!"); + + if (obj instanceof String) { + bencodeString((String) obj); + } else if (obj instanceof byte[]) { + bencodeString((byte[]) obj); + } else if (obj instanceof Integer) { + bencodeInteger(BigInteger.valueOf((Integer) obj)); + } else if (obj instanceof Long) { + bencodeInteger(BigInteger.valueOf((Long) obj)); + } else if (obj instanceof BigInteger) { + bencodeInteger((BigInteger) obj); + } else if (obj instanceof Collection) { + bencodeList((Collection) obj); + } else if (obj instanceof Map) { + bencodeDictionary((Map) obj); + } else { + throw new IllegalArgumentException("Type " + obj.getClass() + + " is not bencodable."); + } + } + + private void bencodeDictionary(Map dict) throws IOException { + assert out != null; + assert dict != null; + + out.write('d'); + List sorted = new ArrayList(dict.keySet()); + Collections.sort(sorted, BYTE_COMPARATOR); + for (String key : sorted) { + writeElement(key); + writeElement(dict.get(key)); + } + out.write('e'); + } + + private void bencodeList(Collection list) throws IOException { + assert out != null; + assert list != null; + out.write('l'); + for (Object child : list) { + writeElement(child); + } + out.write('e'); + } + + private void bencodeInteger(BigInteger integer) throws IOException { + assert integer != null; + out.write('i'); + out.write(bytesOf(integer.toString())); + out.write('e'); + } + + private void bencodeString(String string) throws IOException { + assert string != null; + byte[] bytes = bytesOf(string); + bencodeString(bytes); + } + + private void bencodeString(byte[] data) throws IOException { + assert data != null; + out.write(bytesOf(Integer.toString(data.length))); + out.write(':'); + out.write(data); + } + + @Override + public void write(int arg0) throws IOException { + out.write(arg0); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + + public static final String encode(Object object) throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(2048); + new BEncoder(out).writeElement(object); + return new String(out.toByteArray()); + } + + public static final byte[] encodeToByteArray(Object object) + throws IOException { + try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { + new BEncoder(out).writeElement(object); + return out.toByteArray(); + } + } +} diff --git a/src/main/java/com/torrent4j/util/bencoding/BList.java b/src/main/java/com/torrent4j/util/bencoding/BList.java new file mode 100644 index 0000000..159a450 --- /dev/null +++ b/src/main/java/com/torrent4j/util/bencoding/BList.java @@ -0,0 +1,21 @@ +package com.torrent4j.util.bencoding; + +import java.util.List; + +/** + * Representation of a bencoded list. + * + * @author Bytekeeper + * + */ +public interface BList extends List { + String getString(int index) throws BTypeException; + + Integer getInteger(int index) throws BTypeException; + + Long getLong(int index) throws BTypeException; + + BList getList(int index) throws BTypeException; + + BMap getMap(int index) throws BTypeException; +} diff --git a/src/main/java/com/torrent4j/util/bencoding/BMap.java b/src/main/java/com/torrent4j/util/bencoding/BMap.java new file mode 100644 index 0000000..6be4e4d --- /dev/null +++ b/src/main/java/com/torrent4j/util/bencoding/BMap.java @@ -0,0 +1,21 @@ +package com.torrent4j.util.bencoding; + +import java.util.Map; + +/** + * Representation of a bencoded map. + * + * @author Bytekeeper + * + */ +public interface BMap extends Map { + String getString(String key) throws BTypeException; + + Integer getInteger(String key) throws BTypeException; + + Long getLong(String key) throws BTypeException; + + BList getList(String key) throws BTypeException; + + BMap getMap(String key) throws BTypeException; +} diff --git a/src/main/java/com/torrent4j/util/bencoding/BTypeException.java b/src/main/java/com/torrent4j/util/bencoding/BTypeException.java new file mode 100644 index 0000000..acab8e1 --- /dev/null +++ b/src/main/java/com/torrent4j/util/bencoding/BTypeException.java @@ -0,0 +1,11 @@ +package com.torrent4j.util.bencoding; + +import java.io.IOException; + +public class BTypeException extends IOException { + private static final long serialVersionUID = 1L; + + public BTypeException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/torrent4j/util/bencoding/HashBMap.java b/src/main/java/com/torrent4j/util/bencoding/HashBMap.java new file mode 100644 index 0000000..75475e4 --- /dev/null +++ b/src/main/java/com/torrent4j/util/bencoding/HashBMap.java @@ -0,0 +1,32 @@ +package com.torrent4j.util.bencoding; + +import java.util.HashMap; + +public class HashBMap extends HashMap implements BMap { + private static final long serialVersionUID = 1L; + + @Override + public Integer getInteger(String key) { + return (Integer) get(key); + } + + @Override + public BList getList(String key) { + return (BList) get(key); + } + + @Override + public Long getLong(String key) { + return (Long) get(key); + } + + @Override + public BMap getMap(String key) { + return (BMap) get(key); + } + + @Override + public String getString(String key) { + return (String) get(key); + } +}