1 {-# LANGUAGE OverloadedStrings #-}
2 module FuncTorrent.Peer
7 import Prelude hiding (lookup, concat, replicate, splitAt, take, filter)
9 import System.IO (Handle, BufferMode(..), hSetBuffering)
10 import Data.ByteString (ByteString, unpack, concat, hGet, hPut, take, empty)
11 import qualified Data.ByteString.Char8 as BC (length)
12 import Network (connectTo, PortID(..))
13 import Control.Monad.State
15 import Data.Word (Word8)
16 import Data.Map (Map, fromList, toList, (!), mapWithKey, adjust, filter)
17 import qualified Crypto.Hash.SHA1 as SHA1 (hash)
20 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
21 import FuncTorrent.Utils (splitN, splitNum)
22 import FuncTorrent.Fileops (createDummyFile, writeFileAtOffset)
23 import FuncTorrent.PeerMsgs (Peer(..), PeerMsg(..), sendMsg, getMsg, genHandshakeMsg)
25 data PState = PState { handle :: Handle
28 , meInterested :: Bool
30 , heInterested :: Bool}
32 type PeerState = State PState
34 data PieceDlState = Pending
39 -- todo - map with index to a new data structure (peers who have that piece and state)
40 data PieceData = PieceData { peers :: [Peer] -- ^ list of peers who have this piece
41 , dlstate :: PieceDlState -- ^ state of the piece from download perspective.
42 , hash :: ByteString -- ^ piece hash
43 , len :: Integer } -- ^ piece length
45 -- which piece is with which peers
46 type PieceMap = Map Integer PieceData
49 -- Make the initial Piece map, with the assumption that no peer has the
50 -- piece and that every piece is pending download.
51 mkPieceMap :: Integer -> ByteString -> [Integer] -> PieceMap
52 mkPieceMap numPieces pieceHash pLengths = fromList kvs
53 where kvs = [(i, PieceData { peers = []
57 | (i, h, pLen) <- zip3 [0..numPieces] hashes pLengths]
58 hashes = splitN 20 pieceHash
60 havePiece :: PieceMap -> Integer -> Bool
62 dlstate (pm ! index) == Have
64 connectToPeer :: Peer -> IO Handle
65 connectToPeer (Peer _ ip port) = do
66 h <- connectTo ip (PortNumber (fromIntegral port))
67 hSetBuffering h LineBuffering
70 doHandshake :: Handle -> Peer -> ByteString -> String -> IO ()
71 doHandshake h peer infoHash peerid = do
72 let hs = genHandshakeMsg infoHash peerid
74 putStrLn $ "--> handhake to peer: " ++ show peer
75 _ <- hGet h (length (unpack hs))
76 putStrLn $ "<-- handshake from peer: " ++ show peer
79 bitfieldToList :: [Word8] -> [Integer]
80 bitfieldToList bs = go bs 0
83 let setBits = [pos*8 + toInteger i | i <- [0..8], testBit b i]
85 setBits ++ go bs' (pos + 1)
87 -- helper functions to manipulate PeerState
90 -> Bool -- ^ meChoking
91 -> Bool -- ^ meInterested
92 -> Bool -- ^ heChoking
93 -> Bool -- ^ heInterested
95 toPeerState h p meCh meIn heCh heIn =
101 , meInterested = meIn }
103 -- simple algorithm to pick piece.
104 -- pick the first piece from 0 that is not downloaded yet.
105 pickPiece :: PieceMap -> Maybe Integer
107 (fst `liftM`) . headMay . toList . filter (\v -> dlstate v == Pending)
109 bytesDownloaded :: PieceMap -> Integer
111 sum . (map (len . snd)) . toList . filter (\v -> dlstate v == Have)
113 updatePieceAvailability :: PieceMap -> Peer -> [Integer] -> PieceMap
114 updatePieceAvailability pieceStatus p pieceList =
115 mapWithKey (\k pd -> if k `elem` pieceList
116 then (pd { peers = p : peers pd })
119 handlePeerMsgs :: Peer -> Metainfo -> String -> IO ()
120 handlePeerMsgs p m peerId = do
122 doHandshake h p (infoHash m) peerId
123 let pstate = toPeerState h p False False True True
124 pieceHash = pieces (info m)
125 numPieces = (toInteger . (`quot` 20) . BC.length) pieceHash
126 pLen = pieceLength (info m)
127 fileLen = lengthInBytes (info m)
128 fileName = name (info m)
129 pieceStatus = mkPieceMap numPieces pieceHash (splitNum fileLen pLen)
130 createDummyFile fileName (fromIntegral fileLen)
131 (r, _) <- runStateT (msgLoop pieceStatus fileName) pstate
134 msgLoop :: PieceMap -> FilePath -> StateT PState IO ()
135 msgLoop pieceStatus file = do
139 PState { meInterested = False, heChoking = True } -> do
140 liftIO $ sendMsg h InterestedMsg
141 gets peer >>= (\p -> liftIO $ putStrLn $ "--> InterestedMsg to peer: " ++ show p)
142 modify (\st -> st { meInterested = True })
143 msgLoop pieceStatus file
144 PState { meInterested = True, heChoking = False } -> do
145 case pickPiece pieceStatus of
146 Nothing -> liftIO $ putStrLn "Nothing to download"
148 let pLen = len (pieceStatus ! workPiece)
149 liftIO $ putStrLn $ "piece length = " ++ show pLen
150 pBS <- liftIO $ downloadPiece h workPiece pLen
151 if not $ verifyHash pBS (hash (pieceStatus ! workPiece))
153 liftIO $ putStrLn $ "Hash mismatch: " ++ show (hash (pieceStatus ! workPiece)) ++ " vs " ++ show (take 20 (SHA1.hash pBS))
155 let fileOffset = if workPiece == 0 then 0 else workPiece * len (pieceStatus ! (workPiece - 1))
156 liftIO $ putStrLn $ "Write into file at offset: " ++ show fileOffset
157 liftIO $ writeFileAtOffset file fileOffset pBS
158 msgLoop (adjust (\pieceData -> pieceData { dlstate = Have }) workPiece pieceStatus) file
160 msg <- liftIO $ getMsg h
161 gets peer >>= (\p -> liftIO $ putStrLn $ "<-- " ++ show msg ++ "from peer: " ++ show p)
164 liftIO $ sendMsg h KeepAliveMsg
165 gets peer >>= (\p -> liftIO $ putStrLn $ "--> " ++ "KeepAliveMsg to peer: " ++ show p)
166 msgLoop pieceStatus file
167 BitFieldMsg bss -> do
169 let pieceList = bitfieldToList (unpack bss)
170 pieceStatus' = updatePieceAvailability pieceStatus p pieceList
171 liftIO $ putStrLn $ show (length pieceList) ++ " Pieces"
172 -- for each pieceIndex in pieceList, make an entry in the pieceStatus
173 -- map with pieceIndex as the key and modify the value to add the peer.
174 -- download each of the piece in order
175 msgLoop pieceStatus' file
177 modify (\st -> st {heChoking = False })
178 msgLoop pieceStatus file
181 downloadPiece :: Handle -> Integer -> Integer -> IO ByteString
182 downloadPiece h index pieceLength = do
183 let chunks = splitNum pieceLength 16384
184 liftM concat $ forM (zip [0..] chunks) (\(i, pLen) -> do
185 sendMsg h (RequestMsg index (i*pLen) pLen)
186 putStrLn $ "--> " ++ "RequestMsg for Piece "
187 ++ show index ++ ", part: " ++ show i ++ " of length: "
191 PieceMsg index begin block -> do
192 putStrLn $ " <-- PieceMsg for Piece: "
198 putStrLn "ignoring irrelevant msg"
201 verifyHash :: ByteString -> ByteString -> Bool
202 verifyHash bs pieceHash =
203 take 20 (SHA1.hash bs) == pieceHash