summaryrefslogtreecommitdiff
path: root/src/Network/BitTorrent/Tracker/RPC/UDP.hs
diff options
context:
space:
mode:
authorSam Truzjan <pxqr.sta@gmail.com>2013-11-30 11:10:38 +0400
committerSam Truzjan <pxqr.sta@gmail.com>2013-11-30 11:10:38 +0400
commit7f54308b57615bc61c0727538af2b5a54366eadb (patch)
tree8d47fb66163a8e9d114f1debf5711eca45500055 /src/Network/BitTorrent/Tracker/RPC/UDP.hs
parentd4ee859973b200d3f81ea56b2e40847ed8c93510 (diff)
Redesign tracker subsustem
Diffstat (limited to 'src/Network/BitTorrent/Tracker/RPC/UDP.hs')
-rw-r--r--src/Network/BitTorrent/Tracker/RPC/UDP.hs344
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 #-}
16module Network.BitTorrent.Tracker.RPC.UDP
17 ( UDPTracker
18 , connect
19 , announce
20 , scrape
21 , retransmission
22
23 -- * Debug
24 , putTracker
25 ) where
26
27import Control.Applicative
28import Control.Exception
29import Control.Monad
30import Data.ByteString (ByteString)
31import Data.IORef
32import Data.List as L
33import Data.Map as M
34import Data.Maybe
35import Data.Monoid
36import Data.Serialize
37import Data.Text as T
38import Data.Text.Encoding
39import Data.Time
40import Data.Word
41import Text.Read (readMaybe)
42import Network.Socket hiding (Connected)
43import Network.Socket.ByteString as BS
44import Network.URI
45import System.Entropy
46import System.Timeout
47import Numeric
48
49import Network.BitTorrent.Tracker.RPC.Message
50
51{-----------------------------------------------------------------------
52 Tokens
53-----------------------------------------------------------------------}
54
55genToken :: IO Word64
56genToken = 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.
63newtype ConnectionId = ConnectionId Word64
64 deriving (Eq, Serialize)
65
66instance Show ConnectionId where
67 showsPrec _ (ConnectionId cid) = showString "0x" <> showHex cid
68
69genConnectionId :: IO ConnectionId
70genConnectionId = ConnectionId <$> genToken
71
72initialConnectionId :: ConnectionId
73initialConnectionId = ConnectionId 0x41727101980
74
75-- TODO rename
76-- | Transaction Id is used within a UDP RPC.
77newtype TransactionId = TransactionId Word32
78 deriving (Eq, Serialize)
79
80instance Show TransactionId where
81 showsPrec _ (TransactionId tid) = showString "0x" <> showHex tid
82
83genTransactionId :: IO TransactionId
84genTransactionId = (TransactionId . fromIntegral) <$> genToken
85
86{-----------------------------------------------------------------------
87 Transactions
88-----------------------------------------------------------------------}
89
90data Request = Connect
91 | Announce AnnounceQuery
92 | Scrape ScrapeQuery
93 deriving Show
94
95data Response = Connected ConnectionId
96 | Announced AnnounceInfo
97 | Scraped [ScrapeInfo]
98 | Failed Text
99 deriving Show
100
101data family Transaction a
102data instance Transaction Request = TransactionQ
103 { connIdQ :: {-# UNPACK #-} !ConnectionId
104 , transIdQ :: {-# UNPACK #-} !TransactionId
105 , request :: !Request
106 } deriving Show
107data instance Transaction Response = TransactionR
108 { transIdR :: {-# UNPACK #-} !TransactionId
109 , response :: !Response
110 } deriving Show
111
112-- TODO newtype
113newtype MessageId = MessageId Word32
114 deriving (Show, Eq, Num, Serialize)
115
116connectId, announceId, scrapeId, errorId :: MessageId
117connectId = 0
118announceId = 1
119scrapeId = 2
120errorId = 3
121
122instance 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
156instance 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
198connectionLifetime :: NominalDiffTime
199connectionLifetime = 60
200
201connectionLifetimeServer :: NominalDiffTime
202connectionLifetimeServer = 120
203
204data Connection = Connection
205 { connectionId :: ConnectionId
206 , connectionTimestamp :: UTCTime
207 } deriving Show
208
209initialConnection :: IO Connection
210initialConnection = Connection initialConnectionId <$> getCurrentTime
211
212isExpired :: Connection -> IO Bool
213isExpired Connection {..} = do
214 currentTime <- getCurrentTime
215 let timeDiff = diffUTCTime currentTime connectionTimestamp
216 return $ timeDiff > connectionLifetime
217
218{-----------------------------------------------------------------------
219 RPC
220-----------------------------------------------------------------------}
221
222maxPacketSize :: Int
223maxPacketSize = 98 -- announce request packet
224
225setPort :: PortNumber -> SockAddr -> SockAddr
226setPort p (SockAddrInet _ h) = SockAddrInet p h
227setPort p (SockAddrInet6 _ f h s) = SockAddrInet6 p f h s
228setPort _ addr = addr
229
230getTrackerAddr :: URI -> IO SockAddr
231getTrackerAddr 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"
237getTrackerAddr _ = fail "getTrackerAddr: hostname unknown"
238
239call :: SockAddr -> ByteString -> IO ByteString
240call 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
252data UDPTracker = UDPTracker
253 { trackerURI :: URI
254 , trackerConnection :: IORef Connection
255 }
256
257updateConnection :: ConnectionId -> UDPTracker -> IO ()
258updateConnection cid UDPTracker {..} = do
259 newConnection <- Connection cid <$> getCurrentTime
260 writeIORef trackerConnection newConnection
261
262getConnectionId :: UDPTracker -> IO ConnectionId
263getConnectionId UDPTracker {..}
264 = connectionId <$> readIORef trackerConnection
265
266putTracker :: UDPTracker -> IO ()
267putTracker UDPTracker {..} = do
268 print trackerURI
269 print =<< readIORef trackerConnection
270
271transaction :: UDPTracker -> Request -> IO Response
272transaction 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
285connectUDP :: UDPTracker -> IO ConnectionId
286connectUDP 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
293initialTracker :: URI -> IO UDPTracker
294initialTracker uri = do
295 tracker <- UDPTracker uri <$> (newIORef =<< initialConnection)
296 connId <- connectUDP tracker
297 updateConnection connId tracker
298 return tracker
299
300freshConnection :: UDPTracker -> IO ()
301freshConnection tracker @ UDPTracker {..} = do
302 conn <- readIORef trackerConnection
303 expired <- isExpired conn
304 when expired $ do
305 connId <- connectUDP tracker
306 updateConnection connId tracker
307
308announce :: UDPTracker -> AnnounceQuery -> IO AnnounceInfo
309announce 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
316scrape :: UDPTracker -> ScrapeQuery -> IO Scrape
317scrape 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
328sec :: Int
329sec = 1000000
330
331minTimeout :: Int
332minTimeout = 15 * sec
333
334maxTimeout :: Int
335maxTimeout = 15 * 2 ^ (8 :: Int) * sec
336
337retransmission :: IO a -> IO a
338retransmission 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