summaryrefslogtreecommitdiff
path: root/src/Network/BitTorrent
diff options
context:
space:
mode:
Diffstat (limited to 'src/Network/BitTorrent')
-rw-r--r--src/Network/BitTorrent/Client/Swarm.hs2
-rw-r--r--src/Network/BitTorrent/Core/PeerAddr.hs134
-rw-r--r--src/Network/BitTorrent/Exchange/Assembler.hs9
-rw-r--r--src/Network/BitTorrent/Exchange/Wire.hs6
-rw-r--r--src/Network/BitTorrent/Tracker/Message.hs33
5 files changed, 137 insertions, 47 deletions
diff --git a/src/Network/BitTorrent/Client/Swarm.hs b/src/Network/BitTorrent/Client/Swarm.hs
index a9dca048..1901905c 100644
--- a/src/Network/BitTorrent/Client/Swarm.hs
+++ b/src/Network/BitTorrent/Client/Swarm.hs
@@ -43,7 +43,7 @@ getAnnounceQuery Swarm {..} = AnnounceQuery
43 , reqEvent = Nothing 43 , reqEvent = Nothing
44 } 44 }
45 45
46askPeers :: Swarm -> IO [PeerAddr] 46askPeers :: Swarm -> IO [PeerAddr IP]
47askPeers s @ Swarm {..} = do 47askPeers s @ Swarm {..} = do
48 AnnounceInfo {..} <- RPC.announce (getAnnounceQuery s) trackerConn 48 AnnounceInfo {..} <- RPC.announce (getAnnounceQuery s) trackerConn
49 return (getPeerList respPeers) 49 return (getPeerList respPeers)
diff --git a/src/Network/BitTorrent/Core/PeerAddr.hs b/src/Network/BitTorrent/Core/PeerAddr.hs
index 94510bba..1da4c81a 100644
--- a/src/Network/BitTorrent/Core/PeerAddr.hs
+++ b/src/Network/BitTorrent/Core/PeerAddr.hs
@@ -12,12 +12,18 @@
12{-# LANGUAGE StandaloneDeriving #-} 12{-# LANGUAGE StandaloneDeriving #-}
13{-# LANGUAGE GeneralizedNewtypeDeriving #-} 13{-# LANGUAGE GeneralizedNewtypeDeriving #-}
14{-# LANGUAGE DeriveDataTypeable #-} 14{-# LANGUAGE DeriveDataTypeable #-}
15{-# LANGUAGE FlexibleInstances #-}
16{-# LANGUAGE DeriveFunctor #-}
15{-# OPTIONS -fno-warn-orphans #-} -- for PortNumber instances 17{-# OPTIONS -fno-warn-orphans #-} -- for PortNumber instances
16module Network.BitTorrent.Core.PeerAddr 18module Network.BitTorrent.Core.PeerAddr
17 ( -- * Peer address 19 ( -- * Peer address
18 PeerAddr(..) 20 PeerAddr(..)
19 , defaultPorts 21 , defaultPorts
20 , peerSockAddr 22 , peerSockAddr
23 , mergeIPLists
24 , splitIPList
25 , IP, IPv4, IPv6 --re-export Data.IP constructors
26 , IPAddress ()
21 ) where 27 ) where
22 28
23import Control.Applicative 29import Control.Applicative
@@ -38,10 +44,14 @@ import Data.String
38import Data.Typeable 44import Data.Typeable
39import Data.Word 45import Data.Word
40import Data.IP 46import Data.IP
47import Data.Maybe
48import Data.Foldable
49import Data.Either
41import Network.Socket 50import Network.Socket
42import Text.PrettyPrint 51import Text.PrettyPrint
43import Text.PrettyPrint.Class 52import Text.PrettyPrint.Class
44import Text.Read (readMaybe) 53import Text.Read (readMaybe)
54import qualified Text.ParserCombinators.ReadP as RP
45import System.IO.Unsafe 55import System.IO.Unsafe
46 56
47import Data.Torrent.JSON 57import Data.Torrent.JSON
@@ -61,32 +71,74 @@ instance Serialize PortNumber where
61 put = putWord16be . fromIntegral 71 put = putWord16be . fromIntegral
62 {-# INLINE put #-} 72 {-# INLINE put #-}
63 73
74class (Show i, Read i) => IPAddress i where
75 showIp :: i -> String
76 showIp = show
77
78 readIp :: String -> i
79 readIp = read
80
81 toHostAddr :: i -> Either HostAddress HostAddress6
82
83instance IPAddress IPv4 where
84 toHostAddr = Left . toHostAddress
85
86instance IPAddress IPv6 where
87 toHostAddr = Right . toHostAddress6
88
89instance IPAddress IP where
90 toHostAddr (IPv4 ip) = toHostAddr ip
91 toHostAddr (IPv6 ip) = toHostAddr ip
92
93
94deriving instance Typeable IP
95deriving instance Typeable IPv4
96deriving instance Typeable IPv6
97
98ipToBEncode ip = BString $ BS8.pack $ showIp ip
99ipFromBEncode (BString ip) = return $ readIp $ BS8.unpack ip
100
101instance BEncode IP where
102 toBEncode = ipToBEncode
103 fromBEncode = ipFromBEncode
104
105instance BEncode IPv4 where
106 toBEncode = ipToBEncode
107 fromBEncode = ipFromBEncode
108
109instance BEncode IPv6 where
110 toBEncode = ipToBEncode
111 fromBEncode = ipFromBEncode
112
113instance Serialize IPv4 where
114 put ip = put $ toHostAddress ip
115 get = fromHostAddress <$> get
116
117instance Serialize IPv6 where
118 put ip = put $ toHostAddress6 ip
119 get = fromHostAddress6 <$> get
120
64-- TODO check semantic of ord and eq instances 121-- TODO check semantic of ord and eq instances
65-- TODO use SockAddr instead of peerIP and peerPort 122-- TODO use SockAddr instead of peerIP and peerPort
66 123
67-- | Peer address info normally extracted from peer list or peer 124-- | Peer address info normally extracted from peer list or peer
68-- compact list encoding. 125-- compact list encoding.
69data PeerAddr = PeerAddr 126data PeerAddr a = PeerAddr
70 { peerId :: !(Maybe PeerId) 127 { peerId :: !(Maybe PeerId)
71 , peerIP :: {-# UNPACK #-} !IP 128 , peerAddr :: a
72 , peerPort :: {-# UNPACK #-} !PortNumber 129 , peerPort :: {-# UNPACK #-} !PortNumber
73 } deriving (Show, Eq, Typeable) 130 } deriving (Show, Eq, Typeable, Functor)
74
75instance BEncode IP where
76 toBEncode ip = toBEncode $ BS8.pack $ show ip
77 fromBEncode (BString ip) = return $ fromString $ BS8.unpack ip
78 131
79peer_id_key, peer_ip_key, peer_port_key :: BKey 132peer_id_key, peer_ip_key, peer_port_key :: BKey
80peer_id_key = "peer id" 133peer_id_key = "peer id"
81peer_ip_key = "ip" 134peer_ip_key = "ip"
82peer_port_key = "port" 135peer_port_key = "port"
83 136
84-- FIXME do we need to byteswap peerIP in bencode instance?
85-- | The tracker's 'announce response' compatible encoding. 137-- | The tracker's 'announce response' compatible encoding.
86instance BEncode PeerAddr where 138instance (Typeable a, BEncode a) => BEncode (PeerAddr a) where
87 toBEncode PeerAddr {..} = toDict $ 139 toBEncode PeerAddr {..} = toDict $
88 peer_id_key .=? peerId 140 peer_id_key .=? peerId
89 .: peer_ip_key .=! BS8.pack (show peerIP) 141 .: peer_ip_key .=! peerAddr
90 .: peer_port_key .=! peerPort 142 .: peer_port_key .=! peerPort
91 .: endDict 143 .: endDict
92 144
@@ -95,19 +147,32 @@ instance BEncode PeerAddr where
95 <*>! peer_ip_key 147 <*>! peer_ip_key
96 <*>! peer_port_key 148 <*>! peer_port_key
97 149
150mergeIPLists :: [PeerAddr IPv4] -> Maybe [PeerAddr IPv6] -> [PeerAddr IP]
151mergeIPLists v4 v6 = (fmap IPv4 `L.map` v4)
152 ++ (fmap IPv6 `L.map` Data.Foldable.concat v6)
153
154splitIPList :: [PeerAddr IP] -> ([PeerAddr IPv4],[PeerAddr IPv6])
155splitIPList xs = partitionEithers $ toEither <$> xs
156 where
157 toEither :: PeerAddr IP -> Either (PeerAddr IPv4) (PeerAddr IPv6)
158 toEither pa@(PeerAddr _ (IPv4 _) _) = Left (ipv4 <$> pa)
159 toEither pa@(PeerAddr _ (IPv6 _) _) = Right (ipv6 <$> pa)
160
161
98-- | The tracker's 'compact peer list' compatible encoding. The 162-- | The tracker's 'compact peer list' compatible encoding. The
99-- 'peerId' is always 'Nothing'. 163-- 'peerId' is always 'Nothing'.
100-- 164--
101-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html> 165-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
102-- 166--
103instance Serialize PeerAddr where -- TODO do it properly 167-- TODO: test byte order
104 put PeerAddr {..} = (putWord32host $ toHostAddress $ ipv4 peerIP) >> put peerPort 168instance (Serialize a) => Serialize (PeerAddr a) where
105 {-# INLINE put #-} 169 put PeerAddr {..} =
106 get = PeerAddr Nothing <$> (IPv4 . fromHostAddress <$> getWord32host) <*> get 170 put peerAddr >> put peerPort
107 {-# INLINE get #-} 171 get =
172 PeerAddr Nothing <$> get <*> get
108 173
109-- | @127.0.0.1:6881@ 174-- | @127.0.0.1:6881@
110instance Default PeerAddr where 175instance Default (PeerAddr IPv4) where
111 def = "127.0.0.1:6881" 176 def = "127.0.0.1:6881"
112 177
113-- inet_addr is pure; so it is safe to throw IO 178-- inet_addr is pure; so it is safe to throw IO
@@ -122,30 +187,49 @@ unsafeCatchIO m = unsafePerformIO $
122-- 187--
123-- @peerPort \"127.0.0.1:6881\" == 6881@ 188-- @peerPort \"127.0.0.1:6881\" == 6881@
124-- 189--
125instance IsString PeerAddr where 190instance IsString (PeerAddr IPv4) where
126 fromString str -- TODO IPv6 191 fromString str
127 | [hostAddrStr, portStr] <- splitWhen (== ':') str 192 | [hostAddrStr, portStr] <- splitWhen (== ':') str
128 , Just hostAddr <- read hostAddrStr 193 , hostAddr <- read hostAddrStr
129 , Just portNum <- toEnum <$> readMaybe portStr 194 , Just portNum <- toEnum <$> readMaybe portStr
130 = PeerAddr Nothing hostAddr portNum 195 = PeerAddr Nothing hostAddr portNum
131 | otherwise = error $ "fromString: unable to parse PeerAddr: " ++ str 196 | otherwise = error $ "fromString: unable to parse (PeerAddr IPv4): " ++ str
197
198readsIPv6_port :: String -> [((IPv6, PortNumber), String)]
199readsIPv6_port = RP.readP_to_S $ do
200 ip <- RP.char '[' *> (RP.readS_to_P reads) <* RP.char ']'
201 RP.char ':'
202 port <- toEnum <$> read <$> (RP.many1 $ RP.satisfy isDigit) <* RP.eof
203 return (ip,port)
204
205instance IsString (PeerAddr IPv6) where
206 fromString str
207 | [((ip,port),"")] <- readsIPv6_port str =
208 PeerAddr Nothing ip port
209 | otherwise = error $ "fromString: unable to parse (PeerAddr IPv6): " ++ str
132 210
133-- | fingerprint + "at" + dotted.host.inet.addr:port 211-- | fingerprint + "at" + dotted.host.inet.addr:port
134instance Pretty PeerAddr where 212-- TODO: instances for IPv6, HostName
213instance Pretty (PeerAddr IP) where
135 pretty p @ PeerAddr {..} 214 pretty p @ PeerAddr {..}
136 | Just pid <- peerId = pretty (fingerprint pid) <+> "at" <+> paddr 215 | Just pid <- peerId = pretty (fingerprint pid) <+> "at" <+> paddr
137 | otherwise = paddr 216 | otherwise = paddr
138 where 217 where
139 paddr = text (show (peerSockAddr p)) 218 paddr = text (show peerAddr ++ ":" ++ show peerPort)
140 219
141-- | Ports typically reserved for bittorrent P2P listener. 220-- | Ports typically reserved for bittorrent P2P listener.
142defaultPorts :: [PortNumber] 221defaultPorts :: [PortNumber]
143defaultPorts = [6881..6889] 222defaultPorts = [6881..6889]
144 223
224resolvePeerAddr :: (IPAddress i) => PeerAddr HostName -> PeerAddr i
225resolvePeerAddr = undefined
226
145-- | Convert peer info from tracker response to socket address. Used 227-- | Convert peer info from tracker response to socket address. Used
146-- for establish connection between peers. 228-- for establish connection between peers.
147-- 229--
148peerSockAddr :: PeerAddr -> SockAddr 230peerSockAddr :: (IPAddress i) => PeerAddr i -> SockAddr
149peerSockAddr PeerAddr {..} 231peerSockAddr PeerAddr {..}
150 | IPv4 v4 <- peerIP = SockAddrInet peerPort (toHostAddress v4) 232 | Left hAddr <- toHostAddr peerAddr =
151 | IPv6 v6 <- peerIP = SockAddrInet6 peerPort 0 (toHostAddress6 v6) 0 233 SockAddrInet peerPort hAddr
234 | Right hAddr <- toHostAddr peerAddr =
235 SockAddrInet6 peerPort 0 hAddr 0
diff --git a/src/Network/BitTorrent/Exchange/Assembler.hs b/src/Network/BitTorrent/Exchange/Assembler.hs
index 5dc7c5ca..aa009f49 100644
--- a/src/Network/BitTorrent/Exchange/Assembler.hs
+++ b/src/Network/BitTorrent/Exchange/Assembler.hs
@@ -65,6 +65,7 @@ import Data.IntMap.Strict as IM
65import Data.List as L 65import Data.List as L
66import Data.Map as M 66import Data.Map as M
67import Data.Maybe 67import Data.Maybe
68import Data.IP
68 69
69import Data.Torrent.Piece 70import Data.Torrent.Piece
70import Network.BitTorrent.Core 71import Network.BitTorrent.Core
@@ -79,7 +80,7 @@ type PieceMap = IntMap
79 80
80data Assembler = Assembler 81data Assembler = Assembler
81 { -- | A set of blocks that have been 'Request'ed but not yet acked. 82 { -- | A set of blocks that have been 'Request'ed but not yet acked.
82 _inflight :: Map PeerAddr (PieceMap [BlockRange]) 83 _inflight :: Map (PeerAddr IP) (PieceMap [BlockRange])
83 84
84 -- | A set of blocks that but not yet assembled. 85 -- | A set of blocks that but not yet assembled.
85 , _pending :: PieceMap Bucket 86 , _pending :: PieceMap Bucket
@@ -114,7 +115,7 @@ allowPiece pix a @ Assembler {..} = over pending (IM.insert pix bkt) a
114 where 115 where
115 bkt = B.empty (piPieceLength info) 116 bkt = B.empty (piPieceLength info)
116 117
117allowedSet :: PeerAddr -> Assembler -> [BlockIx] 118allowedSet :: (PeerAddr IP) -> Assembler -> [BlockIx]
118allowedSet = undefined 119allowedSet = undefined
119 120
120--inflight :: PeerAddr -> BlockIx -> Assembler -> Assembler 121--inflight :: PeerAddr -> BlockIx -> Assembler -> Assembler
@@ -123,7 +124,7 @@ allowedSet = undefined
123-- You should check if a returned by peer block is actually have 124-- You should check if a returned by peer block is actually have
124-- been requested and in-flight. This is needed to avoid "I send 125-- been requested and in-flight. This is needed to avoid "I send
125-- random corrupted block" attacks. 126-- random corrupted block" attacks.
126insert :: PeerAddr -> Block a -> Assembler -> Assembler 127insert :: (PeerAddr IP) -> Block a -> Assembler -> Assembler
127insert = undefined 128insert = undefined
128 129
129{- 130{-
@@ -156,4 +157,4 @@ inserta :: Block a
156 -> (PieceMap [Block a], Maybe (Piece a)) 157 -> (PieceMap [Block a], Maybe (Piece a))
157inserta = undefined 158inserta = undefined
158 159
159-} \ No newline at end of file 160-}
diff --git a/src/Network/BitTorrent/Exchange/Wire.hs b/src/Network/BitTorrent/Exchange/Wire.hs
index ae9babb3..27b4be12 100644
--- a/src/Network/BitTorrent/Exchange/Wire.hs
+++ b/src/Network/BitTorrent/Exchange/Wire.hs
@@ -505,7 +505,7 @@ initiateHandshake sock hs = do
505 recvHandshake sock 505 recvHandshake sock
506 506
507-- | Tries to connect to peer using reasonable default parameters. 507-- | Tries to connect to peer using reasonable default parameters.
508connectToPeer :: PeerAddr -> IO Socket 508connectToPeer :: (IPAddress i) => PeerAddr i -> IO Socket
509connectToPeer p = do 509connectToPeer p = do
510 sock <- socket AF_INET Stream Network.Socket.defaultProtocol 510 sock <- socket AF_INET Stream Network.Socket.defaultProtocol
511 connect sock (peerSockAddr p) 511 connect sock (peerSockAddr p)
@@ -628,7 +628,7 @@ reconnect = undefined
628-- 628--
629-- This function can throw 'WireFailure' exception. 629-- This function can throw 'WireFailure' exception.
630-- 630--
631connectWire :: Handshake -> PeerAddr -> ExtendedCaps -> Wire () -> IO () 631connectWire :: (IPAddress i) => Handshake -> PeerAddr i -> ExtendedCaps -> Wire () -> IO ()
632connectWire hs addr extCaps wire = 632connectWire hs addr extCaps wire =
633 bracket (connectToPeer addr) close $ \ sock -> do 633 bracket (connectToPeer addr) close $ \ sock -> do
634 hs' <- initiateHandshake sock hs 634 hs' <- initiateHandshake sock hs
@@ -673,7 +673,7 @@ connectWire hs addr extCaps wire =
673-- 673--
674-- This function can throw 'WireFailure' exception. 674-- This function can throw 'WireFailure' exception.
675-- 675--
676acceptWire :: Socket -> PeerAddr -> Wire () -> IO () 676acceptWire :: (IPAddress i) => Socket -> PeerAddr i -> Wire () -> IO ()
677acceptWire sock peerAddr wire = do 677acceptWire sock peerAddr wire = do
678 bracket (return sock) close $ \ _ -> do 678 bracket (return sock) close $ \ _ -> do
679 error "acceptWire: not implemented" 679 error "acceptWire: not implemented"
diff --git a/src/Network/BitTorrent/Tracker/Message.hs b/src/Network/BitTorrent/Tracker/Message.hs
index 6249cdc4..95b9c7ca 100644
--- a/src/Network/BitTorrent/Tracker/Message.hs
+++ b/src/Network/BitTorrent/Tracker/Message.hs
@@ -22,6 +22,8 @@
22{-# LANGUAGE GeneralizedNewtypeDeriving #-} 22{-# LANGUAGE GeneralizedNewtypeDeriving #-}
23{-# LANGUAGE TemplateHaskell #-} 23{-# LANGUAGE TemplateHaskell #-}
24{-# LANGUAGE DeriveDataTypeable #-} 24{-# LANGUAGE DeriveDataTypeable #-}
25{-# LANGUAGE DeriveFunctor #-}
26{-# LANGUAGE ScopedTypeVariables #-}
25{-# OPTIONS -fno-warn-orphans #-} 27{-# OPTIONS -fno-warn-orphans #-}
26module Network.BitTorrent.Tracker.Message 28module Network.BitTorrent.Tracker.Message
27 ( -- * Announce 29 ( -- * Announce
@@ -83,6 +85,7 @@ import Data.Text (Text)
83import Data.Text.Encoding 85import Data.Text.Encoding
84import Data.Typeable 86import Data.Typeable
85import Data.Word 87import Data.Word
88import Data.IP
86import Network 89import Network
87import Network.HTTP.Types.QueryLike 90import Network.HTTP.Types.QueryLike
88import Network.HTTP.Types.URI hiding (urlEncode) 91import Network.HTTP.Types.URI hiding (urlEncode)
@@ -431,18 +434,18 @@ renderAnnounceRequest = queryToSimpleQuery . toQuery
431-- 434--
432-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html> 435-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
433-- 436--
434data PeerList 437data PeerList a
435 = PeerList { getPeerList :: [PeerAddr] } 438 = PeerList { getPeerList :: [PeerAddr a] }
436 | CompactPeerList { getPeerList :: [PeerAddr] } 439 | CompactPeerList { getPeerList :: [PeerAddr a] }
437 deriving (Show, Eq, Typeable) 440 deriving (Show, Eq, Typeable, Functor)
438 441
439putCompactPeerList :: S.Putter [PeerAddr] 442putCompactPeerList :: (Serialize a) => S.Putter [PeerAddr a]
440putCompactPeerList = mapM_ put 443putCompactPeerList = mapM_ put
441 444
442getCompactPeerList :: S.Get [PeerAddr] 445getCompactPeerList :: (Serialize a) => S.Get [PeerAddr a]
443getCompactPeerList = many get 446getCompactPeerList = many get
444 447
445instance BEncode PeerList where 448instance (Typeable a, BEncode a, Serialize a) => BEncode (PeerList a) where
446 toBEncode (PeerList xs) = toBEncode xs 449 toBEncode (PeerList xs) = toBEncode xs
447 toBEncode (CompactPeerList xs) = toBEncode $ runPut (putCompactPeerList xs) 450 toBEncode (CompactPeerList xs) = toBEncode $ runPut (putCompactPeerList xs)
448 451
@@ -473,7 +476,7 @@ data AnnounceInfo =
473 , respMinInterval :: !(Maybe Int) 476 , respMinInterval :: !(Maybe Int)
474 477
475 -- | Peers that must be contacted. 478 -- | Peers that must be contacted.
476 , respPeers :: !PeerList 479 , respPeers :: !(PeerList IP)
477 480
478 -- | Human readable warning. 481 -- | Human readable warning.
479 , respWarning :: !(Maybe Text) 482 , respWarning :: !(Maybe Text)
@@ -490,19 +493,21 @@ instance BEncode AnnounceInfo where
490 .: "incomplete" .=? respIncomplete 493 .: "incomplete" .=? respIncomplete
491 .: "interval" .=! respInterval 494 .: "interval" .=! respInterval
492 .: "min interval" .=? respMinInterval 495 .: "min interval" .=? respMinInterval
493 .: "peers" .=! respPeers 496 .: "peers" .=! peers
497 .: "peers6" .=! peers6
494 .: "warning message" .=? respWarning 498 .: "warning message" .=? respWarning
495 .: endDict 499 .: endDict
500 where (peers,peers6) = splitIPList $ getPeerList respPeers
496 501
497 fromBEncode (BDict d) 502 fromBEncode (BDict d)
498 | Just t <- BE.lookup "failure reason" d = Failure <$> fromBEncode t 503 | Just t <- BE.lookup "failure reason" d = Failure <$> fromBEncode t
499 | otherwise = (`fromDict` (BDict d)) $ do 504 | otherwise = (`fromDict` (BDict d)) $
500 AnnounceInfo 505 AnnounceInfo
501 <$>? "complete" 506 <$>? "complete"
502 <*>? "incomplete" 507 <*>? "incomplete"
503 <*>! "interval" 508 <*>! "interval"
504 <*>? "min interval" 509 <*>? "min interval"
505 <*>! "peers" 510 <*> (PeerList <$> (mergeIPLists <$>! "peers" <*>? "peers6"))
506 <*>? "warning message" 511 <*>? "warning message"
507 fromBEncode _ = decodingError "Announce info" 512 fromBEncode _ = decodingError "Announce info"
508 513
@@ -513,13 +518,13 @@ instance Serialize AnnounceInfo where
513 putWord32be $ fromIntegral respInterval 518 putWord32be $ fromIntegral respInterval
514 putWord32be $ fromIntegral $ fromMaybe 0 respIncomplete 519 putWord32be $ fromIntegral $ fromMaybe 0 respIncomplete
515 putWord32be $ fromIntegral $ fromMaybe 0 respComplete 520 putWord32be $ fromIntegral $ fromMaybe 0 respComplete
516 forM_ (getPeerList respPeers) put 521 forM_ (fmap ipv4 <$> getPeerList respPeers) put
517 522
518 get = do 523 get = do
519 interval <- getWord32be 524 interval <- getWord32be
520 leechers <- getWord32be 525 leechers <- getWord32be
521 seeders <- getWord32be 526 seeders <- getWord32be
522 peers <- many get 527 peers <- many $ fmap IPv4 <$> get
523 528
524 return $ AnnounceInfo { 529 return $ AnnounceInfo {
525 respWarning = Nothing 530 respWarning = Nothing