diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Network/BitTorrent/Tracker.hs | 271 | ||||
-rw-r--r-- | src/Network/BitTorrent/Tracker/Protocol.hs | 204 |
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 | -- |
12 | module Network.BitTorrent.Tracker | 11 | module 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 | ||
30 | import Control.Applicative | 29 | import Control.Applicative |
31 | import Data.Char as Char | 30 | import Control.Concurrent |
32 | import Data.Word (Word32) | 31 | import Control.Concurrent.STM |
33 | import Data.List as L | 32 | import Control.Exception |
34 | import Data.Map as M | 33 | import Control.Monad |
35 | import Data.Monoid | 34 | import Data.IORef |
36 | import Data.BEncode | ||
37 | import Data.ByteString as B | ||
38 | import Data.ByteString.Char8 as BC | ||
39 | import Data.Text as T | ||
40 | import Data.Serialize.Get hiding (Result) | ||
41 | import Data.URLEncoded as URL | ||
42 | import Data.Torrent | 35 | import Data.Torrent |
43 | |||
44 | import Network | 36 | import Network |
45 | import Network.Socket | ||
46 | import Network.HTTP | ||
47 | import Network.URI | 37 | import Network.URI |
48 | 38 | ||
49 | import Network.BitTorrent.Peer | 39 | import Network.BitTorrent.Peer |
50 | import Network.BitTorrent.PeerID | 40 | import Network.BitTorrent.PeerID |
41 | import Network.BitTorrent.Tracker.Protocol | ||
51 | import Network.BitTorrent.Tracker.Scrape | 42 | import Network.BitTorrent.Tracker.Scrape |
52 | 43 | ||
53 | 44 | ||
54 | data 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 | |||
59 | data 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 | |||
73 | data 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 | |||
84 | instance BEncodable PortNumber where | ||
85 | toBEncode = toBEncode . fromEnum | ||
86 | fromBEncode b = toEnum <$> fromBEncode b | ||
87 | |||
88 | instance 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 | |||
102 | instance 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 | |||
140 | instance URLShow PortNumber where | ||
141 | urlShow = urlShow . fromEnum | ||
142 | |||
143 | instance URLShow Word32 where | ||
144 | urlShow = show | ||
145 | |||
146 | instance 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 | |||
152 | instance 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 | |||
165 | encodeRequest :: TRequest -> URI | ||
166 | encodeRequest req = URL.urlEncode req | ||
167 | `addToURI` reqAnnounce req | ||
168 | `addHashToURI` reqInfoHash req | ||
169 | |||
170 | |||
171 | -- | Ports typically reserved for bittorrent. | ||
172 | defaultPorts :: [PortNumber] | ||
173 | defaultPorts = [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 | -- | ||
180 | defaultNumWant :: Int | ||
181 | defaultNumWant = 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 | -- |
189 | data TSession = TSession { | 50 | data 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 | ||
196 | tsession :: Torrent -> PeerID -> PortNumber -> TSession | 57 | tconnection :: Torrent -> PeerID -> PortNumber -> TConnection |
197 | tsession t = TSession (tAnnounce t) (tInfoHash t) | 58 | tconnection 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 |
216 | genericReq :: TSession -> Progress -> TRequest | 77 | genericReq :: TConnection -> Progress -> TRequest |
217 | genericReq ses pr = TRequest { | 78 | genericReq 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 | -- |
236 | startedReq :: TSession -> Progress -> TRequest | 97 | startedReq :: TConnection -> Progress -> TRequest |
237 | startedReq ses pr = (genericReq ses pr) { | 98 | startedReq 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 | -- |
247 | regularReq :: Int -> TSession -> Progress -> TRequest | 108 | regularReq :: Int -> TConnection -> Progress -> TRequest |
248 | regularReq numWant ses pr = (genericReq ses pr) { | 109 | regularReq 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 | -- |
256 | stoppedReq :: TSession -> Progress -> TRequest | 117 | stoppedReq :: TConnection -> Progress -> TRequest |
257 | stoppedReq ses pr = (genericReq ses pr) { | 118 | stoppedReq 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 | -- |
266 | completedReq :: TSession -> Progress -> TRequest | 127 | completedReq :: TConnection -> Progress -> TRequest |
267 | completedReq ses pr = (genericReq ses pr) { | 128 | completedReq 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 | -- | ||
276 | sendRequest :: TRequest -> IO (Result TResponse) | ||
277 | sendRequest 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 | ||
137 | data TSession = TSession { | ||
138 | seProgress :: TVar Progress | ||
139 | , seInterval :: IORef Int | ||
140 | , sePeers :: TVar [Peer] | ||
141 | } | ||
142 | |||
143 | newSession :: Progress -> Int -> [Peer] -> IO TSession | ||
144 | newSession pr i ps = TSession <$> newTVarIO pr <*> newIORef i <*> newTVarIO ps | ||
145 | |||
146 | getPeerList :: TSession -> IO [Peer] | ||
147 | getPeerList = readTVarIO . sePeers | ||
148 | |||
149 | getProgress :: TSession -> IO Progress | ||
150 | getProgress = readTVarIO . seProgress | ||
151 | |||
152 | waitInterval :: TSession -> IO () | ||
153 | waitInterval = readIORef . seInterval >=> threadDelay | ||
154 | |||
155 | withTracker :: Progress -> TConnection -> (TSession -> IO a) -> IO a | ||
156 | withTracker 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 | ||
25 | module 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 | |||
36 | import Control.Applicative | ||
37 | import Data.Char as Char | ||
38 | import Data.Word (Word32) | ||
39 | import Data.List as L | ||
40 | import Data.Map as M | ||
41 | import Data.Monoid | ||
42 | import Data.BEncode | ||
43 | import Data.ByteString as B | ||
44 | import Data.ByteString.Char8 as BC | ||
45 | import Data.Text as T | ||
46 | import Data.Serialize.Get hiding (Result) | ||
47 | import Data.URLEncoded as URL | ||
48 | import Data.Torrent | ||
49 | |||
50 | import Network | ||
51 | import Network.Socket | ||
52 | import Network.HTTP | ||
53 | import Network.URI | ||
54 | |||
55 | import Network.BitTorrent.Peer | ||
56 | import Network.BitTorrent.PeerID | ||
57 | import Network.BitTorrent.Tracker.Scrape | ||
58 | |||
59 | |||
60 | data 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 | |||
65 | data 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 | |||
79 | data 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 | |||
90 | instance BEncodable PortNumber where | ||
91 | toBEncode = toBEncode . fromEnum | ||
92 | fromBEncode b = toEnum <$> fromBEncode b | ||
93 | |||
94 | instance 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 | |||
108 | instance 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 | |||
146 | instance URLShow PortNumber where | ||
147 | urlShow = urlShow . fromEnum | ||
148 | |||
149 | instance URLShow Word32 where | ||
150 | urlShow = show | ||
151 | |||
152 | instance 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 | |||
158 | instance 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 | |||
171 | encodeRequest :: TRequest -> URI | ||
172 | encodeRequest req = URL.urlEncode req | ||
173 | `addToURI` reqAnnounce req | ||
174 | `addHashToURI` reqInfoHash req | ||
175 | |||
176 | |||
177 | -- | Ports typically reserved for bittorrent. | ||
178 | defaultPorts :: [PortNumber] | ||
179 | defaultPorts = [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 | -- | ||
186 | defaultNumWant :: Int | ||
187 | defaultNumWant = 25 | ||
188 | |||
189 | |||
190 | |||
191 | |||
192 | -- | TODO rename to ask for peers | ||
193 | -- | ||
194 | sendRequest :: TRequest -> IO (Result TResponse) | ||
195 | sendRequest 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 [] "" | ||