summaryrefslogtreecommitdiff
path: root/src/Network/BitTorrent/Tracker/RPC
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
parentd4ee859973b200d3f81ea56b2e40847ed8c93510 (diff)
Redesign tracker subsustem
Diffstat (limited to 'src/Network/BitTorrent/Tracker/RPC')
-rw-r--r--src/Network/BitTorrent/Tracker/RPC/HTTP.hs106
-rw-r--r--src/Network/BitTorrent/Tracker/RPC/Message.hs539
-rw-r--r--src/Network/BitTorrent/Tracker/RPC/UDP.hs344
3 files changed, 989 insertions, 0 deletions
diff --git a/src/Network/BitTorrent/Tracker/RPC/HTTP.hs b/src/Network/BitTorrent/Tracker/RPC/HTTP.hs
new file mode 100644
index 00000000..0eef2b7e
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker/RPC/HTTP.hs
@@ -0,0 +1,106 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : non-portable
7--
8-- The tracker is an HTTP/HTTPS service used to discovery peers for
9-- a particular existing torrent and keep statistics about the
10-- swarm. This module also provides a way to easily request scrape
11-- info for a particular torrent list.
12--
13-- For more information see:
14-- <https://wiki.theory.org/BitTorrentSpecification#Tracker_HTTP.2FHTTPS_Protocol>
15--
16module Network.BitTorrent.Tracker.RPC.HTTP
17 ( Connection
18 , connect
19 , announce
20 , scrape
21 ) where
22
23import Control.Applicative
24import Control.Exception
25import Data.BEncode as BE
26import Data.ByteString as B
27import Data.ByteString.Char8 as BC
28import Data.ByteString.Lazy as BL
29import Data.List as L
30import Data.Map as M
31import Data.Monoid
32import Network.URI
33import Network.HTTP.Conduit
34
35import Data.Torrent.InfoHash
36import Network.BitTorrent.Tracker.RPC.Message
37
38
39data Connection = Connection
40 { announceURI :: URI
41 } deriving Show
42
43connect :: URI -> IO Connection
44connect = return . Connection
45
46-- | Send request and receive response from the tracker specified in
47-- announce list. This function throws 'IOException' if it couldn't
48-- send request or receive response or decode response.
49--
50announce :: AnnounceQuery -> Connection -> IO (Result AnnounceInfo)
51announce req = do
52 let uri = undefined
53 resp <- BL.toStrict <$> simpleHttp uri
54 return $ BE.decode resp
55
56scrape :: ScrapeQuery -> Connection -> IO (Result Scrape)
57scrape = undefined
58
59{-
60-- | Trying to convert /announce/ URL to /scrape/ URL. If 'scrapeURL'
61-- gives 'Nothing' then tracker do not support scraping. The info hash
62-- list is used to restrict the tracker's report to that particular
63-- torrents. Note that scrapping of multiple torrents may not be
64-- supported. (Even if scrapping convention is supported)
65--
66scrapeURL :: URI -> [InfoHash] -> Maybe URI
67scrapeURL uri ihs = do
68 newPath <- replace (BC.pack (uriPath uri))
69 let newURI = uri { uriPath = BC.unpack newPath }
70 return (L.foldl addHashToURI newURI ihs)
71 where
72 replace :: ByteString -> Maybe ByteString
73 replace p
74 | ps <- BC.splitWith (== '/') p
75 , "announce" `B.isPrefixOf` L.last ps
76 = let newSuff = "scrape" <> B.drop (B.length "announce") (L.last ps)
77 in Just (B.intercalate "/" (L.init ps ++ [newSuff]))
78 | otherwise = Nothing
79
80
81-- | For each 'InfoHash' of torrents request scrape info from the tracker.
82-- However if the info hash list is 'null', the tracker should list
83-- all available torrents.
84-- Note that the 'URI' should be /announce/ URI, not /scrape/ URI.
85--
86scrapeHTTP :: HTTPTracker -- ^ Announce 'URI'.
87 -> [InfoHash] -- ^ Torrents to be scrapped.
88 -> IO Scrape -- ^ 'ScrapeInfo' for each torrent.
89scrapeHTTP HTTPTracker {..} ihs
90 | Just uri <- scrapeURL announceURI ihs = do
91 rawResp <- simpleHTTP (Request uri GET [] "")
92 respBody <- getResponseBody rawResp
93 case decode (BC.pack respBody) of
94 Left e -> throwIO $ userError $ e ++ " in scrape response"
95 Right r -> return r
96
97 | otherwise = throwIO $ userError "Tracker do not support scraping"
98
99-- | More particular version of 'scrape', just for one torrent.
100--
101scrapeOne :: Tracker t => t -> InfoHash -> IO ScrapeInfo
102scrapeOne uri ih = scrape uri [ih] >>= maybe err return . M.lookup ih
103 where
104 err = throwIO $ userError "unable to find info hash in response dict"
105
106-} \ No newline at end of file
diff --git a/src/Network/BitTorrent/Tracker/RPC/Message.hs b/src/Network/BitTorrent/Tracker/RPC/Message.hs
new file mode 100644
index 00000000..18c1a4c7
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker/RPC/Message.hs
@@ -0,0 +1,539 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Every tracker should support announce query. This query is used
9-- to discover peers within a swarm and have two-fold effect:
10--
11-- * peer doing announce discover other peers using peer list from
12-- the response to the announce query.
13--
14-- * tracker store peer information and use it in the succeeding
15-- requests made by other peers, until the peer info expires.
16--
17-- By convention most trackers support another form of request —
18-- scrape query — which queries the state of a given torrent (or
19-- a list of torrents) that the tracker is managing.
20--
21{-# LANGUAGE FlexibleInstances #-}
22{-# LANGUAGE GeneralizedNewtypeDeriving #-}
23{-# LANGUAGE TemplateHaskell #-}
24{-# LANGUAGE DeriveDataTypeable #-}
25{-# OPTIONS -fno-warn-orphans #-}
26module Network.BitTorrent.Tracker.RPC.Message
27 ( -- * Announce
28 -- ** Request
29 Event(..)
30 , AnnounceQuery(..)
31 , renderAnnounceQuery
32 , ParamParseFailure
33 , parseAnnounceQuery
34
35 -- ** Response
36 , PeerList (..)
37 , AnnounceInfo(..)
38 , defaultNumWant
39 , parseFailureStatus
40
41 -- * Scrape
42 , ScrapeQuery
43 , ScrapeInfo(..)
44 , Scrape
45 )
46 where
47
48import Control.Applicative
49import Control.Monad
50import Data.Aeson (ToJSON(..), FromJSON(..))
51import Data.Aeson.TH
52import Data.BEncode as BE hiding (Result)
53import Data.BEncode.BDict as BE
54import Data.ByteString as BS
55import Data.ByteString.Char8 as BC
56import Data.Char as Char
57import Data.Convertible
58import Data.List as L
59import Data.Map as M
60import Data.Maybe
61import Data.Serialize as S hiding (Result)
62import Data.Text (Text)
63import Data.Text.Encoding
64import Data.Typeable
65import Data.Word
66import Network
67import Network.HTTP.Types.QueryLike
68import Network.HTTP.Types.URI hiding (urlEncode)
69import Network.HTTP.Types.Status
70import Network.Socket
71import Text.Read (readMaybe)
72
73import Data.Torrent.InfoHash
74import Data.Torrent.Progress
75import Network.BitTorrent.Core.PeerId
76import Network.BitTorrent.Core.PeerAddr
77
78
79{-----------------------------------------------------------------------
80-- Events
81-----------------------------------------------------------------------}
82
83-- | Events used to specify which kind of announce query is performed.
84data Event = Started
85 -- ^ For the first request: when a peer join the swarm.
86 | Stopped
87 -- ^ Sent when the peer is shutting down.
88 | Completed
89 -- ^ To be sent when the peer completes a download.
90 deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable)
91
92$(deriveJSON (L.map toLower . L.dropWhile isLower) ''Event)
93
94-- | HTTP tracker protocol compatible encoding.
95instance QueryValueLike Event where
96 toQueryValue e = toQueryValue (Char.toLower x : xs)
97 where
98 (x : xs) = show e -- INVARIANT: this is always nonempty list
99
100type EventId = Word32
101
102-- | UDP tracker encoding event codes.
103eventId :: Event -> EventId
104eventId Completed = 1
105eventId Started = 2
106eventId Stopped = 3
107
108-- TODO add Regular event
109putEvent :: Putter (Maybe Event)
110putEvent Nothing = putWord32be 0
111putEvent (Just e) = putWord32be (eventId e)
112
113getEvent :: S.Get (Maybe Event)
114getEvent = do
115 eid <- getWord32be
116 case eid of
117 0 -> return Nothing
118 1 -> return $ Just Completed
119 2 -> return $ Just Started
120 3 -> return $ Just Stopped
121 _ -> fail "unknown event id"
122
123{-----------------------------------------------------------------------
124 Announce query
125-----------------------------------------------------------------------}
126
127-- | A tracker request is HTTP GET request; used to include metrics
128-- from clients that help the tracker keep overall statistics about
129-- the torrent. The most important, requests are used by the tracker
130-- to keep track lists of active peer for a particular torrent.
131--
132data AnnounceQuery = AnnounceQuery
133 {
134 -- | Hash of info part of the torrent usually obtained from
135 -- 'Torrent' or 'Magnet'.
136 reqInfoHash :: !InfoHash
137
138 -- | ID of the peer doing request.
139 , reqPeerId :: !PeerId
140
141 -- | Port to listen to for connections from other
142 -- peers. Tracker should respond with this port when
143 -- some /other/ peer request the tracker with the same info hash.
144 -- Normally, this port is choosed from 'defaultPorts'.
145 , reqPort :: !PortNumber
146
147 -- | Current progress of peer doing request.
148 , reqProgress :: !Progress
149
150 -- | The peer IP. Needed only when client communicated with
151 -- tracker throught a proxy.
152 , reqIP :: Maybe HostAddress
153
154 -- | Number of peers that the peers wants to receive from. See
155 -- note for 'defaultNumWant'.
156 , reqNumWant :: Maybe Int
157
158 -- | If not specified, the request is regular periodic request.
159 , reqEvent :: Maybe Event
160 } deriving (Show, Eq, Typeable)
161
162$(deriveJSON (L.map toLower . L.dropWhile isLower) ''AnnounceQuery)
163
164-- | UDP tracker protocol compatible encoding.
165instance Serialize AnnounceQuery where
166 put AnnounceQuery {..} = do
167 put reqInfoHash
168 put reqPeerId
169 put reqProgress
170 putEvent reqEvent
171 putWord32be $ fromMaybe 0 reqIP
172 putWord32be $ 0 -- TODO what the fuck is "key"?
173 putWord32be $ fromIntegral $ fromMaybe (-1) reqNumWant
174
175 put reqPort
176
177 get = do
178 ih <- get
179 pid <- get
180
181 progress <- get
182
183 ev <- getEvent
184 ip <- getWord32be
185-- key <- getWord32be -- TODO
186 want <- getWord32be
187
188 port <- get
189
190 return $ AnnounceQuery {
191 reqInfoHash = ih
192 , reqPeerId = pid
193 , reqPort = port
194 , reqProgress = progress
195 , reqIP = if ip == 0 then Nothing else Just ip
196 , reqNumWant = if want == -1 then Nothing
197 else Just (fromIntegral want)
198 , reqEvent = ev
199 }
200
201instance QueryValueLike PortNumber where
202 toQueryValue = toQueryValue . show . fromEnum
203
204instance QueryValueLike Word32 where
205 toQueryValue = toQueryValue . show
206
207instance QueryValueLike Int where
208 toQueryValue = toQueryValue . show
209
210-- | HTTP tracker protocol compatible encoding.
211instance QueryLike AnnounceQuery where
212 toQuery AnnounceQuery {..} =
213 toQuery reqProgress ++
214 [ ("info_hash", toQueryValue reqInfoHash)
215 , ("peer_id" , toQueryValue reqPeerId)
216 , ("port" , toQueryValue reqPort)
217 , ("ip" , toQueryValue reqIP)
218 , ("numwant" , toQueryValue reqNumWant)
219 , ("event" , toQueryValue reqEvent)
220 ]
221
222--renderAnnounceQueryBuilder :: AnnounceQuery -> BS.Builder
223--renderAnnounceQueryBuilder = undefined
224
225-- | Encode announce query and add it to the base tracker URL.
226renderAnnounceQuery :: AnnounceQuery -> SimpleQuery
227renderAnnounceQuery = filterMaybes . toQuery
228 where
229 filterMaybes :: [(a, Maybe b)] -> [(a, b)]
230 filterMaybes = catMaybes . L.map f
231 where
232 f (_, Nothing) = Nothing
233 f (a, Just b ) = Just (a, b)
234
235data QueryParam
236 = ParamInfoHash
237 | ParamPeerId
238 | ParamPort
239 | ParamUploaded
240 | ParamLeft
241 | ParamDownloaded
242 | ParamIP
243 | ParamNumWant
244 | ParamEvent
245 deriving (Show, Eq, Ord, Enum)
246
247paramName :: QueryParam -> BS.ByteString
248paramName ParamInfoHash = "info_hash"
249paramName ParamPeerId = "peer_id"
250paramName ParamPort = "port"
251paramName ParamUploaded = "uploaded"
252paramName ParamLeft = "left"
253paramName ParamDownloaded = "downloaded"
254paramName ParamIP = "ip"
255paramName ParamNumWant = "numwant"
256paramName ParamEvent = "event"
257
258class FromParam a where
259 fromParam :: BS.ByteString -> Maybe a
260
261instance FromParam InfoHash where
262 fromParam = either (const Nothing) pure . safeConvert
263
264instance FromParam PeerId where
265 fromParam = either (const Nothing) pure . safeConvert
266
267instance FromParam Word32 where
268 fromParam = readMaybe . BC.unpack
269
270instance FromParam Word64 where
271 fromParam = readMaybe . BC.unpack
272
273instance FromParam Int where
274 fromParam = readMaybe . BC.unpack
275
276instance FromParam PortNumber where
277 fromParam bs = fromIntegral <$> (fromParam bs :: Maybe Word32)
278
279instance FromParam Event where
280 fromParam bs = do
281 (x, xs) <- BC.uncons bs
282 readMaybe $ BC.unpack $ BC.cons (Char.toUpper x) xs
283
284data ParamParseFailure
285 = Missing QueryParam -- ^ param not found in query string;
286 | Invalid QueryParam BS.ByteString -- ^ param present but not valid.
287 deriving (Show, Eq)
288
289type Result = Either ParamParseFailure
290
291withError :: ParamParseFailure -> Maybe a -> Result a
292withError e = maybe (Left e) Right
293
294reqParam :: FromParam a => QueryParam -> SimpleQuery -> Result a
295reqParam param xs = do
296 val <- withError (Missing param) $ L.lookup (paramName param) xs
297 withError (Invalid param val) (fromParam val)
298
299optParam :: FromParam a => QueryParam -> SimpleQuery -> Result (Maybe a)
300optParam param ps
301 | Just x <- L.lookup (paramName param) ps
302 = pure <$> withError (Invalid param x) (fromParam x)
303 | otherwise = pure Nothing
304
305parseProgress :: SimpleQuery -> Result Progress
306parseProgress params = Progress
307 <$> reqParam ParamDownloaded params
308 <*> reqParam ParamLeft params
309 <*> reqParam ParamUploaded params
310
311-- | Parse announce request from a query string.
312parseAnnounceQuery :: SimpleQuery -> Either ParamParseFailure AnnounceQuery
313parseAnnounceQuery params = AnnounceQuery
314 <$> reqParam ParamInfoHash params
315 <*> reqParam ParamPeerId params
316 <*> reqParam ParamPort params
317 <*> parseProgress params
318 <*> optParam ParamIP params
319 <*> optParam ParamNumWant params
320 <*> optParam ParamEvent params
321
322-- TODO add extension datatype
323--type AnnounceRequest = ()
324
325{-----------------------------------------------------------------------
326-- Announce response
327-----------------------------------------------------------------------}
328
329-- | Tracker can return peer list in either compact(BEP23) or not
330-- compact form.
331--
332-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
333--
334data PeerList
335 = PeerList { getPeerList :: [PeerAddr] }
336 | CompactPeerList { getPeerList :: [PeerAddr] }
337 deriving (Show, Eq, Typeable)
338
339instance ToJSON PeerList where
340 toJSON = toJSON . getPeerList
341
342instance FromJSON PeerList where
343 parseJSON v = PeerList <$> parseJSON v
344
345putCompactPeerList :: S.Putter [PeerAddr]
346putCompactPeerList = mapM_ put
347
348getCompactPeerList :: S.Get [PeerAddr]
349getCompactPeerList = many get
350
351instance BEncode PeerList where
352 toBEncode (PeerList xs) = toBEncode xs
353 toBEncode (CompactPeerList xs) = toBEncode $ runPut (putCompactPeerList xs)
354
355 fromBEncode (BList l ) = PeerList <$> fromBEncode (BList l)
356 fromBEncode (BString s ) = CompactPeerList <$> runGet getCompactPeerList s
357 fromBEncode _ = decodingError "Peer list"
358
359-- | The tracker response includes a peer list that helps the client
360-- participate in the torrent. The most important is 'respPeer' list
361-- used to join the swarm.
362--
363data AnnounceInfo =
364 Failure !Text -- ^ Failure reason in human readable form.
365 | AnnounceInfo {
366 -- | Number of peers completed the torrent. (seeders)
367 respComplete :: !(Maybe Int)
368
369 -- | Number of peers downloading the torrent. (leechers)
370 , respIncomplete :: !(Maybe Int)
371
372 -- | Recommended interval to wait between requests, in seconds.
373 , respInterval :: !Int
374
375 -- | Minimal amount of time between requests, in seconds. A
376 -- peer /should/ make timeout with at least 'respMinInterval'
377 -- value, otherwise tracker might not respond. If not specified
378 -- the same applies to 'respInterval'.
379 , respMinInterval :: !(Maybe Int)
380
381 -- | Peers that must be contacted.
382 , respPeers :: !PeerList
383
384 -- | Human readable warning.
385 , respWarning :: !(Maybe Text)
386 } deriving (Show, Typeable)
387
388$(deriveJSON (L.map toLower . L.dropWhile isLower) ''AnnounceInfo)
389
390-- | HTTP tracker protocol compatible encoding.
391instance BEncode AnnounceInfo where
392 toBEncode (Failure t) = toDict $
393 "failure reason" .=! t
394 .: endDict
395
396 toBEncode AnnounceInfo {..} = toDict $
397 "complete" .=? respComplete
398 .: "incomplete" .=? respIncomplete
399 .: "interval" .=! respInterval
400 .: "min interval" .=? respMinInterval
401 .: "peers" .=! respPeers
402 .: "warning message" .=? respWarning
403 .: endDict
404
405 fromBEncode (BDict d)
406 | Just t <- BE.lookup "failure reason" d = Failure <$> fromBEncode t
407 | otherwise = (`fromDict` (BDict d)) $ do
408 AnnounceInfo
409 <$>? "complete"
410 <*>? "incomplete"
411 <*>! "interval"
412 <*>? "min interval"
413 <*>! "peers"
414 <*>? "warning message"
415 fromBEncode _ = decodingError "Announce info"
416
417-- | UDP tracker protocol compatible encoding.
418instance Serialize AnnounceInfo where
419 put (Failure msg) = put $ encodeUtf8 msg
420 put AnnounceInfo {..} = do
421 putWord32be $ fromIntegral respInterval
422 putWord32be $ fromIntegral $ fromMaybe 0 respIncomplete
423 putWord32be $ fromIntegral $ fromMaybe 0 respComplete
424 forM_ (getPeerList respPeers) put
425
426 get = do
427 interval <- getWord32be
428 leechers <- getWord32be
429 seeders <- getWord32be
430 peers <- many get
431
432 return $ AnnounceInfo {
433 respWarning = Nothing
434 , respInterval = fromIntegral interval
435 , respMinInterval = Nothing
436 , respIncomplete = Just $ fromIntegral leechers
437 , respComplete = Just $ fromIntegral seeders
438 , respPeers = PeerList peers
439 }
440
441-- | Above 25, new peers are highly unlikely to increase download
442-- speed. Even 30 peers is /plenty/, the official client version 3
443-- in fact only actively forms new connections if it has less than
444-- 30 peers and will refuse connections if it has 55.
445--
446-- <https://wiki.theory.org/BitTorrent_Tracker_Protocol#Basic_Tracker_Announce_Request>
447--
448defaultNumWant :: Int
449defaultNumWant = 50
450
451missingOffset :: Int
452missingOffset = 101
453
454invalidOffset :: Int
455invalidOffset = 150
456
457-- | Get HTTP response error code from a announce params parse
458-- failure.
459--
460-- For more info see:
461-- <https://wiki.theory.org/BitTorrent_Tracker_Protocol#Response_Codes>
462--
463parseFailureCode :: ParamParseFailure -> Int
464parseFailureCode (Missing param ) = missingOffset + fromEnum param
465parseFailureCode (Invalid param _) = invalidOffset + fromEnum param
466
467-- | Human readable message
468parseFailureMessage :: ParamParseFailure -> BS.ByteString
469parseFailureMessage e = BS.concat $ case e of
470 Missing p -> ["Missing parameter: ", paramName p]
471 Invalid p v -> ["Invalid parameter: ", paramName p, " = ", v]
472
473parseFailureStatus :: ParamParseFailure -> Status
474parseFailureStatus = mkStatus <$> parseFailureCode <*> parseFailureMessage
475
476--type AnnounceResponse = Either Status AnnounceInfo -- TODO
477--type TrackerResponse = () -- TODO
478
479{-----------------------------------------------------------------------
480 Scrape message
481-----------------------------------------------------------------------}
482
483type ScrapeQuery = [InfoHash]
484
485-- | Overall information about particular torrent.
486data ScrapeInfo = ScrapeInfo {
487 -- | Number of seeders - peers with the entire file.
488 siComplete :: {-# UNPACK #-} !Int
489
490 -- | Total number of times the tracker has registered a completion.
491 , siDownloaded :: {-# UNPACK #-} !Int
492
493 -- | Number of leechers.
494 , siIncomplete :: {-# UNPACK #-} !Int
495
496 -- | Name of the torrent file, as specified by the "name"
497 -- file in the info section of the .torrent file.
498 , siName :: !(Maybe Text)
499 } deriving (Show, Eq, Typeable)
500
501$(deriveJSON (L.map toLower . L.dropWhile isLower) ''ScrapeInfo)
502
503-- TODO hash map
504-- | Scrape info about a set of torrents.
505type Scrape = Map InfoHash ScrapeInfo
506
507-- | HTTP tracker protocol compatible encoding.
508instance BEncode ScrapeInfo where
509 toBEncode ScrapeInfo {..} = toDict $
510 "complete" .=! siComplete
511 .: "downloaded" .=! siDownloaded
512 .: "incomplete" .=! siIncomplete
513 .: "name" .=? siName
514 .: endDict
515
516 fromBEncode = fromDict $ do
517 ScrapeInfo <$>! "complete"
518 <*>! "downloaded"
519 <*>! "incomplete"
520 <*>? "name"
521
522-- | UDP tracker protocol compatible encoding.
523instance Serialize ScrapeInfo where
524 put ScrapeInfo {..} = do
525 putWord32be $ fromIntegral siComplete
526 putWord32be $ fromIntegral siDownloaded
527 putWord32be $ fromIntegral siIncomplete
528
529 get = do
530 seeders <- getWord32be
531 downTimes <- getWord32be
532 leechers <- getWord32be
533
534 return $ ScrapeInfo {
535 siComplete = fromIntegral seeders
536 , siDownloaded = fromIntegral downTimes
537 , siIncomplete = fromIntegral leechers
538 , siName = Nothing
539 }
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