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 Data.Binary (Binary(..), encode, decode)
16 import Data.Binary.Put (putWord16be, putWord32be, putWord64be, putByteString)
17 import Data.Binary.Get (Get, isEmpty, getByteString, getWord16be, getWord32be)
18 import Data.ByteString (ByteString, hGet, hPut)
19 import Data.ByteString.Char8 as BC (pack, unpack, splitAt)
20 import Data.ByteString.Lazy (fromStrict, toStrict)
21 import Data.Char (chr)
22 import Data.List (intercalate, isPrefixOf)
23 import Data.Map as M (lookup)
24 import Network (connectTo, PortID(..), PortNumber, Socket)
25 import Network.Socket (defaultProtocol, SocketType( Datagram ), Family( AF_INET ), SockAddr( SockAddrInet ), inet_addr, socket)
26 import Network.Socket.ByteString (sendTo, recv)
27 import Network.HTTP.Base (urlEncode)
28 import qualified Data.ByteString.Base16 as B16 (encode)
30 import FuncTorrent.Bencode (BVal(..))
31 import qualified FuncTorrent.Bencode as Benc
32 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
33 import FuncTorrent.Network (sendGetRequest)
34 import FuncTorrent.Peer (Peer(..))
35 import FuncTorrent.Utils (splitN)
37 data TrackerProtocol = Http
43 data TrackerResponse = TrackerResponse {
46 , complete :: Maybe Integer
47 , incomplete :: Maybe Integer
50 data TrackerEventState = None
56 data TState = TState {
57 uploaded :: MVar Integer
58 , downloaded :: MVar Integer
60 , currentState :: TrackerEventState
61 , connectedPeers :: MVar [Peer]
64 -- UDP tracker: http://bittorrent.org/beps/bep_0015.html
73 data UDPRequest = ConnectReq Integer
74 | AnnounceReq Integer Integer ByteString String Integer Integer Integer TrackerEventState Integer
75 | ScrapeReq Integer Integer ByteString
78 data UDPResponse = ConnectResp Integer Integer -- transaction_id connection_id
79 | AnnounceResp Integer Integer Integer Integer [(IP, Port)] -- transaction_id interval leechers seeders [(ip, port)]
80 | ScrapeResp Integer Integer Integer Integer
83 actionToInteger :: Action -> Integer
84 actionToInteger Connect = 0
85 actionToInteger Announce = 1
86 actionToInteger Scrape = 2
88 intToAction :: Integer -> Action
89 intToAction 0 = Connect
90 intToAction 1 = Announce
91 intToAction 2 = Scrape
93 eventToInteger :: TrackerEventState -> Integer
94 eventToInteger None = 0
95 eventToInteger Completed = 1
96 eventToInteger Started = 2
97 eventToInteger Stopped = 3
99 instance Binary UDPRequest where
100 put (ConnectReq transId) = do
101 putWord64be 0x41727101980
102 putWord32be $ fromIntegral (actionToInteger Connect)
103 putWord32be (fromIntegral transId)
104 put (AnnounceReq connId transId infohash peerId down left up event port) = do
105 putWord64be $ fromIntegral connId
106 putWord32be $ fromIntegral (actionToInteger Announce)
107 putWord32be $ fromIntegral transId
108 putByteString infohash
109 putByteString (BC.pack peerId)
110 putWord64be (fromIntegral down)
111 putWord64be (fromIntegral left)
112 putWord64be (fromIntegral up)
113 putWord32be $ fromIntegral (eventToInteger None)
115 -- key is optional, we will not send it for now
116 putWord32be $ fromIntegral (-1)
117 putWord16be $ fromIntegral port
118 put (ScrapeReq _ _ _) = undefined
121 instance Binary UDPResponse where
124 a <- getWord32be -- action
126 0 -> liftA2 ConnectResp (fromIntegral <$> getWord32be) (fromIntegral <$> getWord32be)
128 tid <- fromIntegral <$> getWord32be
129 interval' <- fromIntegral <$> getWord32be
130 _ <- getWord32be -- leechers
131 _ <- getWord32be -- seeders
132 ipportpairs <- getIPPortPairs -- [(ip, port)]
133 return $ AnnounceResp tid interval' 0 0 ipportpairs
135 tid <- fromIntegral <$> getWord32be
139 return $ ScrapeResp tid 0 0 0
140 _ -> error ("unknown response action type: " ++ show a)
142 getIPPortPairs :: Get [(IP, Port)]
148 ip <- toIP <$> getByteString 6
149 port <- toPort <$> getByteString 2
150 ipportpairs <- getIPPortPairs
151 return $ (ip, port) : ipportpairs
153 initialTrackerState :: Integer -> IO TState
154 initialTrackerState sz = do
158 return $ TState { currentState = None
159 , connectedPeers = ps
164 -- | Deserialize HTTP tracker response
165 parseTrackerResponse :: BVal -> Either ByteString TrackerResponse
166 parseTrackerResponse resp =
167 case lookup "failure reason" body of
168 Just (Bstr err) -> Left err
169 Just _ -> Left "Unknown failure"
171 let (Just (Bint i)) = lookup "interval" body
172 (Just (Bstr peersBS)) = lookup "peers" body
173 pl = map makePeer (splitN 6 peersBS)
174 in Right TrackerResponse {
178 , incomplete = Nothing
183 toInt :: String -> Integer
186 makePeer :: ByteString -> Peer
187 makePeer peer = Peer "" (toIP ip') (toPort port')
188 where (ip', port') = splitAt 4 peer
190 toPort :: ByteString -> Port
191 toPort = read . ("0x" ++) . unpack . B16.encode
193 toIP :: ByteString -> IP
194 toIP = Data.List.intercalate "." .
195 map (show . toInt . ("0x" ++) . unpack) .
196 splitN 2 . B16.encode
198 --- | URL encode hash as per RFC1738
200 --- REVIEW: Why is this not written in terms of `Network.HTTP.Base.urlEncode` or
201 --- equivalent library function?
202 urlEncodeHash :: ByteString -> String
203 urlEncodeHash bs = concatMap (encode' . unpack) (splitN 2 bs)
204 where encode' b@[c1, c2] = let c = chr (read ("0x" ++ b))
207 escape i c1 c2 | i `elem` nonSpecialChars = [i]
208 | otherwise = "%" ++ [c1] ++ [c2]
210 nonSpecialChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "-_.~"
212 -- | Make arguments that should be posted to tracker.
213 -- This is a separate pure function for testability.
214 mkArgs :: PortNumber -> String -> Integer -> Integer -> Metainfo -> [(String, ByteString)]
215 mkArgs port peer_id up down m =
216 let fileSize = lengthInBytes $ info m
217 bytesLeft = fileSize - down
219 [("info_hash", pack . urlEncodeHash . B16.encode . infoHash $ m),
220 ("peer_id", pack . urlEncode $ peer_id),
221 ("port", pack $ show port),
222 ("uploaded", pack $ show up),
223 ("downloaded", pack $ show down),
224 ("left", pack $ show bytesLeft),
226 ("event", "started")]
228 trackerLoop :: PortNumber -> String -> Metainfo -> TState -> IO ByteString
229 trackerLoop port peerId m st = do
230 up <- readMVar $ uploaded st
231 down <- readMVar $ downloaded st
232 resp <- sendGetRequest (head . announceList $ m) $ mkArgs port peerId up down m
233 case Benc.decode resp of
234 Left e -> return $ pack (show e)
236 case parseTrackerResponse trackerInfo of
239 _ <- threadDelay $ fromIntegral (interval tresp)
240 _ <- putMVar (connectedPeers st) (peers tresp)
241 trackerLoop port peerId m st
244 getResponse :: Socket -> IO UDPResponse
246 -- connect packet is 16 bytes long
247 -- announce packet is atleast 20 bytes long
248 bs <- recv s (16*1024)
249 return $ decode $ fromStrict bs
251 sendRequest :: Socket -> String -> PortNumber -> UDPRequest -> IO ()
252 sendRequest s ip port req = do
253 hostaddr <- inet_addr ip
254 _ <- sendTo s bsReq $ SockAddrInet (fromIntegral port) hostaddr
256 where bsReq = toStrict $ encode req
258 getTrackerType :: String -> TrackerProtocol
259 getTrackerType url | isPrefixOf "http://" url = Http
260 | isPrefixOf "udp://" url = Udp
261 | otherwise = UnknownProtocol
263 udpTrackerLoop :: PortNumber -> String -> Metainfo -> TState -> IO String
264 udpTrackerLoop port peerId m st = do
265 -- h <- connectTo "exodus.desync.com" (PortNumber 6969)
266 s <- socket AF_INET Datagram defaultProtocol
267 hostAddr <- inet_addr "185.37.101.229"
268 putStrLn "connected to tracker"
269 _ <- sendTo s (toStrict $ encode (ConnectReq 42)) (SockAddrInet 2710 hostAddr)
270 putStrLn "--> sent ConnectReq to tracker"
272 putStrLn "<-- recv ConnectResp from tracker"