summaryrefslogtreecommitdiff
path: root/src/Network/Torrent/THP.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Network/Torrent/THP.hs')
-rw-r--r--src/Network/Torrent/THP.hs205
1 files changed, 205 insertions, 0 deletions
diff --git a/src/Network/Torrent/THP.hs b/src/Network/Torrent/THP.hs
new file mode 100644
index 00000000..e440d0f2
--- /dev/null
+++ b/src/Network/Torrent/THP.hs
@@ -0,0 +1,205 @@
1{-# OPTIONS -fno-warn-orphans #-}
2{-# LANGUAGE OverloadedStrings #-}
3module Network.Torrent.THP
4 ( Peer(..), Event(..), TRequest(..), TResponse(..)
5 , sendRequest, defaultRequest
6 )
7 where
8
9import Control.Applicative
10import Data.Maybe
11import Data.BEncode
12import Data.Char as Char
13import Data.Monoid
14import Data.List as L
15import Data.Map as M
16import Data.ByteString as B
17import qualified Data.ByteString.Lazy as Lazy
18import Data.ByteString.Char8 as BC
19import qualified Data.ByteString.Builder as B
20import qualified Data.ByteString.Builder.Prim as BP
21import Data.Text as T
22import Data.Serialize.Get hiding (Result)
23import Data.URLEncoded as URL
24
25import Network
26import Network.HTTP
27import Network.URI
28import Network.Torrent.PeerID
29
30import Numeric
31
32type IP = Int
33type Hash = ByteString
34
35data Peer = Peer {
36 peerID :: Maybe PeerID
37 , peerIP :: IP
38 , peerPort :: PortNumber
39 } deriving Show
40
41data Event = Started -- ^ For first request.
42 | Stopped -- ^ Sent when the peer is shutting down.
43 | Completed -- ^ To be sent when the peer completes a download.
44 deriving (Show, Read, Eq, Ord, Enum, Bounded)
45
46data TRequest = TRequest { -- TODO peer here -- TODO detach announce
47 reqAnnounce :: URI -- ^ Announce url of the torrent.
48 , reqInfoHash :: Hash -- ^ Hash of info part of the torrent.
49 , reqPeerID :: PeerID -- ^ Id of the peer doing request. ()
50 , reqPort :: PortNumber -- ^ Port to listen to for connection from other peers.
51 , reqUploaded :: Int -- ^ # of bytes that the peer has uploaded in the swarm.
52 , reqDownloaded :: Int -- ^ # of bytes downloaded in the swarm by the peer.
53 , reqLeft :: Int -- ^ # of bytes needed in order to complete download.
54 , reqIP :: Maybe IP -- ^ The peer IP.
55 , reqNumWant :: Maybe Int -- ^ Number of peers that the peers wants to receive from.
56 , reqEvent :: Maybe Event -- ^ If not specified,
57 -- the request is regular periodic request.
58 } deriving Show
59
60data TResponse =
61 Failure Text -- ^ Failure reason in human readable form.
62 | OK {
63 respWarning :: Maybe Text
64 , respInterval :: Int -- ^ Recommended interval to wait between requests.
65 , respMinInterval :: Maybe Int -- ^ Minimal amount of time between requests.
66 , respComplete :: Maybe Int -- ^ Number of peers completed the torrent. (seeders)
67 , respIncomplete :: Maybe Int -- ^ Number of peers downloading the torrent.
68 , respPeers :: [Peer] -- ^ Peers that must be contacted.
69 } deriving Show
70
71instance BEncodable PortNumber where
72 toBEncode = toBEncode . fromEnum
73 fromBEncode b = toEnum <$> fromBEncode b
74
75instance BEncodable Peer where
76 toBEncode (Peer pid pip pport) = fromAssocs
77 [ "peer id" -->? pid
78 , "ip" --> pip
79 , "port" --> pport
80 ]
81
82 fromBEncode (BDict d) =
83 Peer <$> d >--? "peer id"
84 <*> d >-- "ip"
85 <*> d >-- "port"
86
87 fromBEncode _ = decodingError "Peer"
88
89instance BEncodable TResponse where
90 toBEncode (Failure t) = fromAssocs ["failure reason" --> t]
91 toBEncode resp@(OK {}) = fromAssocs
92 [ "interval" --> respInterval resp
93 , "min interval" -->? respMinInterval resp
94 , "complete" -->? respComplete resp
95 , "incomplete" -->? respIncomplete resp
96 , "peers" --> respPeers resp
97 ]
98
99 fromBEncode (BDict d)
100 | Just t <- M.lookup "failure reason" d = Failure <$> fromBEncode t
101 | otherwise = OK <$> d >--? "warning message"
102 <*> d >-- "interval"
103 <*> d >--? "min interval"
104 <*> d >--? "complete"
105 <*> d >--? "incomplete"
106 <*> getPeers (M.lookup "peers" d)
107
108 where
109 getPeers :: Maybe BEncode -> Result [Peer]
110 getPeers (Just (BList l)) = fromBEncode (BList l)
111 getPeers (Just (BString s))
112 | B.length s `mod` 6 == 0 =
113 let cnt = B.length s `div` 6 in
114 runGet (sequence (L.replicate cnt peerG)) s
115 | otherwise = decodingError "peers length not a multiple of 6"
116 where
117 peerG = do
118 pip <- getWord32be
119 pport <- getWord16be
120 return (Peer Nothing (fromIntegral pip) (fromIntegral pport))
121
122 getPeers _ = decodingError "Peers"
123
124 fromBEncode _ = decodingError "TResponse"
125
126
127instance URLShow PortNumber where
128 urlShow = urlShow . fromEnum
129
130instance URLShow PeerID where
131 urlShow = BC.unpack . getPeerID
132
133instance URLShow Event where
134 urlShow e = urlShow (Char.toLower x : xs)
135 where
136 -- this is always nonempty list
137 (x : xs) = show e
138
139instance URLEncode TRequest where
140 urlEncode req = mconcat
141 [ s "peer_id" %= reqPeerID req
142 , s "port" %= reqPort req
143 , s "uploaded" %= reqUploaded req
144 , s "downloaded" %= reqDownloaded req
145 , s "left" %= reqLeft req
146 , s "ip" %=? reqIP req
147 , s "numwant" %=? reqNumWant req
148 , s "event" %=? reqEvent req
149 ]
150 where s :: String -> String; s = id; {-# INLINE s #-}
151
152encodeRequest :: TRequest -> URI
153encodeRequest req = URL.urlEncode req `addToURI` reqAnnounce req
154 `addHash` BC.unpack (reqInfoHash req)
155 where
156 addHash :: URI -> String -> URI
157 addHash uri s = uri { uriQuery = uriQuery uri ++ "&info_hash=" ++ rfc1738Encode s }
158
159 rfc1738Encode :: String -> String
160 rfc1738Encode = L.concatMap (\c -> if unreserved c then [c] else encode c)
161 where
162 unreserved = (`L.elem` chars)
163 chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_./"
164 encode :: Char -> String
165 encode c = '%' : pHex c
166 pHex c =
167 let p = (showHex . ord $ c) ""
168 in if L.length p == 1 then '0' : p else p
169
170
171-- | Ports typically reserved for bittorrent.
172defaultPorts :: [PortNumber]
173defaultPorts = [6881..6889]
174
175defaultRequest :: URI -> Hash -> PeerID -> TRequest
176defaultRequest announce hash pid =
177 TRequest {
178 reqAnnounce = announce
179 , reqInfoHash = hash
180 , reqPeerID = pid
181 , reqPort = L.head defaultPorts
182 , reqUploaded = 0
183 , reqDownloaded = 0
184 , reqLeft = 0
185 , reqIP = Nothing
186 , reqNumWant = Just 50
187 , reqEvent = Just Started
188 }
189
190
191-- | TODO rename to ask for peers
192--
193sendRequest :: TRequest -> IO (Result TResponse)
194sendRequest req = do
195 let r = mkHTTPRequest (encodeRequest req)
196 print r
197
198 rawResp <- simpleHTTP r
199 respBody <- getResponseBody rawResp
200 print respBody
201 return (decoded (BC.pack respBody))
202
203 where
204 mkHTTPRequest :: URI -> Request String
205 mkHTTPRequest uri = Request uri GET [] ""