1 {-# LANGUAGE OverloadedStrings #-}
2 module FuncTorrent.Tracker
7 import Prelude hiding (lookup, splitAt)
9 import Data.ByteString (ByteString)
10 import Data.ByteString.Char8 as BC (pack, unpack, splitAt)
11 import Data.Char (chr)
12 import Data.List (intercalate)
13 import Data.Map as M (lookup)
14 import Network.HTTP.Base (urlEncode)
15 import qualified Data.ByteString.Base16 as B16 (encode)
17 import FuncTorrent.Bencode (BVal(..), decode)
18 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
19 import FuncTorrent.Network (get)
20 import FuncTorrent.Peer (Peer(..))
21 import FuncTorrent.Utils (splitN)
24 data TrackerResponse = TrackerResponse {
25 interval :: Maybe Integer
27 , complete :: Maybe Integer
28 , incomplete :: Maybe Integer
31 -- | Deserialize tracker response
32 mkTrackerResponse :: BVal -> Either ByteString TrackerResponse
33 mkTrackerResponse resp =
34 case lookup "failure reason" body of
35 Just (Bstr err) -> Left err
36 Just _ -> Left "Unknown failure"
38 let (Just (Bint i)) = lookup "interval" body
39 (Just (Bstr peersBS)) = lookup "peers" body
40 pl = map makePeer (splitN 6 peersBS)
41 in Right TrackerResponse {
45 , incomplete = Nothing
50 toInt :: String -> Integer
53 toPort :: ByteString -> Integer
54 toPort = read . ("0x" ++) . unpack . B16.encode
56 toIP :: ByteString -> String
57 toIP = Data.List.intercalate "." .
58 map (show . toInt . ("0x" ++) . unpack) .
61 makePeer :: ByteString -> Peer
62 makePeer peer = Peer "" (toIP ip') (toPort port')
63 where (ip', port') = splitAt 4 peer
65 -- | Connect to a tracker and get peer info
66 tracker :: String -> Metainfo -> IO ByteString
67 tracker peer_id m = do
68 get (head . announceList $ m) $ mkArgs peer_id m
70 getTrackerResponse :: String -> Metainfo -> IO (Either ByteString TrackerResponse)
71 getTrackerResponse peerId m = do
72 resp <- tracker peerId m
74 Right trackerInfo -> return $ mkTrackerResponse trackerInfo
75 Left e -> return $ Left (pack (show e))
77 --- | URL encode hash as per RFC1738
79 --- REVIEW: Why is this not written in terms of `Network.HTTP.Base.urlEncode` or
80 --- equivalent library function?
81 urlEncodeHash :: ByteString -> String
82 urlEncodeHash bs = concatMap (encode' . unpack) (splitN 2 bs)
83 where encode' b@[c1, c2] = let c = chr (read ("0x" ++ b))
86 escape i c1 c2 | i `elem` nonSpecialChars = [i]
87 | otherwise = "%" ++ [c1] ++ [c2]
89 nonSpecialChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "-_.~"
91 -- | Make arguments that should be posted to tracker.
92 -- This is a separate pure function for testability.
93 mkArgs :: String -> Metainfo -> [(String, ByteString)]
94 mkArgs peer_id m = [("info_hash", pack . urlEncodeHash . B16.encode . infoHash $ m),
95 ("peer_id", pack . urlEncode $ peer_id),
99 ("left", pack . show . lengthInBytes $ info m),
101 ("event", "started")]