diff options
author | Sam Truzjan <pxqr.sta@gmail.com> | 2013-11-30 11:10:38 +0400 |
---|---|---|
committer | Sam Truzjan <pxqr.sta@gmail.com> | 2013-11-30 11:10:38 +0400 |
commit | 7f54308b57615bc61c0727538af2b5a54366eadb (patch) | |
tree | 8d47fb66163a8e9d114f1debf5711eca45500055 /src/Network/BitTorrent/Tracker/RPC/UDP.hs | |
parent | d4ee859973b200d3f81ea56b2e40847ed8c93510 (diff) |
Redesign tracker subsustem
Diffstat (limited to 'src/Network/BitTorrent/Tracker/RPC/UDP.hs')
-rw-r--r-- | src/Network/BitTorrent/Tracker/RPC/UDP.hs | 344 |
1 files changed, 344 insertions, 0 deletions
diff --git a/src/Network/BitTorrent/Tracker/RPC/UDP.hs b/src/Network/BitTorrent/Tracker/RPC/UDP.hs new file mode 100644 index 00000000..beff6b4f --- /dev/null +++ b/src/Network/BitTorrent/Tracker/RPC/UDP.hs | |||
@@ -0,0 +1,344 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- This module implement low-level UDP tracker protocol. | ||
9 | -- For more info see: | ||
10 | -- <http://www.bittorrent.org/beps/bep_0015.html> | ||
11 | -- | ||
12 | {-# LANGUAGE RecordWildCards #-} | ||
13 | {-# LANGUAGE FlexibleInstances #-} | ||
14 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
15 | {-# LANGUAGE TypeFamilies #-} | ||
16 | module Network.BitTorrent.Tracker.RPC.UDP | ||
17 | ( UDPTracker | ||
18 | , connect | ||
19 | , announce | ||
20 | , scrape | ||
21 | , retransmission | ||
22 | |||
23 | -- * Debug | ||
24 | , putTracker | ||
25 | ) where | ||
26 | |||
27 | import Control.Applicative | ||
28 | import Control.Exception | ||
29 | import Control.Monad | ||
30 | import Data.ByteString (ByteString) | ||
31 | import Data.IORef | ||
32 | import Data.List as L | ||
33 | import Data.Map as M | ||
34 | import Data.Maybe | ||
35 | import Data.Monoid | ||
36 | import Data.Serialize | ||
37 | import Data.Text as T | ||
38 | import Data.Text.Encoding | ||
39 | import Data.Time | ||
40 | import Data.Word | ||
41 | import Text.Read (readMaybe) | ||
42 | import Network.Socket hiding (Connected) | ||
43 | import Network.Socket.ByteString as BS | ||
44 | import Network.URI | ||
45 | import System.Entropy | ||
46 | import System.Timeout | ||
47 | import Numeric | ||
48 | |||
49 | import Network.BitTorrent.Tracker.RPC.Message | ||
50 | |||
51 | {----------------------------------------------------------------------- | ||
52 | Tokens | ||
53 | -----------------------------------------------------------------------} | ||
54 | |||
55 | genToken :: IO Word64 | ||
56 | genToken = do | ||
57 | bs <- getEntropy 8 | ||
58 | either err return $ runGet getWord64be bs | ||
59 | where | ||
60 | err = error "genToken: impossible happen" | ||
61 | |||
62 | -- | Connection Id is used for entire tracker session. | ||
63 | newtype ConnectionId = ConnectionId Word64 | ||
64 | deriving (Eq, Serialize) | ||
65 | |||
66 | instance Show ConnectionId where | ||
67 | showsPrec _ (ConnectionId cid) = showString "0x" <> showHex cid | ||
68 | |||
69 | genConnectionId :: IO ConnectionId | ||
70 | genConnectionId = ConnectionId <$> genToken | ||
71 | |||
72 | initialConnectionId :: ConnectionId | ||
73 | initialConnectionId = ConnectionId 0x41727101980 | ||
74 | |||
75 | -- TODO rename | ||
76 | -- | Transaction Id is used within a UDP RPC. | ||
77 | newtype TransactionId = TransactionId Word32 | ||
78 | deriving (Eq, Serialize) | ||
79 | |||
80 | instance Show TransactionId where | ||
81 | showsPrec _ (TransactionId tid) = showString "0x" <> showHex tid | ||
82 | |||
83 | genTransactionId :: IO TransactionId | ||
84 | genTransactionId = (TransactionId . fromIntegral) <$> genToken | ||
85 | |||
86 | {----------------------------------------------------------------------- | ||
87 | Transactions | ||
88 | -----------------------------------------------------------------------} | ||
89 | |||
90 | data Request = Connect | ||
91 | | Announce AnnounceQuery | ||
92 | | Scrape ScrapeQuery | ||
93 | deriving Show | ||
94 | |||
95 | data Response = Connected ConnectionId | ||
96 | | Announced AnnounceInfo | ||
97 | | Scraped [ScrapeInfo] | ||
98 | | Failed Text | ||
99 | deriving Show | ||
100 | |||
101 | data family Transaction a | ||
102 | data instance Transaction Request = TransactionQ | ||
103 | { connIdQ :: {-# UNPACK #-} !ConnectionId | ||
104 | , transIdQ :: {-# UNPACK #-} !TransactionId | ||
105 | , request :: !Request | ||
106 | } deriving Show | ||
107 | data instance Transaction Response = TransactionR | ||
108 | { transIdR :: {-# UNPACK #-} !TransactionId | ||
109 | , response :: !Response | ||
110 | } deriving Show | ||
111 | |||
112 | -- TODO newtype | ||
113 | newtype MessageId = MessageId Word32 | ||
114 | deriving (Show, Eq, Num, Serialize) | ||
115 | |||
116 | connectId, announceId, scrapeId, errorId :: MessageId | ||
117 | connectId = 0 | ||
118 | announceId = 1 | ||
119 | scrapeId = 2 | ||
120 | errorId = 3 | ||
121 | |||
122 | instance Serialize (Transaction Request) where | ||
123 | put TransactionQ {..} = do | ||
124 | case request of | ||
125 | Connect -> do | ||
126 | put initialConnectionId | ||
127 | put connectId | ||
128 | put transIdQ | ||
129 | |||
130 | Announce ann -> do | ||
131 | put connIdQ | ||
132 | put announceId | ||
133 | put transIdQ | ||
134 | put ann | ||
135 | |||
136 | Scrape hashes -> do | ||
137 | put connIdQ | ||
138 | put scrapeId | ||
139 | put transIdQ | ||
140 | forM_ hashes put | ||
141 | |||
142 | get = do | ||
143 | cid <- get | ||
144 | mid <- get | ||
145 | TransactionQ cid <$> get <*> getBody mid | ||
146 | where | ||
147 | getBody :: MessageId -> Get Request | ||
148 | getBody msgId | ||
149 | | msgId == connectId = pure Connect | ||
150 | | msgId == announceId = Announce <$> get | ||
151 | | msgId == scrapeId = Scrape <$> many get | ||
152 | | otherwise = fail errMsg | ||
153 | where | ||
154 | errMsg = "unknown request: " ++ show msgId | ||
155 | |||
156 | instance Serialize (Transaction Response) where | ||
157 | put TransactionR {..} = do | ||
158 | case response of | ||
159 | Connected conn -> do | ||
160 | put connectId | ||
161 | put transIdR | ||
162 | put conn | ||
163 | |||
164 | Announced info -> do | ||
165 | put announceId | ||
166 | put transIdR | ||
167 | put info | ||
168 | |||
169 | Scraped infos -> do | ||
170 | put scrapeId | ||
171 | put transIdR | ||
172 | forM_ infos put | ||
173 | |||
174 | Failed info -> do | ||
175 | put errorId | ||
176 | put transIdR | ||
177 | put (encodeUtf8 info) | ||
178 | |||
179 | |||
180 | get = do | ||
181 | mid <- get | ||
182 | TransactionR <$> get <*> getBody mid | ||
183 | where | ||
184 | getBody :: MessageId -> Get Response | ||
185 | getBody msgId | ||
186 | | msgId == connectId = Connected <$> get | ||
187 | | msgId == announceId = Announced <$> get | ||
188 | | msgId == scrapeId = Scraped <$> many get | ||
189 | | msgId == errorId = (Failed . decodeUtf8) <$> get | ||
190 | | otherwise = fail msg | ||
191 | where | ||
192 | msg = "unknown response: " ++ show msgId | ||
193 | |||
194 | {----------------------------------------------------------------------- | ||
195 | Connection | ||
196 | -----------------------------------------------------------------------} | ||
197 | |||
198 | connectionLifetime :: NominalDiffTime | ||
199 | connectionLifetime = 60 | ||
200 | |||
201 | connectionLifetimeServer :: NominalDiffTime | ||
202 | connectionLifetimeServer = 120 | ||
203 | |||
204 | data Connection = Connection | ||
205 | { connectionId :: ConnectionId | ||
206 | , connectionTimestamp :: UTCTime | ||
207 | } deriving Show | ||
208 | |||
209 | initialConnection :: IO Connection | ||
210 | initialConnection = Connection initialConnectionId <$> getCurrentTime | ||
211 | |||
212 | isExpired :: Connection -> IO Bool | ||
213 | isExpired Connection {..} = do | ||
214 | currentTime <- getCurrentTime | ||
215 | let timeDiff = diffUTCTime currentTime connectionTimestamp | ||
216 | return $ timeDiff > connectionLifetime | ||
217 | |||
218 | {----------------------------------------------------------------------- | ||
219 | RPC | ||
220 | -----------------------------------------------------------------------} | ||
221 | |||
222 | maxPacketSize :: Int | ||
223 | maxPacketSize = 98 -- announce request packet | ||
224 | |||
225 | setPort :: PortNumber -> SockAddr -> SockAddr | ||
226 | setPort p (SockAddrInet _ h) = SockAddrInet p h | ||
227 | setPort p (SockAddrInet6 _ f h s) = SockAddrInet6 p f h s | ||
228 | setPort _ addr = addr | ||
229 | |||
230 | getTrackerAddr :: URI -> IO SockAddr | ||
231 | getTrackerAddr URI { uriAuthority = Just (URIAuth {..}) } = do | ||
232 | infos <- getAddrInfo Nothing (Just uriRegName) Nothing | ||
233 | let port = fromMaybe 0 (readMaybe (L.drop 1 uriPort) :: Maybe Int) | ||
234 | case infos of | ||
235 | AddrInfo {..} : _ -> return $ setPort (fromIntegral port) addrAddress | ||
236 | _ -> fail "getTrackerAddr: unable to lookup host addr" | ||
237 | getTrackerAddr _ = fail "getTrackerAddr: hostname unknown" | ||
238 | |||
239 | call :: SockAddr -> ByteString -> IO ByteString | ||
240 | call addr arg = bracket open close rpc | ||
241 | where | ||
242 | open = socket AF_INET Datagram defaultProtocol | ||
243 | rpc sock = do | ||
244 | BS.sendAllTo sock arg addr | ||
245 | (res, addr') <- BS.recvFrom sock maxPacketSize | ||
246 | unless (addr' == addr) $ do | ||
247 | throwIO $ userError "address mismatch" | ||
248 | return res | ||
249 | |||
250 | -- TODO retransmissions | ||
251 | -- TODO blocking | ||
252 | data UDPTracker = UDPTracker | ||
253 | { trackerURI :: URI | ||
254 | , trackerConnection :: IORef Connection | ||
255 | } | ||
256 | |||
257 | updateConnection :: ConnectionId -> UDPTracker -> IO () | ||
258 | updateConnection cid UDPTracker {..} = do | ||
259 | newConnection <- Connection cid <$> getCurrentTime | ||
260 | writeIORef trackerConnection newConnection | ||
261 | |||
262 | getConnectionId :: UDPTracker -> IO ConnectionId | ||
263 | getConnectionId UDPTracker {..} | ||
264 | = connectionId <$> readIORef trackerConnection | ||
265 | |||
266 | putTracker :: UDPTracker -> IO () | ||
267 | putTracker UDPTracker {..} = do | ||
268 | print trackerURI | ||
269 | print =<< readIORef trackerConnection | ||
270 | |||
271 | transaction :: UDPTracker -> Request -> IO Response | ||
272 | transaction tracker @ UDPTracker {..} request = do | ||
273 | cid <- getConnectionId tracker | ||
274 | tid <- genTransactionId | ||
275 | let trans = TransactionQ cid tid request | ||
276 | |||
277 | addr <- getTrackerAddr trackerURI | ||
278 | res <- call addr (encode trans) | ||
279 | case decode res of | ||
280 | Right (TransactionR {..}) | ||
281 | | tid == transIdR -> return response | ||
282 | | otherwise -> throwIO $ userError "transaction id mismatch" | ||
283 | Left msg -> throwIO $ userError msg | ||
284 | |||
285 | connectUDP :: UDPTracker -> IO ConnectionId | ||
286 | connectUDP tracker = do | ||
287 | resp <- transaction tracker Connect | ||
288 | case resp of | ||
289 | Connected cid -> return cid | ||
290 | Failed msg -> throwIO $ userError $ T.unpack msg | ||
291 | _ -> throwIO $ userError "message type mismatch" | ||
292 | |||
293 | initialTracker :: URI -> IO UDPTracker | ||
294 | initialTracker uri = do | ||
295 | tracker <- UDPTracker uri <$> (newIORef =<< initialConnection) | ||
296 | connId <- connectUDP tracker | ||
297 | updateConnection connId tracker | ||
298 | return tracker | ||
299 | |||
300 | freshConnection :: UDPTracker -> IO () | ||
301 | freshConnection tracker @ UDPTracker {..} = do | ||
302 | conn <- readIORef trackerConnection | ||
303 | expired <- isExpired conn | ||
304 | when expired $ do | ||
305 | connId <- connectUDP tracker | ||
306 | updateConnection connId tracker | ||
307 | |||
308 | announce :: UDPTracker -> AnnounceQuery -> IO AnnounceInfo | ||
309 | announce tracker ann = do | ||
310 | freshConnection tracker | ||
311 | resp <- transaction tracker (Announce ann) | ||
312 | case resp of | ||
313 | Announced info -> return info | ||
314 | _ -> fail "announce: response type mismatch" | ||
315 | |||
316 | scrape :: UDPTracker -> ScrapeQuery -> IO Scrape | ||
317 | scrape tracker scr = do | ||
318 | freshConnection tracker | ||
319 | resp <- transaction tracker (Scrape scr) | ||
320 | case resp of | ||
321 | Scraped info -> return $ M.fromList $ L.zip scr info | ||
322 | _ -> fail "scrape: response type mismatch" | ||
323 | |||
324 | {----------------------------------------------------------------------- | ||
325 | Retransmission | ||
326 | -----------------------------------------------------------------------} | ||
327 | |||
328 | sec :: Int | ||
329 | sec = 1000000 | ||
330 | |||
331 | minTimeout :: Int | ||
332 | minTimeout = 15 * sec | ||
333 | |||
334 | maxTimeout :: Int | ||
335 | maxTimeout = 15 * 2 ^ (8 :: Int) * sec | ||
336 | |||
337 | retransmission :: IO a -> IO a | ||
338 | retransmission action = go minTimeout | ||
339 | where | ||
340 | go curTimeout | ||
341 | | maxTimeout < curTimeout = throwIO $ userError "tracker down" | ||
342 | | otherwise = do | ||
343 | r <- timeout curTimeout action | ||
344 | maybe (go (2 * curTimeout)) return r | ||