## Goals
+- [Optimized for Fun](http://www.slideshare.net/autang/ofun-optimizing-for-fun).
+ (should have called it "funtorrent")
- Become more profient with Haskell.
- Implement something non-trivial with Haskell (crypto, file operations, network
operations, concurrency, bit twiddling, DHT).
- talk to the tracker and get the peer list
- the `main' program takes a torrent file (in the local file system) as input and
prints the {ip,port} for each peer, after talking to the tracker.
+- can handshake with the peer.
+- peer wire protocol (in progress)
## TODO
* Test suite.
-* Peer protocol.
+* Peer protocol (in progress).
* Get the file download working in the simplest possible way.
* Concurrency (threads per peer)
* other advanced features of Bit Torrent (like DHT).
--- /dev/null
+# This file was auto-generated by cabal2nix. Please do NOT edit manually!
+
+{ cabal, base16Bytestring, binary, cryptohash, doctest, hlint, HTTP
+, networkUri, parsec, QuickCheck, tasty, tastyHunit
+, tastyQuickcheck, testFrameworkQuickcheck2
+}:
+
+cabal.mkDerivation (self: {
+ pname = "functorrent";
+ version = "0.1.0.0";
+ src = ./.;
+ isLibrary = true;
+ isExecutable = true;
+ buildDepends = [
+ base16Bytestring binary cryptohash HTTP networkUri parsec
+ QuickCheck tasty tastyHunit
+ ];
+ testDepends = [
+ doctest hlint QuickCheck tasty tastyHunit tastyQuickcheck
+ testFrameworkQuickcheck2
+ ];
+ meta = {
+ description = "A Bit-torrent client";
+ license = self.stdenv.lib.licenses.gpl3;
+ platforms = self.ghc.meta.platforms;
+ };
+})
cryptohash,
directory,
HTTP,
+ network,
network-uri,
parsec,
QuickCheck,
directory,
HTTP,
QuickCheck,
+ network,
network-uri,
parsec
containers,
directory,
doctest,
+ QuickCheck,
tasty,
tasty-hunit,
QuickCheck,
-# This file was auto-generated by cabal2nix. Please do NOT edit manually!
-
-{ haskellPackages ? (import <nixpkgs> {}).haskellPackages }:
-
-with haskellPackages; cabal.mkDerivation (self: {
- pname = "functorrent";
- version = "0.1.0.0";
- src = "./.";
- isLibrary = true;
- isExecutable = true;
- buildTools = [ cabalInstall ];
- buildDepends = [
- base16Bytestring binary cryptohash HTTP networkUri parsec tasty
- tastyHunit
+let pkgs = (import <nixpkgs> {});
+ haskellPackages = pkgs.recurseIntoAttrs (pkgs.haskellPackages.override {
+ extension = self : super :
+ let callPackage = self.callPackage;
+ in {
+ thisPackage = haskellPackages.callPackage (import ./default.nix) {};
+ };});
+in pkgs.lib.overrideDerivation haskellPackages.thisPackage (old: {
+ buildInputs = old.buildInputs ++ [
+ haskellPackages.cabalInstall
];
- testDepends = [ doctest hlint tasty tastyHunit ];
- meta = {
- description = "A Bit-torrent client";
- license = self.stdenv.lib.licenses.gpl3;
- platforms = self.ghc.meta.platforms;
- };
})
tracker,
decode,
encode,
- handShakeMsg,
+ handShake,
initLogger,
logMessage,
logStop,
{-# LANGUAGE OverloadedStrings #-}
module FuncTorrent.Peer
(Peer(..),
- handShakeMsg
+ handShake
) where
import Prelude hiding (lookup, concat, replicate, splitAt)
-import Data.ByteString.Char8 (ByteString, pack, concat, replicate)
-import Data.ByteString.Lazy (toChunks)
-import Data.Int (Int8)
-import qualified Data.Binary as Bin (encode)
+import System.IO
+import Data.ByteString (ByteString, unpack, concat, hGet, hPut, singleton)
+import Data.ByteString.Char8 (replicate, pack)
+import Network (connectTo, PortID(..))
-import FuncTorrent.Metainfo (Metainfo(..))
+type ID = String
+type IP = String
+type Port = Integer
--- | Peer is a IP address, port tuple
-data Peer = Peer String Integer
- deriving (Show, Eq)
+data PeerState = PeerState { am_choking :: Bool
+ , am_interested :: Bool
+ , peer_choking :: Bool
+ , peer_interested :: Bool }
-handShakeMsg :: Metainfo -> String -> ByteString
-handShakeMsg m peer_id = concat [pstrlen, pstr, reserved, infoH, peerID]
- where pstrlen = concat $ toChunks $ Bin.encode (19 :: Int8)
- pstr = pack "BitTorrent protocol"
- reserved = replicate 8 '\0'
- infoH = infoHash m
- peerID = pack peer_id
+-- | Peer is a PeerID, IP address, port tuple
+data Peer = Peer ID IP Port
+ deriving (Show, Eq)
+
+data Msg = HandShakeMsg ByteString ID
+ | KeepAliveMsg
+ | ChokeMsg
+ | UnChokeMsg
+ | InterestedMsg
+ | NotInterestedMsg
+ | HaveMsg Integer
+ | BitFieldMsg Integer
+ | RequestMsg Integer Integer Integer
+ | PieceMsg Integer Integer Integer
+ | CancelMsg Integer Integer Integer
+ | PortMsg Port
+ deriving (Show)
+
+genHandShakeMsg :: ByteString -> String -> ByteString
+genHandShakeMsg infoHash peer_id = concat [pstrlen, pstr, reserved, infoHash, peerID]
+ where pstrlen = singleton 19
+ pstr = pack "BitTorrent protocol"
+ reserved = replicate 8 '\0'
+ peerID = pack peer_id
+
+handShake :: Peer -> ByteString -> String -> IO ByteString
+handShake (Peer _ ip port) infoHash peerid = do
+ let hs = genHandShakeMsg infoHash peerid
+ handle <- connectTo ip (PortNumber (fromIntegral port))
+ hSetBuffering handle LineBuffering
+ hPut handle hs
+ rlenBS <- hGet handle 1
+ let rlen = fromIntegral $ (unpack rlenBS) !! 0
+ hGet handle rlen
+
+-- sendMsg :: Peer -> Handle -> PeerMsg -> IO ()
+-- recvMsg :: Peer -> Handle -> Msg
splitN 2 . B16.encode
makePeer :: ByteString -> Peer
- makePeer peer = Peer (toIP ip') (toPort port')
+ makePeer peer = Peer "" (toIP ip') (toPort port')
where (ip', port') = splitAt 4 peer
-- | Connect to a tracker and get peer info
module Main where
import Prelude hiding (log, length, readFile, writeFile)
-import Data.ByteString.Char8 (ByteString, readFile, writeFile, length, unpack)
+import Data.ByteString.Char8 (ByteString, readFile, writeFile, unpack)
import System.Environment (getArgs)
import System.Exit (exitSuccess)
import System.Directory (doesFileExist)
import FuncTorrent.Bencode (decode)
import FuncTorrent.Logger (initLogger, logMessage, logStop)
import FuncTorrent.Metainfo (Info(..), Metainfo(..), mkMetaInfo)
-import FuncTorrent.Peer (handShakeMsg)
+import FuncTorrent.Peer (handShake)
import FuncTorrent.Tracker (tracker, peers, mkTrackerResponse)
logError :: ParseError -> (String -> IO ()) -> IO ()
log $ "Trackers: " ++ head (announceList m)
response <- tracker m peerId
- let hsMsgLen = show $ length $ handShakeMsg m peerId
- log $ "Hand-shake message length : " ++ hsMsgLen
-
-- TODO: Write to ~/.functorrent/caches
writeFile (name (info m) ++ ".cache") response
case decode response of
Right trackerInfo ->
case mkTrackerResponse trackerInfo of
- Right peerResp ->
+ Right peerResp -> do
log $ "Peers List : " ++ (show . peers $ peerResp)
+ let p1 = head (peers peerResp)
+ msg <- handShake p1 (infoHash m) peerId
+ log $ "handshake: " ++ (show msg)
+ return ()
Left e -> log $ "Error" ++ unpack e
Left e -> logError e log
expectation :: Either a TrackerResponse
expectation = Right TrackerResponse {
interval = Just 900,
- peers = [Peer "85.25.201.101" 51413, Peer "37.59.28.236" 22222, Peer "76.21.149.43" 51866, Peer "31.183.33.205" 43467, Peer "213.210.120.86" 27480, Peer "213.239.216.205" 6914, Peer "91.192.163.152" 11834, Peer "62.210.240.65" 6999, Peer "84.250.103.161" 6949, Peer "88.195.241.192" 51413, Peer "88.165.61.223" 6881, Peer "86.157.234.243" 59583, Peer "213.41.137.242" 51413, Peer "91.10.84.195" 46941, Peer "64.56.249.183" 7023, Peer "202.62.16.71" 59929, Peer "31.43.126.122" 57816, Peer "68.169.133.72" 50222, Peer "223.135.97.177" 58813, Peer "5.166.93.118" 64459, Peer "200.148.109.141" 51413, Peer "109.226.236.160" 44444, Peer "78.58.139.154" 22818, Peer "188.244.47.186" 39643, Peer "203.86.204.111" 52411, Peer "80.110.40.98" 6918, Peer "68.187.142.217" 58352, Peer "71.115.139.180" 63065, Peer "70.169.35.173" 51413, Peer "185.3.135.186" 10889, Peer "88.198.224.202" 51413, Peer "183.157.65.217" 9179, Peer "87.251.189.150" 46680, Peer "87.114.202.174" 12393, Peer "93.58.5.16" 51411, Peer "89.102.9.69" 10044, Peer "94.159.19.222" 15783, Peer "95.28.49.176" 58794, Peer "217.114.58.135" 6881, Peer "79.141.162.38" 35806, Peer "136.169.50.72" 54927, Peer "187.67.188.151" 51413, Peer "79.111.218.50" 53636, Peer "62.75.137.129" 51413, Peer "14.204.20.156" 11600, Peer "79.141.162.34" 24531, Peer "82.144.192.7" 63208, Peer "212.34.231.10" 20684, Peer "95.225.246.221" 51413, Peer "124.41.237.102" 24874],
+ peers = [Peer "" "85.25.201.101" 51413, Peer "" "37.59.28.236" 22222, Peer "" "76.21.149.43" 51866, Peer "" "31.183.33.205" 43467, Peer "" "213.210.120.86" 27480, Peer "" "213.239.216.205" 6914, Peer "" "91.192.163.152" 11834, Peer "" "62.210.240.65" 6999, Peer "" "84.250.103.161" 6949, Peer "" "88.195.241.192" 51413, Peer "" "88.165.61.223" 6881, Peer "" "86.157.234.243" 59583, Peer "" "213.41.137.242" 51413, Peer "" "91.10.84.195" 46941, Peer "" "64.56.249.183" 7023, Peer "" "202.62.16.71" 59929, Peer "" "31.43.126.122" 57816, Peer "" "68.169.133.72" 50222, Peer "" "223.135.97.177" 58813, Peer "" "5.166.93.118" 64459, Peer "" "200.148.109.141" 51413, Peer "" "109.226.236.160" 44444, Peer "" "78.58.139.154" 22818, Peer "" "188.244.47.186" 39643, Peer "" "203.86.204.111" 52411, Peer "" "80.110.40.98" 6918, Peer "" "68.187.142.217" 58352, Peer "" "71.115.139.180" 63065, Peer "" "70.169.35.173" 51413, Peer "" "185.3.135.186" 10889, Peer "" "88.198.224.202" 51413, Peer "" "183.157.65.217" 9179, Peer "" "87.251.189.150" 46680, Peer "" "87.114.202.174" 12393, Peer "" "93.58.5.16" 51411, Peer "" "89.102.9.69" 10044, Peer "" "94.159.19.222" 15783, Peer "" "95.28.49.176" 58794, Peer "" "217.114.58.135" 6881, Peer "" "79.141.162.38" 35806, Peer "" "136.169.50.72" 54927, Peer "" "187.67.188.151" 51413, Peer "" "79.111.218.50" 53636, Peer "" "62.75.137.129" 51413, Peer "" "14.204.20.156" 11600, Peer "" "79.141.162.34" 24531, Peer "" "82.144.192.7" 63208, Peer "" "212.34.231.10" 20684, Peer "" "95.225.246.221" 51413, Peer "" "124.41.237.102" 24874],
complete = Nothing,
incomplete = Nothing
}