1 {-# LANGUAGE OverloadedStrings #-}
2 module FuncTorrent.Peer
8 import Prelude hiding (lookup, concat, replicate, splitAt, take, filter)
10 import System.IO (Handle, BufferMode(..), hSetBuffering)
11 import System.Directory (doesFileExist)
12 import Data.ByteString (ByteString, unpack, concat, hGet, hPut, take, empty)
13 import qualified Data.ByteString.Char8 as BC (length)
14 import Network (connectTo, PortID(..))
15 import Control.Monad.State
17 import Data.Word (Word8)
18 import Data.Map (Map, fromList, toList, (!), mapWithKey, traverseWithKey, adjust, filter)
19 import qualified Crypto.Hash.SHA1 as SHA1 (hash)
22 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
23 import FuncTorrent.Utils (splitN, splitNum)
24 import FuncTorrent.Fileops (createDummyFile, writeFileAtOffset, readFileAtOffset)
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 updatePieceMap :: FilePath -> PieceMap -> IO PieceMap
66 updatePieceMap filePath pieceMap = do
67 dfe <- doesFileExist filePath
68 -- TODO: this is not enough, file should have the same size as well
70 then pieceMapFromFile filePath pieceMap
73 pieceMapFromFile :: FilePath -> PieceMap -> IO PieceMap
74 pieceMapFromFile filePath pieceMap = do
75 traverseWithKey f pieceMap
78 let offset = if k == 0 then 0 else k * len (pieceMap ! (k - 1))
79 isHashValid <- (flip verifyHash) (hash v) <$> (readFileAtOffset filePath offset (len v))
81 then return $ v { dlstate = Have }
84 havePiece :: PieceMap -> Integer -> Bool
86 dlstate (pm ! index) == Have
88 connectToPeer :: Peer -> IO Handle
89 connectToPeer (Peer _ ip port) = do
90 h <- connectTo ip (PortNumber (fromIntegral port))
91 hSetBuffering h LineBuffering
94 doHandshake :: Handle -> Peer -> ByteString -> String -> IO ()
95 doHandshake h peer infoHash peerid = do
96 let hs = genHandshakeMsg infoHash peerid
98 putStrLn $ "--> handhake to peer: " ++ show peer
99 _ <- hGet h (length (unpack hs))
100 putStrLn $ "<-- handshake from peer: " ++ show peer
103 bitfieldToList :: [Word8] -> [Integer]
104 bitfieldToList bs = go bs 0
107 let setBits = [pos*8 + toInteger i | i <- [0..8], testBit b i]
109 setBits ++ go bs' (pos + 1)
111 -- helper functions to manipulate PeerState
112 toPeerState :: Handle
114 -> Bool -- ^ meChoking
115 -> Bool -- ^ meInterested
116 -> Bool -- ^ heChoking
117 -> Bool -- ^ heInterested
119 toPeerState h p meCh meIn heCh heIn =
123 , heInterested = heIn
125 , meInterested = meIn }
127 -- simple algorithm to pick piece.
128 -- pick the first piece from 0 that is not downloaded yet.
129 pickPiece :: PieceMap -> Maybe Integer
131 (fst `liftM`) . headMay . toList . filter (\v -> dlstate v == Pending)
133 bytesDownloaded :: PieceMap -> Integer
135 sum . map (len . snd) . toList . filter (\v -> dlstate v == Have)
137 updatePieceAvailability :: PieceMap -> Peer -> [Integer] -> PieceMap
138 updatePieceAvailability pieceStatus p pieceList =
139 mapWithKey (\k pd -> if k `elem` pieceList
140 then (pd { peers = p : peers pd })
143 handlePeerMsgs :: Peer -> String -> Metainfo -> IO ()
144 handlePeerMsgs p peerId m = do
146 doHandshake h p (infoHash m) peerId
147 let pstate = toPeerState h p False False True True
148 pieceHash = pieces (info m)
149 pLen = pieceLength (info m)
150 fileLen = lengthInBytes (info m)
151 fileName = name (info m)
152 pieceStatus = initPieceMap pieceHash fileLen pLen
153 pieceStatus' <- updatePieceMap fileName pieceStatus
154 createDummyFile fileName (fromIntegral fileLen)
155 _ <- runStateT (msgLoop pieceStatus' fileName) pstate
158 msgLoop :: PieceMap -> FilePath -> StateT PState IO ()
159 msgLoop pieceStatus file = do
163 PState { meInterested = False, heChoking = True } -> do
164 liftIO $ sendMsg h InterestedMsg
165 gets peer >>= (\p -> liftIO $ putStrLn $ "--> InterestedMsg to peer: " ++ show p)
166 modify (\st -> st { meInterested = True })
167 msgLoop pieceStatus file
168 PState { meInterested = True, heChoking = False } ->
169 case pickPiece pieceStatus of
170 Nothing -> liftIO $ putStrLn "Nothing to download"
172 let pLen = len (pieceStatus ! workPiece)
173 liftIO $ putStrLn $ "piece length = " ++ show pLen
174 pBS <- liftIO $ downloadPiece h workPiece pLen
175 if not $ verifyHash pBS (hash (pieceStatus ! workPiece))
177 liftIO $ putStrLn $ "Hash mismatch: " ++ show (hash (pieceStatus ! workPiece)) ++ " vs " ++ show (take 20 (SHA1.hash pBS))
179 let fileOffset = if workPiece == 0 then 0 else workPiece * len (pieceStatus ! (workPiece - 1))
180 liftIO $ putStrLn $ "Write into file at offset: " ++ show fileOffset
181 liftIO $ writeFileAtOffset file fileOffset pBS
182 msgLoop (adjust (\pieceData -> pieceData { dlstate = Have }) workPiece pieceStatus) file
184 msg <- liftIO $ getMsg h
185 gets peer >>= (\p -> liftIO $ putStrLn $ "<-- " ++ show msg ++ "from peer: " ++ show p)
188 liftIO $ sendMsg h KeepAliveMsg
189 gets peer >>= (\p -> liftIO $ putStrLn $ "--> " ++ "KeepAliveMsg to peer: " ++ show p)
190 msgLoop pieceStatus file
191 BitFieldMsg bss -> do
193 let pieceList = bitfieldToList (unpack bss)
194 pieceStatus' = updatePieceAvailability pieceStatus p pieceList
195 liftIO $ putStrLn $ show (length pieceList) ++ " Pieces"
196 -- for each pieceIndex in pieceList, make an entry in the pieceStatus
197 -- map with pieceIndex as the key and modify the value to add the peer.
198 -- download each of the piece in order
199 msgLoop pieceStatus' file
201 modify (\st -> st {heChoking = False })
202 msgLoop pieceStatus file
205 downloadPiece :: Handle -> Integer -> Integer -> IO ByteString
206 downloadPiece h index pieceLength = do
207 let chunks = splitNum pieceLength 16384
208 concat `liftM` forM (zip [0..] chunks) (\(i, pLen) -> do
209 sendMsg h (RequestMsg index (i*pLen) pLen)
210 putStrLn $ "--> " ++ "RequestMsg for Piece "
211 ++ show index ++ ", part: " ++ show i ++ " of length: "
215 PieceMsg index begin block -> do
216 putStrLn $ " <-- PieceMsg for Piece: "
222 putStrLn "ignoring irrelevant msg"
225 verifyHash :: ByteString -> ByteString -> Bool
226 verifyHash bs pieceHash =
227 take 20 (SHA1.hash bs) == pieceHash