summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Network/BitTorrent/Tracker.hs271
-rw-r--r--src/Network/BitTorrent/Tracker/Protocol.hs204
2 files changed, 288 insertions, 187 deletions
diff --git a/src/Network/BitTorrent/Tracker.hs b/src/Network/BitTorrent/Tracker.hs
index 99ffc280..6ecf4fc2 100644
--- a/src/Network/BitTorrent/Tracker.hs
+++ b/src/Network/BitTorrent/Tracker.hs
@@ -1,4 +1,3 @@
1-- TODO: add "compact" field to TRequest
2-- | 1-- |
3-- Copyright : (c) Sam T. 2013 2-- Copyright : (c) Sam T. 2013
4-- License : MIT 3-- License : MIT
@@ -6,195 +5,57 @@
6-- Stability : experimental 5-- Stability : experimental
7-- Portability : non-portable 6-- Portability : non-portable
8-- 7--
9{-# OPTIONS -fno-warn-orphans #-} 8-- This module provides high level API for peer->tracker
10{-# LANGUAGE OverloadedStrings #-} 9-- communication.
11-- make higher level api 10--
12module Network.BitTorrent.Tracker 11module Network.BitTorrent.Tracker
13 ( module Network.BitTorrent.Tracker.Scrape 12 ( module Network.BitTorrent.Tracker.Scrape
14 , Progress(..), TSession(..)
15 , tsession, startProgress
16 13
17 -- * Requests 14 , withTracker, completedReq
18 , Event(..), TRequest(..) 15
19 , startedReq, regularReq, stoppedReq, completedReq 16 -- * Progress
17 , Progress(..), startProgress
20 18
21 -- * Responses 19 -- * Connection
22 , TResponse(..) 20 , TConnection(..), tconnection
23 , sendRequest
24 21
25 -- * Defaults 22 -- * Session
26 , defaultPorts, defaultNumWant 23 , TSession, getPeerList, getProgress, waitInterval
27 ) 24
28 where 25 -- * Re-export
26 , defaultPorts
27 ) where
29 28
30import Control.Applicative 29import Control.Applicative
31import Data.Char as Char 30import Control.Concurrent
32import Data.Word (Word32) 31import Control.Concurrent.STM
33import Data.List as L 32import Control.Exception
34import Data.Map as M 33import Control.Monad
35import Data.Monoid 34import Data.IORef
36import Data.BEncode
37import Data.ByteString as B
38import Data.ByteString.Char8 as BC
39import Data.Text as T
40import Data.Serialize.Get hiding (Result)
41import Data.URLEncoded as URL
42import Data.Torrent 35import Data.Torrent
43
44import Network 36import Network
45import Network.Socket
46import Network.HTTP
47import Network.URI 37import Network.URI
48 38
49import Network.BitTorrent.Peer 39import Network.BitTorrent.Peer
50import Network.BitTorrent.PeerID 40import Network.BitTorrent.PeerID
41import Network.BitTorrent.Tracker.Protocol
51import Network.BitTorrent.Tracker.Scrape 42import Network.BitTorrent.Tracker.Scrape
52 43
53 44
54data Event = Started -- ^ For first request. 45-- | 'TConnection' (shorthand for Tracker session) combines tracker request
55 | Stopped -- ^ Sent when the peer is shutting down.
56 | Completed -- ^ To be sent when the peer completes a download.
57 deriving (Show, Read, Eq, Ord, Enum, Bounded)
58
59data TRequest = TRequest { -- TODO peer here -- TODO detach announce
60 reqAnnounce :: URI -- ^ Announce url of the torrent.
61 , reqInfoHash :: InfoHash -- ^ Hash of info part of the torrent.
62 , reqPeerID :: PeerID -- ^ Id of the peer doing request. ()
63 , reqPort :: PortNumber -- ^ Port to listen to for connection from other peers.
64 , reqUploaded :: Integer -- ^ # of bytes that the peer has uploaded in the swarm.
65 , reqDownloaded :: Integer -- ^ # of bytes downloaded in the swarm by the peer.
66 , reqLeft :: Integer -- ^ # of bytes needed in order to complete download.
67 , reqIP :: Maybe HostAddress -- ^ The peer IP.
68 , reqNumWant :: Maybe Int -- ^ Number of peers that the peers wants to receive from.
69 , reqEvent :: Maybe Event -- ^ If not specified,
70 -- the request is regular periodic request.
71 } deriving Show
72
73data TResponse =
74 Failure Text -- ^ Failure reason in human readable form.
75 | OK {
76 respWarning :: Maybe Text
77 , respInterval :: Int -- ^ Recommended interval to wait between requests.
78 , respMinInterval :: Maybe Int -- ^ Minimal amount of time between requests.
79 , respComplete :: Maybe Int -- ^ Number of peers completed the torrent. (seeders)
80 , respIncomplete :: Maybe Int -- ^ Number of peers downloading the torrent.
81 , respPeers :: [Peer] -- ^ Peers that must be contacted.
82 } deriving Show
83
84instance BEncodable PortNumber where
85 toBEncode = toBEncode . fromEnum
86 fromBEncode b = toEnum <$> fromBEncode b
87
88instance BEncodable Peer where
89 toBEncode (Peer pid pip pport) = fromAssocs
90 [ "peer id" -->? pid
91 , "ip" --> pip
92 , "port" --> pport
93 ]
94
95 fromBEncode (BDict d) =
96 Peer <$> d >--? "peer id"
97 <*> d >-- "ip"
98 <*> d >-- "port"
99
100 fromBEncode _ = decodingError "Peer"
101
102instance BEncodable TResponse where
103 toBEncode (Failure t) = fromAssocs ["failure reason" --> t]
104 toBEncode resp@(OK {}) = fromAssocs
105 [ "interval" --> respInterval resp
106 , "min interval" -->? respMinInterval resp
107 , "complete" -->? respComplete resp
108 , "incomplete" -->? respIncomplete resp
109 , "peers" --> respPeers resp
110 ]
111
112 fromBEncode (BDict d)
113 | Just t <- M.lookup "failure reason" d = Failure <$> fromBEncode t
114 | otherwise = OK <$> d >--? "warning message"
115 <*> d >-- "interval"
116 <*> d >--? "min interval"
117 <*> d >--? "complete"
118 <*> d >--? "incomplete"
119 <*> getPeers (M.lookup "peers" d)
120
121 where
122 getPeers :: Maybe BEncode -> Result [Peer]
123 getPeers (Just (BList l)) = fromBEncode (BList l)
124 getPeers (Just (BString s))
125 | B.length s `mod` 6 == 0 =
126 let cnt = B.length s `div` 6 in
127 runGet (sequence (L.replicate cnt peerG)) s
128 | otherwise = decodingError "peers length not a multiple of 6"
129 where
130 peerG = do
131 pip <- getWord32be
132 pport <- getWord16be
133 return (Peer Nothing (fromIntegral pip) (fromIntegral pport))
134
135 getPeers _ = decodingError "Peers"
136
137 fromBEncode _ = decodingError "TResponse"
138
139
140instance URLShow PortNumber where
141 urlShow = urlShow . fromEnum
142
143instance URLShow Word32 where
144 urlShow = show
145
146instance URLShow Event where
147 urlShow e = urlShow (Char.toLower x : xs)
148 where
149 -- this is always nonempty list
150 (x : xs) = show e
151
152instance URLEncode TRequest where
153 urlEncode req = mconcat
154 [ s "peer_id" %= reqPeerID req
155 , s "port" %= reqPort req
156 , s "uploaded" %= reqUploaded req
157 , s "downloaded" %= reqDownloaded req
158 , s "left" %= reqLeft req
159 , s "ip" %=? reqIP req
160 , s "numwant" %=? reqNumWant req
161 , s "event" %=? reqEvent req
162 ]
163 where s :: String -> String; s = id; {-# INLINE s #-}
164
165encodeRequest :: TRequest -> URI
166encodeRequest req = URL.urlEncode req
167 `addToURI` reqAnnounce req
168 `addHashToURI` reqInfoHash req
169
170
171-- | Ports typically reserved for bittorrent.
172defaultPorts :: [PortNumber]
173defaultPorts = [6881..6889]
174
175-- | Above 25, new peers are highly unlikely to increase download speed.
176-- Even 30 peers is _plenty_, the official client version 3 in fact only
177-- actively forms new connections if it has less than 30 peers and will
178-- refuse connections if it has 55. So default value is set to 25.
179--
180defaultNumWant :: Int
181defaultNumWant = 25
182
183
184-- | 'TSession' (shorthand for Tracker session) combines tracker request
185-- fields neccessary for tracker, torrent and client identification. 46-- fields neccessary for tracker, torrent and client identification.
186-- 47--
187-- This data is considered as static within one session. 48-- This data is considered as static within one session.
188-- 49--
189data TSession = TSession { 50data TConnection = TConnection {
190 tsesAnnounce :: URI -- ^ Announce URL. 51 tconnAnnounce :: URI -- ^ Announce URL.
191 , tsesInfoHash :: InfoHash -- ^ Hash of info part of current .torrent file. 52 , tconnInfoHash :: InfoHash -- ^ Hash of info part of current .torrent file.
192 , tsesPeerID :: PeerID -- ^ Client peer ID. 53 , tconnPeerID :: PeerID -- ^ Client peer ID.
193 , tsesPort :: PortNumber -- ^ The port number the client is listenning on. 54 , tconnPort :: PortNumber -- ^ The port number the client is listenning on.
194 } deriving Show 55 } deriving Show
195 56
196tsession :: Torrent -> PeerID -> PortNumber -> TSession 57tconnection :: Torrent -> PeerID -> PortNumber -> TConnection
197tsession t = TSession (tAnnounce t) (tInfoHash t) 58tconnection t = TConnection (tAnnounce t) (tInfoHash t)
198 59
199 60
200-- | 'Progress' contains upload/download/left stats about 61-- | 'Progress' contains upload/download/left stats about
@@ -213,12 +74,12 @@ startProgress = Progress 0 0
213 74
214 75
215-- | used to avoid boilerplate; do NOT export me 76-- | used to avoid boilerplate; do NOT export me
216genericReq :: TSession -> Progress -> TRequest 77genericReq :: TConnection -> Progress -> TRequest
217genericReq ses pr = TRequest { 78genericReq ses pr = TRequest {
218 reqAnnounce = tsesAnnounce ses 79 reqAnnounce = tconnAnnounce ses
219 , reqInfoHash = tsesInfoHash ses 80 , reqInfoHash = tconnInfoHash ses
220 , reqPeerID = tsesPeerID ses 81 , reqPeerID = tconnPeerID ses
221 , reqPort = tsesPort ses 82 , reqPort = tconnPort ses
222 83
223 , reqUploaded = prUploaded pr 84 , reqUploaded = prUploaded pr
224 , reqDownloaded = prDownloaded pr 85 , reqDownloaded = prDownloaded pr
@@ -233,7 +94,7 @@ genericReq ses pr = TRequest {
233-- | The first request to the tracker that should be created is 'startedReq'. 94-- | The first request to the tracker that should be created is 'startedReq'.
234-- It includes necessary 'Started' event field. 95-- It includes necessary 'Started' event field.
235-- 96--
236startedReq :: TSession -> Progress -> TRequest 97startedReq :: TConnection -> Progress -> TRequest
237startedReq ses pr = (genericReq ses pr) { 98startedReq ses pr = (genericReq ses pr) {
238 reqIP = Nothing 99 reqIP = Nothing
239 , reqNumWant = Just defaultNumWant 100 , reqNumWant = Just defaultNumWant
@@ -244,7 +105,7 @@ startedReq ses pr = (genericReq ses pr) {
244-- notify tracker about current state of the client 105-- notify tracker about current state of the client
245-- so new peers could connect to the client. 106-- so new peers could connect to the client.
246-- 107--
247regularReq :: Int -> TSession -> Progress -> TRequest 108regularReq :: Int -> TConnection -> Progress -> TRequest
248regularReq numWant ses pr = (genericReq ses pr) { 109regularReq numWant ses pr = (genericReq ses pr) {
249 reqIP = Nothing 110 reqIP = Nothing
250 , reqNumWant = Just numWant 111 , reqNumWant = Just numWant
@@ -253,7 +114,7 @@ regularReq numWant ses pr = (genericReq ses pr) {
253 114
254-- | Must be sent to the tracker if the client is shutting down gracefully. 115-- | Must be sent to the tracker if the client is shutting down gracefully.
255-- 116--
256stoppedReq :: TSession -> Progress -> TRequest 117stoppedReq :: TConnection -> Progress -> TRequest
257stoppedReq ses pr = (genericReq ses pr) { 118stoppedReq ses pr = (genericReq ses pr) {
258 reqIP = Nothing 119 reqIP = Nothing
259 , reqNumWant = Nothing 120 , reqNumWant = Nothing
@@ -263,7 +124,7 @@ stoppedReq ses pr = (genericReq ses pr) {
263-- | Must be sent to the tracker when the download completes. 124-- | Must be sent to the tracker when the download completes.
264-- However, must not be sent if the download was already 100% complete. 125-- However, must not be sent if the download was already 100% complete.
265-- 126--
266completedReq :: TSession -> Progress -> TRequest 127completedReq :: TConnection -> Progress -> TRequest
267completedReq ses pr = (genericReq ses pr) { 128completedReq ses pr = (genericReq ses pr) {
268 reqIP = Nothing 129 reqIP = Nothing
269 , reqNumWant = Nothing 130 , reqNumWant = Nothing
@@ -271,16 +132,52 @@ completedReq ses pr = (genericReq ses pr) {
271 } 132 }
272 133
273 134
274-- | TODO rename to ask for peers
275--
276sendRequest :: TRequest -> IO (Result TResponse)
277sendRequest req = do
278 let r = mkHTTPRequest (encodeRequest req)
279 135
280 rawResp <- simpleHTTP r
281 respBody <- getResponseBody rawResp
282 return (decoded (BC.pack respBody))
283 136
137data TSession = TSession {
138 seProgress :: TVar Progress
139 , seInterval :: IORef Int
140 , sePeers :: TVar [Peer]
141 }
142
143newSession :: Progress -> Int -> [Peer] -> IO TSession
144newSession pr i ps = TSession <$> newTVarIO pr <*> newIORef i <*> newTVarIO ps
145
146getPeerList :: TSession -> IO [Peer]
147getPeerList = readTVarIO . sePeers
148
149getProgress :: TSession -> IO Progress
150getProgress = readTVarIO . seProgress
151
152waitInterval :: TSession -> IO ()
153waitInterval = readIORef . seInterval >=> threadDelay
154
155withTracker :: Progress -> TConnection -> (TSession -> IO a) -> IO a
156withTracker initProgress conn action = bracket start end (action . fst)
284 where 157 where
285 mkHTTPRequest :: URI -> Request String 158 start = do
286 mkHTTPRequest uri = Request uri GET [] "" 159 res <- sendRequest (startedReq conn initProgress)
160 case res of
161 Left err -> ioError (userError err)
162 Right (Failure err) -> ioError (userError (show err))
163 Right resp -> do
164 se <- newSession initProgress (respInterval resp) (respPeers resp)
165 tid <- forkIO (syncSession se)
166 return (se, tid)
167
168
169 syncSession se = do
170 waitInterval se
171 pr <- getProgress se
172 eresp <- sendRequest (regularReq defaultNumWant conn pr)
173 case eresp of
174 Right (OK { respInterval = i, respPeers = ps }) -> do
175 writeIORef (seInterval se) i
176 atomically $ writeTVar (sePeers se) ps
177 _ -> return ()
178 syncSession se
179
180
181 end (se, tid) = do
182 killThread tid
183 getProgress se >>= sendRequest . stoppedReq conn
diff --git a/src/Network/BitTorrent/Tracker/Protocol.hs b/src/Network/BitTorrent/Tracker/Protocol.hs
new file mode 100644
index 00000000..000b38c1
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker/Protocol.hs
@@ -0,0 +1,204 @@
1-- |
2-- Copyright : (c) Sam T. 2013
3-- License : MIT
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : non-portable
7--
8--
9-- This module provides straigthforward Tracker protocol
10-- implementation. The tracker is an HTTP/HTTPS service:
11--
12-- * A tracker request is HTTP GET request; used to include
13-- metrics from clients that help the tracker keep overall
14-- statistics about the torrent.
15--
16-- * The tracker response includes a peer list that helps the
17-- client participate in the torrent.
18--
19-- For more convenient high level API see Network.BitTorrent.Tracker
20-- module.
21--
22{-# OPTIONS -fno-warn-orphans #-}
23{-# LANGUAGE OverloadedStrings #-}
24-- TODO: add "compact" field to TRequest
25module Network.BitTorrent.Tracker.Protocol
26 ( module Network.BitTorrent.Tracker.Scrape
27
28 , Event(..), TRequest(..), TResponse(..)
29 , sendRequest
30
31 -- * Defaults
32 , defaultPorts, defaultNumWant
33 )
34 where
35
36import Control.Applicative
37import Data.Char as Char
38import Data.Word (Word32)
39import Data.List as L
40import Data.Map as M
41import Data.Monoid
42import Data.BEncode
43import Data.ByteString as B
44import Data.ByteString.Char8 as BC
45import Data.Text as T
46import Data.Serialize.Get hiding (Result)
47import Data.URLEncoded as URL
48import Data.Torrent
49
50import Network
51import Network.Socket
52import Network.HTTP
53import Network.URI
54
55import Network.BitTorrent.Peer
56import Network.BitTorrent.PeerID
57import Network.BitTorrent.Tracker.Scrape
58
59
60data Event = Started -- ^ For first request.
61 | Stopped -- ^ Sent when the peer is shutting down.
62 | Completed -- ^ To be sent when the peer completes a download.
63 deriving (Show, Read, Eq, Ord, Enum, Bounded)
64
65data TRequest = TRequest { -- TODO peer here -- TODO detach announce
66 reqAnnounce :: URI -- ^ Announce url of the torrent.
67 , reqInfoHash :: InfoHash -- ^ Hash of info part of the torrent.
68 , reqPeerID :: PeerID -- ^ Id of the peer doing request. ()
69 , reqPort :: PortNumber -- ^ Port to listen to for connection from other peers.
70 , reqUploaded :: Integer -- ^ # of bytes that the peer has uploaded in the swarm.
71 , reqDownloaded :: Integer -- ^ # of bytes downloaded in the swarm by the peer.
72 , reqLeft :: Integer -- ^ # of bytes needed in order to complete download.
73 , reqIP :: Maybe HostAddress -- ^ The peer IP.
74 , reqNumWant :: Maybe Int -- ^ Number of peers that the peers wants to receive from.
75 , reqEvent :: Maybe Event -- ^ If not specified,
76 -- the request is regular periodic request.
77 } deriving Show
78
79data TResponse =
80 Failure Text -- ^ Failure reason in human readable form.
81 | OK {
82 respWarning :: Maybe Text
83 , respInterval :: Int -- ^ Recommended interval to wait between requests.
84 , respMinInterval :: Maybe Int -- ^ Minimal amount of time between requests.
85 , respComplete :: Maybe Int -- ^ Number of peers completed the torrent. (seeders)
86 , respIncomplete :: Maybe Int -- ^ Number of peers downloading the torrent.
87 , respPeers :: [Peer] -- ^ Peers that must be contacted.
88 } deriving Show
89
90instance BEncodable PortNumber where
91 toBEncode = toBEncode . fromEnum
92 fromBEncode b = toEnum <$> fromBEncode b
93
94instance BEncodable Peer where
95 toBEncode (Peer pid pip pport) = fromAssocs
96 [ "peer id" -->? pid
97 , "ip" --> pip
98 , "port" --> pport
99 ]
100
101 fromBEncode (BDict d) =
102 Peer <$> d >--? "peer id"
103 <*> d >-- "ip"
104 <*> d >-- "port"
105
106 fromBEncode _ = decodingError "Peer"
107
108instance BEncodable TResponse where
109 toBEncode (Failure t) = fromAssocs ["failure reason" --> t]
110 toBEncode resp@(OK {}) = fromAssocs
111 [ "interval" --> respInterval resp
112 , "min interval" -->? respMinInterval resp
113 , "complete" -->? respComplete resp
114 , "incomplete" -->? respIncomplete resp
115 , "peers" --> respPeers resp
116 ]
117
118 fromBEncode (BDict d)
119 | Just t <- M.lookup "failure reason" d = Failure <$> fromBEncode t
120 | otherwise = OK <$> d >--? "warning message"
121 <*> d >-- "interval"
122 <*> d >--? "min interval"
123 <*> d >--? "complete"
124 <*> d >--? "incomplete"
125 <*> getPeers (M.lookup "peers" d)
126
127 where
128 getPeers :: Maybe BEncode -> Result [Peer]
129 getPeers (Just (BList l)) = fromBEncode (BList l)
130 getPeers (Just (BString s))
131 | B.length s `mod` 6 == 0 =
132 let cnt = B.length s `div` 6 in
133 runGet (sequence (L.replicate cnt peerG)) s
134 | otherwise = decodingError "peers length not a multiple of 6"
135 where
136 peerG = do
137 pip <- getWord32be
138 pport <- getWord16be
139 return (Peer Nothing (fromIntegral pip) (fromIntegral pport))
140
141 getPeers _ = decodingError "Peers"
142
143 fromBEncode _ = decodingError "TResponse"
144
145
146instance URLShow PortNumber where
147 urlShow = urlShow . fromEnum
148
149instance URLShow Word32 where
150 urlShow = show
151
152instance URLShow Event where
153 urlShow e = urlShow (Char.toLower x : xs)
154 where
155 -- this is always nonempty list
156 (x : xs) = show e
157
158instance URLEncode TRequest where
159 urlEncode req = mconcat
160 [ s "peer_id" %= reqPeerID req
161 , s "port" %= reqPort req
162 , s "uploaded" %= reqUploaded req
163 , s "downloaded" %= reqDownloaded req
164 , s "left" %= reqLeft req
165 , s "ip" %=? reqIP req
166 , s "numwant" %=? reqNumWant req
167 , s "event" %=? reqEvent req
168 ]
169 where s :: String -> String; s = id; {-# INLINE s #-}
170
171encodeRequest :: TRequest -> URI
172encodeRequest req = URL.urlEncode req
173 `addToURI` reqAnnounce req
174 `addHashToURI` reqInfoHash req
175
176
177-- | Ports typically reserved for bittorrent.
178defaultPorts :: [PortNumber]
179defaultPorts = [6881..6889]
180
181-- | Above 25, new peers are highly unlikely to increase download speed.
182-- Even 30 peers is _plenty_, the official client version 3 in fact only
183-- actively forms new connections if it has less than 30 peers and will
184-- refuse connections if it has 55. So default value is set to 25.
185--
186defaultNumWant :: Int
187defaultNumWant = 25
188
189
190
191
192-- | TODO rename to ask for peers
193--
194sendRequest :: TRequest -> IO (Result TResponse)
195sendRequest req = do
196 let r = mkHTTPRequest (encodeRequest req)
197
198 rawResp <- simpleHTTP r
199 respBody <- getResponseBody rawResp
200 return (decoded (BC.pack respBody))
201
202 where
203 mkHTTPRequest :: URI -> Request String
204 mkHTTPRequest uri = Request uri GET [] ""