1 {-# LANGUAGE OverloadedStrings #-}
2 module FuncTorrent.Tracker
8 import Prelude hiding (lookup, splitAt)
10 import Control.Concurrent (threadDelay)
11 import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar, readMVar, putMVar)
12 import Data.ByteString (ByteString)
13 import Data.ByteString.Char8 as BC (pack, unpack, splitAt)
14 import Data.Char (chr)
15 import Data.List (intercalate)
16 import Data.Map as M (lookup)
17 import Network (PortNumber)
18 import Network.HTTP.Base (urlEncode)
19 import qualified Data.ByteString.Base16 as B16 (encode)
21 import FuncTorrent.Bencode (BVal(..), decode)
22 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
23 import FuncTorrent.Network (sendGetRequest)
24 import FuncTorrent.Peer (Peer(..))
25 import FuncTorrent.Utils (splitN)
28 data TrackerResponse = TrackerResponse {
31 , complete :: Maybe Integer
32 , incomplete :: Maybe Integer
35 data TrackerEventState = Started
40 data TState = TState {
41 uploaded :: MVar Integer
42 , downloaded :: MVar Integer
44 , currentState :: TrackerEventState
45 , connectedPeers :: MVar [Peer]
48 initialTrackerState :: Integer -> IO TState
49 initialTrackerState sz = do
53 return $ TState { currentState = Started
59 -- | Deserialize tracker response
60 mkTrackerResponse :: BVal -> Either ByteString TrackerResponse
61 mkTrackerResponse resp =
62 case lookup "failure reason" body of
63 Just (Bstr err) -> Left err
64 Just _ -> Left "Unknown failure"
66 let (Just (Bint i)) = lookup "interval" body
67 (Just (Bstr peersBS)) = lookup "peers" body
68 pl = map makePeer (splitN 6 peersBS)
69 in Right TrackerResponse {
73 , incomplete = Nothing
78 toInt :: String -> Integer
81 toPort :: ByteString -> Integer
82 toPort = read . ("0x" ++) . unpack . B16.encode
84 toIP :: ByteString -> String
85 toIP = Data.List.intercalate "." .
86 map (show . toInt . ("0x" ++) . unpack) .
89 makePeer :: ByteString -> Peer
90 makePeer peer = Peer "" (toIP ip') (toPort port')
91 where (ip', port') = splitAt 4 peer
93 --- | URL encode hash as per RFC1738
95 --- REVIEW: Why is this not written in terms of `Network.HTTP.Base.urlEncode` or
96 --- equivalent library function?
97 urlEncodeHash :: ByteString -> String
98 urlEncodeHash bs = concatMap (encode' . unpack) (splitN 2 bs)
99 where encode' b@[c1, c2] = let c = chr (read ("0x" ++ b))
102 escape i c1 c2 | i `elem` nonSpecialChars = [i]
103 | otherwise = "%" ++ [c1] ++ [c2]
105 nonSpecialChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "-_.~"
107 -- | Make arguments that should be posted to tracker.
108 -- This is a separate pure function for testability.
109 mkArgs :: PortNumber -> String -> Integer -> Integer -> Metainfo -> [(String, ByteString)]
110 mkArgs port peer_id up down m =
111 let fileSize = lengthInBytes $ info m
112 bytesLeft = fileSize - down
114 [("info_hash", pack . urlEncodeHash . B16.encode . infoHash $ m),
115 ("peer_id", pack . urlEncode $ peer_id),
116 ("port", pack $ show port),
117 ("uploaded", pack $ show up),
118 ("downloaded", pack $ show down),
119 ("left", pack $ show bytesLeft),
121 ("event", "started")]
123 trackerLoop :: PortNumber -> String -> Metainfo -> TState -> IO ByteString
124 trackerLoop port peerId m st = do
125 up <- readMVar $ uploaded st
126 down <- readMVar $ downloaded st
127 resp <- sendGetRequest (head . announceList $ m) $ mkArgs port peerId up down m
129 Left e -> return $ pack (show e)
131 case mkTrackerResponse trackerInfo of
134 _ <- threadDelay $ fromIntegral (interval tresp)
135 _ <- putMVar (connectedPeers st) (peers tresp)
136 trackerLoop port peerId m st