1 {-# LANGUAGE OverloadedStrings #-}
2 module FuncTorrent.Tracker
11 import Prelude hiding (lookup, concat, replicate, splitAt)
13 import Data.ByteString (ByteString)
14 import Data.ByteString.Char8 as BC (pack, unpack, splitAt, concat, intercalate)
15 import Data.Char (chr)
16 import Data.List (intercalate)
17 import Data.Map as M (lookup)
18 import Network.HTTP (simpleHTTP, defaultGETRequest_, getResponseBody)
19 import Network.HTTP.Base (urlEncode)
20 import Network.URI (parseURI)
21 import qualified Data.ByteString.Base16 as B16 (encode)
23 import FuncTorrent.Bencode (BVal(..))
24 import FuncTorrent.Peer (Peer(..))
25 import FuncTorrent.Utils (splitN)
26 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
30 data TrackerResponse = TrackerResponse {
31 interval :: Maybe Integer
33 , complete :: Maybe Integer
34 , incomplete :: Maybe Integer
37 -- | Deserialize tracker response
38 mkTrackerResponse :: BVal -> Either ByteString TrackerResponse
39 mkTrackerResponse resp =
40 case lookup "failure reason" body of
41 Just (Bstr err) -> Left err
42 Just _ -> Left "Unknown failure"
44 let (Just (Bint i)) = lookup "interval" body
45 (Just (Bstr peersBS)) = lookup "peers" body
46 pl = map makePeer (splitN 6 peersBS)
47 in Right TrackerResponse {
51 , incomplete = Nothing
56 toInt :: String -> Integer
59 toPort :: ByteString -> Integer
60 toPort = read . ("0x" ++) . unpack . B16.encode
62 toIP :: ByteString -> String
63 toIP = Data.List.intercalate "." .
64 map (show . toInt . ("0x" ++) . unpack) .
67 makePeer :: ByteString -> Peer
68 makePeer peer = Peer (toIP ip') (toPort port')
69 where (ip', port') = splitAt 4 peer
71 -- | Connect to a tracker and get peer info
72 connect :: Metainfo -> String -> IO ByteString
73 connect m peer_id = get (head . announceList $ m) $ mkArgs m peer_id
75 --- | URL encode hash as per RFC1738
77 --- REVIEW: Why is this not written in terms of `Network.HTTP.Base.urlEncode` or
78 --- equivalent library function?
79 urlEncodeHash :: ByteString -> String
80 urlEncodeHash bs = concatMap (encode' . unpack) (splitN 2 bs)
81 where encode' b@[c1, c2] = let c = chr (read ("0x" ++ b))
84 escape i c1 c2 | i `elem` nonSpecialChars = [i]
85 | otherwise = "%" ++ [c1] ++ [c2]
87 nonSpecialChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "-_.~"
89 -- | Make arguments that should be posted to tracker.
90 -- This is a separate pure function for testability.
91 mkArgs :: Metainfo -> String -> [(String, ByteString)]
92 mkArgs m peer_id = [("info_hash", pack . urlEncodeHash . B16.encode . infoHash $ m),
93 ("peer_id", pack . urlEncode $ peer_id),
97 ("left", pack . show . lengthInBytes $ info m),
101 -- | Make a query string from a alist of k, v
102 -- TODO: Url encode each argument
103 mkParams :: [(String, ByteString)] -> ByteString
104 mkParams params = BC.intercalate "&" [concat [pack f, "=", s] | (f,s) <- params]
106 get :: String -> [(String, ByteString)] -> IO ByteString
107 get url args = simpleHTTP (defaultGETRequest_ url') >>= getResponseBody
108 where url' = case parseURI $ unpack $ concat [pack url, "?", qstr] of
110 _ -> error "Bad tracker URL"