diff options
Diffstat (limited to 'src/Network/Torrent')
-rw-r--r-- | src/Network/Torrent/THP.hs | 205 |
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 #-} | ||
3 | module Network.Torrent.THP | ||
4 | ( Peer(..), Event(..), TRequest(..), TResponse(..) | ||
5 | , sendRequest, defaultRequest | ||
6 | ) | ||
7 | where | ||
8 | |||
9 | import Control.Applicative | ||
10 | import Data.Maybe | ||
11 | import Data.BEncode | ||
12 | import Data.Char as Char | ||
13 | import Data.Monoid | ||
14 | import Data.List as L | ||
15 | import Data.Map as M | ||
16 | import Data.ByteString as B | ||
17 | import qualified Data.ByteString.Lazy as Lazy | ||
18 | import Data.ByteString.Char8 as BC | ||
19 | import qualified Data.ByteString.Builder as B | ||
20 | import qualified Data.ByteString.Builder.Prim as BP | ||
21 | import Data.Text as T | ||
22 | import Data.Serialize.Get hiding (Result) | ||
23 | import Data.URLEncoded as URL | ||
24 | |||
25 | import Network | ||
26 | import Network.HTTP | ||
27 | import Network.URI | ||
28 | import Network.Torrent.PeerID | ||
29 | |||
30 | import Numeric | ||
31 | |||
32 | type IP = Int | ||
33 | type Hash = ByteString | ||
34 | |||
35 | data Peer = Peer { | ||
36 | peerID :: Maybe PeerID | ||
37 | , peerIP :: IP | ||
38 | , peerPort :: PortNumber | ||
39 | } deriving Show | ||
40 | |||
41 | data 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 | |||
46 | data 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 | |||
60 | data 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 | |||
71 | instance BEncodable PortNumber where | ||
72 | toBEncode = toBEncode . fromEnum | ||
73 | fromBEncode b = toEnum <$> fromBEncode b | ||
74 | |||
75 | instance 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 | |||
89 | instance 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 | |||
127 | instance URLShow PortNumber where | ||
128 | urlShow = urlShow . fromEnum | ||
129 | |||
130 | instance URLShow PeerID where | ||
131 | urlShow = BC.unpack . getPeerID | ||
132 | |||
133 | instance 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 | |||
139 | instance 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 | |||
152 | encodeRequest :: TRequest -> URI | ||
153 | encodeRequest 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. | ||
172 | defaultPorts :: [PortNumber] | ||
173 | defaultPorts = [6881..6889] | ||
174 | |||
175 | defaultRequest :: URI -> Hash -> PeerID -> TRequest | ||
176 | defaultRequest 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 | -- | ||
193 | sendRequest :: TRequest -> IO (Result TResponse) | ||
194 | sendRequest 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 [] "" | ||