1 {-# LANGUAGE OverloadedStrings #-}
2 module FuncTorrent.Peer
7 import Prelude hiding (lookup, concat, replicate, splitAt, take)
9 import System.IO (Handle, BufferMode(..), hSetBuffering)
10 import Data.ByteString (ByteString, pack, unpack, concat, hGet, hPut, singleton, take, empty)
11 import Data.ByteString.Lazy (fromStrict, fromChunks, toStrict)
12 import qualified Data.ByteString.Char8 as BC (replicate, pack, length)
13 import Network (connectTo, PortID(..))
14 import Data.Binary (Binary(..), decode, encode)
15 import Data.Binary.Put (putWord32be, putWord16be, putWord8)
16 import Data.Binary.Get (getWord32be, getWord16be, getWord8, runGet)
17 import Control.Monad (replicateM, liftM, forM)
18 import Control.Applicative ((<$>), liftA3)
20 import Data.Word (Word8)
21 import Data.Map (Map, fromList, toList, (!), mapWithKey, adjust)
22 import qualified Crypto.Hash.SHA1 as SHA1 (hash)
24 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
25 import FuncTorrent.Utils (splitN, splitNum)
26 import FuncTorrent.Fileops (createDummyFile, writeFileAtOffset)
32 -- PeerState is a misnomer
33 data PeerState = PeerState { handle :: Handle
36 , meInterested :: Bool
38 , heInterested :: Bool}
40 data PieceDlState = Pending
45 -- todo - map with index to a new data structure (peers who have that piece amd state)
46 data PieceData = PieceData { peers :: [Peer] -- ^ list of peers who have this piece
47 , state :: PieceDlState -- ^ state of the piece from download perspective.
48 , hash :: ByteString -- ^ piece hash
49 , len :: Integer } -- ^ piece length
51 -- which piece is with which peers
52 type PieceMap = Map Integer PieceData
54 -- | Peer is a PeerID, IP address, port tuple
55 data Peer = Peer ID IP Port
58 data PeerMsg = KeepAliveMsg
64 | BitFieldMsg ByteString
65 | RequestMsg Integer Integer Integer
66 | PieceMsg Integer Integer ByteString
67 | CancelMsg Integer Integer Integer
71 -- Make the initial Piece map, with the assumption that no peer has the
72 -- piece and that every piece is pending download.
73 mkPieceMap :: Integer -> ByteString -> [Integer] -> PieceMap
74 mkPieceMap numPieces pieceHash pLengths = fromList kvs
75 where kvs = [(i, PieceData { peers = []
79 | (i, h, pLen) <- zip3 [0..numPieces] hashes pLengths]
80 hashes = splitN 20 pieceHash
82 havePiece :: PieceMap -> Integer -> Bool
84 state (pm ! index) == Have
86 genHandShakeMsg :: ByteString -> String -> ByteString
87 genHandShakeMsg infoHash peer_id = concat [pstrlen, pstr, reserved, infoHash, peerID]
88 where pstrlen = singleton 19
89 pstr = BC.pack "BitTorrent protocol"
90 reserved = BC.replicate 8 '\0'
91 peerID = BC.pack peer_id
93 handShake :: Peer -> ByteString -> String -> IO Handle
94 handShake peer@(Peer _ ip port) infoHash peerid = do
95 let hs = genHandShakeMsg infoHash peerid
96 h <- connectTo ip (PortNumber (fromIntegral port))
97 hSetBuffering h LineBuffering
99 putStrLn $ "--> handhake to peer: " ++ show peer
100 _ <- hGet h (length (unpack hs))
101 putStrLn $ "<-- handshake from peer: " ++ show peer
104 instance Binary PeerMsg where
105 put msg = case msg of
106 KeepAliveMsg -> putWord32be 0
107 ChokeMsg -> do putWord32be 1
109 UnChokeMsg -> do putWord32be 1
111 InterestedMsg -> do putWord32be 1
113 NotInterestedMsg -> do putWord32be 1
115 HaveMsg i -> do putWord32be 5
117 putWord32be (fromIntegral i)
118 BitFieldMsg bf -> do putWord32be $ fromIntegral (1 + bfListLen)
120 mapM_ putWord8 bfList
121 where bfList = unpack bf
122 bfListLen = length bfList
123 RequestMsg i o l -> do putWord32be 13
125 putWord32be (fromIntegral i)
126 putWord32be (fromIntegral o)
127 putWord32be (fromIntegral l)
128 PieceMsg i o b -> do putWord32be $ fromIntegral (9 + blocklen)
130 putWord32be (fromIntegral i)
131 putWord32be (fromIntegral o)
132 mapM_ putWord8 blockList
133 where blockList = unpack b
134 blocklen = length blockList
135 CancelMsg i o l -> do putWord32be 13
137 putWord32be (fromIntegral i)
138 putWord32be (fromIntegral o)
139 putWord32be (fromIntegral l)
140 PortMsg p -> do putWord32be 3
142 putWord16be (fromIntegral p)
148 1 -> return UnChokeMsg
149 2 -> return InterestedMsg
150 3 -> return NotInterestedMsg
151 4 -> liftM (HaveMsg . fromIntegral) getWord32be
152 5 -> liftM (BitFieldMsg . pack) (replicateM (fromIntegral l - 1) getWord8)
153 6 -> liftA3 RequestMsg getInteger getInteger getInteger
154 where getInteger = fromIntegral <$> getWord32be
155 7 -> liftA3 PieceMsg getInteger getInteger (pack <$> replicateM (fromIntegral l - 9) getWord8)
156 where getInteger = fromIntegral <$> getWord32be
157 8 -> liftA3 CancelMsg getInteger getInteger getInteger
158 where getInteger = fromIntegral <$> getWord32be
159 9 -> liftM (PortMsg . fromIntegral) getWord16be
160 _ -> error ("unknown message ID: " ++ show msgid)
162 getMsg :: Handle -> IO PeerMsg
167 then return KeepAliveMsg
170 return $ decode $ fromStrict $ concat [lBS, msg]
172 sendMsg :: Handle -> PeerMsg -> IO ()
174 let bsMsg = toStrict $ encode msg
178 bsToInt :: ByteString -> Int
179 bsToInt x = fromIntegral (runGet getWord32be (fromChunks (return x)))
181 bitfieldToList :: [Word8] -> [Integer]
182 bitfieldToList bs = go bs 0
185 let setBits = [pos*8 + toInteger i | i <- [0..8], testBit b i]
187 setBits ++ go bs' (pos + 1)
189 -- recvMsg :: Peer -> Handle -> Msg
190 msgLoop :: PeerState -> PieceMap -> IO ()
191 msgLoop pState pieceStatus | not (meInterested pState) && heChoking pState = do
192 -- if me NOT Interested and she is Choking, tell her that
194 let h = handle pState
195 sendMsg h InterestedMsg
196 putStrLn $ "--> InterestedMsg to peer: " ++ show (peer pState)
197 msgLoop (pState { meInterested = True }) pieceStatus
198 | meInterested pState && not (heChoking pState) =
199 -- if me Interested and she not Choking, send her a request
201 case pickPiece pieceStatus of
202 Nothing -> putStrLn "Nothing to download"
204 let pLen = len (pieceStatus ! workPiece)
205 putStrLn $ "piece length = " ++ show pLen
206 pBS <- downloadPiece (handle pState) workPiece pLen
207 if not $ verifyHash pBS (hash (pieceStatus ! workPiece))
209 putStrLn $ "Hash mismatch: " ++ show (hash (pieceStatus ! workPiece)) ++ " vs " ++ show (take 20 (SHA1.hash pBS))
211 let fileOffset = if workPiece == 0 then 0 else workPiece * len (pieceStatus ! (workPiece - 1))
212 putStrLn $ "Write into file at offset: " ++ show fileOffset
213 writeFileAtOffset "/tmp/download.file" fileOffset pBS
214 msgLoop pState (adjust (\pieceData -> pieceData { state = Have }) workPiece pieceStatus)
216 msg <- getMsg (handle pState)
217 putStrLn $ "<-- " ++ show msg ++ "from peer: " ++ show (peer pState)
220 sendMsg (handle pState) KeepAliveMsg
221 putStrLn $ "--> " ++ "KeepAliveMsg to peer: " ++ show (peer pState)
222 msgLoop pState pieceStatus
223 BitFieldMsg bss -> do
224 let pieceList = bitfieldToList (unpack bss)
225 pieceStatus' = updatePieceAvailability pieceStatus (peer pState) pieceList
226 putStrLn $ show (length pieceList) ++ " Pieces"
227 -- for each pieceIndex in pieceList, make an entry in the pieceStatus
228 -- map with pieceIndex as the key and modify the value to add the peer.
229 -- download each of the piece in order
230 msgLoop pState pieceStatus'
232 msgLoop (pState { heChoking = False }) pieceStatus
234 msgLoop pState pieceStatus
236 -- simple algorithm to pick piece.
237 -- pick the first piece from 0 that is not downloaded yet.
238 pickPiece :: PieceMap -> Maybe Integer
240 let pieceList = toList m
241 allPending = filter (\(_, v) -> state v == Pending) pieceList
247 updatePieceAvailability :: PieceMap -> Peer -> [Integer] -> PieceMap
248 updatePieceAvailability pieceStatus p pieceList =
249 mapWithKey (\k pd -> if k `elem` pieceList
250 then (pd { peers = p : peers pd })
253 handlePeerMsgs :: Peer -> Metainfo -> String -> IO ()
254 handlePeerMsgs p m peerId = do
255 h <- handShake p (infoHash m) peerId
256 let state = PeerState { handle = h
258 , heInterested = False
260 , meInterested = False
262 pieceHash = pieces (info m)
263 numPieces = (toInteger . (`quot` 20) . BC.length) pieceHash
264 pLen = pieceLength (info m)
265 fileLen = lengthInBytes (info m)
266 pieceStatus = mkPieceMap numPieces pieceHash (splitNum fileLen pLen)
267 createDummyFile "/tmp/download.file" (fromIntegral fileLen)
268 msgLoop state pieceStatus
270 downloadPiece :: Handle -> Integer -> Integer -> IO ByteString
271 downloadPiece h index pieceLength = do
272 let chunks = splitNum pieceLength 16384
273 liftM concat $ forM (zip [0..] chunks) (\(i, pLen) -> do
274 sendMsg h (RequestMsg index (i*pLen) pLen)
275 putStrLn $ "--> " ++ "RequestMsg for Piece "
276 ++ show index ++ ", part: " ++ show i ++ " of length: "
280 PieceMsg index begin block -> do
281 putStrLn $ " <-- PieceMsg for Piece: "
287 putStrLn "ignoring irrelevant msg"
290 verifyHash :: ByteString -> ByteString -> Bool
291 verifyHash bs pieceHash =
292 take 20 (SHA1.hash bs) == pieceHash