1 {-# LANGUAGE OverloadedStrings #-}
2 module FuncTorrent.Peer
11 import Prelude hiding (lookup, concat, replicate, splitAt, take, drop, filter)
13 import System.IO (Handle, BufferMode(..), hSetBuffering, hClose)
14 import Data.ByteString (ByteString, unpack, concat, hGet, hPut, take, drop, empty)
15 import qualified Data.ByteString.Char8 as BC (length)
16 import Network (connectTo, PortID(..))
17 import Control.Monad.State
19 import Data.Word (Word8)
20 import Data.Map (Map, fromList, toList, (!), mapWithKey, traverseWithKey, adjust, filter)
23 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
24 import FuncTorrent.Utils (splitN, splitNum, writeFileAtOffset, readFileAtOffset, verifyHash)
25 import FuncTorrent.PeerMsgs (Peer(..), PeerMsg(..), sendMsg, getMsg, genHandshakeMsg)
27 data PState = PState { handle :: Handle
30 , meInterested :: Bool
32 , heInterested :: Bool}
34 type PeerState = State PState
36 data PieceDlState = Pending
41 -- todo - map with index to a new data structure (peers who have that piece and state)
42 data PieceData = PieceData { peers :: [Peer] -- ^ list of peers who have this piece
43 , dlstate :: PieceDlState -- ^ state of the piece from download perspective.
44 , hash :: ByteString -- ^ piece hash
45 , len :: Integer } -- ^ piece length
47 -- which piece is with which peers
48 type PieceMap = Map Integer PieceData
51 -- Make the initial Piece map, with the assumption that no peer has the
52 -- piece and that every piece is pending download.
53 initPieceMap :: ByteString -> Integer -> Integer -> PieceMap
54 initPieceMap pieceHash fileLen pieceLen = fromList kvs
56 numPieces = (toInteger . (`quot` 20) . BC.length) pieceHash
57 kvs = [(i, PieceData { peers = []
61 | (i, h, pLen) <- zip3 [0..numPieces] hashes pLengths]
62 hashes = splitN 20 pieceHash
63 pLengths = (splitNum fileLen pieceLen)
65 pieceMapFromFile :: FilePath -> PieceMap -> IO PieceMap
66 pieceMapFromFile filePath pieceMap = do
67 traverseWithKey f pieceMap
70 let offset = if k == 0 then 0 else k * len (pieceMap ! (k - 1))
71 isHashValid <- (flip verifyHash) (hash v) <$> (readFileAtOffset filePath offset (len v))
73 then return $ v { dlstate = Have }
76 havePiece :: PieceMap -> Integer -> Bool
78 dlstate (pm ! index) == Have
80 connectToPeer :: Peer -> IO Handle
81 connectToPeer (Peer _ ip port) = do
82 h <- connectTo ip (PortNumber (fromIntegral port))
83 hSetBuffering h LineBuffering
86 doHandshake :: Bool -> Handle -> Peer -> ByteString -> String -> IO ()
87 doHandshake isClient h peer infoHash peerid =
88 let hs = genHandshakeMsg infoHash peerid
93 putStrLn $ "--> handhake to peer: " ++ show peer
94 _ <- hGet h (length (unpack hs))
95 putStrLn $ "<-- handshake from peer: " ++ show peer
98 putStrLn $ "waiting for a handshake"
99 hsMsg <- hGet h (length (unpack hs))
100 putStrLn $ "<-- handshake from peer: " ++ show peer
101 let rxInfoHash = take 20 $ drop 28 hsMsg
102 if rxInfoHash /= infoHash
104 putStrLn $ "infoHashes does not match"
109 putStrLn $ "--> handhake to peer: " ++ show peer
112 bitfieldToList :: [Word8] -> [Integer]
113 bitfieldToList bs = go bs 0
116 let setBits = [pos*8 + toInteger i | i <- [0..8], testBit b i]
118 setBits ++ go bs' (pos + 1)
120 -- helper functions to manipulate PeerState
121 toPeerState :: Handle
123 -> Bool -- ^ meChoking
124 -> Bool -- ^ meInterested
125 -> Bool -- ^ heChoking
126 -> Bool -- ^ heInterested
128 toPeerState h p meCh meIn heCh heIn =
132 , heInterested = heIn
134 , meInterested = meIn }
136 -- simple algorithm to pick piece.
137 -- pick the first piece from 0 that is not downloaded yet.
138 pickPiece :: PieceMap -> Maybe Integer
140 (fst `liftM`) . headMay . toList . filter (\v -> dlstate v == Pending)
142 bytesDownloaded :: PieceMap -> Integer
144 sum . map (len . snd) . toList . filter (\v -> dlstate v == Have)
146 updatePieceAvailability :: PieceMap -> Peer -> [Integer] -> PieceMap
147 updatePieceAvailability pieceStatus p pieceList =
148 mapWithKey (\k pd -> if k `elem` pieceList
149 then (pd { peers = p : peers pd })
152 handlePeerMsgs :: Peer -> String -> Metainfo -> PieceMap -> Bool -> IO ()
153 handlePeerMsgs p peerId m pieceMap isClient = do
155 doHandshake isClient h p (infoHash m) peerId
156 let pstate = toPeerState h p False False True True
157 filePath = name (info m)
158 _ <- runStateT (msgLoop pieceMap filePath) pstate
161 msgLoop :: PieceMap -> FilePath -> StateT PState IO ()
162 msgLoop pieceStatus file = do
166 PState { meInterested = False, heChoking = True } -> do
167 liftIO $ sendMsg h InterestedMsg
168 gets peer >>= (\p -> liftIO $ putStrLn $ "--> InterestedMsg to peer: " ++ show p)
169 modify (\st -> st { meInterested = True })
170 msgLoop pieceStatus file
171 PState { meInterested = True, heChoking = False } ->
172 case pickPiece pieceStatus of
173 Nothing -> liftIO $ putStrLn "Nothing to download"
175 let pLen = len (pieceStatus ! workPiece)
176 liftIO $ putStrLn $ "piece length = " ++ show pLen
177 pBS <- liftIO $ downloadPiece h workPiece pLen
178 if not $ verifyHash pBS (hash (pieceStatus ! workPiece))
180 liftIO $ putStrLn $ "Hash mismatch"
182 let fileOffset = if workPiece == 0 then 0 else workPiece * len (pieceStatus ! (workPiece - 1))
183 liftIO $ putStrLn $ "Write into file at offset: " ++ show fileOffset
184 liftIO $ writeFileAtOffset file fileOffset pBS
185 msgLoop (adjust (\pieceData -> pieceData { dlstate = Have }) workPiece pieceStatus) file
187 msg <- liftIO $ getMsg h
188 gets peer >>= (\p -> liftIO $ putStrLn $ "<-- " ++ show msg ++ "from peer: " ++ show p)
191 liftIO $ sendMsg h KeepAliveMsg
192 gets peer >>= (\p -> liftIO $ putStrLn $ "--> " ++ "KeepAliveMsg to peer: " ++ show p)
193 msgLoop pieceStatus file
194 BitFieldMsg bss -> do
196 let pieceList = bitfieldToList (unpack bss)
197 pieceStatus' = updatePieceAvailability pieceStatus p pieceList
198 liftIO $ putStrLn $ show (length pieceList) ++ " Pieces"
199 -- for each pieceIndex in pieceList, make an entry in the pieceStatus
200 -- map with pieceIndex as the key and modify the value to add the peer.
201 -- download each of the piece in order
202 msgLoop pieceStatus' file
204 modify (\st -> st {heChoking = False })
205 msgLoop pieceStatus file
208 downloadPiece :: Handle -> Integer -> Integer -> IO ByteString
209 downloadPiece h index pieceLength = do
210 let chunks = splitNum pieceLength 16384
211 concat `liftM` forM (zip [0..] chunks) (\(i, pLen) -> do
212 sendMsg h (RequestMsg index (i*pLen) pLen)
213 putStrLn $ "--> " ++ "RequestMsg for Piece "
214 ++ show index ++ ", part: " ++ show i ++ " of length: "
218 PieceMsg index begin block -> do
219 putStrLn $ " <-- PieceMsg for Piece: "
225 putStrLn "ignoring irrelevant msg"