diff options
author | Sam Truzjan <pxqr.sta@gmail.com> | 2014-02-06 21:43:56 +0400 |
---|---|---|
committer | Sam Truzjan <pxqr.sta@gmail.com> | 2014-02-06 21:43:56 +0400 |
commit | 25c46cddb6498155e2b8b07d85f900c4a950267e (patch) | |
tree | fcbf9c32ca74eae000883b127c49a46fb25c629f | |
parent | b0d2f2883c9d134b04944d6ec4a4ac15fa516cab (diff) |
Add BEP12: Multitracker Protocol Extension
-rw-r--r-- | bittorrent.cabal | 9 | ||||
-rw-r--r-- | src/Network/BitTorrent/Tracker/List.hs | 153 | ||||
-rw-r--r-- | tests/Network/BitTorrent/Tracker/ListSpec.hs | 44 |
3 files changed, 203 insertions, 3 deletions
diff --git a/bittorrent.cabal b/bittorrent.cabal index f8e598c1..4b2431ce 100644 --- a/bittorrent.cabal +++ b/bittorrent.cabal | |||
@@ -74,7 +74,7 @@ library | |||
74 | -- Network.BitTorrent.Exchange.Session | 74 | -- Network.BitTorrent.Exchange.Session |
75 | Network.BitTorrent.Exchange.Status | 75 | Network.BitTorrent.Exchange.Status |
76 | Network.BitTorrent.Exchange.Wire | 76 | Network.BitTorrent.Exchange.Wire |
77 | -- Network.BitTorrent.Tracker | 77 | Network.BitTorrent.Tracker.List |
78 | Network.BitTorrent.Tracker.Message | 78 | Network.BitTorrent.Tracker.Message |
79 | Network.BitTorrent.Tracker.RPC | 79 | Network.BitTorrent.Tracker.RPC |
80 | Network.BitTorrent.Tracker.RPC.HTTP | 80 | Network.BitTorrent.Tracker.RPC.HTTP |
@@ -153,12 +153,15 @@ library | |||
153 | , wai >= 1.4 && < 2.0 | 153 | , wai >= 1.4 && < 2.0 |
154 | , iproute | 154 | , iproute |
155 | 155 | ||
156 | -- RNG/PRNG | ||
157 | , entropy >= 0.2 | ||
158 | , random >= 1.0.0.2 | ||
159 | , random-shuffle >= 0.0.0.4 | ||
160 | |||
156 | -- System | 161 | -- System |
157 | , directory >= 1.2 | 162 | , directory >= 1.2 |
158 | , entropy >= 0.2 | ||
159 | , filepath >= 1.3 | 163 | , filepath >= 1.3 |
160 | , mmap >= 0.5 | 164 | , mmap >= 0.5 |
161 | , random >= 1.0.0.2 | ||
162 | 165 | ||
163 | ghc-options: -Wall | 166 | ghc-options: -Wall |
164 | ghc-prof-options: | 167 | ghc-prof-options: |
diff --git a/src/Network/BitTorrent/Tracker/List.hs b/src/Network/BitTorrent/Tracker/List.hs new file mode 100644 index 00000000..b6371f1d --- /dev/null +++ b/src/Network/BitTorrent/Tracker/List.hs | |||
@@ -0,0 +1,153 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2014 | ||
3 | -- License : BSD | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Multitracker Metadata Extension support. | ||
9 | -- | ||
10 | -- For more info see: <http://www.bittorrent.org/beps/bep_0012.html> | ||
11 | -- | ||
12 | module Network.BitTorrent.Tracker.List | ||
13 | ( -- * Tracker list | ||
14 | TrackerList | ||
15 | |||
16 | -- * Construction | ||
17 | , trackerList | ||
18 | , shuffleTiers | ||
19 | |||
20 | -- * Traversals | ||
21 | , traverseAll | ||
22 | , traverseTiers | ||
23 | ) where | ||
24 | |||
25 | import Prelude hiding (mapM, foldr) | ||
26 | import Control.Applicative | ||
27 | import Control.Exception | ||
28 | import Data.List as L (elem, any, filter, null) | ||
29 | import Data.Foldable | ||
30 | import Data.Traversable | ||
31 | import Network.URI | ||
32 | import System.Random.Shuffle | ||
33 | |||
34 | import Data.Torrent | ||
35 | import Network.BitTorrent.Tracker.RPC as RPC | ||
36 | |||
37 | {----------------------------------------------------------------------- | ||
38 | -- Tracker list datatype | ||
39 | -----------------------------------------------------------------------} | ||
40 | |||
41 | type Tier a = [a] | ||
42 | |||
43 | -- | Tracker list is either a single tracker or list of tiers. All | ||
44 | -- trackers in each tier must be checked before the client goes on to | ||
45 | -- the next tier. | ||
46 | data TrackerList a | ||
47 | = Announce a -- ^ torrent file 'announce' field only | ||
48 | | TierList [Tier a] -- ^ torrent file 'announce-list' field only | ||
49 | deriving (Show, Eq) | ||
50 | |||
51 | instance Functor TrackerList where | ||
52 | fmap f (Announce a) = Announce (f a) | ||
53 | fmap f (TierList a) = TierList (fmap (fmap f) a) | ||
54 | |||
55 | instance Foldable TrackerList where | ||
56 | foldr f z (Announce a ) = f a z | ||
57 | foldr f z (TierList xs) = foldr (flip (foldr f)) z xs | ||
58 | |||
59 | instance Traversable TrackerList where | ||
60 | traverse f (Announce a ) = Announce <$> f a | ||
61 | traverse f (TierList xs) = TierList <$> traverse (traverse f) xs | ||
62 | |||
63 | {----------------------------------------------------------------------- | ||
64 | -- List extraction | ||
65 | -----------------------------------------------------------------------} | ||
66 | -- BEP12 do not expose any restrictions for the content of | ||
67 | -- 'announce-list' key - there are some /bad/ cases can happen with | ||
68 | -- poorly designed or even malicious torrent creation software. | ||
69 | -- | ||
70 | -- Bad case #1: announce-list is present, but empty. | ||
71 | -- | ||
72 | -- { tAnnounce = Just "http://a.com" | ||
73 | -- , tAnnounceList = Just [[]] | ||
74 | -- } | ||
75 | -- | ||
76 | -- Bad case #2: announce uri do not present in announce list. | ||
77 | -- | ||
78 | -- { tAnnounce = Just "http://a.com" | ||
79 | -- , tAnnounceList = Just [["udp://a.com"]] | ||
80 | -- } | ||
81 | -- | ||
82 | -- The addBackup function solves both problems by adding announce uri | ||
83 | -- as backup tier. | ||
84 | -- | ||
85 | addBackup :: [[URI]] -> URI -> [[URI]] | ||
86 | addBackup tiers bkp | ||
87 | | L.any (L.elem bkp) tiers = tiers | ||
88 | | otherwise = tiers ++ [[bkp]] | ||
89 | |||
90 | fixList :: Maybe [[URI]] -> Maybe URI -> Maybe [[URI]] | ||
91 | fixList mxss mx = do | ||
92 | xss <- mxss | ||
93 | let xss' = L.filter (not . L.null) xss | ||
94 | return $ maybe xss' (addBackup xss') mx | ||
95 | |||
96 | -- | Extract set of trackers from torrent file. The 'tAnnounce' key is | ||
97 | -- only ignored if the 'tAnnounceList' key is present. | ||
98 | trackerList :: Torrent -> Maybe (TrackerList URI) | ||
99 | trackerList Torrent {..} = TierList <$> (tAnnounceList `fixList` tAnnounce) | ||
100 | <|> Announce <$> tAnnounce | ||
101 | |||
102 | -- | Shuffle /order of trackers/ in each tier, preserving original | ||
103 | -- /order of tiers/. This can help to balance the load between the | ||
104 | -- trackers. | ||
105 | shuffleTiers :: TrackerList a -> IO (TrackerList a) | ||
106 | shuffleTiers (Announce a ) = return (Announce a) | ||
107 | shuffleTiers (TierList xs) = TierList <$> mapM shuffleM xs | ||
108 | |||
109 | {----------------------------------------------------------------------- | ||
110 | -- Special traversals (suppressed RPC exceptions) | ||
111 | -----------------------------------------------------------------------} | ||
112 | |||
113 | catchRPC :: IO a -> IO a -> IO a | ||
114 | catchRPC a b = catch a (f b) | ||
115 | where | ||
116 | f :: a -> RpcException -> a | ||
117 | f = const | ||
118 | |||
119 | throwRPC :: String -> IO a | ||
120 | throwRPC = throwIO . GenericException | ||
121 | |||
122 | -- | Like 'traverse' but ignore 'RpcExceptions'. | ||
123 | traverseAll :: (a -> IO a) -> TrackerList a -> IO (TrackerList a) | ||
124 | traverseAll action = traverse (action $?) | ||
125 | where | ||
126 | f $? x = catchRPC (f x) (return x) | ||
127 | |||
128 | -- | Like 'traverse' but put working trackers to the head of tiers. | ||
129 | -- This can help to avoid exceessive requests to not available | ||
130 | -- trackers at each reannounce. | ||
131 | traverseTiers :: (a -> IO a) -> TrackerList a -> IO (TrackerList a) | ||
132 | traverseTiers action (Announce a) = Announce <$> action a | ||
133 | traverseTiers action (TierList tiers) = TierList <$> goTiers (goTier []) tiers | ||
134 | where | ||
135 | goTiers _ [] = throwRPC "traverseTiers: no tiers" | ||
136 | goTiers f (x : xs) = catchRPC shortcut failback | ||
137 | where | ||
138 | shortcut = do | ||
139 | x' <- f x | ||
140 | return (x' : xs) | ||
141 | |||
142 | failback = do | ||
143 | xs' <- goTiers f xs | ||
144 | return (x : xs') | ||
145 | |||
146 | goTier _ [] = throwRPC "traverseTiers: no trackers in tier" | ||
147 | goTier failed (a : as) = catchRPC shortcut failback | ||
148 | where | ||
149 | shortcut = do | ||
150 | a' <- action a | ||
151 | return (a' : as ++ failed) -- failed trackers at the end | ||
152 | |||
153 | failback = goTier (a : failed) as | ||
diff --git a/tests/Network/BitTorrent/Tracker/ListSpec.hs b/tests/Network/BitTorrent/Tracker/ListSpec.hs new file mode 100644 index 00000000..8decd3c9 --- /dev/null +++ b/tests/Network/BitTorrent/Tracker/ListSpec.hs | |||
@@ -0,0 +1,44 @@ | |||
1 | module Network.BitTorrent.Tracker.ListSpec (spec) where | ||
2 | import Control.Exception | ||
3 | import Data.Default | ||
4 | import Data.Foldable as F | ||
5 | import Data.List as L | ||
6 | import Data.Maybe | ||
7 | import Network.URI | ||
8 | import Test.Hspec | ||
9 | |||
10 | import Data.Torrent | ||
11 | import Network.BitTorrent.Tracker.List | ||
12 | import Network.BitTorrent.Tracker.RPC | ||
13 | |||
14 | |||
15 | uris :: [URI] | ||
16 | uris = fmap (fromJust . parseURI . renderURI) [1..10 :: Int] | ||
17 | where | ||
18 | renderURI n = "http://" ++ show n ++ ".org" | ||
19 | |||
20 | list :: TrackerList URI | ||
21 | list = fromJust $ trackerList def { tAnnounceList = Just [uris] } | ||
22 | |||
23 | spec :: Spec | ||
24 | spec = do | ||
25 | describe "TrackerList" $ do | ||
26 | it "trackerList is not empty" $ do | ||
27 | pending | ||
28 | |||
29 | it "shuffleTiers (may fail with very small probability)" $ do | ||
30 | list' <- shuffleTiers list | ||
31 | list' `shouldSatisfy` (/= list) | ||
32 | |||
33 | it "traverseAll" $ do | ||
34 | xs <- traverseAll (\ uri -> if uri == L.last uris | ||
35 | then throwIO (GenericException "") | ||
36 | else return uri { uriScheme = "udp://" }) list | ||
37 | let udps = F.sum $ fmap (fromEnum . ("udp://" ==) . uriScheme) xs | ||
38 | udps `shouldBe` pred (L.length uris) | ||
39 | |||
40 | it "traverseTiers" $ do | ||
41 | xs' <- traverseTiers (\ uri -> if uri == L.last uris then return uri | ||
42 | else throwIO (GenericException "")) list | ||
43 | |||
44 | return () | ||