diff options
Diffstat (limited to 'src/Network/BitTorrent')
-rw-r--r-- | src/Network/BitTorrent/Tracker.hs | 227 |
1 files changed, 25 insertions, 202 deletions
diff --git a/src/Network/BitTorrent/Tracker.hs b/src/Network/BitTorrent/Tracker.hs index 9c7590c4..6af76a16 100644 --- a/src/Network/BitTorrent/Tracker.hs +++ b/src/Network/BitTorrent/Tracker.hs | |||
@@ -11,207 +11,30 @@ | |||
11 | -- | 11 | -- |
12 | {-# LANGUAGE TemplateHaskell #-} | 12 | {-# LANGUAGE TemplateHaskell #-} |
13 | module Network.BitTorrent.Tracker | 13 | module Network.BitTorrent.Tracker |
14 | ( -- * Connection | 14 | ( PeerInfo (..) |
15 | TConnection(..) | 15 | |
16 | , tconnection | 16 | -- * RPC Manager |
17 | 17 | , Options | |
18 | -- * Session | 18 | , Manager |
19 | , TSession | 19 | , newManager |
20 | , tracker | 20 | , closeManager |
21 | 21 | , withManager | |
22 | -- * Re-export | 22 | |
23 | , defaultPorts | 23 | -- * Multitracker session |
24 | , ScrapeInfo | 24 | , trackerList |
25 | , Session | ||
26 | , newSession | ||
27 | , closeSession | ||
28 | |||
29 | -- * Events | ||
30 | , Event (..) | ||
31 | , notify | ||
32 | , askPeers | ||
33 | |||
34 | -- * Query | ||
35 | -- , getSessionState | ||
25 | ) where | 36 | ) where |
26 | 37 | ||
27 | import Control.Applicative | 38 | import Network.BitTorrent.Tracker.List |
28 | import Control.Concurrent | 39 | import Network.BitTorrent.Tracker.RPC |
29 | import Control.Concurrent.BoundedChan as BC | 40 | import Network.BitTorrent.Tracker.Session |
30 | import Control.Concurrent.STM | ||
31 | import Control.Exception | ||
32 | import Control.Monad | ||
33 | import Data.List as L | ||
34 | import Data.IORef | ||
35 | import Data.Text as T | ||
36 | import Network | ||
37 | import Network.URI | ||
38 | |||
39 | import Data.Torrent | ||
40 | import Network.BitTorrent.Peer | ||
41 | import Network.BitTorrent.Tracker.Protocol as Tracker | ||
42 | import Network.BitTorrent.Tracker.HTTP | ||
43 | import Network.BitTorrent.Tracker.UDP | ||
44 | |||
45 | {----------------------------------------------------------------------- | ||
46 | Tracker connection | ||
47 | -----------------------------------------------------------------------} | ||
48 | |||
49 | -- | 'TConnection' (shorthand for Tracker session) combines tracker | ||
50 | -- request fields neccessary for tracker, torrent and client | ||
51 | -- identification. | ||
52 | -- | ||
53 | -- This data is considered as static within one session. | ||
54 | -- | ||
55 | data TConnection = TConnection { | ||
56 | tconnAnnounce :: URI | ||
57 | -- ^ Announce URL. | ||
58 | , tconnInfoHash :: InfoHash | ||
59 | -- ^ Hash of info part of current .torrent file. | ||
60 | , tconnPeerId :: PeerId | ||
61 | -- ^ Client peer ID. | ||
62 | , tconnPort :: PortNumber | ||
63 | -- ^ The port number the client is listenning on. | ||
64 | } deriving Show | ||
65 | |||
66 | -- TODO tconnection :: SwarmSession -> TConnection | ||
67 | tconnection :: Torrent -> PeerId -> PortNumber -> TConnection | ||
68 | tconnection t = TConnection (tAnnounce t) (tInfoHash t) | ||
69 | |||
70 | -- | used to avoid boilerplate; do NOT export me | ||
71 | genericReq :: TConnection -> Progress -> AnnounceQuery | ||
72 | genericReq ses pr = AnnounceQuery { | ||
73 | reqInfoHash = tconnInfoHash ses | ||
74 | , reqPeerId = tconnPeerId ses | ||
75 | , reqPort = tconnPort ses | ||
76 | |||
77 | , reqProgress = pr | ||
78 | |||
79 | , reqIP = Nothing | ||
80 | , reqNumWant = Nothing | ||
81 | , reqEvent = Nothing | ||
82 | } | ||
83 | |||
84 | -- | The first request to the tracker that should be created is | ||
85 | -- 'startedReq'. It includes necessary 'Started' event field. | ||
86 | -- | ||
87 | startedReq :: TConnection -> Progress -> AnnounceQuery | ||
88 | startedReq ses pr = (genericReq ses pr) | ||
89 | { reqNumWant = Just defaultNumWant | ||
90 | , reqEvent = Just Started | ||
91 | } | ||
92 | |||
93 | -- | Regular request must be sent to keep track new peers and | ||
94 | -- notify tracker about current state of the client | ||
95 | -- so new peers could connect to the client. | ||
96 | -- | ||
97 | regularReq :: Int -> TConnection -> Progress -> AnnounceQuery | ||
98 | regularReq numWant ses pr = (genericReq ses pr) | ||
99 | { reqNumWant = Just numWant | ||
100 | , reqEvent = Nothing | ||
101 | } | ||
102 | |||
103 | -- | Must be sent to the tracker if the client is shutting down | ||
104 | -- gracefully. | ||
105 | -- | ||
106 | stoppedReq :: TConnection -> Progress -> AnnounceQuery | ||
107 | stoppedReq ses pr = (genericReq ses pr) | ||
108 | { reqNumWant = Nothing | ||
109 | , reqEvent = Just Stopped | ||
110 | } | ||
111 | |||
112 | -- | Must be sent to the tracker when the download completes. | ||
113 | -- However, must not be sent if the download was already 100% | ||
114 | -- complete. | ||
115 | -- | ||
116 | completedReq :: TConnection -> Progress -> AnnounceQuery | ||
117 | completedReq ses pr = (genericReq ses pr) | ||
118 | { reqNumWant = Nothing | ||
119 | , reqEvent = Just Completed | ||
120 | } | ||
121 | |||
122 | {----------------------------------------------------------------------- | ||
123 | Tracker session | ||
124 | -----------------------------------------------------------------------} | ||
125 | |||
126 | {- Why use BoundedChan? | ||
127 | |||
128 | Because most times we need just a list of peer at the start and all | ||
129 | the rest time we will take little by little. On the other hand tracker | ||
130 | will give us some constant count of peers and channel will grow with | ||
131 | time. To avoid space leaks and long lists of peers (which we don't | ||
132 | need) we use bounded chaan. | ||
133 | |||
134 | Chan size. | ||
135 | |||
136 | Should be at least (count_of_workers * 2) to accumulate long enough | ||
137 | peer list. | ||
138 | |||
139 | Order of peers in chan. | ||
140 | |||
141 | Old peers in head, new ones in tail. Old peers should be used in the | ||
142 | first place because by statistics they are most likely will present in | ||
143 | network a long time than a new. | ||
144 | |||
145 | -} | ||
146 | |||
147 | type TimeInterval = Int | ||
148 | |||
149 | waitInterval :: TSession -> IO () | ||
150 | waitInterval TSession {..} = do | ||
151 | delay <- readIORef seInterval | ||
152 | threadDelay (delay * sec) | ||
153 | where | ||
154 | sec = 1000 * 1000 :: Int | ||
155 | |||
156 | data TSession = TSession | ||
157 | { seConnection :: !TConnection | ||
158 | , seTracker :: !BitTracker | ||
159 | , seProgress :: !(TVar Progress) | ||
160 | , sePeers :: !(BoundedChan PeerAddr) | ||
161 | , seInterval :: {-# UNPACK #-} !(IORef TimeInterval) | ||
162 | } | ||
163 | |||
164 | openSession :: BoundedChan PeerAddr | ||
165 | -> TVar Progress | ||
166 | -> TConnection -> IO TSession | ||
167 | openSession chan progress conn @ TConnection {..} = do | ||
168 | trac <- Tracker.connect tconnAnnounce | ||
169 | pr <- readTVarIO progress | ||
170 | resp <- Tracker.announce trac $ startedReq conn pr | ||
171 | print resp | ||
172 | case resp of | ||
173 | Failure e -> throwIO $ userError $ T.unpack e | ||
174 | AnnounceInfo {..} -> do | ||
175 | -- TODO make use of rest AnnounceInfo fields | ||
176 | BC.writeList2Chan chan respPeers | ||
177 | TSession conn trac progress chan | ||
178 | <$> newIORef respInterval | ||
179 | |||
180 | closeSession :: TSession -> IO () | ||
181 | closeSession TSession {..} = do | ||
182 | pr <- readTVarIO seProgress | ||
183 | _ <- Tracker.announce seTracker (stoppedReq seConnection pr) | ||
184 | return () | ||
185 | |||
186 | withSession :: BoundedChan PeerAddr | ||
187 | -> TVar Progress | ||
188 | -> TConnection -> (TSession -> IO a) -> IO a | ||
189 | withSession chan prog conn | ||
190 | = bracket (openSession chan prog conn) closeSession | ||
191 | |||
192 | askPeers :: TSession -> IO () | ||
193 | askPeers se @ TSession {..} = forever $ do | ||
194 | waitInterval se | ||
195 | pr <- readTVarIO seProgress | ||
196 | resp <- tryJust isIOException $ do | ||
197 | let req = regularReq defaultNumWant seConnection pr | ||
198 | Tracker.announce seTracker req | ||
199 | print resp | ||
200 | case resp of | ||
201 | Left _ -> return () | ||
202 | Right (Failure e) -> throwIO $ userError $ T.unpack e | ||
203 | Right (AnnounceInfo {..}) -> do | ||
204 | writeIORef seInterval respInterval | ||
205 | |||
206 | -- we rely on the fact that union on lists is not | ||
207 | -- commutative: this implements the heuristic "old peers | ||
208 | -- in head" | ||
209 | old <- BC.getChanContents sePeers | ||
210 | let combined = L.union old respPeers | ||
211 | BC.writeList2Chan sePeers combined | ||
212 | where | ||
213 | isIOException :: IOException -> Maybe IOException | ||
214 | isIOException = return | ||
215 | |||
216 | tracker :: BoundedChan PeerAddr -> TVar Progress -> TConnection -> IO () | ||
217 | tracker chan prog conn = withSession chan prog conn askPeers \ No newline at end of file | ||