1 {-# LANGUAGE OverloadedStrings #-}
2 module FuncTorrent.Tracker
9 import Prelude hiding (lookup, splitAt)
12 import Control.Applicative (liftA2)
13 import Control.Concurrent (threadDelay)
14 import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar, readMVar, putMVar)
15 import Control.Exception (try)
16 import Data.Binary (Binary(..), encode, decode)
17 import Data.Binary.Put (putWord16be, putWord32be, putWord64be, putByteString)
18 import Data.Binary.Get (Get, isEmpty, getByteString, getWord16be, getWord32be)
19 import Data.ByteString (ByteString, hGet, hPut)
20 import Data.ByteString.Char8 as BC (pack, unpack, splitAt)
21 import Data.ByteString.Lazy (fromStrict, toStrict)
22 import Data.Char (chr)
23 import Data.List (intercalate, isPrefixOf)
24 import Data.Map as M (lookup)
25 import Network (connectTo, PortID(..), PortNumber, Socket)
26 import Network.Socket (defaultProtocol, SocketType( Datagram ), Family( AF_INET ), SockAddr( SockAddrInet ), inet_addr, socket)
27 import Network.Socket.ByteString (sendTo, recv)
28 import Network.HTTP.Base (urlEncode)
29 import qualified Data.ByteString.Base16 as B16 (encode)
31 import FuncTorrent.Bencode (BVal(..))
32 import qualified FuncTorrent.Bencode as Benc
33 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
34 import FuncTorrent.Network (sendGetRequest)
35 import FuncTorrent.Peer (Peer(..))
36 import FuncTorrent.Utils (splitN)
38 data TrackerProtocol = Http
44 data TrackerResponse = TrackerResponse {
47 , complete :: Maybe Integer
48 , incomplete :: Maybe Integer
51 data TrackerEventState = None
57 data TState = TState {
58 uploaded :: MVar Integer
59 , downloaded :: MVar Integer
61 , currentState :: TrackerEventState
62 , connectedPeers :: MVar [Peer]
65 -- UDP tracker: http://bittorrent.org/beps/bep_0015.html
74 data UDPRequest = ConnectReq Integer
75 | AnnounceReq Integer Integer ByteString String Integer Integer Integer TrackerEventState Integer
76 | ScrapeReq Integer Integer ByteString
79 data UDPResponse = ConnectResp Integer Integer -- transaction_id connection_id
80 | AnnounceResp Integer Integer Integer Integer [(IP, Port)] -- transaction_id interval leechers seeders [(ip, port)]
81 | ScrapeResp Integer Integer Integer Integer
84 actionToInteger :: Action -> Integer
85 actionToInteger Connect = 0
86 actionToInteger Announce = 1
87 actionToInteger Scrape = 2
89 intToAction :: Integer -> Action
90 intToAction 0 = Connect
91 intToAction 1 = Announce
92 intToAction 2 = Scrape
94 eventToInteger :: TrackerEventState -> Integer
95 eventToInteger None = 0
96 eventToInteger Completed = 1
97 eventToInteger Started = 2
98 eventToInteger Stopped = 3
100 instance Binary UDPRequest where
101 put (ConnectReq transId) = do
102 putWord64be 0x41727101980
103 putWord32be $ fromIntegral (actionToInteger Connect)
104 putWord32be (fromIntegral transId)
105 put (AnnounceReq connId transId infohash peerId down left up event port) = do
106 putWord64be $ fromIntegral connId
107 putWord32be $ fromIntegral (actionToInteger Announce)
108 putWord32be $ fromIntegral transId
109 putByteString infohash
110 putByteString (BC.pack peerId)
111 putWord64be (fromIntegral down)
112 putWord64be (fromIntegral left)
113 putWord64be (fromIntegral up)
114 putWord32be $ fromIntegral (eventToInteger None)
116 -- key is optional, we will not send it for now
117 putWord32be $ fromIntegral (-1)
118 putWord16be $ fromIntegral port
119 put (ScrapeReq _ _ _) = undefined
122 instance Binary UDPResponse where
125 a <- getWord32be -- action
127 0 -> liftA2 ConnectResp (fromIntegral <$> getWord32be) (fromIntegral <$> getWord32be)
129 tid <- fromIntegral <$> getWord32be
130 interval' <- fromIntegral <$> getWord32be
131 _ <- getWord32be -- leechers
132 _ <- getWord32be -- seeders
133 ipportpairs <- getIPPortPairs -- [(ip, port)]
134 return $ AnnounceResp tid interval' 0 0 ipportpairs
136 tid <- fromIntegral <$> getWord32be
140 return $ ScrapeResp tid 0 0 0
141 _ -> error ("unknown response action type: " ++ show a)
143 getIPPortPairs :: Get [(IP, Port)]
149 ip <- toIP <$> getByteString 6
150 port <- toPort <$> getByteString 2
151 ipportpairs <- getIPPortPairs
152 return $ (ip, port) : ipportpairs
154 initialTrackerState :: Integer -> IO TState
155 initialTrackerState sz = do
159 return $ TState { currentState = None
160 , connectedPeers = ps
165 -- | Deserialize HTTP tracker response
166 parseTrackerResponse :: BVal -> Either ByteString TrackerResponse
167 parseTrackerResponse resp =
168 case lookup "failure reason" body of
169 Just (Bstr err) -> Left err
170 Just _ -> Left "Unknown failure"
172 let (Just (Bint i)) = lookup "interval" body
173 (Just (Bstr peersBS)) = lookup "peers" body
174 pl = map makePeer (splitN 6 peersBS)
175 in Right TrackerResponse {
179 , incomplete = Nothing
184 toInt :: String -> Integer
187 makePeer :: ByteString -> Peer
188 makePeer peer = Peer "" (toIP ip') (toPort port')
189 where (ip', port') = splitAt 4 peer
191 toPort :: ByteString -> Port
192 toPort = read . ("0x" ++) . unpack . B16.encode
194 toIP :: ByteString -> IP
195 toIP = Data.List.intercalate "." .
196 map (show . toInt . ("0x" ++) . unpack) .
197 splitN 2 . B16.encode
199 --- | URL encode hash as per RFC1738
201 --- REVIEW: Why is this not written in terms of `Network.HTTP.Base.urlEncode` or
202 --- equivalent library function?
203 urlEncodeHash :: ByteString -> String
204 urlEncodeHash bs = concatMap (encode' . unpack) (splitN 2 bs)
205 where encode' b@[c1, c2] = let c = chr (read ("0x" ++ b))
208 escape i c1 c2 | i `elem` nonSpecialChars = [i]
209 | otherwise = "%" ++ [c1] ++ [c2]
211 nonSpecialChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "-_.~"
213 -- | Make arguments that should be posted to tracker.
214 -- This is a separate pure function for testability.
215 mkArgs :: PortNumber -> String -> Integer -> Integer -> Metainfo -> [(String, ByteString)]
216 mkArgs port peer_id up down m =
217 let fileSize = lengthInBytes $ info m
218 bytesLeft = fileSize - down
220 [("info_hash", pack . urlEncodeHash . B16.encode . infoHash $ m),
221 ("peer_id", pack . urlEncode $ peer_id),
222 ("port", pack $ show port),
223 ("uploaded", pack $ show up),
224 ("downloaded", pack $ show down),
225 ("left", pack $ show bytesLeft),
227 ("event", "started")]
229 trackerLoop :: PortNumber -> String -> Metainfo -> TState -> IO ByteString
230 trackerLoop port peerId m st = do
231 up <- readMVar $ uploaded st
232 down <- readMVar $ downloaded st
233 resp <- sendGetRequest (head . announceList $ m) $ mkArgs port peerId up down m
234 case Benc.decode resp of
235 Left e -> return $ pack (show e)
237 case parseTrackerResponse trackerInfo of
240 _ <- threadDelay $ fromIntegral (interval tresp)
241 _ <- putMVar (connectedPeers st) (peers tresp)
242 trackerLoop port peerId m st
245 getResponse :: Socket -> IO UDPResponse
247 -- connect packet is 16 bytes long
248 -- announce packet is atleast 20 bytes long
249 bs <- recv s (16*1024)
250 return $ decode $ fromStrict bs
252 sendRequest :: Socket -> String -> PortNumber -> UDPRequest -> IO ()
253 sendRequest s ip port req = do
254 hostaddr <- inet_addr ip
255 _ <- sendTo s bsReq $ SockAddrInet (fromIntegral port) hostaddr
257 where bsReq = toStrict $ encode req
259 getTrackerType :: String -> TrackerProtocol
260 getTrackerType url | isPrefixOf "http://" url = Http
261 | isPrefixOf "udp://" url = Udp
262 | otherwise = UnknownProtocol
264 udpTrackerLoop :: PortNumber -> String -> Metainfo -> TState -> IO String
265 udpTrackerLoop port peerId m st = do
266 -- h <- connectTo "exodus.desync.com" (PortNumber 6969)
267 s <- socket AF_INET Datagram defaultProtocol
268 hostAddr <- inet_addr "185.37.101.229"
269 putStrLn "connected to tracker"
270 _ <- sendTo s (toStrict $ encode (ConnectReq 42)) (SockAddrInet 2710 hostAddr)
271 putStrLn "--> sent ConnectReq to tracker"
273 putStrLn "<-- recv ConnectResp from tracker"