summaryrefslogtreecommitdiff
path: root/src/Network/BitTorrent
diff options
context:
space:
mode:
Diffstat (limited to 'src/Network/BitTorrent')
-rw-r--r--src/Network/BitTorrent/Address.hs1254
-rw-r--r--src/Network/BitTorrent/Client.hs195
-rw-r--r--src/Network/BitTorrent/Client/Handle.hs188
-rw-r--r--src/Network/BitTorrent/Client/Types.hs163
-rw-r--r--src/Network/BitTorrent/DHT.hs285
-rw-r--r--src/Network/BitTorrent/DHT/ContactInfo.hs138
-rw-r--r--src/Network/BitTorrent/DHT/Message.hs343
-rw-r--r--src/Network/BitTorrent/DHT/Query.hs325
-rw-r--r--src/Network/BitTorrent/DHT/Readme.md13
-rw-r--r--src/Network/BitTorrent/DHT/Routing.hs565
-rw-r--r--src/Network/BitTorrent/DHT/Session.hs465
-rw-r--r--src/Network/BitTorrent/DHT/Token.hs121
-rw-r--r--src/Network/BitTorrent/Exchange.hs35
-rw-r--r--src/Network/BitTorrent/Exchange/Bitfield.hs399
-rw-r--r--src/Network/BitTorrent/Exchange/Block.hs369
-rw-r--r--src/Network/BitTorrent/Exchange/Connection.hs1012
-rw-r--r--src/Network/BitTorrent/Exchange/Download.hs296
-rw-r--r--src/Network/BitTorrent/Exchange/Manager.hs62
-rw-r--r--src/Network/BitTorrent/Exchange/Message.hs1232
-rw-r--r--src/Network/BitTorrent/Exchange/Session.hs586
-rw-r--r--src/Network/BitTorrent/Internal/Cache.hs169
-rw-r--r--src/Network/BitTorrent/Internal/Progress.hs154
-rw-r--r--src/Network/BitTorrent/Internal/Types.hs10
-rw-r--r--src/Network/BitTorrent/Readme.md10
-rw-r--r--src/Network/BitTorrent/Tracker.hs50
-rw-r--r--src/Network/BitTorrent/Tracker/List.hs193
-rw-r--r--src/Network/BitTorrent/Tracker/Message.hs920
-rw-r--r--src/Network/BitTorrent/Tracker/RPC.hs175
-rw-r--r--src/Network/BitTorrent/Tracker/RPC/HTTP.hs191
-rw-r--r--src/Network/BitTorrent/Tracker/RPC/UDP.hs454
-rw-r--r--src/Network/BitTorrent/Tracker/Session.hs306
31 files changed, 10678 insertions, 0 deletions
diff --git a/src/Network/BitTorrent/Address.hs b/src/Network/BitTorrent/Address.hs
new file mode 100644
index 00000000..a72a8a07
--- /dev/null
+++ b/src/Network/BitTorrent/Address.hs
@@ -0,0 +1,1254 @@
1-- |
2-- Module : Network.BitTorrent.Address
3-- Copyright : (c) Sam Truzjan 2013
4-- (c) Daniel Gröber 2013
5-- License : BSD3
6-- Maintainer : pxqr.sta@gmail.com
7-- Stability : provisional
8-- Portability : portable
9--
10-- Peer and Node addresses.
11--
12{-# LANGUAGE CPP #-}
13{-# LANGUAGE FlexibleInstances #-}
14{-# LANGUAGE RecordWildCards #-}
15{-# LANGUAGE StandaloneDeriving #-}
16{-# LANGUAGE ViewPatterns #-}
17{-# LANGUAGE GeneralizedNewtypeDeriving #-}
18{-# LANGUAGE MultiParamTypeClasses #-}
19{-# LANGUAGE DeriveDataTypeable #-}
20{-# LANGUAGE DeriveFunctor #-}
21{-# LANGUAGE DeriveFoldable #-}
22{-# LANGUAGE DeriveTraversable #-}
23{-# LANGUAGE TemplateHaskell #-}
24{-# OPTIONS -fno-warn-orphans #-}
25module Network.BitTorrent.Address
26 ( -- * Address
27 Address (..)
28 , fromAddr
29
30 -- ** IP
31 , IPv4
32 , IPv6
33 , IP (..)
34
35 -- * PeerId
36 -- $peer-id
37 , PeerId
38
39 -- ** Generation
40 , genPeerId
41 , timestamp
42 , entropy
43
44 -- ** Encoding
45 , azureusStyle
46 , shadowStyle
47 , defaultClientId
48 , defaultVersionNumber
49
50 -- * PeerAddr
51 -- $peer-addr
52 , PeerAddr(..)
53 , defaultPorts
54 , peerSockAddr
55 , peerSocket
56
57 -- * Node
58 -- ** Id
59 , NodeId(..)
60 , nodeIdSize
61 , testIdBit
62 , NodeDistance
63 , distance
64 , genNodeId
65 , bucketRange
66 , genBucketSample
67 , bep42
68 , bep42s
69
70 -- ** Info
71 , NodeAddr (..)
72 , NodeInfo (..)
73 , rank
74
75 -- * Fingerprint
76 -- $fingerprint
77 , Software (..)
78 , Fingerprint (..)
79 , libFingerprint
80 , fingerprint
81
82 -- * Utils
83 , libUserAgent
84 ) where
85
86import Control.Applicative
87import Control.Monad
88import Data.BEncode as BE
89import Data.BEncode as BS
90import Data.BEncode.BDict (BKey)
91import Data.Bits
92import Data.ByteString as BS
93import Data.ByteString.Internal as BS
94import Data.ByteString.Base16 as Base16
95import Data.ByteString.Char8 as BC
96import Data.ByteString.Char8 as BS8
97import qualified Data.ByteString.Lazy as BL
98import qualified Data.ByteString.Lazy.Builder as BS
99import Data.Char
100import Data.Convertible
101import Data.Default
102import Data.IP
103import Data.List as L
104import Data.List.Split as L
105import Data.Maybe (fromMaybe, catMaybes, mapMaybe)
106import Data.Monoid
107import Data.Hashable
108import Data.Ord
109import Data.Serialize as S
110import Data.String
111import Data.Time
112import Data.Typeable
113import Data.Version
114import Data.Word
115import qualified Text.ParserCombinators.ReadP as RP
116import Text.Read (readMaybe)
117import Network.HTTP.Types.QueryLike
118import Network.Socket
119import Text.PrettyPrint as PP hiding ((<>))
120import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
121#if !MIN_VERSION_time(1,5,0)
122import System.Locale (defaultTimeLocale)
123#endif
124import System.Entropy
125import Data.Digest.CRC32C
126
127-- import Paths_bittorrent (version)
128
129{-----------------------------------------------------------------------
130-- Address
131-----------------------------------------------------------------------}
132
133instance Pretty UTCTime where
134 pPrint = PP.text . show
135
136class (Eq a, Serialize a, Typeable a, Hashable a, Pretty a)
137 => Address a where
138 toSockAddr :: a -> SockAddr
139 fromSockAddr :: SockAddr -> Maybe a
140
141fromAddr :: (Address a, Address b) => a -> Maybe b
142fromAddr = fromSockAddr . toSockAddr
143
144-- | Note that port is zeroed.
145instance Address IPv4 where
146 toSockAddr = SockAddrInet 0 . toHostAddress
147 fromSockAddr (SockAddrInet _ h) = Just (fromHostAddress h)
148 fromSockAddr _ = Nothing
149
150-- | Note that port is zeroed.
151instance Address IPv6 where
152 toSockAddr h = SockAddrInet6 0 0 (toHostAddress6 h) 0
153 fromSockAddr (SockAddrInet6 _ _ h _) = Just (fromHostAddress6 h)
154 fromSockAddr _ = Nothing
155
156-- | Note that port is zeroed.
157instance Address IP where
158 toSockAddr (IPv4 h) = toSockAddr h
159 toSockAddr (IPv6 h) = toSockAddr h
160 fromSockAddr sa =
161 IPv4 <$> fromSockAddr sa
162 <|> IPv6 <$> fromSockAddr sa
163
164setPort :: PortNumber -> SockAddr -> SockAddr
165setPort port (SockAddrInet _ h ) = SockAddrInet port h
166setPort port (SockAddrInet6 _ f h s) = SockAddrInet6 port f h s
167setPort _ addr = addr
168{-# INLINE setPort #-}
169
170getPort :: SockAddr -> Maybe PortNumber
171getPort (SockAddrInet p _ ) = Just p
172getPort (SockAddrInet6 p _ _ _) = Just p
173getPort _ = Nothing
174{-# INLINE getPort #-}
175
176instance Address a => Address (NodeAddr a) where
177 toSockAddr NodeAddr {..} = setPort nodePort $ toSockAddr nodeHost
178 fromSockAddr sa = NodeAddr <$> fromSockAddr sa <*> getPort sa
179
180instance Address a => Address (PeerAddr a) where
181 toSockAddr PeerAddr {..} = setPort peerPort $ toSockAddr peerHost
182 fromSockAddr sa = PeerAddr Nothing <$> fromSockAddr sa <*> getPort sa
183
184{-----------------------------------------------------------------------
185-- Peer id
186-----------------------------------------------------------------------}
187-- $peer-id
188--
189-- 'PeerID' represent self assigned peer identificator. Ideally each
190-- host in the network should have unique peer id to avoid
191-- collisions, therefore for peer ID generation we use good entropy
192-- source. Peer ID is sent in /tracker request/, sent and received in
193-- /peer handshakes/ and used in DHT queries.
194--
195
196-- TODO use unpacked Word160 form (length is known statically)
197
198-- | Peer identifier is exactly 20 bytes long bytestring.
199newtype PeerId = PeerId { getPeerId :: ByteString }
200 deriving (Show, Eq, Ord, BEncode, Typeable)
201
202peerIdLen :: Int
203peerIdLen = 20
204
205-- | For testing purposes only.
206instance Default PeerId where
207 def = azureusStyle defaultClientId defaultVersionNumber ""
208
209instance Hashable PeerId where
210 hashWithSalt = hashUsing getPeerId
211 {-# INLINE hashWithSalt #-}
212
213instance Serialize PeerId where
214 put = putByteString . getPeerId
215 get = PeerId <$> getBytes peerIdLen
216
217instance QueryValueLike PeerId where
218 toQueryValue (PeerId pid) = Just pid
219 {-# INLINE toQueryValue #-}
220
221instance IsString PeerId where
222 fromString str
223 | BS.length bs == peerIdLen = PeerId bs
224 | otherwise = error $ "Peer id should be 20 bytes long: " ++ show str
225 where
226 bs = fromString str
227
228instance Pretty PeerId where
229 pPrint = text . BC.unpack . getPeerId
230
231instance Convertible BS.ByteString PeerId where
232 safeConvert bs
233 | BS.length bs == peerIdLen = pure (PeerId bs)
234 | otherwise = convError "invalid length" bs
235
236------------------------------------------------------------------------
237
238-- | Pad bytestring so it's becomes exactly request length. Conversion
239-- is done like so:
240--
241-- * length < size: Complete bytestring by given charaters.
242--
243-- * length = size: Output bytestring as is.
244--
245-- * length > size: Drop last (length - size) charaters from a
246-- given bytestring.
247--
248byteStringPadded :: ByteString -- ^ bytestring to be padded.
249 -> Int -- ^ size of result builder.
250 -> Char -- ^ character used for padding.
251 -> BS.Builder
252byteStringPadded bs s c =
253 BS.byteString (BS.take s bs) <>
254 BS.byteString (BC.replicate padLen c)
255 where
256 padLen = s - min (BS.length bs) s
257
258-- | Azureus-style encoding have the following layout:
259--
260-- * 1 byte : '-'
261--
262-- * 2 bytes: client id
263--
264-- * 4 bytes: version number
265--
266-- * 1 byte : '-'
267--
268-- * 12 bytes: random number
269--
270azureusStyle :: ByteString -- ^ 2 character client ID, padded with 'H'.
271 -> ByteString -- ^ Version number, padded with 'X'.
272 -> ByteString -- ^ Random number, padded with '0'.
273 -> PeerId -- ^ Azureus-style encoded peer ID.
274azureusStyle cid ver rnd = PeerId $ BL.toStrict $ BS.toLazyByteString $
275 BS.char8 '-' <>
276 byteStringPadded cid 2 'H' <>
277 byteStringPadded ver 4 'X' <>
278 BS.char8 '-' <>
279 byteStringPadded rnd 12 '0'
280
281-- | Shadow-style encoding have the following layout:
282--
283-- * 1 byte : client id.
284--
285-- * 0-4 bytes: version number. If less than 4 then padded with
286-- '-' char.
287--
288-- * 15 bytes : random number. If length is less than 15 then
289-- padded with '0' char.
290--
291shadowStyle :: Char -- ^ Client ID.
292 -> ByteString -- ^ Version number.
293 -> ByteString -- ^ Random number.
294 -> PeerId -- ^ Shadow style encoded peer ID.
295shadowStyle cid ver rnd = PeerId $ BL.toStrict $ BS.toLazyByteString $
296 BS.char8 cid <>
297 byteStringPadded ver 4 '-' <>
298 byteStringPadded rnd 15 '0'
299
300
301-- | 'HS'- 2 bytes long client identifier.
302defaultClientId :: ByteString
303defaultClientId = "HS"
304
305-- | Gives exactly 4 bytes long version number for any version of the
306-- package. Version is taken from .cabal file.
307defaultVersionNumber :: ByteString
308defaultVersionNumber = BS.take 4 $ BC.pack $ foldMap show $
309 versionBranch myVersion
310 where
311 Fingerprint _ myVersion = libFingerprint
312
313------------------------------------------------------------------------
314
315-- | Gives 15 characters long decimal timestamp such that:
316--
317-- * 6 bytes : first 6 characters from picoseconds obtained with %q.
318--
319-- * 1 byte : character \'.\' for readability.
320--
321-- * 9..* bytes: number of whole seconds since the Unix epoch
322-- (!)REVERSED.
323--
324-- Can be used both with shadow and azureus style encoding. This
325-- format is used to make the ID's readable for debugging purposes.
326--
327timestamp :: IO ByteString
328timestamp = (BC.pack . format) <$> getCurrentTime
329 where
330 format t = L.take 6 (formatTime defaultTimeLocale "%q" t) ++ "." ++
331 L.take 9 (L.reverse (formatTime defaultTimeLocale "%s" t))
332
333-- | Gives 15 character long random bytestring. This is more robust
334-- method for generation of random part of peer ID than 'timestamp'.
335entropy :: IO ByteString
336entropy = getEntropy 15
337
338-- NOTE: entropy generates incorrrect peer id
339
340-- | Here we use 'azureusStyle' encoding with the following args:
341--
342-- * 'HS' for the client id; ('defaultClientId')
343--
344-- * Version of the package for the version number;
345-- ('defaultVersionNumber')
346--
347-- * UTC time day ++ day time for the random number. ('timestamp')
348--
349genPeerId :: IO PeerId
350genPeerId = azureusStyle defaultClientId defaultVersionNumber <$> timestamp
351
352{-----------------------------------------------------------------------
353-- Peer Addr
354-----------------------------------------------------------------------}
355-- $peer-addr
356--
357-- 'PeerAddr' is used to represent peer address. Currently it's
358-- just peer IP and peer port but this might change in future.
359--
360
361{-----------------------------------------------------------------------
362-- Port number
363-----------------------------------------------------------------------}
364
365instance BEncode PortNumber where
366 toBEncode = toBEncode . fromEnum
367 fromBEncode = fromBEncode >=> portNumber
368 where
369 portNumber :: Integer -> BS.Result PortNumber
370 portNumber n
371 | 0 <= n && n <= fromIntegral (maxBound :: Word16)
372 = pure $ fromIntegral n
373 | otherwise = decodingError $ "PortNumber: " ++ show n
374
375instance Serialize PortNumber where
376 get = fromIntegral <$> getWord16be
377 {-# INLINE get #-}
378 put = putWord16be . fromIntegral
379 {-# INLINE put #-}
380
381instance Hashable PortNumber where
382 hashWithSalt s = hashWithSalt s . fromEnum
383 {-# INLINE hashWithSalt #-}
384
385instance Pretty PortNumber where
386 pPrint = PP.int . fromEnum
387 {-# INLINE pPrint #-}
388
389{-----------------------------------------------------------------------
390-- IP addr
391-----------------------------------------------------------------------}
392
393class IPAddress i where
394 toHostAddr :: i -> Either HostAddress HostAddress6
395
396instance IPAddress IPv4 where
397 toHostAddr = Left . toHostAddress
398 {-# INLINE toHostAddr #-}
399
400instance IPAddress IPv6 where
401 toHostAddr = Right . toHostAddress6
402 {-# INLINE toHostAddr #-}
403
404instance IPAddress IP where
405 toHostAddr (IPv4 ip) = toHostAddr ip
406 toHostAddr (IPv6 ip) = toHostAddr ip
407 {-# INLINE toHostAddr #-}
408
409deriving instance Typeable IP
410deriving instance Typeable IPv4
411deriving instance Typeable IPv6
412
413ipToBEncode :: Show i => i -> BValue
414ipToBEncode ip = BString $ BS8.pack $ show ip
415{-# INLINE ipToBEncode #-}
416
417ipFromBEncode :: Read a => BValue -> BS.Result a
418ipFromBEncode (BString (BS8.unpack -> ipStr))
419 | Just ip <- readMaybe (ipStr) = pure ip
420 | otherwise = decodingError $ "IP: " ++ ipStr
421ipFromBEncode _ = decodingError $ "IP: addr should be a bstring"
422
423instance BEncode IP where
424 toBEncode = ipToBEncode
425 {-# INLINE toBEncode #-}
426 fromBEncode = ipFromBEncode
427 {-# INLINE fromBEncode #-}
428
429instance BEncode IPv4 where
430 toBEncode = ipToBEncode
431 {-# INLINE toBEncode #-}
432 fromBEncode = ipFromBEncode
433 {-# INLINE fromBEncode #-}
434
435instance BEncode IPv6 where
436 toBEncode = ipToBEncode
437 {-# INLINE toBEncode #-}
438 fromBEncode = ipFromBEncode
439 {-# INLINE fromBEncode #-}
440
441-- | When 'get'ing an IP it must be 'isolate'd to the appropriate
442-- number of bytes since we have no other way of telling which
443-- address type we are trying to parse
444instance Serialize IP where
445 put (IPv4 ip) = put ip
446 put (IPv6 ip) = put ip
447
448 get = do
449 n <- remaining
450 case n of
451 4 -> IPv4 <$> get
452 16 -> IPv6 <$> get
453 _ -> fail (show n ++ " is the wrong number of remaining bytes to parse IP")
454
455instance Serialize IPv4 where
456 put = putWord32host . toHostAddress
457 get = fromHostAddress <$> getWord32host
458
459instance Serialize IPv6 where
460 put ip = put $ toHostAddress6 ip
461 get = fromHostAddress6 <$> get
462
463instance Pretty IPv4 where
464 pPrint = PP.text . show
465 {-# INLINE pPrint #-}
466
467instance Pretty IPv6 where
468 pPrint = PP.text . show
469 {-# INLINE pPrint #-}
470
471instance Pretty IP where
472 pPrint = PP.text . show
473 {-# INLINE pPrint #-}
474
475instance Hashable IPv4 where
476 hashWithSalt = hashUsing toHostAddress
477 {-# INLINE hashWithSalt #-}
478
479instance Hashable IPv6 where
480 hashWithSalt s a = hashWithSalt s (toHostAddress6 a)
481
482instance Hashable IP where
483 hashWithSalt s (IPv4 h) = hashWithSalt s h
484 hashWithSalt s (IPv6 h) = hashWithSalt s h
485
486{-----------------------------------------------------------------------
487-- Peer addr
488-----------------------------------------------------------------------}
489-- TODO check semantic of ord and eq instances
490
491-- | Peer address info normally extracted from peer list or peer
492-- compact list encoding.
493data PeerAddr a = PeerAddr
494 { peerId :: !(Maybe PeerId)
495
496 -- | This is usually 'IPv4', 'IPv6', 'IP' or unresolved
497 -- 'HostName'.
498 , peerHost :: !a
499
500 -- | The port the peer listenning for incoming P2P sessions.
501 , peerPort :: {-# UNPACK #-} !PortNumber
502 } deriving (Show, Eq, Ord, Typeable, Functor)
503
504peer_ip_key, peer_id_key, peer_port_key :: BKey
505peer_ip_key = "ip"
506peer_id_key = "peer id"
507peer_port_key = "port"
508
509-- | The tracker's 'announce response' compatible encoding.
510instance (Typeable a, BEncode a) => BEncode (PeerAddr a) where
511 toBEncode PeerAddr {..} = toDict $
512 peer_ip_key .=! peerHost
513 .: peer_id_key .=? peerId
514 .: peer_port_key .=! peerPort
515 .: endDict
516
517 fromBEncode = fromDict $ do
518 peerAddr <$>! peer_ip_key
519 <*>? peer_id_key
520 <*>! peer_port_key
521 where
522 peerAddr = flip PeerAddr
523
524-- | The tracker's 'compact peer list' compatible encoding. The
525-- 'peerId' is always 'Nothing'.
526--
527-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
528--
529-- TODO: test byte order
530instance (Serialize a) => Serialize (PeerAddr a) where
531 put PeerAddr {..} = put peerHost >> put peerPort
532 get = PeerAddr Nothing <$> get <*> get
533
534-- | @127.0.0.1:6881@
535instance Default (PeerAddr IPv4) where
536 def = "127.0.0.1:6881"
537
538-- | @127.0.0.1:6881@
539instance Default (PeerAddr IP) where
540 def = IPv4 <$> def
541
542-- | Example:
543--
544-- @peerPort \"127.0.0.1:6881\" == 6881@
545--
546instance IsString (PeerAddr IPv4) where
547 fromString str
548 | [hostAddrStr, portStr] <- splitWhen (== ':') str
549 , Just hostAddr <- readMaybe hostAddrStr
550 , Just portNum <- toEnum <$> readMaybe portStr
551 = PeerAddr Nothing hostAddr portNum
552 | otherwise = error $ "fromString: unable to parse (PeerAddr IPv4): " ++ str
553
554instance Read (PeerAddr IPv4) where
555 readsPrec i = RP.readP_to_S $ do
556 ipv4 <- RP.readS_to_P (readsPrec i)
557 _ <- RP.char ':'
558 port <- toEnum <$> RP.readS_to_P (readsPrec i)
559 return $ PeerAddr Nothing ipv4 port
560
561readsIPv6_port :: String -> [((IPv6, PortNumber), String)]
562readsIPv6_port = RP.readP_to_S $ do
563 ip <- RP.char '[' *> (RP.readS_to_P reads) <* RP.char ']'
564 _ <- RP.char ':'
565 port <- toEnum <$> read <$> (RP.many1 $ RP.satisfy isDigit) <* RP.eof
566 return (ip,port)
567
568instance IsString (PeerAddr IPv6) where
569 fromString str
570 | [((ip,port),"")] <- readsIPv6_port str =
571 PeerAddr Nothing ip port
572 | otherwise = error $ "fromString: unable to parse (PeerAddr IPv6): " ++ str
573
574instance IsString (PeerAddr IP) where
575 fromString str
576 | '[' `L.elem` str = IPv6 <$> fromString str
577 | otherwise = IPv4 <$> fromString str
578
579-- | fingerprint + "at" + dotted.host.inet.addr:port
580-- TODO: instances for IPv6, HostName
581instance Pretty a => Pretty (PeerAddr a) where
582 pPrint PeerAddr {..}
583 | Just pid <- peerId = pPrint (fingerprint pid) <+> "at" <+> paddr
584 | otherwise = paddr
585 where
586 paddr = pPrint peerHost <> ":" <> text (show peerPort)
587
588instance Hashable a => Hashable (PeerAddr a) where
589 hashWithSalt s PeerAddr {..} =
590 s `hashWithSalt` peerId `hashWithSalt` peerHost `hashWithSalt` peerPort
591
592-- | Ports typically reserved for bittorrent P2P listener.
593defaultPorts :: [PortNumber]
594defaultPorts = [6881..6889]
595
596_resolvePeerAddr :: (IPAddress i) => PeerAddr HostName -> PeerAddr i
597_resolvePeerAddr = undefined
598
599_peerSockAddr :: PeerAddr IP -> (Family, SockAddr)
600_peerSockAddr PeerAddr {..} =
601 case peerHost of
602 IPv4 ipv4 ->
603 (AF_INET, SockAddrInet peerPort (toHostAddress ipv4))
604 IPv6 ipv6 ->
605 (AF_INET6, SockAddrInet6 peerPort 0 (toHostAddress6 ipv6) 0)
606
607peerSockAddr :: PeerAddr IP -> SockAddr
608peerSockAddr = snd . _peerSockAddr
609
610-- | Create a socket connected to the address specified in a peerAddr
611peerSocket :: SocketType -> PeerAddr IP -> IO Socket
612peerSocket socketType pa = do
613 let (family, addr) = _peerSockAddr pa
614 sock <- socket family socketType defaultProtocol
615 connect sock addr
616 return sock
617
618{-----------------------------------------------------------------------
619-- Node info
620-----------------------------------------------------------------------}
621-- $node-info
622--
623-- A \"node\" is a client\/server listening on a UDP port
624-- implementing the distributed hash table protocol. The DHT is
625-- composed of nodes and stores the location of peers. BitTorrent
626-- clients include a DHT node, which is used to contact other nodes
627-- in the DHT to get the location of peers to download from using
628-- the BitTorrent protocol.
629
630-- TODO more compact representation ('ShortByteString's?)
631
632-- | Each node has a globally unique identifier known as the \"node
633-- ID.\"
634--
635-- Normally, /this/ node id should be saved between invocations
636-- of the client software.
637newtype NodeId = NodeId ByteString
638 deriving (Show, Eq, Ord, BEncode, Typeable)
639
640nodeIdSize :: Int
641nodeIdSize = 20
642
643-- | Meaningless node id, for testing purposes only.
644instance Default NodeId where
645 def = NodeId (BS.replicate nodeIdSize 0)
646
647instance Serialize NodeId where
648 get = NodeId <$> getByteString nodeIdSize
649 {-# INLINE get #-}
650 put (NodeId bs) = putByteString bs
651 {-# INLINE put #-}
652
653-- | ASCII encoded.
654instance IsString NodeId where
655 fromString str
656 | L.length str == nodeIdSize = NodeId (fromString str)
657 | L.length str == 2 * nodeIdSize = NodeId (fst $ Base16.decode $ fromString str)
658 | otherwise = error "fromString: invalid NodeId length"
659 {-# INLINE fromString #-}
660
661-- | base16 encoded.
662instance Pretty NodeId where
663 pPrint (NodeId nid) = PP.text $ BC.unpack $ Base16.encode nid
664
665-- | Test if the nth bit is set.
666testIdBit :: NodeId -> Word -> Bool
667testIdBit (NodeId bs) i
668 | fromIntegral i < nodeIdSize * 8
669 , (q, r) <- quotRem (fromIntegral i) 8
670 = testBit (BS.index bs q) (7 - r)
671 | otherwise = False
672{-# INLINE testIdBit #-}
673
674-- TODO WARN is the 'system' random suitable for this?
675-- | Generate random NodeID used for the entire session.
676-- Distribution of ID's should be as uniform as possible.
677--
678genNodeId :: IO NodeId
679genNodeId = NodeId <$> getEntropy nodeIdSize
680
681------------------------------------------------------------------------
682
683-- | In Kademlia, the distance metric is XOR and the result is
684-- interpreted as an unsigned integer.
685newtype NodeDistance = NodeDistance BS.ByteString
686 deriving (Eq, Ord)
687
688instance Pretty NodeDistance where
689 pPrint (NodeDistance bs) = foldMap bitseq $ BS.unpack bs
690 where
691 listBits w = L.map (testBit w) (L.reverse [0..finiteBitSize w - 1])
692 bitseq = foldMap (int . fromEnum) . listBits
693
694-- | distance(A,B) = |A xor B| Smaller values are closer.
695distance :: NodeId -> NodeId -> NodeDistance
696distance (NodeId a) (NodeId b) = NodeDistance (BS.pack (BS.zipWith xor a b))
697
698-- | Accepts a depth/index of a bucket and whether or not it is the last one,
699-- yields:
700--
701-- count of leading bytes to be copied from your node id.
702--
703-- mask to clear the extra bits of the last copied byte
704--
705-- mask to toggle the last copied bit if it is not the last bucket
706--
707-- Normally this is used with 'genBucketSample' to obtain a random id suitable
708-- for refreshing a particular bucket.
709bucketRange :: Int -> Bool -> (Int, Word8, Word8)
710bucketRange depth is_last = (q,m,b)
711 where
712 (q,r) = divMod ((if is_last then (+7) else (+8)) depth) 8
713 m = 2^(7-r) - 1
714 b = if is_last then 0 else 2^(7-r)
715
716-- | Generate a random 'NodeId' within a range suitable for a bucket. To
717-- obtain a sample for bucket number /index/ where /is_last/ indicates if this
718-- is for the current deepest bucket in our routing table:
719--
720-- > sample <- genBucketSample nid (bucketRange index is_last)
721genBucketSample :: NodeId -> (Int,Word8,Word8) -> IO NodeId
722genBucketSample n qmb = genBucketSample' getEntropy n qmb
723
724-- | Generalizion of 'genBucketSample' that accepts a byte generator
725-- function to use instead of the system entropy.
726genBucketSample' :: Applicative m =>
727 (Int -> m ByteString) -> NodeId -> (Int,Word8,Word8) -> m NodeId
728genBucketSample' gen (NodeId self) (q,m,b)
729 | q <= 0 = NodeId <$> gen nodeIdSize
730 | q >= nodeIdSize = pure (NodeId self)
731 | otherwise = NodeId . build <$> gen (nodeIdSize - q + 1)
732 where
733 build tl = BS.init hd <> BS.cons (h .|. t) (BS.tail tl)
734 where
735 hd = BS.take q self
736 h = xor b (complement m .&. BS.last hd)
737 t = m .&. BS.head tl
738
739------------------------------------------------------------------------
740
741data NodeAddr a = NodeAddr
742 { nodeHost :: !a
743 , nodePort :: {-# UNPACK #-} !PortNumber
744 } deriving (Eq, Typeable, Functor, Foldable, Traversable)
745
746instance Show a => Show (NodeAddr a) where
747 showsPrec i NodeAddr {..}
748 = showsPrec i nodeHost <> showString ":" <> showsPrec i nodePort
749
750instance Read (NodeAddr IPv4) where
751 readsPrec i x = [ (fromPeerAddr a, s) | (a, s) <- readsPrec i x ]
752
753-- | @127.0.0.1:6882@
754instance Default (NodeAddr IPv4) where
755 def = "127.0.0.1:6882"
756
757-- | KRPC compatible encoding.
758instance Serialize a => Serialize (NodeAddr a) where
759 get = NodeAddr <$> get <*> get
760 {-# INLINE get #-}
761 put NodeAddr {..} = put nodeHost >> put nodePort
762 {-# INLINE put #-}
763
764-- | Torrent file compatible encoding.
765instance BEncode a => BEncode (NodeAddr a) where
766 toBEncode NodeAddr {..} = toBEncode (nodeHost, nodePort)
767 {-# INLINE toBEncode #-}
768 fromBEncode b = uncurry NodeAddr <$> fromBEncode b
769 {-# INLINE fromBEncode #-}
770
771instance Hashable a => Hashable (NodeAddr a) where
772 hashWithSalt s NodeAddr {..} = hashWithSalt s (nodeHost, nodePort)
773 {-# INLINE hashWithSalt #-}
774
775instance Pretty ip => Pretty (NodeAddr ip) where
776 pPrint NodeAddr {..} = pPrint nodeHost <> ":" <> pPrint nodePort
777
778-- | Example:
779--
780-- @nodePort \"127.0.0.1:6881\" == 6881@
781--
782instance IsString (NodeAddr IPv4) where
783 fromString = fromPeerAddr . fromString
784
785fromPeerAddr :: PeerAddr a -> NodeAddr a
786fromPeerAddr PeerAddr {..} = NodeAddr
787 { nodeHost = peerHost
788 , nodePort = peerPort
789 }
790
791------------------------------------------------------------------------
792
793data NodeInfo a = NodeInfo
794 { nodeId :: !NodeId
795 , nodeAddr :: !(NodeAddr a)
796 } deriving (Show, Eq, Functor, Foldable, Traversable)
797
798instance Eq a => Ord (NodeInfo a) where
799 compare = comparing nodeId
800
801-- | KRPC 'compact list' compatible encoding: contact information for
802-- nodes is encoded as a 26-byte string. Also known as "Compact node
803-- info" the 20-byte Node ID in network byte order has the compact
804-- IP-address/port info concatenated to the end.
805instance Serialize a => Serialize (NodeInfo a) where
806 get = NodeInfo <$> get <*> get
807 put NodeInfo {..} = put nodeId >> put nodeAddr
808
809instance Pretty ip => Pretty (NodeInfo ip) where
810 pPrint NodeInfo {..} = pPrint nodeId <> "@(" <> pPrint nodeAddr <> ")"
811
812instance Pretty ip => Pretty [NodeInfo ip] where
813 pPrint = PP.vcat . PP.punctuate "," . L.map pPrint
814
815-- | Order by closeness: nearest nodes first.
816rank :: (x -> NodeId) -> NodeId -> [x] -> [x]
817rank f nid = L.sortBy (comparing (distance nid . f))
818
819{-----------------------------------------------------------------------
820-- Fingerprint
821-----------------------------------------------------------------------}
822-- $fingerprint
823--
824-- 'Fingerprint' is used to identify the client implementation and
825-- version which also contained in 'Peer'. For exsample first 6
826-- bytes of peer id of this this library are @-HS0100-@ while for
827-- mainline we have @M4-3-6--@. We could extract this info and
828-- print in human-friendly form: this is useful for debugging and
829-- logging.
830--
831-- For more information see:
832-- <http://bittorrent.org/beps/bep_0020.html>
833--
834--
835-- NOTE: Do /not/ use this information to control client
836-- capabilities (such as supported enchancements), this should be
837-- done using 'Network.BitTorrent.Extension'!
838--
839
840-- TODO FIXME
841version :: Version
842version = Version [0, 0, 0, 3] []
843
844-- | List of registered client versions + 'IlibHSbittorrent' (this
845-- package) + 'IUnknown' (for not recognized software). All names are
846-- prefixed by \"I\" because some of them starts from lowercase letter
847-- but that is not a valid Haskell constructor name.
848--
849data Software =
850 IUnknown
851
852 | IMainline
853
854 | IABC
855 | IOspreyPermaseed
856 | IBTQueue
857 | ITribler
858 | IShadow
859 | IBitTornado
860
861-- UPnP(!) Bit Torrent !???
862-- 'U' - UPnP NAT Bit Torrent
863 | IBitLord
864 | IOpera
865 | IMLdonkey
866
867 | IAres
868 | IArctic
869 | IAvicora
870 | IBitPump
871 | IAzureus
872 | IBitBuddy
873 | IBitComet
874 | IBitflu
875 | IBTG
876 | IBitRocket
877 | IBTSlave
878 | IBittorrentX
879 | IEnhancedCTorrent
880 | ICTorrent
881 | IDelugeTorrent
882 | IPropagateDataClient
883 | IEBit
884 | IElectricSheep
885 | IFoxTorrent
886 | IGSTorrent
887 | IHalite
888 | IlibHSbittorrent
889 | IHydranode
890 | IKGet
891 | IKTorrent
892 | ILH_ABC
893 | ILphant
894 | ILibtorrent
895 | ILibTorrent
896 | ILimeWire
897 | IMonoTorrent
898 | IMooPolice
899 | IMiro
900 | IMoonlightTorrent
901 | INetTransport
902 | IPando
903 | IqBittorrent
904 | IQQDownload
905 | IQt4TorrentExample
906 | IRetriever
907 | IShareaza
908 | ISwiftbit
909 | ISwarmScope
910 | ISymTorrent
911 | Isharktorrent
912 | ITorrentDotNET
913 | ITransmission
914 | ITorrentstorm
915 | ITuoTu
916 | IuLeecher
917 | IuTorrent
918 | IVagaa
919 | IBitLet
920 | IFireTorrent
921 | IXunlei
922 | IXanTorrent
923 | IXtorrent
924 | IZipTorrent
925 deriving (Show, Eq, Ord, Enum, Bounded)
926
927parseSoftware :: ByteString -> Software
928parseSoftware = f . BC.unpack
929 where
930 f "AG" = IAres
931 f "A~" = IAres
932 f "AR" = IArctic
933 f "AV" = IAvicora
934 f "AX" = IBitPump
935 f "AZ" = IAzureus
936 f "BB" = IBitBuddy
937 f "BC" = IBitComet
938 f "BF" = IBitflu
939 f "BG" = IBTG
940 f "BR" = IBitRocket
941 f "BS" = IBTSlave
942 f "BX" = IBittorrentX
943 f "CD" = IEnhancedCTorrent
944 f "CT" = ICTorrent
945 f "DE" = IDelugeTorrent
946 f "DP" = IPropagateDataClient
947 f "EB" = IEBit
948 f "ES" = IElectricSheep
949 f "FT" = IFoxTorrent
950 f "GS" = IGSTorrent
951 f "HL" = IHalite
952 f "HS" = IlibHSbittorrent
953 f "HN" = IHydranode
954 f "KG" = IKGet
955 f "KT" = IKTorrent
956 f "LH" = ILH_ABC
957 f "LP" = ILphant
958 f "LT" = ILibtorrent
959 f "lt" = ILibTorrent
960 f "LW" = ILimeWire
961 f "MO" = IMonoTorrent
962 f "MP" = IMooPolice
963 f "MR" = IMiro
964 f "ML" = IMLdonkey
965 f "MT" = IMoonlightTorrent
966 f "NX" = INetTransport
967 f "PD" = IPando
968 f "qB" = IqBittorrent
969 f "QD" = IQQDownload
970 f "QT" = IQt4TorrentExample
971 f "RT" = IRetriever
972 f "S~" = IShareaza
973 f "SB" = ISwiftbit
974 f "SS" = ISwarmScope
975 f "ST" = ISymTorrent
976 f "st" = Isharktorrent
977 f "SZ" = IShareaza
978 f "TN" = ITorrentDotNET
979 f "TR" = ITransmission
980 f "TS" = ITorrentstorm
981 f "TT" = ITuoTu
982 f "UL" = IuLeecher
983 f "UT" = IuTorrent
984 f "VG" = IVagaa
985 f "WT" = IBitLet
986 f "WY" = IFireTorrent
987 f "XL" = IXunlei
988 f "XT" = IXanTorrent
989 f "XX" = IXtorrent
990 f "ZT" = IZipTorrent
991 f _ = IUnknown
992
993-- | Used to represent a not recognized implementation
994instance Default Software where
995 def = IUnknown
996 {-# INLINE def #-}
997
998-- | Example: @\"BitLet\" == 'IBitLet'@
999instance IsString Software where
1000 fromString str
1001 | Just impl <- L.lookup str alist = impl
1002 | otherwise = error $ "fromString: not recognized " ++ str
1003 where
1004 alist = L.map mk [minBound..maxBound]
1005 mk x = (L.tail $ show x, x)
1006
1007-- | Example: @pPrint 'IBitLet' == \"IBitLet\"@
1008instance Pretty Software where
1009 pPrint = text . L.tail . show
1010
1011-- | Just the '0' version.
1012instance Default Version where
1013 def = Version [0] []
1014 {-# INLINE def #-}
1015
1016-- | For dot delimited version strings.
1017-- Example: @fromString \"0.1.0.2\" == Version [0, 1, 0, 2]@
1018--
1019instance IsString Version where
1020 fromString str
1021 | Just nums <- chunkNums str = Version nums []
1022 | otherwise = error $ "fromString: invalid version string " ++ str
1023 where
1024 chunkNums = sequence . L.map readMaybe . L.linesBy ('.' ==)
1025
1026instance Pretty Version where
1027 pPrint = text . showVersion
1028
1029-- | The all sensible infomation that can be obtained from a peer
1030-- identifier or torrent /createdBy/ field.
1031data Fingerprint = Fingerprint Software Version
1032 deriving (Show, Eq, Ord)
1033
1034-- | Unrecognized client implementation.
1035instance Default Fingerprint where
1036 def = Fingerprint def def
1037 {-# INLINE def #-}
1038
1039-- | Example: @\"BitComet-1.2\" == ClientInfo IBitComet (Version [1, 2] [])@
1040instance IsString Fingerprint where
1041 fromString str
1042 | _ : ver <- _ver = Fingerprint (fromString impl) (fromString ver)
1043 | otherwise = error $ "fromString: invalid client info string" ++ str
1044 where
1045 (impl, _ver) = L.span ((/=) '-') str
1046
1047instance Pretty Fingerprint where
1048 pPrint (Fingerprint s v) = pPrint s <+> "version" <+> pPrint v
1049
1050-- | Fingerprint of this (the bittorrent library) package. Normally,
1051-- applications should introduce its own fingerprints, otherwise they
1052-- can use 'libFingerprint' value.
1053--
1054libFingerprint :: Fingerprint
1055libFingerprint = Fingerprint IlibHSbittorrent version
1056
1057-- | HTTP user agent of this (the bittorrent library) package. Can be
1058-- used in HTTP tracker requests.
1059libUserAgent :: String
1060libUserAgent = render (pPrint IlibHSbittorrent <> "/" <> pPrint version)
1061
1062{-----------------------------------------------------------------------
1063-- For torrent file
1064-----------------------------------------------------------------------}
1065-- TODO collect information about createdBy torrent field
1066-- renderImpl :: ClientImpl -> Text
1067-- renderImpl = T.pack . L.tail . show
1068--
1069-- renderVersion :: Version -> Text
1070-- renderVersion = undefined
1071--
1072-- renderClientInfo :: ClientInfo -> Text
1073-- renderClientInfo ClientInfo {..} = renderImpl ciImpl <> "/" <> renderVersion ciVersion
1074--
1075-- parseClientInfo :: Text -> ClientImpl
1076-- parseClientInfo t = undefined
1077
1078
1079-- code used for generation; remove it later on
1080--
1081-- mkEnumTyDef :: NM -> String
1082-- mkEnumTyDef = unlines . map (" | I" ++) . nub . map snd
1083--
1084-- mkPars :: NM -> String
1085-- mkPars = unlines . map (\(code, impl) -> " f \"" ++ code ++ "\" = " ++ "I" ++ impl)
1086--
1087-- type NM = [(String, String)]
1088-- nameMap :: NM
1089-- nameMap =
1090-- [ ("AG", "Ares")
1091-- , ("A~", "Ares")
1092-- , ("AR", "Arctic")
1093-- , ("AV", "Avicora")
1094-- , ("AX", "BitPump")
1095-- , ("AZ", "Azureus")
1096-- , ("BB", "BitBuddy")
1097-- , ("BC", "BitComet")
1098-- , ("BF", "Bitflu")
1099-- , ("BG", "BTG")
1100-- , ("BR", "BitRocket")
1101-- , ("BS", "BTSlave")
1102-- , ("BX", "BittorrentX")
1103-- , ("CD", "EnhancedCTorrent")
1104-- , ("CT", "CTorrent")
1105-- , ("DE", "DelugeTorrent")
1106-- , ("DP", "PropagateDataClient")
1107-- , ("EB", "EBit")
1108-- , ("ES", "ElectricSheep")
1109-- , ("FT", "FoxTorrent")
1110-- , ("GS", "GSTorrent")
1111-- , ("HL", "Halite")
1112-- , ("HS", "libHSnetwork_bittorrent")
1113-- , ("HN", "Hydranode")
1114-- , ("KG", "KGet")
1115-- , ("KT", "KTorrent")
1116-- , ("LH", "LH_ABC")
1117-- , ("LP", "Lphant")
1118-- , ("LT", "Libtorrent")
1119-- , ("lt", "LibTorrent")
1120-- , ("LW", "LimeWire")
1121-- , ("MO", "MonoTorrent")
1122-- , ("MP", "MooPolice")
1123-- , ("MR", "Miro")
1124-- , ("MT", "MoonlightTorrent")
1125-- , ("NX", "NetTransport")
1126-- , ("PD", "Pando")
1127-- , ("qB", "qBittorrent")
1128-- , ("QD", "QQDownload")
1129-- , ("QT", "Qt4TorrentExample")
1130-- , ("RT", "Retriever")
1131-- , ("S~", "Shareaza")
1132-- , ("SB", "Swiftbit")
1133-- , ("SS", "SwarmScope")
1134-- , ("ST", "SymTorrent")
1135-- , ("st", "sharktorrent")
1136-- , ("SZ", "Shareaza")
1137-- , ("TN", "TorrentDotNET")
1138-- , ("TR", "Transmission")
1139-- , ("TS", "Torrentstorm")
1140-- , ("TT", "TuoTu")
1141-- , ("UL", "uLeecher")
1142-- , ("UT", "uTorrent")
1143-- , ("VG", "Vagaa")
1144-- , ("WT", "BitLet")
1145-- , ("WY", "FireTorrent")
1146-- , ("XL", "Xunlei")
1147-- , ("XT", "XanTorrent")
1148-- , ("XX", "Xtorrent")
1149-- , ("ZT", "ZipTorrent")
1150-- ]
1151
1152-- TODO use regexps
1153
1154-- | Tries to extract meaningful information from peer ID bytes. If
1155-- peer id uses unknown coding style then client info returned is
1156-- 'def'.
1157--
1158fingerprint :: PeerId -> Fingerprint
1159fingerprint pid = either (const def) id $ runGet getCI (getPeerId pid)
1160 where
1161 getCI = do
1162 leading <- BS.w2c <$> getWord8
1163 case leading of
1164 '-' -> Fingerprint <$> getAzureusImpl <*> getAzureusVersion
1165 'M' -> Fingerprint <$> pure IMainline <*> getMainlineVersion
1166 'e' -> Fingerprint <$> getBitCometImpl <*> getBitCometVersion
1167 'F' -> Fingerprint <$> getBitCometImpl <*> getBitCometVersion
1168 c -> do
1169 c1 <- w2c <$> S.lookAhead getWord8
1170 if c1 == 'P'
1171 then do
1172 _ <- getWord8
1173 Fingerprint <$> pure IOpera <*> getOperaVersion
1174 else Fingerprint <$> pure (getShadowImpl c) <*> getShadowVersion
1175
1176 getMainlineVersion = do
1177 str <- BC.unpack <$> getByteString 7
1178 let mnums = L.filter (not . L.null) $ L.linesBy ('-' ==) str
1179 return $ Version (fromMaybe [] $ sequence $ L.map readMaybe mnums) []
1180
1181 getAzureusImpl = parseSoftware <$> getByteString 2
1182 getAzureusVersion = mkVer <$> getByteString 4
1183 where
1184 mkVer bs = Version [fromMaybe 0 $ readMaybe $ BC.unpack bs] []
1185
1186 getBitCometImpl = do
1187 bs <- getByteString 3
1188 S.lookAhead $ do
1189 _ <- getByteString 2
1190 lr <- getByteString 4
1191 return $
1192 if lr == "LORD" then IBitLord else
1193 if bs == "UTB" then IBitComet else
1194 if bs == "xbc" then IBitComet else def
1195
1196 getBitCometVersion = do
1197 x <- getWord8
1198 y <- getWord8
1199 return $ Version [fromIntegral x, fromIntegral y] []
1200
1201 getOperaVersion = do
1202 str <- BC.unpack <$> getByteString 4
1203 return $ Version [fromMaybe 0 $ readMaybe str] []
1204
1205 getShadowImpl 'A' = IABC
1206 getShadowImpl 'O' = IOspreyPermaseed
1207 getShadowImpl 'Q' = IBTQueue
1208 getShadowImpl 'R' = ITribler
1209 getShadowImpl 'S' = IShadow
1210 getShadowImpl 'T' = IBitTornado
1211 getShadowImpl _ = IUnknown
1212
1213 decodeShadowVerNr :: Char -> Maybe Int
1214 decodeShadowVerNr c
1215 | '0' < c && c <= '9' = Just (fromEnum c - fromEnum '0')
1216 | 'A' < c && c <= 'Z' = Just ((fromEnum c - fromEnum 'A') + 10)
1217 | 'a' < c && c <= 'z' = Just ((fromEnum c - fromEnum 'a') + 36)
1218 | otherwise = Nothing
1219
1220 getShadowVersion = do
1221 str <- BC.unpack <$> getByteString 5
1222 return $ Version (catMaybes $ L.map decodeShadowVerNr str) []
1223
1224
1225-- | Yields all 8 DHT neighborhoods available to you given a particular ip
1226-- address.
1227bep42s :: Address a => a -> NodeId -> [NodeId]
1228bep42s addr (NodeId r) = mapMaybe (bep42 addr) rs
1229 where
1230 rs = L.map (NodeId . change3bits r) [0..7]
1231
1232change3bits :: ByteString -> Word8 -> ByteString
1233change3bits bs n = BS.snoc (BS.init bs) (BS.last bs .&. 0xF8 .|. n)
1234
1235-- | Modifies a purely random 'NodeId' to one that is related to a given
1236-- routable address in accordance with BEP 42.
1237bep42 :: Address a => a -> NodeId -> Maybe NodeId
1238bep42 addr (NodeId r)
1239 | Just ip <- fmap S.encode (fromAddr addr :: Maybe IPv4)
1240 <|> fmap S.encode (fromAddr addr :: Maybe IPv6)
1241 = genBucketSample' retr (NodeId $ crc $ applyMask ip) (3,0x07,0)
1242 | otherwise
1243 = Nothing
1244 where
1245 ip4mask = "\x03\x0f\x3f\xff" :: ByteString
1246 ip6mask = "\x01\x03\x07\x0f\x1f\x3f\x7f\xff" :: ByteString
1247 nbhood_select = BS.last r .&. 7
1248 retr n = pure $ BS.drop (BS.length r - n) r
1249 crc = S.encode . crc32c . BS.pack
1250 applyMask ip = case BS.zipWith (.&.) msk ip of
1251 (b:bs) -> (b .|. shiftL nbhood_select 5) : bs
1252 bs -> bs
1253 where msk | BS.length ip == 4 = ip4mask
1254 | otherwise = ip6mask
diff --git a/src/Network/BitTorrent/Client.hs b/src/Network/BitTorrent/Client.hs
new file mode 100644
index 00000000..b9a59f45
--- /dev/null
+++ b/src/Network/BitTorrent/Client.hs
@@ -0,0 +1,195 @@
1{-# LANGUAGE FlexibleInstances #-}
2{-# LANGUAGE TypeSynonymInstances #-}
3{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4{-# LANGUAGE TemplateHaskell #-}
5module Network.BitTorrent.Client
6 ( -- * Options
7 Options (..)
8
9 -- * Client session
10 , Client
11
12 -- ** Session data
13 , clientPeerId
14 , clientListenerPort
15 , allowedExtensions
16
17 -- ** Session initialization
18 , LogFun
19 , newClient
20 , closeClient
21 , withClient
22 , simpleClient
23
24 -- * BitTorrent monad
25 , MonadBitTorrent (..)
26 , BitTorrent
27 , runBitTorrent
28 , getClient
29
30 -- * Handle
31 , Handle
32 , handleTopic
33 , handleTrackers
34 , handleExchange
35
36 -- ** Construction
37 , TorrentSource (..)
38 , closeHandle
39
40 -- ** Query
41 , getHandle
42 , getIndex
43
44 -- ** Management
45 , start
46 , pause
47 , stop
48 ) where
49
50import Control.Applicative
51import Control.Exception
52import Control.Concurrent
53import Control.Concurrent.Chan.Split as CS
54import Control.Monad.Logger
55import Control.Monad.Trans
56import Control.Monad.Trans.Resource
57
58import Data.Default
59import Data.HashMap.Strict as HM
60import Data.Text
61import Network
62
63import Data.Torrent
64import Network.BitTorrent.Address
65import Network.BitTorrent.Client.Types
66import Network.BitTorrent.Client.Handle
67import Network.BitTorrent.DHT as DHT hiding (Options)
68import Network.BitTorrent.Tracker as Tracker hiding (Options)
69import Network.BitTorrent.Exchange as Exchange hiding (Options)
70import qualified Network.BitTorrent.Exchange as Exchange (Options(..))
71
72
73data Options = Options
74 { optFingerprint :: Fingerprint
75 , optName :: Text
76 , optPort :: PortNumber
77 , optExtensions :: [Extension]
78 , optNodeAddr :: NodeAddr IPv4
79 , optBootNode :: Maybe (NodeAddr IPv4)
80 }
81
82instance Default Options where
83 def = Options
84 { optFingerprint = def
85 , optName = "hs-bittorrent"
86 , optPort = 6882
87 , optExtensions = []
88 , optNodeAddr = "0.0.0.0:6882"
89 , optBootNode = Nothing
90 }
91
92exchangeOptions :: PeerId -> Options -> Exchange.Options
93exchangeOptions pid Options {..} = Exchange.Options
94 { optPeerAddr = PeerAddr (Just pid) (peerHost def) optPort
95 , optBacklog = optBacklog def
96 }
97
98connHandler :: MVar (HashMap InfoHash Handle) -> Exchange.Handler
99connHandler tmap ih = do
100 m <- readMVar tmap
101 case HM.lookup ih m of
102 Nothing -> error "torrent not found"
103 Just (Handle {..}) -> return handleExchange
104
105initClient :: Options -> LogFun -> ResIO Client
106initClient opts @ Options {..} logFun = do
107 pid <- liftIO genPeerId
108 tmap <- liftIO $ newMVar HM.empty
109
110 let peerInfo = PeerInfo pid Nothing optPort
111 let mkTracker = Tracker.newManager def peerInfo
112 (_, tmgr) <- allocate mkTracker Tracker.closeManager
113
114 let mkEx = Exchange.newManager (exchangeOptions pid opts) (connHandler tmap)
115 (_, emgr) <- allocate mkEx Exchange.closeManager
116
117 let mkNode = DHT.newNode defaultHandlers def optNodeAddr logFun Nothing
118 (_, node) <- allocate mkNode DHT.closeNode
119
120 resourceMap <- getInternalState
121 eventStream <- liftIO newSendPort
122
123 return Client
124 { clientPeerId = pid
125 , clientListenerPort = optPort
126 , allowedExtensions = toCaps optExtensions
127 , clientResources = resourceMap
128 , trackerManager = tmgr
129 , exchangeManager = emgr
130 , clientNode = node
131 , clientTorrents = tmap
132 , clientLogger = logFun
133 , clientEvents = eventStream
134 }
135
136newClient :: Options -> LogFun -> IO Client
137newClient opts logFun = do
138 s <- createInternalState
139 runInternalState (initClient opts logFun) s
140 `onException` closeInternalState s
141
142closeClient :: Client -> IO ()
143closeClient Client {..} = closeInternalState clientResources
144
145withClient :: Options -> LogFun -> (Client -> IO a) -> IO a
146withClient opts lf action = bracket (newClient opts lf) closeClient action
147
148-- do not perform IO in 'initClient', do it in the 'boot'
149--boot :: BitTorrent ()
150--boot = do
151-- Options {..} <- asks options
152-- liftDHT $ bootstrap (maybeToList optBootNode)
153
154-- | Run bittorrent client with default options and log to @stderr@.
155--
156-- For testing purposes only.
157--
158simpleClient :: BitTorrent () -> IO ()
159simpleClient m = do
160 runStderrLoggingT $ LoggingT $ \ logger -> do
161 withClient def logger (`runBitTorrent` m)
162
163{-----------------------------------------------------------------------
164-- Torrent identifiers
165-----------------------------------------------------------------------}
166
167class TorrentSource s where
168 openHandle :: FilePath -> s -> BitTorrent Handle
169
170instance TorrentSource InfoHash where
171 openHandle path ih = openMagnet path (nullMagnet ih)
172 {-# INLINE openHandle #-}
173
174instance TorrentSource Magnet where
175 openHandle = openMagnet
176 {-# INLINE openHandle #-}
177
178instance TorrentSource InfoDict where
179 openHandle path dict = openTorrent path (nullTorrent dict)
180 {-# INLINE openHandle #-}
181
182instance TorrentSource Torrent where
183 openHandle = openTorrent
184 {-# INLINE openHandle #-}
185
186instance TorrentSource FilePath where
187 openHandle contentDir torrentPath = do
188 t <- liftIO $ fromFile torrentPath
189 openTorrent contentDir t
190 {-# INLINE openHandle #-}
191
192getIndex :: BitTorrent [Handle]
193getIndex = do
194 Client {..} <- getClient
195 elems <$> liftIO (readMVar clientTorrents)
diff --git a/src/Network/BitTorrent/Client/Handle.hs b/src/Network/BitTorrent/Client/Handle.hs
new file mode 100644
index 00000000..66baac48
--- /dev/null
+++ b/src/Network/BitTorrent/Client/Handle.hs
@@ -0,0 +1,188 @@
1module Network.BitTorrent.Client.Handle
2 ( -- * Handle
3 Handle
4
5 -- * Initialization
6 , openTorrent
7 , openMagnet
8 , closeHandle
9
10 -- * Control
11 , start
12 , pause
13 , stop
14
15 -- * Query
16 , getHandle
17 , getStatus
18 ) where
19
20import Control.Concurrent.Chan.Split
21import Control.Concurrent.Lifted as L
22import Control.Monad
23import Control.Monad.Trans
24import Data.Default
25import Data.List as L
26import Data.HashMap.Strict as HM
27
28import Data.Torrent
29import Network.BitTorrent.Client.Types as Types
30import Network.BitTorrent.DHT as DHT
31import Network.BitTorrent.Exchange as Exchange
32import Network.BitTorrent.Tracker as Tracker
33
34{-----------------------------------------------------------------------
35-- Safe handle set manupulation
36-----------------------------------------------------------------------}
37
38allocHandle :: InfoHash -> BitTorrent Handle -> BitTorrent Handle
39allocHandle ih m = do
40 Client {..} <- getClient
41
42 (h, added) <- modifyMVar clientTorrents $ \ handles -> do
43 case HM.lookup ih handles of
44 Just h -> return (handles, (h, False))
45 Nothing -> do
46 h <- m
47 return (HM.insert ih h handles, (h, True))
48
49 when added $ do
50 liftIO $ send clientEvents (TorrentAdded ih)
51
52 return h
53
54freeHandle :: InfoHash -> BitTorrent () -> BitTorrent ()
55freeHandle ih finalizer = do
56 Client {..} <- getClient
57
58 modifyMVar_ clientTorrents $ \ handles -> do
59 case HM.lookup ih handles of
60 Nothing -> return handles
61 Just _ -> do
62 finalizer
63 return (HM.delete ih handles)
64
65lookupHandle :: InfoHash -> BitTorrent (Maybe Handle)
66lookupHandle ih = do
67 Client {..} <- getClient
68 handles <- readMVar clientTorrents
69 return (HM.lookup ih handles)
70
71{-----------------------------------------------------------------------
72-- Initialization
73-----------------------------------------------------------------------}
74
75newExchangeSession :: FilePath -> Either InfoHash InfoDict -> BitTorrent Exchange.Session
76newExchangeSession rootPath source = do
77 c @ Client {..} <- getClient
78 liftIO $ Exchange.newSession clientLogger (externalAddr c) rootPath source
79
80-- | Open a torrent in 'stop'ed state. Use 'nullTorrent' to open
81-- handle from 'InfoDict'. This operation do not block.
82openTorrent :: FilePath -> Torrent -> BitTorrent Handle
83openTorrent rootPath t @ Torrent {..} = do
84 let ih = idInfoHash tInfoDict
85 allocHandle ih $ do
86 statusVar <- newMVar Types.Stopped
87 tses <- liftIO $ Tracker.newSession ih (trackerList t)
88 eses <- newExchangeSession rootPath (Right tInfoDict)
89 eventStream <- liftIO newSendPort
90 return $ Handle
91 { handleTopic = ih
92 , handlePrivate = idPrivate tInfoDict
93 , handleStatus = statusVar
94 , handleTrackers = tses
95 , handleExchange = eses
96 , handleEvents = eventStream
97 }
98
99-- | Use 'nullMagnet' to open handle from 'InfoHash'.
100openMagnet :: FilePath -> Magnet -> BitTorrent Handle
101openMagnet rootPath Magnet {..} = do
102 allocHandle exactTopic $ do
103 statusVar <- newMVar Types.Stopped
104 tses <- liftIO $ Tracker.newSession exactTopic def
105 eses <- newExchangeSession rootPath (Left exactTopic)
106 eventStream <- liftIO newSendPort
107 return $ Handle
108 { handleTopic = exactTopic
109 , handlePrivate = False
110 , handleStatus = statusVar
111 , handleTrackers = tses
112 , handleExchange = eses
113 , handleEvents = eventStream
114 }
115
116-- | Stop torrent and destroy all sessions. You don't need to close
117-- handles at application exit, all handles will be automatically
118-- closed at 'Network.BitTorrent.Client.closeClient'. This operation
119-- may block.
120closeHandle :: Handle -> BitTorrent ()
121closeHandle h @ Handle {..} = do
122 freeHandle handleTopic $ do
123 Client {..} <- getClient
124 stop h
125 liftIO $ Exchange.closeSession handleExchange
126 liftIO $ Tracker.closeSession trackerManager handleTrackers
127
128{-----------------------------------------------------------------------
129-- Control
130-----------------------------------------------------------------------}
131
132modifyStatus :: HandleStatus -> Handle -> (HandleStatus -> BitTorrent ()) -> BitTorrent ()
133modifyStatus targetStatus Handle {..} targetAction = do
134 modifyMVar_ handleStatus $ \ actualStatus -> do
135 unless (actualStatus == targetStatus) $ do
136 targetAction actualStatus
137 return targetStatus
138 liftIO $ send handleEvents (StatusChanged targetStatus)
139
140-- | Start downloading, uploading and announcing this torrent.
141--
142-- This operation is blocking, use
143-- 'Control.Concurrent.Async.Lifted.async' if needed.
144start :: Handle -> BitTorrent ()
145start h @ Handle {..} = do
146 modifyStatus Types.Running h $ \ status -> do
147 case status of
148 Types.Running -> return ()
149 Types.Stopped -> do
150 Client {..} <- getClient
151 liftIO $ Tracker.notify trackerManager handleTrackers Tracker.Started
152 unless handlePrivate $ do
153 liftDHT $ DHT.insert handleTopic (error "start")
154 liftIO $ do
155 peers <- askPeers trackerManager handleTrackers
156 print $ "got: " ++ show (L.length peers) ++ " peers"
157 forM_ peers $ \ peer -> do
158 Exchange.connect peer handleExchange
159
160-- | Stop downloading this torrent.
161pause :: Handle -> BitTorrent ()
162pause _ = return ()
163
164-- | Stop downloading, uploading and announcing this torrent.
165stop :: Handle -> BitTorrent ()
166stop h @ Handle {..} = do
167 modifyStatus Types.Stopped h $ \ status -> do
168 case status of
169 Types.Stopped -> return ()
170 Types.Running -> do
171 Client {..} <- getClient
172 unless handlePrivate $ do
173 liftDHT $ DHT.delete handleTopic (error "stop")
174 liftIO $ Tracker.notify trackerManager handleTrackers Tracker.Stopped
175
176{-----------------------------------------------------------------------
177-- Query
178-----------------------------------------------------------------------}
179
180getHandle :: InfoHash -> BitTorrent Handle
181getHandle ih = do
182 mhandle <- lookupHandle ih
183 case mhandle of
184 Nothing -> error "should we throw some exception?"
185 Just h -> return h
186
187getStatus :: Handle -> IO HandleStatus
188getStatus Handle {..} = readMVar handleStatus
diff --git a/src/Network/BitTorrent/Client/Types.hs b/src/Network/BitTorrent/Client/Types.hs
new file mode 100644
index 00000000..7f228276
--- /dev/null
+++ b/src/Network/BitTorrent/Client/Types.hs
@@ -0,0 +1,163 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE TypeFamilies #-}
4{-# LANGUAGE MultiParamTypeClasses #-}
5{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6module Network.BitTorrent.Client.Types
7 ( -- * Core types
8 HandleStatus (..)
9 , Handle (..)
10 , Client (..)
11 , externalAddr
12
13 -- * Monad BitTorrent
14 , BitTorrent (..)
15 , runBitTorrent
16 , getClient
17
18 , MonadBitTorrent (..)
19
20 -- * Events
21 , Types.Event (..)
22 ) where
23
24import Control.Applicative
25import Control.Concurrent
26import Control.Concurrent.Chan.Split as CS
27import Control.Monad.Base
28import Control.Monad.Logger
29import Control.Monad.Reader
30import Control.Monad.Trans.Control
31import Control.Monad.Trans.Resource
32import Data.Function
33import Data.HashMap.Strict as HM
34import Data.Ord
35import Network
36import System.Log.FastLogger
37
38import Data.Torrent
39import Network.BitTorrent.Address
40import Network.BitTorrent.Internal.Types as Types
41import Network.BitTorrent.DHT as DHT
42import Network.BitTorrent.Exchange as Exchange
43import Network.BitTorrent.Tracker as Tracker hiding (Event)
44
45data HandleStatus
46 = Running
47 | Stopped
48 deriving (Show, Eq)
49
50data Handle = Handle
51 { handleTopic :: !InfoHash
52 , handlePrivate :: !Bool
53
54 , handleStatus :: !(MVar HandleStatus)
55 , handleTrackers :: !Tracker.Session
56 , handleExchange :: !Exchange.Session
57 , handleEvents :: !(SendPort (Event Handle))
58 }
59
60instance EventSource Handle where
61 data Event Handle = StatusChanged HandleStatus
62 listen Handle {..} = CS.listen undefined
63
64data Client = Client
65 { clientPeerId :: !PeerId
66 , clientListenerPort :: !PortNumber
67 , allowedExtensions :: !Caps
68 , clientResources :: !InternalState
69 , trackerManager :: !Tracker.Manager
70 , exchangeManager :: !Exchange.Manager
71 , clientNode :: !(Node IPv4)
72 , clientTorrents :: !(MVar (HashMap InfoHash Handle))
73 , clientLogger :: !LogFun
74 , clientEvents :: !(SendPort (Event Client))
75 }
76
77instance Eq Client where
78 (==) = (==) `on` clientPeerId
79
80instance Ord Client where
81 compare = comparing clientPeerId
82
83instance EventSource Client where
84 data Event Client = TorrentAdded InfoHash
85 listen Client {..} = CS.listen clientEvents
86
87-- | External IP address of a host running a bittorrent client
88-- software may be used to acknowledge remote peer the host connected
89-- to. See 'Network.BitTorrent.Exchange.Message.ExtendedHandshake'.
90externalAddr :: Client -> PeerAddr (Maybe IP)
91externalAddr Client {..} = PeerAddr
92 { peerId = Just clientPeerId
93 , peerHost = Nothing -- TODO return external IP address, if known
94 , peerPort = clientListenerPort
95 }
96
97{-----------------------------------------------------------------------
98-- BitTorrent monad
99-----------------------------------------------------------------------}
100
101newtype BitTorrent a = BitTorrent
102 { unBitTorrent :: ReaderT Client IO a
103 } deriving ( Functor, Applicative, Monad
104 , MonadIO, MonadThrow, MonadBase IO
105 )
106
107class MonadBitTorrent m where
108 liftBT :: BitTorrent a -> m a
109
110#if MIN_VERSION_monad_control(1,0,0)
111newtype BTStM a = BTStM { unBTSt :: StM (ReaderT Client IO) a }
112
113instance MonadBaseControl IO BitTorrent where
114 type StM BitTorrent a = BTStM a
115 liftBaseWith cc = BitTorrent $ liftBaseWith $ \ cc' ->
116 cc $ \ (BitTorrent m) -> BTStM <$> cc' m
117 {-# INLINE liftBaseWith #-}
118
119 restoreM = BitTorrent . restoreM . unBTSt
120 {-# INLINE restoreM #-}
121#else
122instance MonadBaseControl IO BitTorrent where
123 newtype StM BitTorrent a = StM { unSt :: StM (ReaderT Client IO) a }
124 liftBaseWith cc = BitTorrent $ liftBaseWith $ \ cc' ->
125 cc $ \ (BitTorrent m) -> StM <$> cc' m
126 {-# INLINE liftBaseWith #-}
127
128 restoreM = BitTorrent . restoreM . unSt
129 {-# INLINE restoreM #-}
130#endif
131
132-- | NOP.
133instance MonadBitTorrent BitTorrent where
134 liftBT = id
135
136instance MonadTrans t => MonadBitTorrent (t BitTorrent) where
137 liftBT = lift
138
139-- | Registered but not closed manually resources will be
140-- automatically closed at 'Network.BitTorrent.Client.closeClient'
141instance MonadResource BitTorrent where
142 liftResourceT m = BitTorrent $ do
143 s <- asks clientResources
144 liftIO $ runInternalState m s
145
146-- | Run DHT operation, only if the client node is running.
147instance MonadDHT BitTorrent where
148 liftDHT action = BitTorrent $ do
149 node <- asks clientNode
150 liftIO $ runDHT node action
151
152instance MonadLogger BitTorrent where
153 monadLoggerLog loc src lvl msg = BitTorrent $ do
154 logger <- asks clientLogger
155 liftIO $ logger loc src lvl (toLogStr msg)
156
157runBitTorrent :: Client -> BitTorrent a -> IO a
158runBitTorrent client action = runReaderT (unBitTorrent action) client
159{-# INLINE runBitTorrent #-}
160
161getClient :: BitTorrent Client
162getClient = BitTorrent ask
163{-# INLINE getClient #-}
diff --git a/src/Network/BitTorrent/DHT.hs b/src/Network/BitTorrent/DHT.hs
new file mode 100644
index 00000000..45c87831
--- /dev/null
+++ b/src/Network/BitTorrent/DHT.hs
@@ -0,0 +1,285 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- BitTorrent uses a \"distributed sloppy hash table\" (DHT) for
9-- storing peer contact information for \"trackerless\" torrents. In
10-- effect, each peer becomes a tracker.
11--
12-- Normally you don't need to import other DHT modules.
13--
14-- For more info see:
15-- <http://www.bittorrent.org/beps/bep_0005.html>
16--
17{-# LANGUAGE FlexibleInstances #-}
18{-# LANGUAGE TemplateHaskell #-}
19{-# LANGUAGE TypeOperators #-}
20module Network.BitTorrent.DHT
21 ( -- * Distributed Hash Table
22 DHT
23 , Options (..)
24 , fullLogging
25 , dht
26
27 -- * Bootstrapping
28 -- $bootstrapping-terms
29 , tNodes
30 , defaultBootstrapNodes
31 , resolveHostName
32 , bootstrap
33 , isBootstrapped
34
35 -- * Initialization
36 , snapshot
37
38 -- * Operations
39 , Network.BitTorrent.DHT.lookup
40 , Network.BitTorrent.DHT.insert
41 , Network.BitTorrent.DHT.delete
42
43 -- * Embedding
44 -- ** Session
45 , LogFun
46 , Node
47 , defaultHandlers
48 , newNode
49 , closeNode
50
51 -- ** Monad
52 , MonadDHT (..)
53 , runDHT
54 ) where
55
56import Control.Monad.Logger
57import Control.Monad.Reader
58import Control.Exception
59import qualified Data.ByteString as BS
60import Data.Conduit as C
61import qualified Data.Conduit.List as C
62import Data.Serialize
63import Network.Socket
64import Text.PrettyPrint.HughesPJClass as PP (pPrint,render)
65
66import Data.Torrent
67import Network.BitTorrent.Address
68import Network.BitTorrent.DHT.Query
69import Network.BitTorrent.DHT.Session
70import Network.BitTorrent.DHT.Routing as T hiding (null)
71import qualified Data.Text as Text
72import Data.Monoid
73
74
75{-----------------------------------------------------------------------
76-- DHT types
77-----------------------------------------------------------------------}
78
79class MonadDHT m where
80 liftDHT :: DHT IPv4 a -> m a
81
82instance MonadDHT (DHT IPv4) where
83 liftDHT = id
84
85-- | Convenience method. Pass this to 'dht' to enable full logging.
86fullLogging :: LogSource -> LogLevel -> Bool
87fullLogging _ _ = True
88
89-- | Run DHT on specified port. <add note about resources>
90dht :: Address ip
91 => Options -- ^ normally you need to use 'Data.Default.def';
92 -> NodeAddr ip -- ^ address to bind this node;
93 -> (LogSource -> LogLevel -> Bool) -- ^ use 'fullLogging' as a noisy default
94 -> DHT ip a -- ^ actions to run: 'bootstrap', 'lookup', etc;
95 -> IO a -- ^ result.
96dht opts addr logfilter action = do
97 runStderrLoggingT $ filterLogger logfilter $ LoggingT $ \ logger -> do
98 bracket (newNode defaultHandlers opts addr logger Nothing) closeNode $
99 \ node -> runDHT node action
100{-# INLINE dht #-}
101
102{-----------------------------------------------------------------------
103-- Bootstrapping
104-----------------------------------------------------------------------}
105-- $bootstrapping-terms
106--
107-- [@Bootstrapping@] DHT @bootstrapping@ is the process of filling
108-- routing 'Table' by /good/ nodes.
109--
110-- [@Bootstrapping time@] Bootstrapping process can take up to 5
111-- minutes. Bootstrapping should only happen at first application
112-- startup, if possible you should use 'snapshot' & 'restore'
113-- mechanism which must work faster.
114--
115-- [@Bootstrap nodes@] DHT @bootstrap node@ is either:
116--
117-- * a specialized high performance node maintained by bittorrent
118-- software authors\/maintainers, like those listed in
119-- 'defaultBootstrapNodes'. /Specialized/ means that those nodes
120-- may not support 'insert' queries and is running for the sake of
121-- bootstrapping only.
122--
123-- * an ordinary bittorrent client running DHT node. The list of
124-- such bootstrapping nodes usually obtained from
125-- 'Data.Torrent.tNodes' field or
126-- 'Network.BitTorrent.Exchange.Message.Port' messages.
127
128-- Do not include the following hosts in the default bootstrap nodes list:
129--
130-- * "dht.aelitis.com" and "dht6.azureusplatform.com" - since
131-- Azureus client have a different (and probably incompatible) DHT
132-- protocol implementation.
133--
134-- * "router.utorrent.com" since it is just an alias to
135-- "router.bittorrent.com".
136-- XXX: ignoring this advise as it resolves to a different
137-- ip address for me.
138
139-- | List of bootstrap nodes maintained by different bittorrent
140-- software authors.
141defaultBootstrapNodes :: [NodeAddr HostName]
142defaultBootstrapNodes =
143 [ NodeAddr "router.bittorrent.com" 6881 -- by BitTorrent Inc.
144
145 -- doesn't work at the moment (use git blame) of commit
146 , NodeAddr "dht.transmissionbt.com" 6881 -- by Transmission project
147
148 , NodeAddr "router.utorrent.com" 6881
149 ]
150
151-- TODO Multihomed hosts
152
153-- | Resolve either a numeric network address or a hostname to a
154-- numeric IP address of the node. Usually used to resolve
155-- 'defaultBootstrapNodes' or 'Data.Torrent.tNodes' lists.
156resolveHostName :: NodeAddr HostName -> IO (NodeAddr IPv4)
157resolveHostName NodeAddr {..} = do
158 let hints = defaultHints { addrFamily = AF_INET, addrSocketType = Datagram }
159 -- getAddrInfo throws exception on empty list, so the pattern matching never fail
160 info : _ <- getAddrInfo (Just hints) (Just nodeHost) (Just (show nodePort))
161 case fromSockAddr (addrAddress info) of
162 Nothing -> error "resolveNodeAddr: impossible"
163 Just addr -> return addr
164
165-- | One good node may be sufficient.
166--
167-- This operation do block, use
168-- 'Control.Concurrent.Async.Lifted.async' if needed.
169bootstrap :: Address ip => Maybe BS.ByteString -> [NodeAddr ip] -> DHT ip ()
170bootstrap mbs startNodes = do
171 restored <-
172 case decode <$> mbs of
173 Just (Right tbl) -> return (T.toList tbl)
174 Just (Left e) -> do $(logWarnS) "restore" (Text.pack e)
175 return []
176 Nothing -> return []
177
178 $(logInfoS) "bootstrap" "Start node bootstrapping"
179 let searchAll aliveNodes = do
180 nid <- myNodeIdAccordingTo (error "FIXME")
181 C.sourceList [aliveNodes] $= search nid (findNodeQ nid) $$ C.consume
182 input_nodes <- (restored ++) . T.toList <$> getTable
183 -- Step 1: Use iterative searches to flesh out the table..
184 do let knowns = map (map $ nodeAddr . fst) input_nodes
185 -- Below, we reverse the nodes since the table serialization puts the
186 -- nearest nodes last and we want to choose a similar node id to bootstrap
187 -- faster.
188 (alive_knowns,_) <- unzip <$> queryParallel (pingQ <$> reverse (concat knowns))
189 b <- isBootstrapped
190 -- If our cached nodes are alive and our IP address did not change, it's possible
191 -- we are already bootsrapped, so no need to do any searches.
192 when (not b) $ do
193 nss <- searchAll $ take 2 alive_knowns
194 -- We only use the supplied bootstrap nodes when we don't know of any
195 -- others to try.
196 when (null nss) $ do
197 -- TODO filter duplicated in startNodes list
198 -- TODO retransmissions for startNodes
199 (aliveNodes,_) <- unzip <$> queryParallel (pingQ <$> startNodes)
200 _ <- searchAll $ take 2 aliveNodes
201 return ()
202 -- Step 2: Repeatedly refresh incomplete buckets until the table is full.
203 maxbuckets <- asks $ optBucketCount . options
204 flip fix 0 $ \loop icnt -> do
205 tbl <- getTable
206 let unfull = filter ((/=defaultBucketSize) . snd)
207 us = zip
208 -- is_last = True for the last bucket
209 (True:repeat False)
210 -- Only non-full buckets unless it is the last one and the
211 -- maximum number of buckets has not been reached.
212 $ case reverse $ zip [0..] $ T.shape tbl of
213 p@(n,_):ps | n+1==maxbuckets -> unfull (p:ps)
214 p:ps -> p:unfull ps
215 [] -> []
216 forM_ us $ \(is_last,(index,_)) -> do
217 nid <- myNodeIdAccordingTo (error "FIXME")
218 sample <- liftIO $ genBucketSample nid (bucketRange index is_last)
219 $(logDebugS) "bootstrapping"
220 $ "BOOTSTRAP sample"
221 <> Text.pack (show (is_last,index,T.shape tbl))
222 <> " " <> Text.pack (render $ pPrint sample)
223 refreshNodes sample
224 $(logDebugS) "bootstrapping"
225 $ "BOOTSTRAP finished iteration "
226 <> Text.pack (show (icnt,T.shape tbl,us,defaultBucketSize))
227 when (not (null us) && icnt < div (3*maxbuckets) 2)
228 $ loop (succ icnt)
229 $(logInfoS) "bootstrap" "Node bootstrapping finished"
230
231-- | Check if this node is already bootstrapped.
232-- @bootstrap [good_node] >> isBootstrapped@@ should always return 'True'.
233--
234-- This operation do not block.
235--
236isBootstrapped :: Eq ip => DHT ip Bool
237isBootstrapped = T.full <$> getTable
238
239{-----------------------------------------------------------------------
240-- Initialization
241-----------------------------------------------------------------------}
242
243-- | Serialize current DHT session to byte string.
244--
245-- This is blocking operation, use
246-- 'Control.Concurrent.Async.Lifted.async' if needed.
247snapshot :: Address ip => DHT ip BS.ByteString
248snapshot = do
249 tbl <- getTable
250 return $ encode tbl
251
252{-----------------------------------------------------------------------
253-- Operations
254-----------------------------------------------------------------------}
255
256-- | Get list of peers which downloading this torrent.
257--
258-- This operation is incremental and do block.
259--
260lookup :: Address ip => InfoHash -> DHT ip `C.Source` [PeerAddr ip]
261lookup topic = do -- TODO retry getClosest if bucket is empty
262 closest <- lift $ getClosest topic
263 C.sourceList [closest] $= search topic (getPeersQ topic)
264
265-- TODO do not republish if the topic is already in announceSet
266
267-- | Announce that /this/ peer may have some pieces of the specified
268-- torrent. DHT will reannounce this data periodically using
269-- 'optReannounce' interval.
270--
271-- This operation is synchronous and do block, use
272-- 'Control.Concurrent.Async.Lifted.async' if needed.
273--
274insert :: Address ip => InfoHash -> PortNumber -> DHT ip ()
275insert ih p = do
276 publish ih p
277 insertTopic ih p
278
279-- | Stop announcing /this/ peer for the specified torrent.
280--
281-- This operation is atomic and may block for a while.
282--
283delete :: InfoHash -> PortNumber -> DHT ip ()
284delete = deleteTopic
285{-# INLINE delete #-}
diff --git a/src/Network/BitTorrent/DHT/ContactInfo.hs b/src/Network/BitTorrent/DHT/ContactInfo.hs
new file mode 100644
index 00000000..d7c92e35
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/ContactInfo.hs
@@ -0,0 +1,138 @@
1module Network.BitTorrent.DHT.ContactInfo
2 ( PeerStore
3 , Network.BitTorrent.DHT.ContactInfo.lookup
4 , Network.BitTorrent.DHT.ContactInfo.insert
5 ) where
6
7import Data.Default
8import Data.List as L
9import Data.Maybe
10import Data.HashMap.Strict as HM
11import Data.Serialize
12
13import Data.Torrent
14import Network.BitTorrent.Address
15
16-- {-
17-- import Data.HashMap.Strict as HM
18--
19-- import Data.Torrent.InfoHash
20-- import Network.BitTorrent.Address
21--
22-- -- increase prefix when table is too large
23-- -- decrease prefix when table is too small
24-- -- filter outdated peers
25--
26-- {-----------------------------------------------------------------------
27-- -- PeerSet
28-- -----------------------------------------------------------------------}
29--
30-- type PeerSet a = [(PeerAddr a, NodeInfo a, Timestamp)]
31--
32-- -- compare PSQueue vs Ordered list
33--
34-- takeNewest :: PeerSet a -> [PeerAddr a]
35-- takeNewest = undefined
36--
37-- dropOld :: Timestamp -> PeerSet a -> PeerSet a
38-- dropOld = undefined
39--
40-- insert :: PeerAddr a -> Timestamp -> PeerSet a -> PeerSet a
41-- insert = undefined
42--
43-- type Mask = Int
44-- type Size = Int
45-- type Timestamp = Int
46--
47-- {-----------------------------------------------------------------------
48-- -- InfoHashMap
49-- -----------------------------------------------------------------------}
50--
51-- -- compare handwritten prefix tree versus IntMap
52--
53-- data Tree a
54-- = Nil
55-- | Tip !InfoHash !(PeerSet a)
56-- | Bin !InfoHash !Mask !Size !Timestamp (Tree a) (Tree a)
57--
58-- insertTree :: InfoHash -> a -> Tree a -> Tree a
59-- insertTree = undefined
60--
61-- type Prio = Int
62--
63-- --shrink :: ContactInfo ip -> Int
64-- shrink Nil = Nil
65-- shrink (Tip _ _) = undefined
66-- shrink (Bin _ _) = undefined
67--
68-- {-----------------------------------------------------------------------
69-- -- InfoHashMap
70-- -----------------------------------------------------------------------}
71--
72-- -- compare new design versus HashMap
73--
74-- data IntMap k p a
75-- type ContactInfo = Map InfoHash Timestamp (Set (PeerAddr IP) Timestamp)
76--
77-- data ContactInfo ip = PeerStore
78-- { maxSize :: Int
79-- , prefixSize :: Int
80-- , thisNodeId :: NodeId
81--
82-- , count :: Int -- ^ Cached size of the 'peerSet'
83-- , peerSet :: HashMap InfoHash [PeerAddr ip]
84-- }
85--
86-- size :: ContactInfo ip -> Int
87-- size = undefined
88--
89-- prefixSize :: ContactInfo ip -> Int
90-- prefixSize = undefined
91--
92-- lookup :: InfoHash -> ContactInfo ip -> [PeerAddr ip]
93-- lookup = undefined
94--
95-- insert :: InfoHash -> PeerAddr ip -> ContactInfo ip -> ContactInfo ip
96-- insert = undefined
97--
98-- -- | Limit in size.
99-- prune :: NodeId -> Int -> ContactInfo ip -> ContactInfo ip
100-- prune pref targetSize Nil = Nil
101-- prune pref targetSize (Tip _ _) = undefined
102--
103-- -- | Remove expired entries.
104-- splitGT :: Timestamp -> ContactInfo ip -> ContactInfo ip
105-- splitGT = undefined
106-- -}
107
108-- | Storage used to keep track a set of known peers in client,
109-- tracker or DHT sessions.
110newtype PeerStore ip = PeerStore (HashMap InfoHash [PeerAddr ip])
111
112-- | Empty store.
113instance Default (PeerStore a) where
114 def = PeerStore HM.empty
115 {-# INLINE def #-}
116
117-- | Monoid under union operation.
118instance Eq a => Monoid (PeerStore a) where
119 mempty = def
120 {-# INLINE mempty #-}
121
122 mappend (PeerStore a) (PeerStore b) =
123 PeerStore (HM.unionWith L.union a b)
124 {-# INLINE mappend #-}
125
126-- | Can be used to store peers between invocations of the client
127-- software.
128instance Serialize (PeerStore a) where
129 get = undefined
130 put = undefined
131
132-- | Used in 'get_peers' DHT queries.
133lookup :: InfoHash -> PeerStore a -> [PeerAddr a]
134lookup ih (PeerStore m) = fromMaybe [] $ HM.lookup ih m
135
136-- | Used in 'announce_peer' DHT queries.
137insert :: Eq a => InfoHash -> PeerAddr a -> PeerStore a -> PeerStore a
138insert ih a (PeerStore m) = PeerStore (HM.insertWith L.union ih [a] m)
diff --git a/src/Network/BitTorrent/DHT/Message.hs b/src/Network/BitTorrent/DHT/Message.hs
new file mode 100644
index 00000000..9d66741f
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/Message.hs
@@ -0,0 +1,343 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This module provides message datatypes which is used for /Node to
9-- Node/ communication. Bittorrent DHT is based on Kademlia
10-- specification, but have a slightly different set of messages
11-- which have been adopted for /peer/ discovery mechanism. Messages
12-- are sent over "Network.KRPC" protocol, but normally you should
13-- use "Network.BitTorrent.DHT.Session" to send and receive
14-- messages.
15--
16-- DHT queries are not /recursive/, they are /iterative/. This means
17-- that /querying/ node . While original specification (namely BEP5)
18-- do not impose any restrictions for /quered/ node behaviour, a
19-- good DHT implementation should follow some rules to guarantee
20-- that unlimit recursion will never happen. The following set of
21-- restrictions:
22--
23-- * 'Ping' query must not trigger any message.
24--
25-- * 'FindNode' query /may/ trigger 'Ping' query to check if a
26-- list of nodes to return is /good/. See
27-- 'Network.BitTorrent.DHT.Routing.Routing' for further explanation.
28--
29-- * 'GetPeers' query may trigger 'Ping' query for the same reason.
30--
31-- * 'Announce' query must trigger 'Ping' query for the same reason.
32--
33-- It is easy to see that the most long RPC chain is:
34--
35-- @
36-- | | |
37-- Node_A | |
38-- | FindNode or GetPeers or Announce | |
39-- | ------------------------------------> Node_B |
40-- | | Ping |
41-- | | -----------> |
42-- | | Node_C
43-- | | Pong |
44-- | NodeFound or GotPeers or Announced | <----------- |
45-- | <------------------------------------- Node_B |
46-- Node_A | |
47-- | | |
48-- @
49--
50-- where in some cases 'Node_C' is 'Node_A'.
51--
52-- For more info see:
53-- <http://www.bittorrent.org/beps/bep_0005.html#dht-queries>
54--
55-- For Kamelia messages see original Kademlia paper:
56-- <http://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf>
57--
58{-# LANGUAGE DeriveDataTypeable #-}
59{-# LANGUAGE FlexibleInstances #-}
60{-# LANGUAGE MultiParamTypeClasses #-}
61{-# LANGUAGE UndecidableInstances #-}
62{-# LANGUAGE ScopedTypeVariables #-}
63module Network.BitTorrent.DHT.Message
64 ( -- * Envelopes
65 Query (..)
66 , Response (..)
67
68 -- * Queries
69 -- ** ping
70 , Ping (..)
71
72 -- ** find_node
73 , FindNode (..)
74 , NodeFound (..)
75
76 -- ** get_peers
77 , PeerList
78 , GetPeers (..)
79 , GotPeers (..)
80
81 -- ** announce_peer
82 , Announce (..)
83 , Announced (..)
84 ) where
85
86import Control.Applicative
87import Data.BEncode as BE
88import Data.BEncode.BDict
89import Data.List as L
90import Data.Monoid
91import Data.Serialize as S
92import Data.Typeable
93import Network
94import Network.KRPC
95import Data.Maybe
96
97import Data.Torrent (InfoHash)
98import Network.BitTorrent.Address
99import Network.BitTorrent.DHT.Token
100import Network.KRPC ()
101
102{-----------------------------------------------------------------------
103-- envelopes
104-----------------------------------------------------------------------}
105
106node_id_key :: BKey
107node_id_key = "id"
108
109read_only_key :: BKey
110read_only_key = "ro"
111
112
113-- | All queries have an \"id\" key and value containing the node ID
114-- of the querying node.
115data Query a = Query
116 { queringNodeId :: NodeId -- ^ node id of /quering/ node;
117 , queryIsReadOnly :: Bool -- ^ node is read-only as per BEP 43
118 , queryParams :: a -- ^ query parameters.
119 } deriving (Show, Eq, Typeable)
120
121instance BEncode a => BEncode (Query a) where
122 toBEncode Query {..} = toDict $
123 node_id_key .=! queringNodeId .: endDict
124 <>
125 dict (toBEncode queryParams)
126 where
127 dict (BDict d) | queryIsReadOnly = Cons read_only_key (BInteger 1) d
128 | otherwise = d
129 dict _ = error "impossible: instance BEncode (Query a)"
130
131 fromBEncode v = do
132 Query <$> fromDict (field (req node_id_key)) v
133 <*> fromDict (fromMaybe False <$>? read_only_key) v
134 <*> fromBEncode v
135
136-- | All responses have an \"id\" key and value containing the node ID
137-- of the responding node.
138data Response a = Response
139 { queredNodeId :: NodeId -- ^ node id of /quered/ node;
140 , responseVals :: a -- ^ query result.
141 } deriving (Show, Eq, Typeable)
142
143instance BEncode a => BEncode (Response a) where
144 toBEncode = toBEncode . toQuery
145 where
146 toQuery (Response nid a) = Query nid False a
147
148 fromBEncode b = fromQuery <$> fromBEncode b
149 where
150 fromQuery (Query nid _ a) = Response nid a
151
152
153{-----------------------------------------------------------------------
154-- ping method
155-----------------------------------------------------------------------}
156
157-- | The most basic query is a ping. Ping query is used to check if a
158-- quered node is still alive.
159data Ping = Ping
160 deriving (Show, Eq, Typeable)
161
162instance BEncode Ping where
163 toBEncode Ping = toDict endDict
164 fromBEncode _ = pure Ping
165
166-- | \"q\" = \"ping\"
167instance KRPC (Query Ping) (Response Ping) where
168 method = "ping"
169
170{-----------------------------------------------------------------------
171-- find_node method
172-----------------------------------------------------------------------}
173
174-- | Find node is used to find the contact information for a node
175-- given its ID.
176newtype FindNode = FindNode NodeId
177 deriving (Show, Eq, Typeable)
178
179target_key :: BKey
180target_key = "target"
181
182instance BEncode FindNode where
183 toBEncode (FindNode nid) = toDict $ target_key .=! nid .: endDict
184 fromBEncode = fromDict $ FindNode <$>! target_key
185
186-- | When a node receives a 'FindNode' query, it should respond with a
187-- the compact node info for the target node or the K (8) closest good
188-- nodes in its own routing table.
189--
190newtype NodeFound ip = NodeFound [NodeInfo ip]
191 deriving (Show, Eq, Typeable)
192
193nodes_key :: BKey
194nodes_key = "nodes"
195
196-- Convert IPv4 address. Useful for using variadic IP type.
197from4 :: forall s. Address s => NodeInfo IPv4 -> Either String (NodeInfo s)
198from4 n = maybe (Left "Error converting IPv4") Right
199 $ traverse (fromAddr :: IPv4 -> Maybe s) n
200
201binary :: Serialize a => BKey -> BE.Get [a]
202binary k = field (req k) >>= either (fail . format) return .
203 runGet (many get)
204 where
205 format str = "fail to deserialize " ++ show k ++ " field: " ++ str
206
207instance Address ip => BEncode (NodeFound ip) where
208 toBEncode (NodeFound ns) = toDict $
209 nodes_key .=! runPut (mapM_ put ns)
210 .: endDict
211
212 -- TODO: handle IPv6 by reading the "nodes6" key (see bep 32)
213 fromBEncode bval = NodeFound <$> (traverse from4 =<< fromDict (binary nodes_key) bval)
214
215-- | \"q\" == \"find_node\"
216instance (Address ip, Typeable ip)
217 => KRPC (Query FindNode) (Response (NodeFound ip)) where
218 method = "find_node"
219
220{-----------------------------------------------------------------------
221-- get_peers method
222-----------------------------------------------------------------------}
223
224-- | Get peers associated with a torrent infohash.
225newtype GetPeers = GetPeers InfoHash
226 deriving (Show, Eq, Typeable)
227
228info_hash_key :: BKey
229info_hash_key = "info_hash"
230
231instance BEncode GetPeers where
232 toBEncode (GetPeers ih) = toDict $ info_hash_key .=! ih .: endDict
233 fromBEncode = fromDict $ GetPeers <$>! info_hash_key
234
235type PeerList ip = Either [NodeInfo ip] [PeerAddr ip]
236
237data GotPeers ip = GotPeers
238 { -- | If the queried node has no peers for the infohash, returned
239 -- the K nodes in the queried nodes routing table closest to the
240 -- infohash supplied in the query.
241 peers :: PeerList ip
242
243 -- | The token value is a required argument for a future
244 -- announce_peer query.
245 , grantedToken :: Token
246 } deriving (Show, Eq, Typeable)
247
248peers_key :: BKey
249peers_key = "values"
250
251token_key :: BKey
252token_key = "token"
253
254instance (Typeable ip, Serialize ip) => BEncode (GotPeers ip) where
255 toBEncode GotPeers {..} = toDict $
256 case peers of
257 Left ns ->
258 nodes_key .=! runPut (mapM_ put ns)
259 .: token_key .=! grantedToken
260 .: endDict
261 Right ps ->
262 token_key .=! grantedToken
263 .: peers_key .=! L.map S.encode ps
264 .: endDict
265
266 fromBEncode = fromDict $ do
267 mns <- optional (binary nodes_key) -- "nodes"
268 tok <- field (req token_key) -- "token"
269 mps <- optional (field (req peers_key) >>= decodePeers) -- "values"
270 case (Right <$> mps) <|> (Left <$> mns) of
271 Nothing -> fail "get_peers: neihter peers nor nodes key is valid"
272 Just xs -> pure $ GotPeers xs tok
273 where
274 decodePeers = either fail pure . mapM S.decode
275
276-- | \"q" = \"get_peers\"
277instance (Typeable ip, Serialize ip) =>
278 KRPC (Query GetPeers) (Response (GotPeers ip)) where
279 method = "get_peers"
280
281{-----------------------------------------------------------------------
282-- announce method
283-----------------------------------------------------------------------}
284
285-- | Announce that the peer, controlling the querying node, is
286-- downloading a torrent on a port.
287data Announce = Announce
288 { -- | If set, the 'port' field should be ignored and the source
289 -- port of the UDP packet should be used as the peer's port
290 -- instead. This is useful for peers behind a NAT that may not
291 -- know their external port, and supporting uTP, they accept
292 -- incoming connections on the same port as the DHT port.
293 impliedPort :: Bool
294
295 -- | infohash of the torrent;
296 , topic :: InfoHash
297
298 -- | the port /this/ peer is listening;
299 , port :: PortNumber
300
301 -- | received in response to a previous get_peers query.
302 , sessionToken :: Token
303 } deriving (Show, Eq, Typeable)
304
305port_key :: BKey
306port_key = "port"
307
308implied_port_key :: BKey
309implied_port_key = "implied_port"
310
311instance BEncode Announce where
312 toBEncode Announce {..} = toDict $
313 implied_port_key .=? flagField impliedPort
314 .: info_hash_key .=! topic
315 .: port_key .=! port
316 .: token_key .=! sessionToken
317 .: endDict
318 where
319 flagField flag = if flag then Just (1 :: Int) else Nothing
320
321 fromBEncode = fromDict $ do
322 Announce <$> (boolField <$> optional (field (req implied_port_key)))
323 <*>! info_hash_key
324 <*>! port_key
325 <*>! token_key
326 where
327 boolField = maybe False (/= (0 :: Int))
328
329-- | The queried node must verify that the token was previously sent
330-- to the same IP address as the querying node. Then the queried node
331-- should store the IP address of the querying node and the supplied
332-- port number under the infohash in its store of peer contact
333-- information.
334data Announced = Announced
335 deriving (Show, Eq, Typeable)
336
337instance BEncode Announced where
338 toBEncode _ = toBEncode Ping
339 fromBEncode _ = pure Announced
340
341-- | \"q" = \"announce\"
342instance KRPC (Query Announce) (Response Announced) where
343 method = "announce_peer"
diff --git a/src/Network/BitTorrent/DHT/Query.hs b/src/Network/BitTorrent/DHT/Query.hs
new file mode 100644
index 00000000..e067ab52
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/Query.hs
@@ -0,0 +1,325 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2014
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This module provides functions to interact with other nodes.
9-- Normally, you don't need to import this module, use
10-- "Network.BitTorrent.DHT" instead.
11--
12{-# LANGUAGE FlexibleContexts #-}
13{-# LANGUAGE ScopedTypeVariables #-}
14{-# LANGUAGE TemplateHaskell #-}
15module Network.BitTorrent.DHT.Query
16 ( -- * Handler
17 -- | To bind specific set of handlers you need to pass
18 -- handler list to the 'startNode' function.
19 pingH
20 , findNodeH
21 , getPeersH
22 , announceH
23 , defaultHandlers
24
25 -- * Query
26 -- ** Basic
27 -- | A basic query perform a single request expecting a
28 -- single response.
29 , Iteration
30 , pingQ
31 , findNodeQ
32 , getPeersQ
33 , announceQ
34
35 -- ** Iterative
36 -- | An iterative query perform multiple basic queries,
37 -- concatenate its responses, optionally yielding result and
38 -- continue to the next iteration.
39 , Search
40 , search
41 , publish
42
43 -- ** Routing table
44 , insertNode
45 , refreshNodes
46
47 -- ** Messaging
48 , queryNode
49 , (<@>)
50 ) where
51
52import Control.Concurrent.Lifted hiding (yield)
53import Control.Exception.Lifted hiding (Handler)
54import Control.Monad.Reader
55import Control.Monad.Logger
56import Data.Maybe
57import Data.Conduit
58import Data.Conduit.List as C hiding (mapMaybe, mapM_)
59import Data.Either
60import Data.List as L
61import Data.Monoid
62import Data.Text as T
63import Network
64import Text.PrettyPrint as PP hiding ((<>), ($$))
65import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
66import Data.Time
67import Data.Time.Clock.POSIX
68
69import Network.KRPC hiding (Options, def)
70import Network.KRPC.Message (ReflectedIP(..))
71import Data.Torrent
72import Network.BitTorrent.Address
73import Network.BitTorrent.DHT.Message
74import Network.BitTorrent.DHT.Routing as R
75import Network.BitTorrent.DHT.Session
76import Control.Concurrent.STM
77
78{-----------------------------------------------------------------------
79-- Handlers
80-----------------------------------------------------------------------}
81
82nodeHandler :: Address ip => KRPC (Query a) (Response b)
83 => (NodeAddr ip -> a -> DHT ip b) -> NodeHandler ip
84nodeHandler action = handler $ \ sockAddr (Query remoteId read_only q) -> do
85 case fromSockAddr sockAddr of
86 Nothing -> throwIO BadAddress
87 Just naddr -> do
88 let ni = NodeInfo remoteId naddr
89 -- Do not route read-only nodes. (bep 43)
90 if read_only
91 then $(logWarnS) "nodeHandler" $ "READ-ONLY " <> T.pack (show $ pPrint ni)
92 else insertNode ni Nothing >> return () -- TODO need to block. why?
93 Response
94 <$> myNodeIdAccordingTo naddr
95 <*> action naddr q
96
97-- | Default 'Ping' handler.
98pingH :: Address ip => NodeHandler ip
99pingH = nodeHandler $ \ _ Ping -> do
100 return Ping
101
102-- | Default 'FindNode' handler.
103findNodeH :: Address ip => NodeHandler ip
104findNodeH = nodeHandler $ \ _ (FindNode nid) -> do
105 NodeFound <$> getClosest nid
106
107-- | Default 'GetPeers' handler.
108getPeersH :: Address ip => NodeHandler ip
109getPeersH = nodeHandler $ \ naddr (GetPeers ih) -> do
110 ps <- getPeerList ih
111 tok <- grantToken naddr
112 $(logDebugS) "getPeersH" $ "INFO-HASH " <> T.pack (show (ih,fmap fromAddr naddr :: NodeAddr (Maybe IP)))
113 return $ GotPeers ps tok
114
115-- | Default 'Announce' handler.
116announceH :: Address ip => NodeHandler ip
117announceH = nodeHandler $ \ naddr @ NodeAddr {..} (Announce {..}) -> do
118 valid <- checkToken naddr sessionToken
119 unless valid $ do
120 throwIO $ InvalidParameter "token"
121
122 let annPort = if impliedPort then nodePort else port
123 let peerAddr = PeerAddr Nothing nodeHost annPort
124 insertPeer topic peerAddr
125 return Announced
126
127-- | Includes all default query handlers.
128defaultHandlers :: Address ip => [NodeHandler ip]
129defaultHandlers = [pingH, findNodeH, getPeersH, announceH]
130
131{-----------------------------------------------------------------------
132-- Basic queries
133-----------------------------------------------------------------------}
134
135type Iteration ip o = NodeInfo ip -> DHT ip (Either [NodeInfo ip] [o ip])
136
137-- | The most basic query. May be used to check if the given node is
138-- alive or get its 'NodeId'.
139pingQ :: Address ip => NodeAddr ip -> DHT ip (NodeInfo ip, Maybe ReflectedIP)
140pingQ addr = do
141 (nid, Ping, mip) <- queryNode' addr Ping
142 return (NodeInfo nid addr, mip)
143
144-- TODO [robustness] match range of returned node ids with the
145-- expected range and either filter bad nodes or discard response at
146-- all throwing an exception
147findNodeQ :: Address ip => TableKey key => key -> Iteration ip NodeInfo
148findNodeQ key NodeInfo {..} = do
149 NodeFound closest <- FindNode (toNodeId key) <@> nodeAddr
150 $(logInfoS) "findNodeQ" $ "NodeFound\n"
151 <> T.pack (L.unlines $ L.map ((' ' :) . show . pPrint) closest)
152 return $ Right closest
153
154getPeersQ :: Address ip => InfoHash -> Iteration ip PeerAddr
155getPeersQ topic NodeInfo {..} = do
156 GotPeers {..} <- GetPeers topic <@> nodeAddr
157 let dist = distance (toNodeId topic) nodeId
158 $(logInfoS) "getPeersQ" $ T.pack
159 $ "distance: " <> render (pPrint dist) <> " , result: "
160 <> case peers of { Left _ -> "NODES"; Right _ -> "PEERS" }
161 return peers
162
163announceQ :: Address ip => InfoHash -> PortNumber -> Iteration ip NodeAddr
164announceQ ih p NodeInfo {..} = do
165 GotPeers {..} <- GetPeers ih <@> nodeAddr
166 case peers of
167 Left ns
168 | False -> undefined -- TODO check if we can announce
169 | otherwise -> return (Left ns)
170 Right _ -> do -- TODO *probably* add to peer cache
171 Announced <- Announce False ih p grantedToken <@> nodeAddr
172 return (Right [nodeAddr])
173
174{-----------------------------------------------------------------------
175-- Iterative queries
176-----------------------------------------------------------------------}
177
178type Search ip o = Conduit [NodeInfo ip] (DHT ip) [o ip]
179
180-- TODO: use reorder and filter (Traversal option) leftovers
181search :: k -> Iteration ip o -> Search ip o
182search _ action = do
183 awaitForever $ \ batch -> unless (L.null batch) $ do
184 $(logWarnS) "search" "start query"
185 responses <- lift $ queryParallel (action <$> batch)
186 let (nodes, results) = partitionEithers responses
187 $(logWarnS) "search" ("done query more:" <> T.pack (show (L.length nodes, L.length results)))
188 leftover $ L.concat nodes
189 mapM_ yield results
190
191publish :: Address ip => InfoHash -> PortNumber -> DHT ip ()
192publish ih p = do
193 nodes <- getClosest ih
194 r <- asks (optReplication . options)
195 _ <- sourceList [nodes] $= search ih (announceQ ih p) $$ C.take r
196 return ()
197
198getTimestamp :: DHT ip Timestamp
199getTimestamp = do
200 utcTime <- liftIO $ getCurrentTime
201 $(logDebugS) "routing.make_timestamp" (T.pack (render (pPrint utcTime)))
202 return $ utcTimeToPOSIXSeconds utcTime
203
204
205probeNode :: Address ip => NodeAddr ip -> DHT ip (Bool, Maybe ReflectedIP)
206probeNode addr = do
207 $(logDebugS) "routing.questionable_node" (T.pack (render (pPrint addr)))
208 result <- try $ pingQ addr
209 let _ = fmap (const ()) result :: Either SomeException ()
210 return $ either (const (False,Nothing)) (\(_,mip)->(True,mip)) result
211
212
213-- FIXME do not use getClosest sinse we should /refresh/ them
214refreshNodes :: Address ip => NodeId -> DHT ip () -- [NodeInfo ip]
215refreshNodes nid = do
216 $(logDebugS) "routing.refresh_bucket" (T.pack (render (pPrint nid)))
217 nodes <- getClosest nid
218 do
219 -- forM (L.take 1 nodes) $ \ addr -> do
220 -- NodeFound ns <- FindNode nid <@> addr
221 -- Expected type: ConduitM [NodeAddr ip] [NodeInfo ip] (DHT ip) ()
222 -- Actual type: ConduitM [NodeInfo ip] [NodeInfo ip] (DHT ip) ()
223 -- nss <- sourceList [[addr]] $= search nid (findNodeQ nid) $$ C.consume
224 nss <- sourceList [nodes] $= search nid (findNodeQ nid) $$ C.consume
225 $(logWarnS) "refreshNodes" $ "received " <> T.pack (show (L.length (L.concat nss))) <> " nodes."
226 _ <- queryParallel $ flip L.map (L.concat nss) $ \n -> do
227 $(logWarnS) "refreshNodes" $ "received node: " <> T.pack (show (pPrint n))
228 pingQ (nodeAddr n)
229 -- pingQ takes care of inserting the node.
230 return ()
231 return () -- $ L.concat nss
232
233-- | This operation do not block but acquire exclusive access to
234-- routing table.
235insertNode :: Address ip => NodeInfo ip -> Maybe ReflectedIP -> DHT ip ThreadId
236insertNode info witnessed_ip0 = fork $ do
237 var <- asks routingInfo
238 tm <- getTimestamp
239 let showTable = do
240 t <- getTable
241 let logMsg = "Routing table: " <> pPrint t
242 $(logDebugS) "insertNode" (T.pack (render logMsg))
243 let arrival0 = TryInsert info
244 arrival4 = TryInsert (fmap fromAddr info) :: Event (Maybe IPv4)
245 $(logDebugS) "insertNode" $ T.pack (show arrival4)
246 maxbuckets <- asks (optBucketCount . options)
247 fallbackid <- asks tentativeNodeId
248 let atomicInsert arrival witnessed_ip = do
249 minfo <- readTVar var
250 let change ip = fromMaybe fallbackid
251 $ listToMaybe
252 $ rank id (nodeId $ foreignNode arrival)
253 $ bep42s ip fallbackid
254 case minfo of
255 Just inf -> do
256 (ps,t') <- R.insert tm arrival $ myBuckets inf
257 writeTVar var $ Just $ inf { myBuckets = t' }
258 return $ do
259 case witnessed_ip of
260 Just (ReflectedIP ip0)
261 | fromSockAddr ip0 /= Just (myAddress inf)
262 -> $(logInfo) ( T.pack $ L.unwords
263 $ [ "Possible NAT?"
264 , show (toSockAddr $ nodeAddr $ foreignNode arrival)
265 , "reports my address:"
266 , show ip0 ] )
267 -- TODO: Let routing table vote on my IP/NodeId.
268 _ -> return ()
269 return ps
270 Nothing ->
271 let dropped = return $ do
272 -- Ignore non-witnessing nodes until somebody tells
273 -- us our ip address.
274 $(logWarnS) "insertNode" ("Dropped "
275 <> T.pack (show (toSockAddr $ nodeAddr $ foreignNode arrival)))
276 return []
277 in fromMaybe dropped $ do
278 ReflectedIP ip0 <- witnessed_ip
279 ip <- fromSockAddr ip0
280 let nil = nullTable (change ip) maxbuckets
281 return $ do
282 (ps,t') <- R.insert tm arrival nil
283 let new_info = R.Info t' (change ip) ip
284 writeTVar var $ Just new_info
285 return $ do
286 $(logInfo) ( T.pack $ L.unwords
287 [ "External IP address:"
288 , show ip0
289 , "(reported by"
290 , show (toSockAddr $ nodeAddr $ foreignNode arrival)
291 <> ")"
292 ] )
293 return ps
294 ps <- join $ liftIO $ atomically $ atomicInsert arrival0 witnessed_ip0
295 showTable
296 _ <- fork $ forM_ ps $ \(CheckPing ns)-> do
297 forM_ ns $ \n -> do
298 (b,mip) <- probeNode (nodeAddr n)
299 let alive = PingResult n b
300 $(logDebugS) "insertNode" $ T.pack ("PingResult "++show (nodeId n,b))
301 _ <- join $ liftIO $ atomically $ atomicInsert alive mip
302 showTable
303 return ()
304
305-- | Throws exception if node is not responding.
306queryNode :: forall a b ip. Address ip => KRPC (Query a) (Response b)
307 => NodeAddr ip -> a -> DHT ip (NodeId, b)
308queryNode addr q = fmap (\(n,b,_) -> (n,b)) $ queryNode' addr q
309
310queryNode' :: forall a b ip. Address ip => KRPC (Query a) (Response b)
311 => NodeAddr ip -> a -> DHT ip (NodeId, b, Maybe ReflectedIP)
312queryNode' addr q = do
313 nid <- myNodeIdAccordingTo addr
314 let read_only = False -- TODO: check for NAT issues. (BEP 43)
315 (Response remoteId r, witnessed_ip) <- query' (toSockAddr addr) (Query nid read_only q)
316 -- $(logDebugS) "queryNode" $ "Witnessed address: " <> T.pack (show witnessed_ip)
317 -- <> " by " <> T.pack (show (toSockAddr addr))
318 _ <- insertNode (NodeInfo remoteId addr) witnessed_ip
319 return (remoteId, r, witnessed_ip)
320
321-- | Infix version of 'queryNode' function.
322(<@>) :: Address ip => KRPC (Query a) (Response b)
323 => a -> NodeAddr ip -> DHT ip b
324q <@> addr = snd <$> queryNode addr q
325{-# INLINE (<@>) #-}
diff --git a/src/Network/BitTorrent/DHT/Readme.md b/src/Network/BitTorrent/DHT/Readme.md
new file mode 100644
index 00000000..e2352f10
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/Readme.md
@@ -0,0 +1,13 @@
1References
2==========
3
4Some good references excluding BEPs:
5
6* [Kademlia wiki page][kademlia-wiki]
7* [Kademlia: A Peer-to-peer Information System Based on the XOR Metric][kademlia-paper]
8* [BitTorrent Mainline DHT Measurement][mldht]
9* Profiling a Million User DHT. (paper)
10
11[kademlia-wiki]: http://en.wikipedia.org/wiki/Kademlia
12[kademlia-paper]: http://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf
13[mldht]: http://www.cs.helsinki.fi/u/jakangas/MLDHT/
diff --git a/src/Network/BitTorrent/DHT/Routing.hs b/src/Network/BitTorrent/DHT/Routing.hs
new file mode 100644
index 00000000..f9d64eea
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/Routing.hs
@@ -0,0 +1,565 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Every node maintains a routing table of known good nodes. The
9-- nodes in the routing table are used as starting points for
10-- queries in the DHT. Nodes from the routing table are returned in
11-- response to queries from other nodes.
12--
13-- For more info see:
14-- <http://www.bittorrent.org/beps/bep_0005.html#routing-table>
15--
16{-# LANGUAGE RecordWildCards #-}
17{-# LANGUAGE ViewPatterns #-}
18{-# LANGUAGE TypeOperators #-}
19{-# LANGUAGE DeriveGeneric #-}
20{-# LANGUAGE ScopedTypeVariables #-}
21{-# OPTIONS_GHC -fno-warn-orphans #-}
22module Network.BitTorrent.DHT.Routing
23 ( -- * Table
24 Table
25 , Info(..)
26
27 -- * Attributes
28 , BucketCount
29 , defaultBucketCount
30 , BucketSize
31 , defaultBucketSize
32 , NodeCount
33
34 -- * Query
35 , Network.BitTorrent.DHT.Routing.null
36 , Network.BitTorrent.DHT.Routing.full
37 , thisId
38 , shape
39 , Network.BitTorrent.DHT.Routing.size
40 , Network.BitTorrent.DHT.Routing.depth
41 , compatibleNodeId
42
43 -- * Lookup
44 , K
45 , defaultK
46 , TableKey (..)
47 , kclosest
48
49 -- * Construction
50 , Network.BitTorrent.DHT.Routing.nullTable
51 , Event(..)
52 , CheckPing(..)
53 , Network.BitTorrent.DHT.Routing.insert
54
55 -- * Conversion
56 , Network.BitTorrent.DHT.Routing.TableEntry
57 , Network.BitTorrent.DHT.Routing.toList
58
59 -- * Routing
60 , Timestamp
61 , Routing
62 , runRouting
63 ) where
64
65import Control.Applicative as A
66import Control.Arrow
67import Control.Monad
68import Data.Function
69import Data.Functor.Identity
70import Data.List as L hiding (insert)
71import Data.Maybe
72import Data.Monoid
73import Data.PSQueue as PSQ
74import Data.Serialize as S hiding (Result, Done)
75import qualified Data.Sequence as Seq
76import Data.Time
77import Data.Time.Clock.POSIX
78import Data.Word
79import GHC.Generics
80import Text.PrettyPrint as PP hiding ((<>))
81import Text.PrettyPrint.HughesPJClass (pPrint,Pretty)
82import qualified Data.ByteString as BS
83import Data.Bits
84
85import Data.Torrent
86import Network.BitTorrent.Address
87
88{-----------------------------------------------------------------------
89-- Routing monad
90-----------------------------------------------------------------------}
91
92-- | Last time the node was responding to our queries.
93--
94-- Not all nodes that we learn about are equal. Some are \"good\" and
95-- some are not. Many nodes using the DHT are able to send queries
96-- and receive responses, but are not able to respond to queries
97-- from other nodes. It is important that each node's routing table
98-- must contain only known good nodes. A good node is a node has
99-- responded to one of our queries within the last 15 minutes. A
100-- node is also good if it has ever responded to one of our queries
101-- and has sent us a query within the last 15 minutes. After 15
102-- minutes of inactivity, a node becomes questionable. Nodes become
103-- bad when they fail to respond to multiple queries in a row. Nodes
104-- that we know are good are given priority over nodes with unknown
105-- status.
106--
107type Timestamp = POSIXTime
108
109-- | Some routing operations might need to perform additional IO.
110data Routing ip result
111 = Full
112 | Done result
113 | GetTime ( Timestamp -> Routing ip result)
114 | NeedPing (NodeAddr ip) ( Bool -> Routing ip result)
115 | Refresh NodeId (Routing ip result)
116
117instance Functor (Routing ip) where
118 fmap _ Full = Full
119 fmap f (Done r) = Done ( f r)
120 fmap f (GetTime g) = GetTime (fmap f . g)
121 fmap f (NeedPing addr g) = NeedPing addr (fmap f . g)
122 fmap f (Refresh nid g) = Refresh nid (fmap f g)
123
124instance Monad (Routing ip) where
125 return = Done
126
127 Full >>= _ = Full
128 Done r >>= m = m r
129 GetTime f >>= m = GetTime $ \ t -> f t >>= m
130 NeedPing a f >>= m = NeedPing a $ \ p -> f p >>= m
131 Refresh n f >>= m = Refresh n $ f >>= m
132
133instance Applicative (Routing ip) where
134 pure = return
135 (<*>) = ap
136
137instance Alternative (Routing ip) where
138 empty = Full
139
140 Full <|> m = m
141 Done a <|> _ = Done a
142 GetTime f <|> m = GetTime $ \ t -> f t <|> m
143 NeedPing a f <|> m = NeedPing a $ \ p -> f p <|> m
144 Refresh n f <|> m = Refresh n (f <|> m)
145
146-- | Run routing table operation.
147runRouting :: Monad m
148 => (NodeAddr ip -> m Bool) -- ^ ping the specific node;
149 -> (NodeId -> m ()) -- ^ refresh nodes;
150 -> m Timestamp -- ^ get current time;
151 -> Routing ip f -- ^ operation to run;
152 -> m (Maybe f) -- ^ operation result;
153runRouting ping_node find_nodes timestamper = go
154 where
155 go Full = return (Nothing)
156 go (Done r) = return (Just r)
157 go (GetTime f) = do
158 t <- timestamper
159 go (f t)
160
161 go (NeedPing addr f) = do
162 pong <- ping_node addr
163 go (f pong)
164
165 go (Refresh nid f) = do
166 find_nodes nid
167 go f
168
169{-----------------------------------------------------------------------
170 Bucket
171-----------------------------------------------------------------------}
172-- TODO: add replacement cache to the bucket
173--
174-- When a k-bucket is full and a new node is discovered for that
175-- k-bucket, the least recently seen node in the k-bucket is
176-- PINGed. If the node is found to be still alive, the new node is
177-- place in a secondary list, a replacement cache. The replacement
178-- cache is used only if a node in the k-bucket stops responding. In
179-- other words: new nodes are used only when older nodes disappear.
180
181-- | Timestamp - last time this node is pinged.
182type NodeEntry ip = Binding (NodeInfo ip) Timestamp
183
184instance (Serialize k, Serialize v) => Serialize (Binding k v) where
185 get = (:->) <$> get <*> get
186 put (k :-> v) = put k >> put v
187
188-- TODO instance Pretty where
189
190-- | Number of nodes in a bucket.
191type BucketSize = Int
192
193-- | Maximum number of 'NodeInfo's stored in a bucket. Most clients
194-- use this value.
195defaultBucketSize :: BucketSize
196defaultBucketSize = 8
197
198data QueueMethods m elem fifo = QueueMethods
199 { pushBack :: elem -> fifo -> m fifo
200 , popFront :: fifo -> m (Maybe elem, fifo)
201 , emptyQueue :: m fifo
202 }
203
204{-
205fromQ :: Functor m =>
206 ( a -> b )
207 -> ( b -> a )
208 -> QueueMethods m elem a
209 -> QueueMethods m elem b
210fromQ embed project QueueMethods{..} =
211 QueueMethods { pushBack = \e -> fmap embed . pushBack e . project
212 , popFront = fmap (second embed) . popFront . project
213 , emptyQueue = fmap embed emptyQueue
214 }
215-}
216
217seqQ :: QueueMethods Identity (NodeInfo ip) (Seq.Seq (NodeInfo ip))
218seqQ = QueueMethods
219 { pushBack = \e fifo -> pure (fifo Seq.|> e)
220 , popFront = \fifo -> case Seq.viewl fifo of
221 e Seq.:< fifo' -> pure (Just e, fifo')
222 Seq.EmptyL -> pure (Nothing, Seq.empty)
223 , emptyQueue = pure Seq.empty
224 }
225
226type BucketQueue ip = Seq.Seq (NodeInfo ip)
227
228bucketQ :: QueueMethods Identity (NodeInfo ip) (BucketQueue ip)
229bucketQ = seqQ
230
231-- | Bucket is also limited in its length — thus it's called k-bucket.
232-- When bucket becomes full, we should split it in two lists by
233-- current span bit. Span bit is defined by depth in the routing
234-- table tree. Size of the bucket should be choosen such that it's
235-- very unlikely that all nodes in bucket fail within an hour of
236-- each other.
237--
238data Bucket ip = Bucket { bktNodes :: PSQ (NodeInfo ip) Timestamp
239 , bktQ :: BucketQueue ip
240 } deriving (Show,Generic)
241
242instance (Eq ip, Serialize ip) => Serialize (Bucket ip)
243
244instance (Serialize k, Serialize v, Ord k, Ord v)
245 => Serialize (PSQ k v) where
246 get = PSQ.fromList <$> get
247 put = put . PSQ.toList
248
249-- | Update interval, in seconds.
250delta :: NominalDiffTime
251delta = 15 * 60
252
253-- | Should maintain a set of stable long running nodes.
254--
255-- Note: pings are triggerd only when a bucket is full.
256insertBucket :: (Eq ip, Alternative f) => Timestamp -> Event ip -> Bucket ip
257 -> f ([CheckPing ip], Bucket ip)
258insertBucket curTime (TryInsert info) bucket
259 -- just update timestamp if a node is already in bucket
260 | already_have
261 = pure ( [], map_ns $ PSQ.insertWith max info curTime )
262 -- bucket is good, but not full => we can insert a new node
263 | PSQ.size (bktNodes bucket) < defaultBucketSize
264 = pure ( [], map_ns $ PSQ.insert info curTime )
265 -- If there are any questionable nodes in the bucket have not been
266 -- seen in the last 15 minutes, the least recently seen node is
267 -- pinged. If any nodes in the bucket are known to have become bad,
268 -- then one is replaced by the new node in the next insertBucket
269 -- iteration.
270 | not (L.null stales)
271 = pure ( [CheckPing stales], map_q $ pushBack bucketQ info )
272 -- When the bucket is full of good nodes, the new node is simply discarded.
273 -- We must return 'A.empty' here to ensure that bucket splitting happens
274 -- inside 'modifyBucket'.
275 | otherwise = A.empty
276 where
277 stales = map key $ PSQ.atMost (curTime - delta) $ bktNodes bucket
278
279 already_have = maybe False (const True) $ PSQ.lookup info (bktNodes bucket)
280
281 map_ns f = bucket { bktNodes = f (bktNodes bucket) }
282 map_q f = bucket { bktQ = runIdentity $ f (bktQ bucket) }
283
284insertBucket curTime (PingResult bad_node got_response) bucket
285 = pure ([], Bucket (upd $ bktNodes bucket) popped)
286 where
287 (top, popped) = runIdentity $ popFront bucketQ (bktQ bucket)
288 upd | got_response = id
289 | Just info <- top = PSQ.insert info curTime . PSQ.delete bad_node
290 | otherwise = id
291
292type BitIx = Word
293
294partitionQ :: Monad f => QueueMethods f elem b -> (elem -> Bool) -> b -> f (b, b)
295partitionQ imp test q0 = do
296 pass0 <- emptyQueue imp
297 fail0 <- emptyQueue imp
298 let flipfix a b f = fix f a b
299 flipfix q0 (pass0,fail0) $ \rec q qs -> do
300 (mb,q') <- popFront imp q
301 case mb of
302 Nothing -> return qs
303 Just e -> do qs' <- select (pushBack imp e) qs
304 rec q' qs'
305 where
306 select :: Functor f => (b -> f b) -> (b, b) -> f (b, b)
307 select f = if test e then \(a,b) -> flip (,) b <$> f a
308 else \(a,b) -> (,) a <$> f b
309
310split :: Eq ip => BitIx -> Bucket ip -> (Bucket ip, Bucket ip)
311split i b = (Bucket ns qs, Bucket ms rs)
312 where
313 (ns,ms) = (PSQ.fromList *** PSQ.fromList) . partition (spanBit . key) . PSQ.toList $ bktNodes b
314 (qs,rs) = runIdentity $ partitionQ bucketQ spanBit $ bktQ b
315 spanBit entry = testIdBit (nodeId entry) i
316
317{-----------------------------------------------------------------------
318-- Table
319-----------------------------------------------------------------------}
320
321-- | Number of buckets in a routing table.
322type BucketCount = Int
323
324defaultBucketCount :: BucketCount
325defaultBucketCount = 20
326
327data Info ip = Info
328 { myBuckets :: Table ip
329 , myNodeId :: NodeId
330 , myAddress :: ip
331 }
332 deriving (Eq, Show, Generic)
333
334instance (Eq ip, Serialize ip) => Serialize (Info ip)
335
336-- | The routing table covers the entire 'NodeId' space from 0 to 2 ^
337-- 160. The routing table is subdivided into 'Bucket's that each cover
338-- a portion of the space. An empty table has one bucket with an ID
339-- space range of @min = 0, max = 2 ^ 160@. When a node with ID \"N\"
340-- is inserted into the table, it is placed within the bucket that has
341-- @min <= N < max@. An empty table has only one bucket so any node
342-- must fit within it. Each bucket can only hold 'K' nodes, currently
343-- eight, before becoming 'Full'. When a bucket is full of known good
344-- nodes, no more nodes may be added unless our own 'NodeId' falls
345-- within the range of the 'Bucket'. In that case, the bucket is
346-- replaced by two new buckets each with half the range of the old
347-- bucket and the nodes from the old bucket are distributed among the
348-- two new ones. For a new table with only one bucket, the full bucket
349-- is always split into two new buckets covering the ranges @0..2 ^
350-- 159@ and @2 ^ 159..2 ^ 160@.
351--
352data Table ip
353 -- most nearest bucket
354 = Tip NodeId BucketCount (Bucket ip)
355
356 -- left biased tree branch
357 | Zero (Table ip) (Bucket ip)
358
359 -- right biased tree branch
360 | One (Bucket ip) (Table ip)
361 deriving (Show, Generic)
362
363instance Eq ip => Eq (Table ip) where
364 (==) = (==) `on` Network.BitTorrent.DHT.Routing.toList
365
366instance Serialize NominalDiffTime where
367 put = putWord32be . fromIntegral . fromEnum
368 get = (toEnum . fromIntegral) <$> getWord32be
369
370-- | Normally, routing table should be saved between invocations of
371-- the client software. Note that you don't need to store /this/
372-- 'NodeId' since it is already included in routing table.
373instance (Eq ip, Serialize ip) => Serialize (Table ip)
374
375-- | Shape of the table.
376instance Pretty (Table ip) where
377 pPrint t
378 | bucketCount < 6 = hcat $ punctuate ", " $ L.map PP.int ss
379 | otherwise = brackets $
380 PP.int (L.sum ss) <> " nodes, " <>
381 PP.int bucketCount <> " buckets"
382 where
383 bucketCount = L.length ss
384 ss = shape t
385
386-- | Empty table with specified /spine/ node id.
387nullTable :: Eq ip => NodeId -> BucketCount -> Table ip
388nullTable nid n = Tip nid (bucketCount (pred n)) (Bucket PSQ.empty (runIdentity $ emptyQueue bucketQ))
389 where
390 bucketCount x = max 0 (min 159 x)
391
392-- | Test if table is empty. In this case DHT should start
393-- bootstrapping process until table becomes 'full'.
394null :: Table ip -> Bool
395null (Tip _ _ b) = PSQ.null $ bktNodes b
396null _ = False
397
398-- | Test if table have maximum number of nodes. No more nodes can be
399-- 'insert'ed, except old ones becomes bad.
400full :: Table ip -> Bool
401full (Tip _ n _) = n == 0
402full (Zero t b) = PSQ.size (bktNodes b) == defaultBucketSize && full t
403full (One b t) = PSQ.size (bktNodes b) == defaultBucketSize && full t
404
405-- | Get the /spine/ node id.
406thisId :: Table ip -> NodeId
407thisId (Tip nid _ _) = nid
408thisId (Zero table _) = thisId table
409thisId (One _ table) = thisId table
410
411-- | Number of nodes in a bucket or a table.
412type NodeCount = Int
413
414-- | Internally, routing table is similar to list of buckets or a
415-- /matrix/ of nodes. This function returns the shape of the matrix.
416shape :: Table ip -> [BucketSize]
417shape = map (PSQ.size . bktNodes) . toBucketList
418
419-- | Get number of nodes in the table.
420size :: Table ip -> NodeCount
421size = L.sum . shape
422
423-- | Get number of buckets in the table.
424depth :: Table ip -> BucketCount
425depth = L.length . shape
426
427lookupBucket :: NodeId -> Table ip -> Maybe (Bucket ip)
428lookupBucket nid = go 0
429 where
430 go i (Zero table bucket)
431 | testIdBit nid i = pure bucket
432 | otherwise = go (succ i) table
433 go i (One bucket table)
434 | testIdBit nid i = go (succ i) table
435 | otherwise = pure bucket
436 go _ (Tip _ _ bucket) = pure bucket
437
438compatibleNodeId :: Table ip -> IO NodeId
439compatibleNodeId tbl = genBucketSample prefix br
440 where
441 br = bucketRange (L.length (shape tbl) - 1) True
442 bs = BS.pack $ take nodeIdSize $ tablePrefix tbl ++ repeat 0
443 prefix = NodeId bs
444
445tablePrefix :: Table ip -> [Word8]
446tablePrefix = map (packByte . take 8 . (++repeat False))
447 . chunksOf 8
448 . tableBits
449 where
450 packByte = foldl1' (.|.) . zipWith bitmask [7,6 .. 0]
451 bitmask ix True = bit ix
452 bitmask _ _ = 0
453
454tableBits :: Table ip -> [Bool]
455tableBits (One _ tbl) = True : tableBits tbl
456tableBits (Zero tbl _) = False : tableBits tbl
457tableBits (Tip _ _ _) = []
458
459chunksOf :: Int -> [e] -> [[e]]
460chunksOf i ls = map (take i) (build (splitter ls)) where
461 splitter :: [e] -> ([e] -> a -> a) -> a -> a
462 splitter [] _ n = n
463 splitter l c n = l `c` splitter (drop i l) c n
464
465build :: ((a -> [a] -> [a]) -> [a] -> [a]) -> [a]
466build g = g (:) []
467
468-- | Count of closest nodes in find_node request.
469type K = Int
470
471-- | Default 'K' is equal to 'defaultBucketSize'.
472defaultK :: K
473defaultK = 8
474
475class TableKey k where
476 toNodeId :: k -> NodeId
477
478instance TableKey NodeId where
479 toNodeId = id
480
481instance TableKey InfoHash where
482 toNodeId = either (error msg) id . S.decode . S.encode
483 where -- TODO unsafe coerse?
484 msg = "tableKey: impossible"
485
486-- | Get a list of /K/ closest nodes using XOR metric. Used in
487-- 'find_node' and 'get_peers' queries.
488kclosest :: Eq ip => TableKey a => K -> a -> Table ip -> [NodeInfo ip]
489kclosest k (toNodeId -> nid)
490 = L.take k . rank nodeId nid
491 . L.map PSQ.key . PSQ.toList . fromMaybe PSQ.empty
492 . fmap bktNodes
493 . lookupBucket nid
494
495{-----------------------------------------------------------------------
496-- Routing
497-----------------------------------------------------------------------}
498
499splitTip :: Eq ip => NodeId -> BucketCount -> BitIx -> Bucket ip -> Table ip
500splitTip nid n i bucket
501 | testIdBit nid i = (One zeros (Tip nid (pred n) ones))
502 | otherwise = (Zero (Tip nid (pred n) zeros) ones)
503 where
504 (ones, zeros) = split i bucket
505
506-- | Used in each query.
507--
508-- TODO: Kademlia non-empty subtrees should should split if they have less than
509-- k nodes in them. Which subtrees I mean is illustrated in Fig 1. of Kademlia
510-- paper. The rule requiring additional splits is in section 2.4.
511modifyBucket
512 :: forall f ip xs. (Alternative f, Eq ip) =>
513 NodeId -> (Bucket ip -> f (xs, Bucket ip)) -> Table ip -> f (xs,Table ip)
514modifyBucket nodeId f = go (0 :: BitIx)
515 where
516 go :: BitIx -> Table ip -> f (xs, Table ip)
517 go i (Zero table bucket)
518 | testIdBit nodeId i = second (Zero table) <$> f bucket
519 | otherwise = second (`Zero` bucket) <$> go (succ i) table
520 go i (One bucket table )
521 | testIdBit nodeId i = second (One bucket) <$> go (succ i) table
522 | otherwise = second (`One` table) <$> f bucket
523 go i (Tip nid n bucket)
524 | n == 0 = second (Tip nid n) <$> f bucket
525 | otherwise = second (Tip nid n) <$> f bucket
526 <|> go i (splitTip nid n i bucket)
527
528-- | Triggering event for atomic table update
529data Event ip = TryInsert { foreignNode :: NodeInfo ip }
530 | PingResult { foreignNode :: NodeInfo ip
531 , ponged :: Bool
532 }
533 deriving (Eq,Ord,Show)
534
535eventId :: Event ip -> NodeId
536eventId (TryInsert NodeInfo{..}) = nodeId
537eventId (PingResult NodeInfo{..} _) = nodeId
538
539-- | Actions requested by atomic table update
540data CheckPing ip = CheckPing [NodeInfo ip]
541 deriving (Eq,Ord,Show)
542
543
544-- | Atomic 'Table' update
545insert :: (Alternative m, Eq ip) => Timestamp -> Event ip -> Table ip -> m ([CheckPing ip], Table ip)
546insert tm event = modifyBucket (eventId event) (insertBucket tm event)
547
548
549{-----------------------------------------------------------------------
550-- Conversion
551-----------------------------------------------------------------------}
552
553type TableEntry ip = (NodeInfo ip, Timestamp)
554
555tableEntry :: NodeEntry ip -> TableEntry ip
556tableEntry (a :-> b) = (a, b)
557
558-- | Non-empty list of buckets.
559toBucketList :: Table ip -> [Bucket ip]
560toBucketList (Tip _ _ b) = [b]
561toBucketList (Zero t b) = b : toBucketList t
562toBucketList (One b t) = b : toBucketList t
563
564toList :: Eq ip => Table ip -> [[TableEntry ip]]
565toList = L.map (L.map tableEntry . PSQ.toList . bktNodes) . toBucketList
diff --git a/src/Network/BitTorrent/DHT/Session.hs b/src/Network/BitTorrent/DHT/Session.hs
new file mode 100644
index 00000000..c8545bfd
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/Session.hs
@@ -0,0 +1,465 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013-2014
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This module defines internal state of a node instance. You can
9-- have multiple nodes per application but usually you don't have
10-- to. Normally, you don't need to import this module, use
11-- "Network.BitTorrent.DHT" instead.
12--
13{-# LANGUAGE CPP #-}
14{-# LANGUAGE RecordWildCards #-}
15{-# LANGUAGE FlexibleContexts #-}
16{-# LANGUAGE FlexibleInstances #-}
17{-# LANGUAGE GeneralizedNewtypeDeriving #-}
18{-# LANGUAGE MultiParamTypeClasses #-}
19{-# LANGUAGE ScopedTypeVariables #-}
20{-# LANGUAGE TypeFamilies #-}
21{-# LANGUAGE TemplateHaskell #-}
22module Network.BitTorrent.DHT.Session
23 ( -- * Options
24 -- | Use @optFooBar def@ to get default 'Alpha' or 'K'.
25 Alpha
26 , K
27 , Options (..)
28
29 -- * Session
30 , Node
31 , options
32 , tentativeNodeId
33 , myNodeIdAccordingTo
34 , routingInfo
35 , routableAddress
36
37 -- ** Initialization
38 , LogFun
39 , NodeHandler
40 , newNode
41 , closeNode
42
43 -- * DHT
44 -- | Use @asks options@ to get options passed to 'startNode'
45 -- or @asks thisNodeId@ to get id of locally running node.
46 , DHT
47 , runDHT
48
49 -- ** Tokens
50 , grantToken
51 , checkToken
52
53 -- ** Routing table
54 , getTable
55 , getClosest
56
57 -- ** Peer storage
58 , insertPeer
59 , getPeerList
60 , insertTopic
61 , deleteTopic
62
63 -- ** Messaging
64 , queryParallel
65 ) where
66
67import Prelude hiding (ioError)
68
69import Control.Concurrent.STM
70import Control.Concurrent.Async.Lifted
71import Control.Exception.Lifted hiding (Handler)
72import Control.Monad.Base
73import Control.Monad.Logger
74import Control.Monad.Reader
75import Control.Monad.Trans.Control
76import Control.Monad.Trans.Resource
77import Data.Conduit.Lazy
78import Data.Default
79import Data.Fixed
80import Data.Hashable
81import Data.List as L
82import Data.Maybe
83import Data.Set as S
84import Data.Time
85import Network (PortNumber)
86import System.Random (randomIO)
87
88import Data.Torrent as Torrent
89import Network.KRPC as KRPC hiding (Options, def)
90import qualified Network.KRPC as KRPC (def)
91import Network.BitTorrent.Address
92import Network.BitTorrent.DHT.ContactInfo as P
93import Network.BitTorrent.DHT.Message
94import Network.BitTorrent.DHT.Routing as R
95import Network.BitTorrent.DHT.Token as T
96
97{-----------------------------------------------------------------------
98-- Options
99-----------------------------------------------------------------------}
100
101-- | Node lookups can proceed asynchronously.
102type Alpha = Int
103
104-- NOTE: libtorrent uses 5, azureus uses 10
105-- | The quantity of simultaneous lookups is typically three.
106defaultAlpha :: Alpha
107defaultAlpha = 3
108
109-- TODO add replication loop
110
111-- TODO do not insert infohash -> peeraddr if infohash is too far from
112-- this node id
113{-
114data Order
115 = NearFirst
116 | FarFirst
117 | Random
118
119data Traversal
120 = Greedy -- ^ aggressive short-circuit traversal
121 | Exhaustive -- ^
122-}
123
124-- | Original Kamelia DHT uses term /publish/ for data replication
125-- process. BitTorrent DHT uses term /announce/ since the purpose of
126-- the DHT is peer discovery. Later in documentation, we use terms
127-- /publish/ and /announce/ interchangible.
128data Options = Options
129 { -- | The degree of parallelism in 'find_node' queries. More
130 -- parallism lead to faster bootstrapping and lookup operations,
131 -- but also increase resource usage.
132 --
133 -- Normally this parameter should not exceed 'optK'.
134 optAlpha :: {-# UNPACK #-} !Alpha
135
136 -- | /K/ parameter - number of nodes to return in 'find_node'
137 -- responses.
138 , optK :: {-# UNPACK #-} !K
139
140 -- | Number of buckets to maintain. This parameter depends on
141 -- amount of nodes in the DHT network.
142 , optBucketCount :: {-# UNPACK #-} !BucketCount
143
144 -- | RPC timeout.
145 , optTimeout :: !NominalDiffTime
146
147 -- | /R/ parameter - how many target nodes the 'announce' query
148 -- should affect.
149 --
150 -- A large replica set compensates for inconsistent routing and
151 -- reduces the need to frequently republish data for
152 -- persistence. This comes at an increased cost for
153 -- 'Network.BitTorrent.DHT.insert' in terms of time, nodes
154 -- contacted, and storage.
155 , optReplication :: {-# UNPACK #-} !NodeCount
156
157 -- | How often this node should republish (or reannounce) its
158 -- data.
159 --
160 -- Large replica set ('optReplication') should require
161 -- smaller reannounce intervals ('optReannounce').
162 , optReannounce :: !NominalDiffTime
163
164 -- | The time it takes for data to expire in the
165 -- network. Publisher of the data should republish (or
166 -- reannounce) data to keep it in the network.
167 --
168 -- The /data expired timeout/ should be more than 'optReannounce'
169 -- interval.
170 , optDataExpired :: !NominalDiffTime
171 } deriving (Show, Eq)
172
173-- | Optimal options for bittorrent client. For short-lifetime
174-- utilities you most likely need to tune 'optAlpha' and
175-- 'optBucketCount'.
176instance Default Options where
177 def = Options
178 { optAlpha = defaultAlpha
179 , optK = defaultK
180
181 -- see Fig.2 from "BitTorrent Mainline DHT Measurement" paper.
182 , optBucketCount = defaultBucketCount
183
184 -- see Fig.4 from "Profiling a Million User DHT" paper.
185 , optTimeout = 5 -- seconds
186 , optReplication = 20 -- nodes
187 , optReannounce = 15 * 60
188 , optDataExpired = 60 * 60
189 }
190
191seconds :: NominalDiffTime -> Int
192seconds dt = fromEnum (realToFrac dt :: Uni)
193
194{-----------------------------------------------------------------------
195-- Tokens policy
196-----------------------------------------------------------------------}
197
198data SessionTokens = SessionTokens
199 { tokenMap :: !TokenMap
200 , lastUpdate :: !UTCTime
201 , maxInterval :: !NominalDiffTime
202 }
203
204nullSessionTokens :: IO SessionTokens
205nullSessionTokens = SessionTokens
206 <$> (tokens <$> liftIO randomIO)
207 <*> liftIO getCurrentTime
208 <*> pure defaultUpdateInterval
209
210-- TODO invalidate *twice* if needed
211invalidateTokens :: UTCTime -> SessionTokens -> SessionTokens
212invalidateTokens curTime ts @ SessionTokens {..}
213 | curTime `diffUTCTime` lastUpdate > maxInterval = SessionTokens
214 { tokenMap = update tokenMap
215 , lastUpdate = curTime
216 , maxInterval = maxInterval
217 }
218 | otherwise = ts
219
220{-----------------------------------------------------------------------
221-- Session
222-----------------------------------------------------------------------}
223
224-- | A set of torrents this peer intends to share.
225type AnnounceSet = Set (InfoHash, PortNumber)
226
227-- | Logger function.
228type LogFun = Loc -> LogSource -> LogLevel -> LogStr -> IO ()
229
230-- | DHT session keep track state of /this/ node.
231data Node ip = Node
232 { -- | Session configuration;
233 options :: !Options
234
235 -- | Pseudo-unique self-assigned session identifier. This value is
236 -- constant during DHT session and (optionally) between sessions.
237 , tentativeNodeId :: !NodeId
238
239 , resources :: !InternalState
240 , manager :: !(Manager (DHT ip )) -- ^ RPC manager;
241 , routingInfo :: !(TVar (Maybe (R.Info ip))) -- ^ search table;
242 , contactInfo :: !(TVar (PeerStore ip )) -- ^ published by other nodes;
243 , announceInfo :: !(TVar AnnounceSet ) -- ^ to publish by this node;
244 , sessionTokens :: !(TVar SessionTokens ) -- ^ query session IDs.
245 , loggerFun :: !LogFun
246 }
247
248-- | DHT keep track current session and proper resource allocation for
249-- safe multithreading.
250newtype DHT ip a = DHT { unDHT :: ReaderT (Node ip) IO a }
251 deriving ( Functor, Applicative, Monad, MonadIO
252 , MonadBase IO, MonadReader (Node ip), MonadThrow
253 )
254
255#if MIN_VERSION_monad_control(1,0,0)
256newtype DHTStM ip a = StM {
257 unSt :: StM (ReaderT (Node ip) IO) a
258 }
259#endif
260
261instance MonadBaseControl IO (DHT ip) where
262#if MIN_VERSION_monad_control(1,0,0)
263 type StM (DHT ip) a = DHTStM ip a
264#else
265 newtype StM (DHT ip) a = StM {
266 unSt :: StM (ReaderT (Node ip) IO) a
267 }
268#endif
269 liftBaseWith cc = DHT $ liftBaseWith $ \ cc' ->
270 cc $ \ (DHT m) -> StM <$> cc' m
271 {-# INLINE liftBaseWith #-}
272
273 restoreM = DHT . restoreM . unSt
274 {-# INLINE restoreM #-}
275
276-- | Check is it is possible to run 'queryNode' or handle pending
277-- query from remote node.
278instance MonadActive (DHT ip) where
279 monadActive = getManager >>= liftIO . isActive
280 {-# INLINE monadActive #-}
281
282-- | All allocated resources will be closed at 'closeNode'.
283instance MonadResource (DHT ip) where
284 liftResourceT m = do
285 s <- asks resources
286 liftIO $ runInternalState m s
287
288instance MonadKRPC (DHT ip) (DHT ip) where
289 getManager = asks manager
290
291instance MonadLogger (DHT ip) where
292 monadLoggerLog loc src lvl msg = do
293 logger <- asks loggerFun
294 liftIO $ logger loc src lvl (toLogStr msg)
295
296type NodeHandler ip = Handler (DHT ip)
297
298-- | Run DHT session. You /must/ properly close session using
299-- 'closeNode' function, otherwise socket or other scarce resources may
300-- leak.
301newNode :: Address ip
302 => [NodeHandler ip] -- ^ handlers to run on accepted queries;
303 -> Options -- ^ various dht options;
304 -> NodeAddr ip -- ^ node address to bind;
305 -> LogFun -- ^
306 -> Maybe NodeId -- ^ use this NodeId, if not given a new one is generated.
307 -> IO (Node ip) -- ^ a new DHT node running at given address.
308newNode hs opts naddr logger mbid = do
309 s <- createInternalState
310 runInternalState initNode s
311 `onException` closeInternalState s
312 where
313 rpcOpts = KRPC.def { optQueryTimeout = seconds (optTimeout opts) }
314 nodeAddr = toSockAddr naddr
315 initNode = do
316 s <- getInternalState
317 (_, m) <- allocate (newManager rpcOpts nodeAddr hs) closeManager
318 liftIO $ do
319 myId <- maybe genNodeId return mbid
320 node <- Node opts myId s m
321 <$> atomically (newTVar Nothing)
322 <*> newTVarIO def
323 <*> newTVarIO S.empty
324 <*> (newTVarIO =<< nullSessionTokens)
325 <*> pure logger
326 runReaderT (unDHT KRPC.listen) node
327 return node
328
329-- | Some resources like listener thread may live for
330-- some short period of time right after this DHT session closed.
331closeNode :: Node ip -> IO ()
332closeNode Node {..} = closeInternalState resources
333
334-- | Run DHT operation on the given session.
335runDHT :: Node ip -> DHT ip a -> IO a
336runDHT node action = runReaderT (unDHT action) node
337{-# INLINE runDHT #-}
338
339{-----------------------------------------------------------------------
340-- Routing
341-----------------------------------------------------------------------}
342
343-- /pick a random ID/ in the range of the bucket and perform a
344-- find_nodes search on it.
345
346{-----------------------------------------------------------------------
347-- Tokens
348-----------------------------------------------------------------------}
349
350tryUpdateSecret :: DHT ip ()
351tryUpdateSecret = do
352 curTime <- liftIO getCurrentTime
353 toks <- asks sessionTokens
354 liftIO $ atomically $ modifyTVar' toks (invalidateTokens curTime)
355
356grantToken :: Hashable a => NodeAddr a -> DHT ip Token
357grantToken addr = do
358 tryUpdateSecret
359 toks <- asks sessionTokens >>= liftIO . readTVarIO
360 return $ T.lookup addr $ tokenMap toks
361
362-- | Throws 'HandlerError' if the token is invalid or already
363-- expired. See 'TokenMap' for details.
364checkToken :: Hashable a => NodeAddr a -> Token -> DHT ip Bool
365checkToken addr questionableToken = do
366 tryUpdateSecret
367 toks <- asks sessionTokens >>= liftIO . readTVarIO
368 return $ T.member addr questionableToken (tokenMap toks)
369
370
371{-----------------------------------------------------------------------
372-- Routing table
373-----------------------------------------------------------------------}
374
375-- | This nodes externally routable address reported by remote peers.
376routableAddress :: DHT ip (Maybe ip)
377routableAddress = do
378 info <- asks routingInfo >>= liftIO . atomically . readTVar
379 return $ myAddress <$> info
380
381-- | The current NodeId that the given remote node should know us by.
382myNodeIdAccordingTo :: NodeAddr ip -> DHT ip NodeId
383myNodeIdAccordingTo _ = do
384 info <- asks routingInfo >>= liftIO . atomically . readTVar
385 fallback <- asks tentativeNodeId
386 return $ maybe fallback myNodeId info
387
388-- | Get current routing table. Normally you don't need to use this
389-- function, but it can be usefull for debugging and profiling purposes.
390getTable :: Eq ip => DHT ip (Table ip)
391getTable = do
392 Node { tentativeNodeId = myId
393 , routingInfo = var
394 , options = opts } <- ask
395 let nil = nullTable myId (optBucketCount opts)
396 liftIO (maybe nil R.myBuckets <$> atomically (readTVar var))
397
398-- | Find a set of closest nodes from routing table of this node. (in
399-- no particular order)
400--
401-- This operation used for 'find_nodes' query.
402--
403getClosest :: Eq ip => TableKey k => k -> DHT ip [NodeInfo ip]
404getClosest node = do
405 k <- asks (optK . options)
406 kclosest k node <$> getTable
407
408{-----------------------------------------------------------------------
409-- Peer storage
410-----------------------------------------------------------------------}
411
412refreshContacts :: DHT ip ()
413refreshContacts =
414 -- TODO limit dht peer store in size (probably by removing oldest peers)
415 return ()
416
417
418-- | Insert peer to peer store. Used to handle announce requests.
419insertPeer :: Eq ip => InfoHash -> PeerAddr ip -> DHT ip ()
420insertPeer ih addr = do
421 refreshContacts
422 var <- asks contactInfo
423 liftIO $ atomically $ modifyTVar' var (P.insert ih addr)
424
425-- | Get peer set for specific swarm.
426lookupPeers :: InfoHash -> DHT ip [PeerAddr ip]
427lookupPeers ih = do
428 refreshContacts
429 var <- asks contactInfo
430 liftIO $ P.lookup ih <$> readTVarIO var
431
432-- | Prepare result for 'get_peers' query.
433--
434-- This operation use 'getClosest' as failback so it may block.
435--
436getPeerList :: Eq ip => InfoHash -> DHT ip (PeerList ip)
437getPeerList ih = do
438 ps <- lookupPeers ih
439 if L.null ps
440 then Left <$> getClosest ih
441 else return (Right ps)
442
443insertTopic :: InfoHash -> PortNumber -> DHT ip ()
444insertTopic ih p = do
445 var <- asks announceInfo
446 liftIO $ atomically $ modifyTVar' var (S.insert (ih, p))
447
448deleteTopic :: InfoHash -> PortNumber -> DHT ip ()
449deleteTopic ih p = do
450 var <- asks announceInfo
451 liftIO $ atomically $ modifyTVar' var (S.delete (ih, p))
452
453{-----------------------------------------------------------------------
454-- Messaging
455-----------------------------------------------------------------------}
456
457-- | Failed queries are ignored.
458queryParallel :: [DHT ip a] -> DHT ip [a]
459queryParallel queries = do
460 -- TODO: use alpha
461 -- alpha <- asks (optAlpha . options)
462 cleanup <$> mapConcurrently try queries
463 where
464 cleanup :: [Either QueryFailure a] -> [a]
465 cleanup = mapMaybe (either (const Nothing) Just)
diff --git a/src/Network/BitTorrent/DHT/Token.hs b/src/Network/BitTorrent/DHT/Token.hs
new file mode 100644
index 00000000..7aaaf2b7
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/Token.hs
@@ -0,0 +1,121 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- The return value for a query for peers includes an opaque value
9-- known as the 'Token'. For a node to announce that its controlling
10-- peer is downloading a torrent, it must present the token received
11-- from the same queried node in a recent query for peers. When a node
12-- attempts to \"announce\" a torrent, the queried node checks the
13-- token against the querying node's 'IP' address. This is to prevent
14-- malicious hosts from signing up other hosts for torrents. Since the
15-- token is merely returned by the querying node to the same node it
16-- received the token from, the implementation is not defined. Tokens
17-- must be accepted for a reasonable amount of time after they have
18-- been distributed.
19--
20{-# LANGUAGE GeneralizedNewtypeDeriving #-}
21module Network.BitTorrent.DHT.Token
22 ( -- * Token
23 Token
24
25 -- * Session tokens
26 , TokenMap
27
28 -- ** Construction
29 , Network.BitTorrent.DHT.Token.tokens
30
31 -- ** Query
32 , Network.BitTorrent.DHT.Token.lookup
33 , Network.BitTorrent.DHT.Token.member
34
35 -- ** Modification
36 , Network.BitTorrent.DHT.Token.defaultUpdateInterval
37 , Network.BitTorrent.DHT.Token.update
38 ) where
39
40import Control.Monad.State
41import Data.BEncode (BEncode)
42import Data.ByteString as BS
43import Data.ByteString.Lazy as BL
44import Data.ByteString.Lazy.Builder as BS
45import Data.Default
46import Data.List as L
47import Data.Hashable
48import Data.String
49import Data.Time
50import System.Random
51
52import Network.BitTorrent.Address
53
54-- TODO use ShortByteString
55
56-- | An opaque value.
57newtype Token = Token BS.ByteString
58 deriving (Show, Eq, BEncode, IsString)
59
60-- | Meaningless token, for testing purposes only.
61instance Default Token where
62 def = Token "0xdeadbeef"
63
64-- | The secret value used as salt.
65type Secret = Int
66
67-- The BitTorrent implementation uses the SHA1 hash of the IP address
68-- concatenated onto a secret, we use hashable instead.
69makeToken :: Hashable a => NodeAddr a -> Secret -> Token
70makeToken n s = Token $ toBS $ hashWithSalt s n
71 where
72 toBS = toStrict . toLazyByteString . int64BE . fromIntegral
73
74-- | Constant space 'Node' to 'Token' map based on the secret value.
75data TokenMap = TokenMap
76 { prevSecret :: {-# UNPACK #-} !Secret
77 , curSecret :: {-# UNPACK #-} !Secret
78 , generator :: {-# UNPACK #-} !StdGen
79 } deriving Show
80
81-- | A new token map based on the specified seed value. Returned token
82-- map should be periodicatically 'update'd.
83--
84-- Normally, the seed value should vary between invocations of the
85-- client software.
86tokens :: Int -> TokenMap
87tokens seed = (`evalState` mkStdGen seed) $
88 TokenMap <$> state next
89 <*> state next
90 <*> get
91
92-- | Get token for the given node. A token becomes invalid after 2
93-- 'update's.
94--
95-- Typically used to handle find_peers query.
96lookup :: Hashable a => NodeAddr a -> TokenMap -> Token
97lookup addr TokenMap {..} = makeToken addr curSecret
98
99-- | Check if token is valid.
100--
101-- Typically used to handle 'Network.BitTorrent.DHT.Message.Announce'
102-- query. If token is invalid the 'Network.KRPC.ProtocolError' should
103-- be sent back to the malicious node.
104member :: Hashable a => NodeAddr a -> Token -> TokenMap -> Bool
105member addr token TokenMap {..} = token `L.elem` valid
106 where valid = makeToken addr <$> [curSecret, prevSecret]
107
108-- | Secret changes every five minutes and tokens up to ten minutes old
109-- are accepted.
110defaultUpdateInterval :: NominalDiffTime
111defaultUpdateInterval = 5 * 60
112
113-- | Update current tokens.
114update :: TokenMap -> TokenMap
115update TokenMap {..} = TokenMap
116 { prevSecret = curSecret
117 , curSecret = newSecret
118 , generator = newGen
119 }
120 where
121 (newSecret, newGen) = next generator
diff --git a/src/Network/BitTorrent/Exchange.hs b/src/Network/BitTorrent/Exchange.hs
new file mode 100644
index 00000000..143bf090
--- /dev/null
+++ b/src/Network/BitTorrent/Exchange.hs
@@ -0,0 +1,35 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8module Network.BitTorrent.Exchange
9 ( -- * Manager
10 Options (..)
11 , Manager
12 , Handler
13 , newManager
14 , closeManager
15
16 -- * Session
17 , Caps
18 , Extension
19 , toCaps
20 , Session
21 , newSession
22 , closeSession
23
24 -- * Query
25 , waitMetadata
26 , takeMetadata
27
28 -- * Connections
29 , connect
30 , connectSink
31 ) where
32
33import Network.BitTorrent.Exchange.Manager
34import Network.BitTorrent.Exchange.Message
35import Network.BitTorrent.Exchange.Session
diff --git a/src/Network/BitTorrent/Exchange/Bitfield.hs b/src/Network/BitTorrent/Exchange/Bitfield.hs
new file mode 100644
index 00000000..7bae3475
--- /dev/null
+++ b/src/Network/BitTorrent/Exchange/Bitfield.hs
@@ -0,0 +1,399 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This modules provides all necessary machinery to work with
9-- bitfields. Bitfields are used to keep track indices of complete
10-- pieces either this peer have or remote peer have.
11--
12-- There are also commonly used piece seletion algorithms
13-- which used to find out which one next piece to download.
14-- Selectors considered to be used in the following order:
15--
16-- * 'randomFirst' - at the start of download.
17--
18-- * 'rarestFirst' - performed to avoid situation when
19-- rarest piece is unaccessible.
20--
21-- * 'endGame' - performed after a peer has requested all
22-- the subpieces of the content.
23--
24-- Note that BitTorrent protocol recommend (TODO link?) the
25-- 'strictFirst' priority policy for /subpiece/ or /blocks/
26-- selection.
27--
28{-# LANGUAGE CPP #-}
29{-# LANGUAGE BangPatterns #-}
30{-# LANGUAGE RecordWildCards #-}
31module Network.BitTorrent.Exchange.Bitfield
32 ( -- * Bitfield
33 PieceIx
34 , PieceCount
35 , Bitfield
36
37 -- * Construction
38 , haveAll
39 , haveNone
40 , have
41 , singleton
42 , interval
43 , adjustSize
44
45 -- * Query
46 -- ** Cardinality
47 , Network.BitTorrent.Exchange.Bitfield.null
48 , Network.BitTorrent.Exchange.Bitfield.full
49 , haveCount
50 , totalCount
51 , completeness
52
53 -- ** Membership
54 , member
55 , notMember
56 , findMin
57 , findMax
58 , isSubsetOf
59
60 -- ** Availability
61 , complement
62 , Frequency
63 , frequencies
64 , rarest
65
66 -- * Combine
67 , insert
68 , union
69 , intersection
70 , difference
71
72 -- * Conversion
73 , toList
74 , fromList
75
76 -- * Serialization
77 , fromBitmap
78 , toBitmap
79
80 -- * Piece selection
81 , Selector
82 , selector
83 , strategyClass
84
85 , strictFirst
86 , strictLast
87 , rarestFirst
88 , randomFirst
89 , endGame
90 ) where
91
92import Control.Monad
93import Control.Monad.ST
94import Data.ByteString (ByteString)
95import qualified Data.ByteString as B
96import qualified Data.ByteString.Lazy as Lazy
97import Data.Vector.Unboxed (Vector)
98import qualified Data.Vector.Unboxed as V
99import qualified Data.Vector.Unboxed.Mutable as VM
100import Data.IntervalSet (IntSet)
101import qualified Data.IntervalSet as S
102import qualified Data.IntervalSet.ByteString as S
103import Data.List (foldl')
104import Data.Monoid
105import Data.Ratio
106
107import Data.Torrent
108
109-- TODO cache some operations
110
111-- | Bitfields are represented just as integer sets but with
112-- restriction: the each set should be within given interval (or
113-- subset of the specified interval). Size is used to specify
114-- interval, so bitfield of size 10 might contain only indices in
115-- interval [0..9].
116--
117data Bitfield = Bitfield {
118 bfSize :: !PieceCount
119 , bfSet :: !IntSet
120 } deriving (Show, Read, Eq)
121
122-- Invariants: all elements of bfSet lie in [0..bfSize - 1];
123
124instance Monoid Bitfield where
125 {-# SPECIALIZE instance Monoid Bitfield #-}
126 mempty = haveNone 0
127 mappend = union
128 mconcat = unions
129
130{-----------------------------------------------------------------------
131 Construction
132-----------------------------------------------------------------------}
133
134-- | The empty bitfield of the given size.
135haveNone :: PieceCount -> Bitfield
136haveNone s = Bitfield s S.empty
137
138-- | The full bitfield containing all piece indices for the given size.
139haveAll :: PieceCount -> Bitfield
140haveAll s = Bitfield s (S.interval 0 (s - 1))
141
142-- | Insert the index in the set ignoring out of range indices.
143have :: PieceIx -> Bitfield -> Bitfield
144have ix Bitfield {..}
145 | 0 <= ix && ix < bfSize = Bitfield bfSize (S.insert ix bfSet)
146 | otherwise = Bitfield bfSize bfSet
147
148singleton :: PieceIx -> PieceCount -> Bitfield
149singleton ix pc = have ix (haveNone pc)
150
151-- | Assign new size to bitfield. FIXME Normally, size should be only
152-- decreased, otherwise exception raised.
153adjustSize :: PieceCount -> Bitfield -> Bitfield
154adjustSize s Bitfield {..} = Bitfield s bfSet
155
156-- | NOTE: for internal use only
157interval :: PieceCount -> PieceIx -> PieceIx -> Bitfield
158interval pc a b = Bitfield pc (S.interval a b)
159
160{-----------------------------------------------------------------------
161 Query
162-----------------------------------------------------------------------}
163
164-- | Test if bitifield have no one index: peer do not have anything.
165null :: Bitfield -> Bool
166null Bitfield {..} = S.null bfSet
167
168-- | Test if bitfield have all pieces.
169full :: Bitfield -> Bool
170full Bitfield {..} = S.size bfSet == bfSize
171
172-- | Count of peer have pieces.
173haveCount :: Bitfield -> PieceCount
174haveCount = S.size . bfSet
175
176-- | Total count of pieces and its indices.
177totalCount :: Bitfield -> PieceCount
178totalCount = bfSize
179
180-- | Ratio of /have/ piece count to the /total/ piece count.
181--
182-- > forall bf. 0 <= completeness bf <= 1
183--
184completeness :: Bitfield -> Ratio PieceCount
185completeness b = haveCount b % totalCount b
186
187inRange :: PieceIx -> Bitfield -> Bool
188inRange ix Bitfield {..} = 0 <= ix && ix < bfSize
189
190member :: PieceIx -> Bitfield -> Bool
191member ix bf @ Bitfield {..}
192 | ix `inRange` bf = ix `S.member` bfSet
193 | otherwise = False
194
195notMember :: PieceIx -> Bitfield -> Bool
196notMember ix bf @ Bitfield {..}
197 | ix `inRange` bf = ix `S.notMember` bfSet
198 | otherwise = True
199
200-- | Find first available piece index.
201findMin :: Bitfield -> PieceIx
202findMin = S.findMin . bfSet
203{-# INLINE findMin #-}
204
205-- | Find last available piece index.
206findMax :: Bitfield -> PieceIx
207findMax = S.findMax . bfSet
208{-# INLINE findMax #-}
209
210-- | Check if all pieces from first bitfield present if the second bitfield
211isSubsetOf :: Bitfield -> Bitfield -> Bool
212isSubsetOf a b = bfSet a `S.isSubsetOf` bfSet b
213{-# INLINE isSubsetOf #-}
214
215-- | Resulting bitfield includes only missing pieces.
216complement :: Bitfield -> Bitfield
217complement Bitfield {..} = Bitfield
218 { bfSet = uni `S.difference` bfSet
219 , bfSize = bfSize
220 }
221 where
222 Bitfield _ uni = haveAll bfSize
223{-# INLINE complement #-}
224
225{-----------------------------------------------------------------------
226-- Availability
227-----------------------------------------------------------------------}
228
229-- | Frequencies are needed in piece selection startegies which use
230-- availability quantity to find out the optimal next piece index to
231-- download.
232type Frequency = Int
233
234-- TODO rename to availability
235-- | How many times each piece index occur in the given bitfield set.
236frequencies :: [Bitfield] -> Vector Frequency
237frequencies [] = V.fromList []
238frequencies xs = runST $ do
239 v <- VM.new size
240 VM.set v 0
241 forM_ xs $ \ Bitfield {..} -> do
242 forM_ (S.toList bfSet) $ \ x -> do
243 fr <- VM.read v x
244 VM.write v x (succ fr)
245 V.unsafeFreeze v
246 where
247 size = maximum (map bfSize xs)
248
249-- TODO it seems like this operation is veeery slow
250
251-- | Find least available piece index. If no piece available return
252-- 'Nothing'.
253rarest :: [Bitfield] -> Maybe PieceIx
254rarest xs
255 | V.null freqMap = Nothing
256 | otherwise
257 = Just $ fst $ V.ifoldr' minIx (0, freqMap V.! 0) freqMap
258 where
259 freqMap = frequencies xs
260
261 minIx :: PieceIx -> Frequency
262 -> (PieceIx, Frequency)
263 -> (PieceIx, Frequency)
264 minIx ix fr acc@(_, fra)
265 | fr < fra && fr > 0 = (ix, fr)
266 | otherwise = acc
267
268
269{-----------------------------------------------------------------------
270 Combine
271-----------------------------------------------------------------------}
272
273insert :: PieceIx -> Bitfield -> Bitfield
274insert pix bf @ Bitfield {..}
275 | 0 <= pix && pix < bfSize = Bitfield
276 { bfSet = S.insert pix bfSet
277 , bfSize = bfSize
278 }
279 | otherwise = bf
280
281-- | Find indices at least one peer have.
282union :: Bitfield -> Bitfield -> Bitfield
283union a b = {-# SCC union #-} Bitfield {
284 bfSize = bfSize a `max` bfSize b
285 , bfSet = bfSet a `S.union` bfSet b
286 }
287
288-- | Find indices both peers have.
289intersection :: Bitfield -> Bitfield -> Bitfield
290intersection a b = {-# SCC intersection #-} Bitfield {
291 bfSize = bfSize a `min` bfSize b
292 , bfSet = bfSet a `S.intersection` bfSet b
293 }
294
295-- | Find indices which have first peer but do not have the second peer.
296difference :: Bitfield -> Bitfield -> Bitfield
297difference a b = {-# SCC difference #-} Bitfield {
298 bfSize = bfSize a -- FIXME is it reasonable?
299 , bfSet = bfSet a `S.difference` bfSet b
300 }
301
302-- | Find indices the any of the peers have.
303unions :: [Bitfield] -> Bitfield
304unions = {-# SCC unions #-} foldl' union (haveNone 0)
305
306{-----------------------------------------------------------------------
307 Serialization
308-----------------------------------------------------------------------}
309
310-- | List all /have/ indexes.
311toList :: Bitfield -> [PieceIx]
312toList Bitfield {..} = S.toList bfSet
313
314-- | Make bitfield from list of /have/ indexes.
315fromList :: PieceCount -> [PieceIx] -> Bitfield
316fromList s ixs = Bitfield {
317 bfSize = s
318 , bfSet = S.splitGT (-1) $ S.splitLT s $ S.fromList ixs
319 }
320
321-- | Unpack 'Bitfield' from tightly packed bit array. Note resulting
322-- size might be more than real bitfield size, use 'adjustSize'.
323fromBitmap :: ByteString -> Bitfield
324fromBitmap bs = {-# SCC fromBitmap #-} Bitfield {
325 bfSize = B.length bs * 8
326 , bfSet = S.fromByteString bs
327 }
328{-# INLINE fromBitmap #-}
329
330-- | Pack a 'Bitfield' to tightly packed bit array.
331toBitmap :: Bitfield -> Lazy.ByteString
332toBitmap Bitfield {..} = {-# SCC toBitmap #-} Lazy.fromChunks [intsetBM, alignment]
333 where
334 byteSize = bfSize `div` 8 + if bfSize `mod` 8 == 0 then 0 else 1
335 alignment = B.replicate (byteSize - B.length intsetBM) 0
336 intsetBM = S.toByteString bfSet
337
338{-----------------------------------------------------------------------
339-- Piece selection
340-----------------------------------------------------------------------}
341
342type Selector = Bitfield -- ^ Indices of client /have/ pieces.
343 -> Bitfield -- ^ Indices of peer /have/ pieces.
344 -> [Bitfield] -- ^ Indices of other peers /have/ pieces.
345 -> Maybe PieceIx -- ^ Zero-based index of piece to request
346 -- to, if any.
347
348selector :: Selector -- ^ Selector to use at the start.
349 -> Ratio PieceCount
350 -> Selector -- ^ Selector to use after the client have
351 -- the C pieces.
352 -> Selector -- ^ Selector that changes behaviour based
353 -- on completeness.
354selector start pt ready h a xs =
355 case strategyClass pt h of
356 SCBeginning -> start h a xs
357 SCReady -> ready h a xs
358 SCEnd -> endGame h a xs
359
360data StartegyClass
361 = SCBeginning
362 | SCReady
363 | SCEnd
364 deriving (Show, Eq, Ord, Enum, Bounded)
365
366
367strategyClass :: Ratio PieceCount -> Bitfield -> StartegyClass
368strategyClass threshold = classify . completeness
369 where
370 classify c
371 | c < threshold = SCBeginning
372 | c + 1 % numerator c < 1 = SCReady
373 -- FIXME numerator have is not total count
374 | otherwise = SCEnd
375
376
377-- | Select the first available piece.
378strictFirst :: Selector
379strictFirst h a _ = Just $ findMin (difference a h)
380
381-- | Select the last available piece.
382strictLast :: Selector
383strictLast h a _ = Just $ findMax (difference a h)
384
385-- |
386rarestFirst :: Selector
387rarestFirst h a xs = rarest (map (intersection want) xs)
388 where
389 want = difference h a
390
391-- | In average random first is faster than rarest first strategy but
392-- only if all pieces are available.
393randomFirst :: Selector
394randomFirst = do
395-- randomIO
396 error "randomFirst"
397
398endGame :: Selector
399endGame = strictLast
diff --git a/src/Network/BitTorrent/Exchange/Block.hs b/src/Network/BitTorrent/Exchange/Block.hs
new file mode 100644
index 00000000..bc9a3d24
--- /dev/null
+++ b/src/Network/BitTorrent/Exchange/Block.hs
@@ -0,0 +1,369 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Blocks are used to transfer pieces.
9--
10{-# LANGUAGE BangPatterns #-}
11{-# LANGUAGE FlexibleInstances #-}
12{-# LANGUAGE TemplateHaskell #-}
13{-# LANGUAGE DeriveFunctor #-}
14{-# LANGUAGE DeriveDataTypeable #-}
15{-# LANGUAGE GeneralizedNewtypeDeriving #-}
16module Network.BitTorrent.Exchange.Block
17 ( -- * Block attributes
18 BlockOffset
19 , BlockCount
20 , BlockSize
21 , defaultTransferSize
22
23 -- * Block index
24 , BlockIx(..)
25 , blockIxRange
26
27 -- * Block data
28 , Block(..)
29 , blockIx
30 , blockSize
31 , blockRange
32 , isPiece
33 , leadingBlock
34
35 -- * Block bucket
36 , Bucket
37
38 -- ** Query
39 , Network.BitTorrent.Exchange.Block.null
40 , Network.BitTorrent.Exchange.Block.full
41 , Network.BitTorrent.Exchange.Block.size
42 , Network.BitTorrent.Exchange.Block.spans
43
44 -- ** Construction
45 , Network.BitTorrent.Exchange.Block.empty
46 , Network.BitTorrent.Exchange.Block.insert
47 , Network.BitTorrent.Exchange.Block.insertLazy
48 , Network.BitTorrent.Exchange.Block.merge
49 , Network.BitTorrent.Exchange.Block.fromList
50
51 -- ** Rendering
52 , Network.BitTorrent.Exchange.Block.toPiece
53
54 -- ** Debug
55 , Network.BitTorrent.Exchange.Block.valid
56 ) where
57
58import Prelude hiding (span)
59import Control.Applicative
60import Data.ByteString as BS hiding (span)
61import Data.ByteString.Lazy as BL hiding (span)
62import Data.ByteString.Lazy.Builder as BS
63import Data.Default
64import Data.Monoid
65import Data.List as L hiding (span)
66import Data.Serialize as S
67import Data.Typeable
68import Numeric
69import Text.PrettyPrint as PP hiding ((<>))
70import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
71
72import Data.Torrent
73
74{-----------------------------------------------------------------------
75-- Block attributes
76-----------------------------------------------------------------------}
77
78-- | Offset of a block in a piece in bytes. Should be multiple of
79-- the choosen block size.
80type BlockOffset = Int
81
82-- | Size of a block in bytes. Should be power of 2.
83--
84-- Normally block size is equal to 'defaultTransferSize'.
85--
86type BlockSize = Int
87
88-- | Number of block in a piece of a torrent. Used to distinguish
89-- block count from piece count.
90type BlockCount = Int
91
92-- | Widely used semi-official block size. Some clients can ignore if
93-- block size of BlockIx in Request message is not equal to this
94-- value.
95--
96defaultTransferSize :: BlockSize
97defaultTransferSize = 16 * 1024
98
99{-----------------------------------------------------------------------
100 Block Index
101-----------------------------------------------------------------------}
102
103-- | BlockIx correspond.
104data BlockIx = BlockIx {
105 -- | Zero-based piece index.
106 ixPiece :: {-# UNPACK #-} !PieceIx
107
108 -- | Zero-based byte offset within the piece.
109 , ixOffset :: {-# UNPACK #-} !BlockOffset
110
111 -- | Block size starting from offset.
112 , ixLength :: {-# UNPACK #-} !BlockSize
113 } deriving (Show, Eq, Typeable)
114
115-- | First block in torrent. Useful for debugging.
116instance Default BlockIx where
117 def = BlockIx 0 0 defaultTransferSize
118
119getInt :: S.Get Int
120getInt = fromIntegral <$> S.getWord32be
121{-# INLINE getInt #-}
122
123putInt :: S.Putter Int
124putInt = S.putWord32be . fromIntegral
125{-# INLINE putInt #-}
126
127instance Serialize BlockIx where
128 {-# SPECIALIZE instance Serialize BlockIx #-}
129 get = BlockIx <$> getInt
130 <*> getInt
131 <*> getInt
132 {-# INLINE get #-}
133
134 put BlockIx {..} = do
135 putInt ixPiece
136 putInt ixOffset
137 putInt ixLength
138 {-# INLINE put #-}
139
140instance Pretty BlockIx where
141 pPrint BlockIx {..} =
142 ("piece = " <> int ixPiece <> ",") <+>
143 ("offset = " <> int ixOffset <> ",") <+>
144 ("length = " <> int ixLength)
145
146-- | Get location of payload bytes in the torrent content.
147blockIxRange :: (Num a, Integral a) => PieceSize -> BlockIx -> (a, a)
148blockIxRange piSize BlockIx {..} = (offset, offset + len)
149 where
150 offset = fromIntegral piSize * fromIntegral ixPiece
151 + fromIntegral ixOffset
152 len = fromIntegral ixLength
153{-# INLINE blockIxRange #-}
154
155{-----------------------------------------------------------------------
156 Block
157-----------------------------------------------------------------------}
158
159data Block payload = Block {
160 -- | Zero-based piece index.
161 blkPiece :: {-# UNPACK #-} !PieceIx
162
163 -- | Zero-based byte offset within the piece.
164 , blkOffset :: {-# UNPACK #-} !BlockOffset
165
166 -- | Payload bytes.
167 , blkData :: !payload
168 } deriving (Show, Eq, Functor, Typeable)
169
170-- | Payload is ommitted.
171instance Pretty (Block BL.ByteString) where
172 pPrint = pPrint . blockIx
173 {-# INLINE pPrint #-}
174
175-- | Get size of block /payload/ in bytes.
176blockSize :: Block BL.ByteString -> BlockSize
177blockSize = fromIntegral . BL.length . blkData
178{-# INLINE blockSize #-}
179
180-- | Get block index of a block.
181blockIx :: Block BL.ByteString -> BlockIx
182blockIx = BlockIx <$> blkPiece <*> blkOffset <*> blockSize
183
184-- | Get location of payload bytes in the torrent content.
185blockRange :: (Num a, Integral a)
186 => PieceSize -> Block BL.ByteString -> (a, a)
187blockRange piSize = blockIxRange piSize . blockIx
188{-# INLINE blockRange #-}
189
190-- | Test if a block can be safely turned into a piece.
191isPiece :: PieceSize -> Block BL.ByteString -> Bool
192isPiece pieceLen blk @ (Block i offset _) =
193 offset == 0 && blockSize blk == pieceLen && i >= 0
194{-# INLINE isPiece #-}
195
196-- | First block in the piece.
197leadingBlock :: PieceIx -> BlockSize -> BlockIx
198leadingBlock pix blockSize = BlockIx
199 { ixPiece = pix
200 , ixOffset = 0
201 , ixLength = blockSize
202 }
203{-# INLINE leadingBlock #-}
204
205{-----------------------------------------------------------------------
206-- Bucket
207-----------------------------------------------------------------------}
208
209type Pos = Int
210type ChunkSize = Int
211
212-- | A sparse set of blocks used to represent an /in progress/ piece.
213data Bucket
214 = Nil
215 | Span {-# UNPACK #-} !ChunkSize !Bucket
216 | Fill {-# UNPACK #-} !ChunkSize !Builder !Bucket
217
218instance Show Bucket where
219 showsPrec i Nil = showString ""
220 showsPrec i (Span s xs) = showString "Span " <> showInt s
221 <> showString " " <> showsPrec i xs
222 showsPrec i (Fill s _ xs) = showString "Fill " <> showInt s
223 <> showString " " <> showsPrec i xs
224
225-- | INVARIANT: 'Nil' should appear only after 'Span' of 'Fill'.
226nilInvFailed :: a
227nilInvFailed = error "Nil: bucket invariant failed"
228
229valid :: Bucket -> Bool
230valid = check Nothing
231 where
232 check Nothing Nil = False -- see 'nilInvFailed'
233 check (Just _) _ = True
234 check prevIsSpan (Span sz xs) =
235 prevIsSpan /= Just True && -- Span n (NotSpan .. ) invariant
236 sz > 0 && -- Span is always non-empty
237 check (Just True) xs
238 check prevIsSpan (Fill sz b xs) =
239 prevIsSpan /= Just True && -- Fill n (NotFill .. ) invariant
240 sz > 0 && -- Fill is always non-empty
241 check (Just False) xs
242
243instance Pretty Bucket where
244 pPrint Nil = nilInvFailed
245 pPrint bkt = go bkt
246 where
247 go Nil = PP.empty
248 go (Span sz xs) = "Span" <+> PP.int sz <+> go xs
249 go (Fill sz b xs) = "Fill" <+> PP.int sz <+> go xs
250
251-- | Smart constructor: use it when some block is /deleted/ from
252-- bucket.
253span :: ChunkSize -> Bucket -> Bucket
254span sz (Span sz' xs) = Span (sz + sz') xs
255span sz xxs = Span sz xxs
256{-# INLINE span #-}
257
258-- | Smart constructor: use it when some block is /inserted/ to
259-- bucket.
260fill :: ChunkSize -> Builder -> Bucket -> Bucket
261fill sz b (Fill sz' b' xs) = Fill (sz + sz') (b <> b') xs
262fill sz b xxs = Fill sz b xxs
263{-# INLINE fill #-}
264
265{-----------------------------------------------------------------------
266-- Bucket queries
267-----------------------------------------------------------------------}
268
269-- | /O(1)/. Test if this bucket is empty.
270null :: Bucket -> Bool
271null Nil = nilInvFailed
272null (Span _ Nil) = True
273null _ = False
274{-# INLINE null #-}
275
276-- | /O(1)/. Test if this bucket is complete.
277full :: Bucket -> Bool
278full Nil = nilInvFailed
279full (Fill _ _ Nil) = True
280full _ = False
281{-# INLINE full #-}
282
283-- | /O(n)/. Total size of the incompleted piece.
284size :: Bucket -> PieceSize
285size Nil = nilInvFailed
286size bkt = go bkt
287 where
288 go Nil = 0
289 go (Span sz xs) = sz + go xs
290 go (Fill sz _ xs) = sz + go xs
291
292-- | /O(n)/. List incomplete blocks to download. If some block have
293-- size more than the specified 'BlockSize' then block is split into
294-- smaller blocks to satisfy given 'BlockSize'. Small (for
295-- e.g. trailing) blocks is not ignored, but returned in-order.
296spans :: BlockSize -> Bucket -> [(BlockOffset, BlockSize)]
297spans expectedSize = go 0
298 where
299 go _ Nil = []
300 go off (Span sz xs) = listChunks off sz ++ go (off + sz) xs
301 go off (Fill sz _ xs) = go (off + sz) xs
302
303 listChunks off restSize
304 | restSize <= 0 = []
305 | otherwise = (off, blkSize)
306 : listChunks (off + blkSize) (restSize - blkSize)
307 where
308 blkSize = min expectedSize restSize
309
310{-----------------------------------------------------------------------
311-- Bucket contstruction
312-----------------------------------------------------------------------}
313
314-- | /O(1)/. A new empty bucket capable to alloof specified size.
315empty :: PieceSize -> Bucket
316empty sz
317 | sz < 0 = error "empty: Bucket size must be a non-negative value"
318 | otherwise = Span sz Nil
319{-# INLINE empty #-}
320
321insertSpan :: Pos -> BS.ByteString -> ChunkSize -> Bucket -> Bucket
322insertSpan !pos !bs !span_sz !xs =
323 let pref_len = pos
324 fill_len = span_sz - pos `min` BS.length bs
325 suff_len = (span_sz - pos) - fill_len
326 in mkSpan pref_len $
327 fill fill_len (byteString (BS.take fill_len bs)) $
328 mkSpan suff_len $
329 xs
330 where
331 mkSpan 0 xs = xs
332 mkSpan sz xs = Span sz xs
333
334-- | /O(n)/. Insert a strict bytestring at specified position.
335--
336-- Best case: if blocks are inserted in sequential order, then this
337-- operation should take /O(1)/.
338--
339insert :: Pos -> BS.ByteString -> Bucket -> Bucket
340insert _ _ Nil = nilInvFailed
341insert dstPos bs bucket = go 0 bucket
342 where
343 intersects curPos sz = dstPos >= curPos && dstPos <= curPos + sz
344
345 go _ Nil = Nil
346 go curPos (Span sz xs)
347 | intersects curPos sz = insertSpan (dstPos - curPos) bs sz xs
348 | otherwise = span sz (go (curPos + sz) xs)
349 go curPos bkt @ (Fill sz br xs)
350 | intersects curPos sz = bkt
351 | otherwise = fill sz br (go (curPos + sz) xs)
352
353fromList :: PieceSize -> [(Pos, BS.ByteString)] -> Bucket
354fromList s = L.foldr (uncurry Network.BitTorrent.Exchange.Block.insert)
355 (Network.BitTorrent.Exchange.Block.empty s)
356
357-- TODO zero-copy
358insertLazy :: Pos -> BL.ByteString -> Bucket -> Bucket
359insertLazy pos bl = Network.BitTorrent.Exchange.Block.insert pos (BL.toStrict bl)
360
361-- | /O(n)/.
362merge :: Bucket -> Bucket -> Bucket
363merge = error "Bucket.merge: not implemented"
364
365-- | /O(1)/.
366toPiece :: Bucket -> Maybe BL.ByteString
367toPiece Nil = nilInvFailed
368toPiece (Fill _ b Nil) = Just (toLazyByteString b)
369toPiece _ = Nothing
diff --git a/src/Network/BitTorrent/Exchange/Connection.hs b/src/Network/BitTorrent/Exchange/Connection.hs
new file mode 100644
index 00000000..d65d322e
--- /dev/null
+++ b/src/Network/BitTorrent/Exchange/Connection.hs
@@ -0,0 +1,1012 @@
1-- |
2-- Module : Network.BitTorrent.Exchange.Wire
3-- Copyright : (c) Sam Truzjan 2013
4-- (c) Daniel Gröber 2013
5-- License : BSD3
6-- Maintainer : pxqr.sta@gmail.com
7-- Stability : experimental
8-- Portability : portable
9--
10-- Each peer wire connection is identified by triple @(topic,
11-- remote_addr, this_addr)@. This means that connections are the
12-- same if and only if their 'ConnectionId' are the same. Of course,
13-- you /must/ avoid duplicated connections.
14--
15-- This module control /integrity/ of data send and received.
16--
17{-# LANGUAGE DeriveDataTypeable #-}
18{-# LANGUAGE TemplateHaskell #-}
19{-# LANGUAGE MultiParamTypeClasses #-}
20{-# LANGUAGE GeneralizedNewtypeDeriving #-}
21module Network.BitTorrent.Exchange.Connection
22 ( -- * Wire
23 Connected
24 , Wire
25 , ChannelSide (..)
26
27 -- * Connection
28 , Connection
29 , connInitiatedBy
30
31 -- ** Identity
32 , connRemoteAddr
33 , connTopic
34 , connRemotePeerId
35 , connThisPeerId
36
37 -- ** Capabilities
38 , connProtocol
39 , connCaps
40 , connExtCaps
41 , connRemoteEhs
42
43 -- ** State
44 , connStatus
45 , connBitfield
46
47 -- ** Env
48 , connOptions
49 , connSession
50 , connStats
51
52 -- ** Status
53 , PeerStatus (..)
54 , ConnectionStatus (..)
55 , updateStatus
56 , statusUpdates
57 , clientStatus
58 , remoteStatus
59 , canUpload
60 , canDownload
61 , defaultUnchokeSlots
62 , defaultRechokeInterval
63
64
65 -- * Setup
66 , ConnectionPrefs (..)
67 , SessionLink (..)
68 , ConnectionConfig (..)
69
70 -- ** Initiate
71 , connectWire
72
73 -- ** Accept
74 , PendingConnection
75 , newPendingConnection
76 , pendingPeer
77 , pendingCaps
78 , pendingTopic
79 , closePending
80 , acceptWire
81
82 -- ** Post setup actions
83 , resizeBitfield
84
85 -- * Messaging
86 , recvMessage
87 , sendMessage
88 , filterQueue
89 , getMaxQueueLength
90
91 -- * Exceptions
92 , ProtocolError (..)
93 , WireFailure (..)
94 , peerPenalty
95 , isWireFailure
96 , disconnectPeer
97
98 -- * Stats
99 , ByteStats (..)
100 , FlowStats (..)
101 , ConnectionStats (..)
102
103 -- * Flood detection
104 , FloodDetector (..)
105
106 -- * Options
107 , Options (..)
108 ) where
109
110import Control.Applicative
111import Control.Concurrent hiding (yield)
112import Control.Exception
113import Control.Monad.Reader
114import Control.Monad.State
115import Control.Monad.Trans.Resource
116import Control.Lens
117import Data.ByteString as BS
118import Data.ByteString.Lazy as BSL
119import Data.Conduit as C
120import Data.Conduit.Cereal
121import Data.Conduit.List
122import Data.Conduit.Network
123import Data.Default
124import Data.IORef
125import Data.List as L
126import Data.Maybe as M
127import Data.Monoid
128import Data.Serialize as S
129import Data.Typeable
130import Network
131import Network.Socket hiding (Connected)
132import Network.Socket.ByteString as BS
133import Text.PrettyPrint as PP hiding ((<>))
134import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
135import Text.Show.Functions ()
136import System.Log.FastLogger (ToLogStr(..))
137import System.Timeout
138
139import Data.Torrent
140import Network.BitTorrent.Address
141import Network.BitTorrent.Exchange.Bitfield as BF
142import Network.BitTorrent.Exchange.Message as Msg
143
144-- TODO handle port message?
145-- TODO handle limits?
146-- TODO filter not requested PIECE messages
147-- TODO metadata piece request flood protection
148-- TODO piece request flood protection
149-- TODO protect against flood attacks
150{-----------------------------------------------------------------------
151-- Exceptions
152-----------------------------------------------------------------------}
153
154-- | Used to specify initiator of 'ProtocolError'.
155data ChannelSide
156 = ThisPeer
157 | RemotePeer
158 deriving (Show, Eq, Enum, Bounded)
159
160instance Default ChannelSide where
161 def = ThisPeer
162
163instance Pretty ChannelSide where
164 pPrint = PP.text . show
165
166-- | A protocol errors occur when a peer violates protocol
167-- specification.
168data ProtocolError
169 -- | Protocol string should be 'BitTorrent Protocol' but remote
170 -- peer have sent a different string.
171 = InvalidProtocol ProtocolName
172
173 -- | Sent and received protocol strings do not match. Can occur
174 -- in 'connectWire' only.
175 | UnexpectedProtocol ProtocolName
176
177 -- | /Remote/ peer replied with invalid 'hsInfoHash' which do not
178 -- match with 'hsInfoHash' /this/ peer have sent. Can occur in
179 -- 'connectWire' or 'acceptWire' only.
180 | UnexpectedTopic InfoHash
181
182 -- | Some trackers or DHT can return 'PeerId' of a peer. If a
183 -- remote peer handshaked with different 'hsPeerId' then this
184 -- exception is raised. Can occur in 'connectWire' only.
185 | UnexpectedPeerId PeerId
186
187 -- | Accepted peer have sent unknown torrent infohash in
188 -- 'hsInfoHash' field. This situation usually happen when /this/
189 -- peer have deleted the requested torrent. The error can occur in
190 -- 'acceptWire' function only.
191 | UnknownTopic InfoHash
192
193 -- | A remote peer have 'ExtExtended' enabled but did not send an
194 -- 'ExtendedHandshake' back.
195 | HandshakeRefused
196
197 -- | 'Network.BitTorrent.Exchange.Message.Bitfield' message MUST
198 -- be send either once or zero times, but either this peer or
199 -- remote peer send a bitfield message the second time.
200 | BitfieldAlreadySent ChannelSide
201
202 -- | Capabilities violation. For example this exception can occur
203 -- when a peer have sent 'Port' message but 'ExtDHT' is not
204 -- allowed in 'connCaps'.
205 | DisallowedMessage
206 { -- | Who sent invalid message.
207 violentSender :: ChannelSide
208
209 -- | If the 'violentSender' reconnect with this extension
210 -- enabled then he can try to send this message.
211 , extensionRequired :: Extension
212 }
213 deriving Show
214
215instance Pretty ProtocolError where
216 pPrint = PP.text . show
217
218errorPenalty :: ProtocolError -> Int
219errorPenalty (InvalidProtocol _) = 1
220errorPenalty (UnexpectedProtocol _) = 1
221errorPenalty (UnexpectedTopic _) = 1
222errorPenalty (UnexpectedPeerId _) = 1
223errorPenalty (UnknownTopic _) = 0
224errorPenalty (HandshakeRefused ) = 1
225errorPenalty (BitfieldAlreadySent _) = 1
226errorPenalty (DisallowedMessage _ _) = 1
227
228-- | Exceptions used to interrupt the current P2P session.
229data WireFailure
230 = ConnectionRefused IOError
231
232 -- | Force termination of wire connection.
233 --
234 -- Normally you should throw only this exception from event loop
235 -- using 'disconnectPeer', other exceptions are thrown
236 -- automatically by functions from this module.
237 --
238 | DisconnectPeer
239
240 -- | A peer not responding and did not send a 'KeepAlive' message
241 -- for a specified period of time.
242 | PeerDisconnected
243
244 -- | A remote peer have sent some unknown message we unable to
245 -- parse.
246 | DecodingError GetException
247
248 -- | See 'ProtocolError' for more details.
249 | ProtocolError ProtocolError
250
251 -- | A possible malicious peer have sent too many control messages
252 -- without making any progress.
253 | FloodDetected ConnectionStats
254 deriving (Show, Typeable)
255
256instance Exception WireFailure
257
258instance Pretty WireFailure where
259 pPrint = PP.text . show
260
261-- TODO
262-- data Penalty = Ban | Penalty Int
263
264peerPenalty :: WireFailure -> Int
265peerPenalty DisconnectPeer = 0
266peerPenalty PeerDisconnected = 0
267peerPenalty (DecodingError _) = 1
268peerPenalty (ProtocolError e) = errorPenalty e
269peerPenalty (FloodDetected _) = 1
270
271-- | Do nothing with exception, used with 'handle' or 'try'.
272isWireFailure :: Monad m => WireFailure -> m ()
273isWireFailure _ = return ()
274
275protocolError :: MonadThrow m => ProtocolError -> m a
276protocolError = monadThrow . ProtocolError
277
278{-----------------------------------------------------------------------
279-- Stats
280-----------------------------------------------------------------------}
281
282-- | Message stats in one direction.
283data FlowStats = FlowStats
284 { -- | Number of the messages sent or received.
285 messageCount :: {-# UNPACK #-} !Int
286 -- | Sum of byte sequences of all messages.
287 , messageBytes :: {-# UNPACK #-} !ByteStats
288 } deriving Show
289
290instance Pretty FlowStats where
291 pPrint FlowStats {..} =
292 PP.int messageCount <+> "messages" $+$
293 pPrint messageBytes
294
295-- | Zeroed stats.
296instance Default FlowStats where
297 def = FlowStats 0 def
298
299-- | Monoid under addition.
300instance Monoid FlowStats where
301 mempty = def
302 mappend a b = FlowStats
303 { messageBytes = messageBytes a <> messageBytes b
304 , messageCount = messageCount a + messageCount b
305 }
306
307-- | Find average length of byte sequences per message.
308avgByteStats :: FlowStats -> ByteStats
309avgByteStats (FlowStats n ByteStats {..}) = ByteStats
310 { overhead = overhead `quot` n
311 , control = control `quot` n
312 , payload = payload `quot` n
313 }
314
315-- | Message stats in both directions. This data can be retrieved
316-- using 'getStats' function.
317--
318-- Note that this stats is completely different from
319-- 'Data.Torrent.Progress.Progress': payload bytes not necessary
320-- equal to downloaded\/uploaded bytes since a peer can send a
321-- broken block.
322--
323data ConnectionStats = ConnectionStats
324 { -- | Received messages stats.
325 incomingFlow :: !FlowStats
326 -- | Sent messages stats.
327 , outcomingFlow :: !FlowStats
328 } deriving Show
329
330instance Pretty ConnectionStats where
331 pPrint ConnectionStats {..} = vcat
332 [ "Recv:" <+> pPrint incomingFlow
333 , "Sent:" <+> pPrint outcomingFlow
334 , "Both:" <+> pPrint (incomingFlow <> outcomingFlow)
335 ]
336
337-- | Zeroed stats.
338instance Default ConnectionStats where
339 def = ConnectionStats def def
340
341-- | Monoid under addition.
342instance Monoid ConnectionStats where
343 mempty = def
344 mappend a b = ConnectionStats
345 { incomingFlow = incomingFlow a <> incomingFlow b
346 , outcomingFlow = outcomingFlow a <> outcomingFlow b
347 }
348
349-- | Aggregate one more message stats in the /specified/ direction.
350addStats :: ChannelSide -> ByteStats -> ConnectionStats -> ConnectionStats
351addStats ThisPeer x s = s { outcomingFlow = (FlowStats 1 x) <> (outcomingFlow s) }
352addStats RemotePeer x s = s { incomingFlow = (FlowStats 1 x) <> (incomingFlow s) }
353
354-- | Sum of overhead and control bytes in both directions.
355wastedBytes :: ConnectionStats -> Int
356wastedBytes ConnectionStats {..} = overhead + control
357 where
358 FlowStats _ ByteStats {..} = incomingFlow <> outcomingFlow
359
360-- | Sum of payload bytes in both directions.
361payloadBytes :: ConnectionStats -> Int
362payloadBytes ConnectionStats {..} =
363 payload (messageBytes (incomingFlow <> outcomingFlow))
364
365-- | Sum of any bytes in both directions.
366transmittedBytes :: ConnectionStats -> Int
367transmittedBytes ConnectionStats {..} =
368 byteLength (messageBytes (incomingFlow <> outcomingFlow))
369
370{-----------------------------------------------------------------------
371-- Flood protection
372-----------------------------------------------------------------------}
373
374defaultFloodFactor :: Int
375defaultFloodFactor = 1
376
377-- | This is a very permissive value, connection setup usually takes
378-- around 10-100KB, including both directions.
379defaultFloodThreshold :: Int
380defaultFloodThreshold = 2 * 1024 * 1024
381
382-- | A flood detection function.
383type Detector stats = Int -- ^ Factor;
384 -> Int -- ^ Threshold;
385 -> stats -- ^ Stats to analyse;
386 -> Bool -- ^ Is this a flooded connection?
387
388defaultDetector :: Detector ConnectionStats
389defaultDetector factor threshold s =
390 transmittedBytes s > threshold &&
391 factor * wastedBytes s > payloadBytes s
392
393-- | Flood detection is used to protect /this/ peer against a /remote/
394-- malicious peer sending meaningless control messages.
395data FloodDetector = FloodDetector
396 { -- | Max ratio of payload bytes to control bytes.
397 floodFactor :: {-# UNPACK #-} !Int
398
399 -- | Max count of bytes connection /setup/ can take including
400 -- 'Handshake', 'ExtendedHandshake', 'Bitfield', 'Have' and 'Port'
401 -- messages. This value is used to avoid false positives at the
402 -- connection initialization.
403 , floodThreshold :: {-# UNPACK #-} !Int
404
405 -- | Flood predicate on the /current/ 'ConnectionStats'.
406 , floodPredicate :: Detector ConnectionStats
407 } deriving Show
408
409instance Eq FloodDetector where
410 a == b = floodFactor a == floodFactor b
411 && floodThreshold a == floodThreshold b
412
413-- | Flood detector with very permissive options.
414instance Default FloodDetector where
415 def = FloodDetector
416 { floodFactor = defaultFloodFactor
417 , floodThreshold = defaultFloodThreshold
418 , floodPredicate = defaultDetector
419 }
420
421-- | This peer might drop connection if the detector gives positive answer.
422runDetector :: FloodDetector -> ConnectionStats -> Bool
423runDetector FloodDetector {..} = floodPredicate floodFactor floodThreshold
424
425{-----------------------------------------------------------------------
426-- Options
427-----------------------------------------------------------------------}
428
429-- | Various connection settings and limits.
430data Options = Options
431 { -- | How often /this/ peer should send 'KeepAlive' messages.
432 keepaliveInterval :: {-# UNPACK #-} !Int
433
434 -- | /This/ peer will drop connection if a /remote/ peer did not
435 -- send any message for this period of time.
436 , keepaliveTimeout :: {-# UNPACK #-} !Int
437
438 , requestQueueLength :: {-# UNPACK #-} !Int
439
440 -- | Used to protect against flood attacks.
441 , floodDetector :: FloodDetector
442
443 -- | Used to protect against flood attacks in /metadata
444 -- exchange/. Normally, a requesting peer should request each
445 -- 'InfoDict' piece only one time, but a malicious peer can
446 -- saturate wire with 'MetadataRequest' messages thus flooding
447 -- responding peer.
448 --
449 -- This value set upper bound for number of 'MetadataRequests'
450 -- for each piece.
451 --
452 , metadataFactor :: {-# UNPACK #-} !Int
453
454 -- | Used to protect against out-of-memory attacks: malicious peer
455 -- can claim that 'totalSize' is, say, 100TB and send some random
456 -- data instead of infodict pieces. Since requesting peer unable
457 -- to check not completed infodict via the infohash, the
458 -- accumulated pieces will allocate the all available memory.
459 --
460 -- This limit set upper bound for 'InfoDict' size. See
461 -- 'ExtendedMetadata' for more info.
462 --
463 , maxInfoDictSize :: {-# UNPACK #-} !Int
464 } deriving (Show, Eq)
465
466-- | Permissive default parameters, most likely you don't need to
467-- change them.
468instance Default Options where
469 def = Options
470 { keepaliveInterval = defaultKeepAliveInterval
471 , keepaliveTimeout = defaultKeepAliveTimeout
472 , requestQueueLength = defaultRequestQueueLength
473 , floodDetector = def
474 , metadataFactor = defaultMetadataFactor
475 , maxInfoDictSize = defaultMaxInfoDictSize
476 }
477
478{-----------------------------------------------------------------------
479-- Peer status
480-----------------------------------------------------------------------}
481
482-- | Connections contain two bits of state on either end: choked or
483-- not, and interested or not.
484data PeerStatus = PeerStatus
485 { -- | Choking is a notification that no data will be sent until
486 -- unchoking happens.
487 _choking :: !Bool
488
489 -- |
490 , _interested :: !Bool
491 } deriving (Show, Eq, Ord)
492
493$(makeLenses ''PeerStatus)
494
495instance Pretty PeerStatus where
496 pPrint PeerStatus {..} =
497 pPrint (Choking _choking) <+> "and" <+> pPrint (Interested _interested)
498
499-- | Connections start out choked and not interested.
500instance Default PeerStatus where
501 def = PeerStatus True False
502
503instance Monoid PeerStatus where
504 mempty = def
505 mappend a b = PeerStatus
506 { _choking = _choking a && _choking b
507 , _interested = _interested a || _interested b
508 }
509
510-- | Can be used to update remote peer status using incoming 'Status'
511-- message.
512updateStatus :: StatusUpdate -> PeerStatus -> PeerStatus
513updateStatus (Choking b) = choking .~ b
514updateStatus (Interested b) = interested .~ b
515
516-- | Can be used to generate outcoming messages.
517statusUpdates :: PeerStatus -> PeerStatus -> [StatusUpdate]
518statusUpdates a b = M.catMaybes $
519 [ if _choking a == _choking b then Nothing
520 else Just $ Choking $ _choking b
521 , if _interested a == _interested b then Nothing
522 else Just $ Interested $ _interested b
523 ]
524
525{-----------------------------------------------------------------------
526-- Connection status
527-----------------------------------------------------------------------}
528
529-- | Status of the both endpoints.
530data ConnectionStatus = ConnectionStatus
531 { _clientStatus :: !PeerStatus
532 , _remoteStatus :: !PeerStatus
533 } deriving (Show, Eq)
534
535$(makeLenses ''ConnectionStatus)
536
537instance Pretty ConnectionStatus where
538 pPrint ConnectionStatus {..} =
539 "this " PP.<+> pPrint _clientStatus PP.$$
540 "remote" PP.<+> pPrint _remoteStatus
541
542-- | Connections start out choked and not interested.
543instance Default ConnectionStatus where
544 def = ConnectionStatus def def
545
546-- | Can the client transfer to the remote peer?
547canUpload :: ConnectionStatus -> Bool
548canUpload ConnectionStatus {..}
549 = _interested _remoteStatus && not (_choking _clientStatus)
550
551-- | Can the client transfer from the remote peer?
552canDownload :: ConnectionStatus -> Bool
553canDownload ConnectionStatus {..}
554 = _interested _clientStatus && not (_choking _remoteStatus)
555
556-- | Indicates how many peers are allowed to download from the client
557-- by default.
558defaultUnchokeSlots :: Int
559defaultUnchokeSlots = 4
560
561-- |
562defaultRechokeInterval :: Int
563defaultRechokeInterval = 10 * 1000 * 1000
564
565{-----------------------------------------------------------------------
566-- Connection
567-----------------------------------------------------------------------}
568
569data ConnectionState = ConnectionState {
570 -- | If @not (allowed ExtExtended connCaps)@ then this set is always
571 -- empty. Otherwise it has the BEP10 extension protocol mandated mapping of
572 -- 'MessageId' to the message type for the remote peer.
573 --
574 -- Note that this value can change in current session if either
575 -- this or remote peer will initiate rehandshaking.
576 --
577 _connExtCaps :: !ExtendedCaps
578
579 -- | Current extended handshake information from the remote peer
580 , _connRemoteEhs :: !ExtendedHandshake
581
582 -- | Various stats about messages sent and received. Stats can be
583 -- used to protect /this/ peer against flood attacks.
584 --
585 -- Note that this value will change with the next sent or received
586 -- message.
587 , _connStats :: !ConnectionStats
588
589 , _connStatus :: !ConnectionStatus
590
591 -- | Bitfield of remote endpoint.
592 , _connBitfield :: !Bitfield
593 }
594
595makeLenses ''ConnectionState
596
597instance Default ConnectionState where
598 def = ConnectionState
599 { _connExtCaps = def
600 , _connRemoteEhs = def
601 , _connStats = def
602 , _connStatus = def
603 , _connBitfield = BF.haveNone 0
604 }
605
606-- | Connection keep various info about both peers.
607data Connection s = Connection
608 { connInitiatedBy :: !ChannelSide
609
610 , connRemoteAddr :: !(PeerAddr IP)
611
612 -- | /Both/ peers handshaked with this protocol string. The only
613 -- value is \"Bittorrent Protocol\" but this can be changed in
614 -- future.
615 , connProtocol :: !ProtocolName
616
617 -- | Set of enabled core extensions, i.e. the pre BEP10 extension
618 -- mechanism. This value is used to check if a message is allowed
619 -- to be sent or received.
620 , connCaps :: !Caps
621
622 -- | /Both/ peers handshaked with this infohash. A connection can
623 -- handle only one topic, use 'reconnect' to change the current
624 -- topic.
625 , connTopic :: !InfoHash
626
627 -- | Typically extracted from handshake.
628 , connRemotePeerId :: !PeerId
629
630 -- | Typically extracted from handshake.
631 , connThisPeerId :: !PeerId
632
633 -- |
634 , connOptions :: !Options
635
636 -- | Mutable connection state, see 'ConnectionState'
637 , connState :: !(IORef ConnectionState)
638
639-- -- | Max request queue length.
640-- , connMaxQueueLen :: !Int
641
642 -- | Environment data.
643 , connSession :: !s
644
645 , connChan :: !(Chan Message)
646 }
647
648instance Pretty (Connection s) where
649 pPrint Connection {..} = "Connection"
650
651instance ToLogStr (Connection s) where
652 toLogStr Connection {..} = mconcat
653 [ toLogStr (show connRemoteAddr)
654 , toLogStr (show connProtocol)
655 , toLogStr (show connCaps)
656 , toLogStr (show connTopic)
657 , toLogStr (show connRemotePeerId)
658 , toLogStr (show connThisPeerId)
659 , toLogStr (show connOptions)
660 ]
661
662-- TODO check extended messages too
663isAllowed :: Connection s -> Message -> Bool
664isAllowed Connection {..} msg
665 | Just ext <- requires msg = ext `allowed` connCaps
666 | otherwise = True
667
668{-----------------------------------------------------------------------
669-- Hanshaking
670-----------------------------------------------------------------------}
671
672sendHandshake :: Socket -> Handshake -> IO ()
673sendHandshake sock hs = sendAll sock (S.encode hs)
674
675recvHandshake :: Socket -> IO Handshake
676recvHandshake sock = do
677 header <- BS.recv sock 1
678 unless (BS.length header == 1) $
679 throw $ userError "Unable to receive handshake header."
680
681 let protocolLen = BS.head header
682 let restLen = handshakeSize protocolLen - 1
683
684 body <- BS.recv sock restLen
685 let resp = BS.cons protocolLen body
686 either (throwIO . userError) return $ S.decode resp
687
688-- | Handshaking with a peer specified by the second argument.
689--
690-- It's important to send handshake first because /accepting/ peer
691-- do not know handshake topic and will wait until /connecting/ peer
692-- will send handshake.
693--
694initiateHandshake :: Socket -> Handshake -> IO Handshake
695initiateHandshake sock hs = do
696 sendHandshake sock hs
697 recvHandshake sock
698
699data HandshakePair = HandshakePair
700 { handshakeSent :: !Handshake
701 , handshakeRecv :: !Handshake
702 } deriving (Show, Eq)
703
704validatePair :: HandshakePair -> PeerAddr IP -> IO ()
705validatePair (HandshakePair hs hs') addr = Prelude.mapM_ checkProp
706 [ (def == hsProtocol hs', InvalidProtocol $ hsProtocol hs')
707 , (hsProtocol hs == hsProtocol hs', UnexpectedProtocol $ hsProtocol hs')
708 , (hsInfoHash hs == hsInfoHash hs', UnexpectedTopic $ hsInfoHash hs')
709 , (hsPeerId hs' == fromMaybe (hsPeerId hs') (peerId addr)
710 , UnexpectedPeerId $ hsPeerId hs')
711 ]
712 where
713 checkProp (t, e) = unless t $ throwIO $ ProtocolError e
714
715-- | Connection state /right/ after handshaking.
716establishedStats :: HandshakePair -> ConnectionStats
717establishedStats HandshakePair {..} = ConnectionStats
718 { outcomingFlow = FlowStats 1 $ handshakeStats handshakeSent
719 , incomingFlow = FlowStats 1 $ handshakeStats handshakeRecv
720 }
721
722{-----------------------------------------------------------------------
723-- Wire
724-----------------------------------------------------------------------}
725
726-- | do not expose this so we can change it without breaking api
727newtype Connected s a = Connected { runConnected :: (ReaderT (Connection s) IO a) }
728 deriving (Functor, Applicative, Monad
729 , MonadIO, MonadReader (Connection s), MonadThrow
730 )
731
732instance MonadState ConnectionState (Connected s) where
733 get = Connected (asks connState) >>= liftIO . readIORef
734 put x = Connected (asks connState) >>= liftIO . flip writeIORef x
735
736-- | A duplex channel connected to a remote peer which keep tracks
737-- connection parameters.
738type Wire s a = ConduitM Message Message (Connected s) a
739
740{-----------------------------------------------------------------------
741-- Wrapper
742-----------------------------------------------------------------------}
743
744putStats :: ChannelSide -> Message -> Connected s ()
745putStats side msg = connStats %= addStats side (stats msg)
746
747validate :: ChannelSide -> Message -> Connected s ()
748validate side msg = do
749 caps <- asks connCaps
750 case requires msg of
751 Nothing -> return ()
752 Just ext
753 | ext `allowed` caps -> return ()
754 | otherwise -> protocolError $ DisallowedMessage side ext
755
756trackFlow :: ChannelSide -> Wire s ()
757trackFlow side = iterM $ do
758 validate side
759 putStats side
760
761{-----------------------------------------------------------------------
762-- Setup
763-----------------------------------------------------------------------}
764
765-- System.Timeout.timeout multiplier
766seconds :: Int
767seconds = 1000000
768
769sinkChan :: MonadIO m => Chan Message -> Sink Message m ()
770sinkChan chan = await >>= maybe (return ()) (liftIO . writeChan chan)
771
772sourceChan :: MonadIO m => Int -> Chan Message -> Source m Message
773sourceChan interval chan = do
774 mmsg <- liftIO $ timeout (interval * seconds) $ readChan chan
775 yield $ fromMaybe Msg.KeepAlive mmsg
776
777-- | Normally you should use 'connectWire' or 'acceptWire'.
778runWire :: Wire s () -> Socket -> Chan Message -> Connection s -> IO ()
779runWire action sock chan conn = flip runReaderT conn $ runConnected $
780 sourceSocket sock $=
781 conduitGet S.get $=
782 trackFlow RemotePeer $=
783 action $=
784 trackFlow ThisPeer C.$$
785 sinkChan chan
786
787-- | This function will block until a peer send new message. You can
788-- also use 'await'.
789recvMessage :: Wire s Message
790recvMessage = await >>= maybe (monadThrow PeerDisconnected) return
791
792-- | You can also use 'yield'.
793sendMessage :: PeerMessage msg => msg -> Wire s ()
794sendMessage msg = do
795 ecaps <- use connExtCaps
796 yield $ envelop ecaps msg
797
798getMaxQueueLength :: Connected s Int
799getMaxQueueLength = do
800 advertisedLen <- ehsQueueLength <$> use connRemoteEhs
801 defaultLen <- asks (requestQueueLength . connOptions)
802 return $ fromMaybe defaultLen advertisedLen
803
804-- | Filter pending messages from send buffer.
805filterQueue :: (Message -> Bool) -> Wire s ()
806filterQueue p = lift $ do
807 chan <- asks connChan
808 liftIO $ getChanContents chan >>= writeList2Chan chan . L.filter p
809
810-- | Forcefully terminate wire session and close socket.
811disconnectPeer :: Wire s a
812disconnectPeer = monadThrow DisconnectPeer
813
814extendedHandshake :: ExtendedCaps -> Wire s ()
815extendedHandshake caps = do
816 -- TODO add other params to the handshake
817 sendMessage $ nullExtendedHandshake caps
818 msg <- recvMessage
819 case msg of
820 Extended (EHandshake remoteEhs@(ExtendedHandshake {..})) -> do
821 connExtCaps .= (ehsCaps <> caps)
822 connRemoteEhs .= remoteEhs
823 _ -> protocolError HandshakeRefused
824
825rehandshake :: ExtendedCaps -> Wire s ()
826rehandshake caps = error "rehandshake"
827
828reconnect :: Wire s ()
829reconnect = error "reconnect"
830
831data ConnectionId = ConnectionId
832 { topic :: !InfoHash
833 , remoteAddr :: !(PeerAddr IP)
834 , thisAddr :: !(PeerAddr (Maybe IP)) -- ^ foreign address of this node.
835 }
836
837-- | /Preffered/ settings of wire. To get the real use 'ask'.
838data ConnectionPrefs = ConnectionPrefs
839 { prefOptions :: !Options
840 , prefProtocol :: !ProtocolName
841 , prefCaps :: !Caps
842 , prefExtCaps :: !ExtendedCaps
843 } deriving (Show, Eq)
844
845instance Default ConnectionPrefs where
846 def = ConnectionPrefs
847 { prefOptions = def
848 , prefProtocol = def
849 , prefCaps = def
850 , prefExtCaps = def
851 }
852
853normalize :: ConnectionPrefs -> ConnectionPrefs
854normalize = error "normalize"
855
856-- | Bridge between 'Connection' and 'Network.BitTorrent.Exchange.Session'.
857data SessionLink s = SessionLink
858 { linkTopic :: !(InfoHash)
859 , linkPeerId :: !(PeerId)
860 , linkMetadataSize :: !(Maybe Int)
861 , linkOutputChan :: !(Maybe (Chan Message))
862 , linkSession :: !(s)
863 }
864
865data ConnectionConfig s = ConnectionConfig
866 { cfgPrefs :: !(ConnectionPrefs)
867 , cfgSession :: !(SessionLink s)
868 , cfgWire :: !(Wire s ())
869 }
870
871configHandshake :: ConnectionConfig s -> Handshake
872configHandshake ConnectionConfig {..} = Handshake
873 { hsProtocol = prefProtocol cfgPrefs
874 , hsReserved = prefCaps cfgPrefs
875 , hsInfoHash = linkTopic cfgSession
876 , hsPeerId = linkPeerId cfgSession
877 }
878
879{-----------------------------------------------------------------------
880-- Pending connections
881-----------------------------------------------------------------------}
882
883-- | Connection in half opened state. A normal usage scenario:
884--
885-- * Opened using 'newPendingConnection', usually in the listener
886-- loop;
887--
888-- * Closed using 'closePending' if 'pendingPeer' is banned,
889-- 'pendingCaps' is prohibited or pendingTopic is unknown;
890--
891-- * Accepted using 'acceptWire' otherwise.
892--
893data PendingConnection = PendingConnection
894 { pendingSock :: Socket
895 , pendingPeer :: PeerAddr IP -- ^ 'peerId' is always non empty;
896 , pendingCaps :: Caps -- ^ advertised by the peer;
897 , pendingTopic :: InfoHash -- ^ possible non-existent topic.
898 }
899
900-- | Reconstruct handshake sent by the remote peer.
901pendingHandshake :: PendingConnection -> Handshake
902pendingHandshake PendingConnection {..} = Handshake
903 { hsProtocol = def
904 , hsReserved = pendingCaps
905 , hsInfoHash = pendingTopic
906 , hsPeerId = fromMaybe (error "pendingHandshake: impossible")
907 (peerId pendingPeer)
908 }
909
910-- |
911--
912-- This function can throw 'WireFailure' exception.
913--
914newPendingConnection :: Socket -> PeerAddr IP -> IO PendingConnection
915newPendingConnection sock addr = do
916 Handshake {..} <- recvHandshake sock
917 unless (hsProtocol == def) $ do
918 throwIO $ ProtocolError $ InvalidProtocol hsProtocol
919 return PendingConnection
920 { pendingSock = sock
921 , pendingPeer = addr { peerId = Just hsPeerId }
922 , pendingCaps = hsReserved
923 , pendingTopic = hsInfoHash
924 }
925
926-- | Release all resources associated with the given connection. Note
927-- that you /must not/ 'closePending' if you 'acceptWire'.
928closePending :: PendingConnection -> IO ()
929closePending PendingConnection {..} = do
930 close pendingSock
931
932{-----------------------------------------------------------------------
933-- Connection setup
934-----------------------------------------------------------------------}
935
936chanToSock :: Int -> Chan Message -> Socket -> IO ()
937chanToSock ka chan sock =
938 sourceChan ka chan $= conduitPut S.put C.$$ sinkSocket sock
939
940afterHandshaking :: ChannelSide -> PeerAddr IP -> Socket -> HandshakePair
941 -> ConnectionConfig s -> IO ()
942afterHandshaking initiator addr sock
943 hpair @ (HandshakePair hs hs')
944 (ConnectionConfig
945 { cfgPrefs = ConnectionPrefs {..}
946 , cfgSession = SessionLink {..}
947 , cfgWire = wire
948 }) = do
949 let caps = hsReserved hs <> hsReserved hs'
950 cstate <- newIORef def { _connStats = establishedStats hpair }
951 chan <- maybe newChan return linkOutputChan
952 let conn = Connection {
953 connInitiatedBy = initiator
954 , connRemoteAddr = addr
955 , connProtocol = hsProtocol hs
956 , connCaps = caps
957 , connTopic = hsInfoHash hs
958 , connRemotePeerId = hsPeerId hs'
959 , connThisPeerId = hsPeerId hs
960 , connOptions = def
961 , connState = cstate
962 , connSession = linkSession
963 , connChan = chan
964 }
965
966 -- TODO make KA interval configurable
967 let kaInterval = defaultKeepAliveInterval
968 wire' = if ExtExtended `allowed` caps
969 then extendedHandshake prefExtCaps >> wire
970 else wire
971
972 bracket (forkIO (chanToSock kaInterval chan sock))
973 (killThread)
974 (\ _ -> runWire wire' sock chan conn)
975
976-- | Initiate 'Wire' connection and handshake with a peer. This function will
977-- also do the BEP10 extension protocol handshake if 'ExtExtended' is enabled on
978-- both sides.
979--
980-- This function can throw 'WireFailure' exception.
981--
982connectWire :: PeerAddr IP -> ConnectionConfig s -> IO ()
983connectWire addr cfg = do
984 let catchRefusal m = try m >>= either (throwIO . ConnectionRefused) return
985 bracket (catchRefusal (peerSocket Stream addr)) close $ \ sock -> do
986 let hs = configHandshake cfg
987 hs' <- initiateHandshake sock hs
988 let hpair = HandshakePair hs hs'
989 validatePair hpair addr
990 afterHandshaking ThisPeer addr sock hpair cfg
991
992-- | Accept 'Wire' connection using already 'Network.Socket.accept'ed
993-- socket. For peer listener loop the 'acceptSafe' should be
994-- prefered against 'accept'. The socket will be closed at exit.
995--
996-- This function can throw 'WireFailure' exception.
997--
998acceptWire :: PendingConnection -> ConnectionConfig s -> IO ()
999acceptWire pc @ PendingConnection {..} cfg = do
1000 bracket (return pendingSock) close $ \ _ -> do
1001 unless (linkTopic (cfgSession cfg) == pendingTopic) $ do
1002 throwIO (ProtocolError (UnexpectedTopic pendingTopic))
1003
1004 let hs = configHandshake cfg
1005 sendHandshake pendingSock hs
1006 let hpair = HandshakePair hs (pendingHandshake pc)
1007
1008 afterHandshaking RemotePeer pendingPeer pendingSock hpair cfg
1009
1010-- | Used when size of bitfield becomes known.
1011resizeBitfield :: Int -> Connected s ()
1012resizeBitfield n = connBitfield %= adjustSize n
diff --git a/src/Network/BitTorrent/Exchange/Download.hs b/src/Network/BitTorrent/Exchange/Download.hs
new file mode 100644
index 00000000..aa754407
--- /dev/null
+++ b/src/Network/BitTorrent/Exchange/Download.hs
@@ -0,0 +1,296 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8--
9--
10{-# LANGUAGE FlexibleContexts #-}
11{-# LANGUAGE FlexibleInstances #-}
12{-# LANGUAGE MultiParamTypeClasses #-}
13{-# LANGUAGE FunctionalDependencies #-}
14{-# LANGUAGE TemplateHaskell #-}
15module Network.BitTorrent.Exchange.Download
16 ( -- * Downloading
17 Download (..)
18 , Updates
19 , runDownloadUpdates
20
21 -- ** Metadata
22 -- $metadata-download
23 , MetadataDownload
24 , metadataDownload
25
26 -- ** Content
27 -- $content-download
28 , ContentDownload
29 , contentDownload
30 ) where
31
32import Control.Applicative
33import Control.Concurrent
34import Control.Lens
35import Control.Monad.State
36import Data.BEncode as BE
37import Data.ByteString as BS
38import Data.ByteString.Lazy as BL
39import Data.Default
40import Data.List as L
41import Data.Maybe
42import Data.Map as M
43import Data.Tuple
44
45import Data.Torrent as Torrent
46import Network.BitTorrent.Address
47import Network.BitTorrent.Exchange.Bitfield as BF
48import Network.BitTorrent.Exchange.Block as Block
49import Network.BitTorrent.Exchange.Message as Msg
50import System.Torrent.Storage (Storage, writePiece)
51
52
53{-----------------------------------------------------------------------
54-- Class
55-----------------------------------------------------------------------}
56
57type Updates s a = StateT s IO a
58
59runDownloadUpdates :: MVar s -> Updates s a -> IO a
60runDownloadUpdates var m = modifyMVar var (fmap swap . runStateT m)
61
62class Download s chunk | s -> chunk where
63 scheduleBlocks :: Int -> PeerAddr IP -> Bitfield -> Updates s [BlockIx]
64
65 -- |
66 scheduleBlock :: PeerAddr IP -> Bitfield -> Updates s (Maybe BlockIx)
67 scheduleBlock addr bf = listToMaybe <$> scheduleBlocks 1 addr bf
68
69 -- | Get number of sent requests to this peer.
70 getRequestQueueLength :: PeerAddr IP -> Updates s Int
71
72 -- | Remove all pending block requests to the remote peer. May be used
73 -- when:
74 --
75 -- * a peer closes connection;
76 --
77 -- * remote peer choked this peer;
78 --
79 -- * timeout expired.
80 --
81 resetPending :: PeerAddr IP -> Updates s ()
82
83 -- | MAY write to storage, if a new piece have been completed.
84 --
85 -- You should check if a returned by peer block is actually have
86 -- been requested and in-flight. This is needed to avoid "I send
87 -- random corrupted block" attacks.
88 pushBlock :: PeerAddr IP -> chunk -> Updates s (Maybe Bool)
89
90{-----------------------------------------------------------------------
91-- Metadata download
92-----------------------------------------------------------------------}
93-- $metadata-download
94-- TODO
95
96data MetadataDownload = MetadataDownload
97 { _pendingPieces :: [(PeerAddr IP, PieceIx)]
98 , _bucket :: Bucket
99 , _topic :: InfoHash
100 }
101
102makeLenses ''MetadataDownload
103
104-- | Create a new scheduler for infodict of the given size.
105metadataDownload :: Int -> InfoHash -> MetadataDownload
106metadataDownload ps = MetadataDownload [] (Block.empty ps)
107
108instance Default MetadataDownload where
109 def = error "instance Default MetadataDownload"
110
111--cancelPending :: PieceIx -> Updates ()
112cancelPending pix = pendingPieces %= L.filter ((pix ==) . snd)
113
114instance Download MetadataDownload (Piece BS.ByteString) where
115 scheduleBlock addr bf = do
116 bkt <- use bucket
117 case spans metadataPieceSize bkt of
118 [] -> return Nothing
119 ((off, _ ) : _) -> do
120 let pix = off `div` metadataPieceSize
121 pendingPieces %= ((addr, pix) :)
122 return (Just (BlockIx pix 0 metadataPieceSize))
123
124 resetPending addr = pendingPieces %= L.filter ((addr ==) . fst)
125
126 pushBlock addr Torrent.Piece {..} = do
127 p <- use pendingPieces
128 when ((addr, pieceIndex) `L.notElem` p) $
129 error "not requested"
130 cancelPending pieceIndex
131
132 bucket %= Block.insert (metadataPieceSize * pieceIndex) pieceData
133 b <- use bucket
134 case toPiece b of
135 Nothing -> return Nothing
136 Just chunks -> do
137 t <- use topic
138 case parseInfoDict (BL.toStrict chunks) t of
139 Right x -> do
140 pendingPieces .= []
141 return undefined -- (Just x)
142 Left e -> do
143 pendingPieces .= []
144 bucket .= Block.empty (Block.size b)
145 return undefined -- Nothing
146 where
147 -- todo use incremental parsing to avoid BS.concat call
148 parseInfoDict :: BS.ByteString -> InfoHash -> Result InfoDict
149 parseInfoDict chunk topic =
150 case BE.decode chunk of
151 Right (infodict @ InfoDict {..})
152 | topic == idInfoHash -> return infodict
153 | otherwise -> Left "broken infodict"
154 Left err -> Left $ "unable to parse infodict " ++ err
155
156{-----------------------------------------------------------------------
157-- Content download
158-----------------------------------------------------------------------}
159-- $content-download
160--
161-- A block can have one of the following status:
162--
163-- 1) /not allowed/: Piece is not in download set.
164--
165-- 2) /waiting/: (allowed?) Block have been allowed to download,
166-- but /this/ peer did not send any 'Request' message for this
167-- block. To allow some piece use
168-- 'Network.BitTorrent.Exchange.Selector' and then 'allowedSet'
169-- and 'allowPiece'.
170--
171-- 3) /inflight/: (pending?) Block have been requested but
172-- /remote/ peer did not send any 'Piece' message for this block.
173-- Related functions 'markInflight'
174--
175-- 4) /pending/: (stalled?) Block have have been downloaded
176-- Related functions 'insertBlock'.
177--
178-- Piece status:
179--
180-- 1) /assembled/: (downloaded?) All blocks in piece have been
181-- downloaded but the piece did not verified yet.
182--
183-- * Valid: go to completed;
184--
185-- * Invalid: go to waiting.
186--
187-- 2) /corrupted/:
188--
189-- 3) /downloaded/: (verified?) A piece have been successfully
190-- verified via the hash. Usually the piece should be stored to
191-- the 'System.Torrent.Storage' and /this/ peer should send 'Have'
192-- messages to the /remote/ peers.
193--
194
195data PieceEntry = PieceEntry
196 { pending :: [(PeerAddr IP, BlockIx)]
197 , stalled :: Bucket
198 }
199
200pieceEntry :: PieceSize -> PieceEntry
201pieceEntry s = PieceEntry [] (Block.empty s)
202
203isEmpty :: PieceEntry -> Bool
204isEmpty PieceEntry {..} = L.null pending && Block.null stalled
205
206_holes :: PieceIx -> PieceEntry -> [BlockIx]
207_holes pix PieceEntry {..} = fmap mkBlockIx (spans defaultTransferSize stalled)
208 where
209 mkBlockIx (off, sz) = BlockIx pix off sz
210
211data ContentDownload = ContentDownload
212 { inprogress :: !(Map PieceIx PieceEntry)
213 , bitfield :: !Bitfield
214 , pieceSize :: !PieceSize
215 , contentStorage :: Storage
216 }
217
218contentDownload :: Bitfield -> PieceSize -> Storage -> ContentDownload
219contentDownload = ContentDownload M.empty
220
221--modifyEntry :: PieceIx -> (PieceEntry -> PieceEntry) -> DownloadUpdates ()
222modifyEntry pix f = modify $ \ s @ ContentDownload {..} -> s
223 { inprogress = alter (g pieceSize) pix inprogress }
224 where
225 g s = h . f . fromMaybe (pieceEntry s)
226 h e
227 | isEmpty e = Nothing
228 | otherwise = Just e
229
230instance Download ContentDownload (Block BL.ByteString) where
231 scheduleBlocks n addr maskBF = do
232 ContentDownload {..} <- get
233 let wantPieces = maskBF `BF.difference` bitfield
234 let wantBlocks = L.concat $ M.elems $ M.mapWithKey _holes $
235 M.filterWithKey (\ pix _ -> pix `BF.member` wantPieces)
236 inprogress
237
238 bixs <- if L.null wantBlocks
239 then do
240 mpix <- choosePiece wantPieces
241 case mpix of -- TODO return 'n' blocks
242 Nothing -> return []
243 Just pix -> return [leadingBlock pix defaultTransferSize]
244 else chooseBlocks wantBlocks n
245
246 forM_ bixs $ \ bix -> do
247 modifyEntry (ixPiece bix) $ \ e @ PieceEntry {..} -> e
248 { pending = (addr, bix) : pending }
249
250 return bixs
251 where
252 -- TODO choose block nearest to pending or stalled sets to reduce disk
253 -- seeks on remote machines
254 --chooseBlocks :: [BlockIx] -> Int -> DownloadUpdates [BlockIx]
255 chooseBlocks xs n = return (L.take n xs)
256
257 -- TODO use selection strategies from Exchange.Selector
258 --choosePiece :: Bitfield -> DownloadUpdates (Maybe PieceIx)
259 choosePiece bf
260 | BF.null bf = return $ Nothing
261 | otherwise = return $ Just $ BF.findMin bf
262
263 getRequestQueueLength addr = do
264 m <- gets (M.map (L.filter ((==) addr . fst) . pending) . inprogress)
265 return $ L.sum $ L.map L.length $ M.elems m
266
267 resetPending addr = modify $ \ s -> s { inprogress = reset (inprogress s) }
268 where
269 reset = fmap $ \ e -> e
270 { pending = L.filter (not . (==) addr . fst) (pending e) }
271
272 pushBlock addr blk @ Block {..} = do
273 mpe <- gets (M.lookup blkPiece . inprogress)
274 case mpe of
275 Nothing -> return Nothing
276 Just (pe @ PieceEntry {..})
277 | blockIx blk `L.notElem` fmap snd pending -> return Nothing
278 | otherwise -> do
279 let bkt' = Block.insertLazy blkOffset blkData stalled
280 case toPiece bkt' of
281 Nothing -> do
282 modifyEntry blkPiece $ \ e @ PieceEntry {..} -> e
283 { pending = L.filter ((==) (blockIx blk) . snd) pending
284 , stalled = bkt'
285 }
286 return (Just False)
287
288 Just pieceData -> do
289 -- TODO verify
290 storage <- gets contentStorage
291 liftIO $ writePiece (Torrent.Piece blkPiece pieceData) storage
292 modify $ \ s @ ContentDownload {..} -> s
293 { inprogress = M.delete blkPiece inprogress
294 , bitfield = BF.insert blkPiece bitfield
295 }
296 return (Just True)
diff --git a/src/Network/BitTorrent/Exchange/Manager.hs b/src/Network/BitTorrent/Exchange/Manager.hs
new file mode 100644
index 00000000..54727805
--- /dev/null
+++ b/src/Network/BitTorrent/Exchange/Manager.hs
@@ -0,0 +1,62 @@
1module Network.BitTorrent.Exchange.Manager
2 ( Options (..)
3 , Manager
4 , Handler
5 , newManager
6 , closeManager
7 ) where
8
9import Control.Concurrent
10import Control.Exception hiding (Handler)
11import Control.Monad
12import Data.Default
13import Network.Socket
14
15import Data.Torrent
16import Network.BitTorrent.Address
17import Network.BitTorrent.Exchange.Connection hiding (Options)
18import Network.BitTorrent.Exchange.Session
19
20
21data Options = Options
22 { optBacklog :: Int
23 , optPeerAddr :: PeerAddr IP
24 } deriving (Show, Eq)
25
26instance Default Options where
27 def = Options
28 { optBacklog = maxListenQueue
29 , optPeerAddr = def
30 }
31
32data Manager = Manager
33 { listener :: !ThreadId
34 }
35
36type Handler = InfoHash -> IO Session
37
38handleNewConn :: Socket -> PeerAddr IP -> Handler -> IO ()
39handleNewConn sock addr handler = do
40 conn <- newPendingConnection sock addr
41 ses <- handler (pendingTopic conn) `onException` closePending conn
42 establish conn ses
43
44listenIncoming :: Options -> Handler -> IO ()
45listenIncoming Options {..} handler = do
46 bracket (socket AF_INET Stream defaultProtocol) close $ \ sock -> do
47 bind sock (toSockAddr optPeerAddr)
48 listen sock optBacklog
49 forever $ do
50 (conn, sockAddr) <- accept sock
51 case fromSockAddr sockAddr of
52 Nothing -> return ()
53 Just addr -> void $ forkIO $ handleNewConn sock addr handler
54
55newManager :: Options -> Handler -> IO Manager
56newManager opts handler = do
57 tid <- forkIO $ listenIncoming opts handler
58 return (Manager tid)
59
60closeManager :: Manager -> IO ()
61closeManager Manager {..} = do
62 killThread listener \ No newline at end of file
diff --git a/src/Network/BitTorrent/Exchange/Message.hs b/src/Network/BitTorrent/Exchange/Message.hs
new file mode 100644
index 00000000..74232b47
--- /dev/null
+++ b/src/Network/BitTorrent/Exchange/Message.hs
@@ -0,0 +1,1232 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Normally peer to peer communication consisting of the following
9-- steps:
10--
11-- * In order to establish the connection between peers we should
12-- send 'Handshake' message. The 'Handshake' is a required message
13-- and must be the first message transmitted by the peer to the
14-- another peer. Another peer should reply with a handshake as well.
15--
16-- * Next peer might sent bitfield message, but might not. In the
17-- former case we should update bitfield peer have. Again, if we
18-- have some pieces we should send bitfield. Normally bitfield
19-- message should sent after the handshake message.
20--
21-- * Regular exchange messages. TODO docs
22--
23-- For more high level API see "Network.BitTorrent.Exchange" module.
24--
25-- For more infomation see:
26-- <https://wiki.theory.org/BitTorrentSpecification#Peer_wire_protocol_.28TCP.29>
27--
28{-# LANGUAGE ViewPatterns #-}
29{-# LANGUAGE FlexibleInstances #-}
30{-# LANGUAGE FlexibleContexts #-}
31{-# LANGUAGE TypeFamilies #-}
32{-# LANGUAGE GeneralizedNewtypeDeriving #-}
33{-# LANGUAGE DeriveDataTypeable #-}
34{-# LANGUAGE TemplateHaskell #-}
35{-# OPTIONS -fno-warn-orphans #-}
36module Network.BitTorrent.Exchange.Message
37 ( -- * Capabilities
38 Capabilities (..)
39 , Extension (..)
40 , Caps
41
42 -- * Handshake
43 , ProtocolName
44 , Handshake(..)
45 , defaultHandshake
46 , handshakeSize
47 , handshakeMaxSize
48 , handshakeStats
49
50 -- * Stats
51 , ByteCount
52 , ByteStats (..)
53 , byteLength
54
55 -- * Messages
56 , Message (..)
57 , defaultKeepAliveTimeout
58 , defaultKeepAliveInterval
59 , PeerMessage (..)
60
61 -- ** Core messages
62 , StatusUpdate (..)
63 , Available (..)
64 , Transfer (..)
65 , defaultRequestQueueLength
66
67 -- ** Fast extension
68 , FastMessage (..)
69
70 -- ** Extension protocol
71 , ExtendedMessage (..)
72
73 -- *** Capabilities
74 , ExtendedExtension (..)
75 , ExtendedCaps (..)
76
77 -- *** Handshake
78 , ExtendedHandshake (..)
79 , defaultQueueLength
80 , nullExtendedHandshake
81
82 -- *** Metadata
83 , ExtendedMetadata (..)
84 , metadataPieceSize
85 , defaultMetadataFactor
86 , defaultMaxInfoDictSize
87 , isLastPiece
88 , isValidPiece
89 ) where
90
91import Control.Applicative
92import Control.Arrow ((&&&), (***))
93import Control.Monad (when)
94import Data.Attoparsec.ByteString.Char8 as BS
95import Data.BEncode as BE
96import Data.BEncode.BDict as BE
97import Data.BEncode.Internal as BE (ppBEncode, parser)
98import Data.BEncode.Types (BDict)
99import Data.Bits
100import Data.ByteString as BS
101import Data.ByteString.Char8 as BC
102import Data.ByteString.Lazy as BL
103import Data.Default
104import Data.List as L
105import Data.Map.Strict as M
106import Data.Maybe
107import Data.Monoid
108import Data.Ord
109import Data.Serialize as S
110import Data.String
111import Data.Text as T
112import Data.Typeable
113import Data.Word
114import Data.IP
115import Network
116import Network.Socket hiding (KeepAlive)
117import Text.PrettyPrint as PP hiding ((<>))
118import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
119
120import Data.Torrent hiding (Piece (..))
121import qualified Data.Torrent as P (Piece (..))
122import Network.BitTorrent.Address
123import Network.BitTorrent.Exchange.Bitfield
124import Network.BitTorrent.Exchange.Block
125
126{-----------------------------------------------------------------------
127-- Capabilities
128-----------------------------------------------------------------------}
129
130-- |
131class Capabilities caps where
132 type Ext caps :: *
133
134 -- | Pack extensions to caps.
135 toCaps :: [Ext caps] -> caps
136
137 -- | Unpack extensions from caps.
138 fromCaps :: caps -> [Ext caps]
139
140 -- | Check if an extension is a member of the specified set.
141 allowed :: Ext caps -> caps -> Bool
142
143ppCaps :: Capabilities caps => Pretty (Ext caps) => caps -> Doc
144ppCaps = hcat . punctuate ", " . L.map pPrint . fromCaps
145
146{-----------------------------------------------------------------------
147-- Extensions
148-----------------------------------------------------------------------}
149
150-- | Enumeration of message extension protocols.
151--
152-- For more info see: <http://www.bittorrent.org/beps/bep_0004.html>
153--
154data Extension
155 = ExtDHT -- ^ BEP 5: allow to send PORT messages.
156 | ExtFast -- ^ BEP 6: allow to send FAST messages.
157 | ExtExtended -- ^ BEP 10: allow to send the extension protocol messages.
158 deriving (Show, Eq, Ord, Enum, Bounded)
159
160-- | Full extension names, suitable for logging.
161instance Pretty Extension where
162 pPrint ExtDHT = "Distributed Hash Table Protocol"
163 pPrint ExtFast = "Fast Extension"
164 pPrint ExtExtended = "Extension Protocol"
165
166-- | Extension bitmask as specified by BEP 4.
167extMask :: Extension -> Word64
168extMask ExtDHT = 0x01
169extMask ExtFast = 0x04
170extMask ExtExtended = 0x100000
171
172{-----------------------------------------------------------------------
173-- Capabilities
174-----------------------------------------------------------------------}
175
176-- | Capabilities is a set of 'Extension's usually sent in 'Handshake'
177-- messages.
178newtype Caps = Caps Word64
179 deriving (Show, Eq)
180
181-- | Render set of extensions as comma separated list.
182instance Pretty Caps where
183 pPrint = ppCaps
184 {-# INLINE pPrint #-}
185
186-- | The empty set.
187instance Default Caps where
188 def = Caps 0
189 {-# INLINE def #-}
190
191-- | Monoid under intersection. 'mempty' includes all known extensions.
192instance Monoid Caps where
193 mempty = toCaps [minBound .. maxBound]
194 {-# INLINE mempty #-}
195
196 mappend (Caps a) (Caps b) = Caps (a .&. b)
197 {-# INLINE mappend #-}
198
199-- | 'Handshake' compatible encoding.
200instance Serialize Caps where
201 put (Caps caps) = S.putWord64be caps
202 {-# INLINE put #-}
203
204 get = Caps <$> S.getWord64be
205 {-# INLINE get #-}
206
207instance Capabilities Caps where
208 type Ext Caps = Extension
209
210 allowed e (Caps caps) = (extMask e .&. caps) /= 0
211 {-# INLINE allowed #-}
212
213 toCaps = Caps . L.foldr (.|.) 0 . L.map extMask
214 fromCaps caps = L.filter (`allowed` caps) [minBound..maxBound]
215
216{-----------------------------------------------------------------------
217 Handshake
218-----------------------------------------------------------------------}
219
220maxProtocolNameSize :: Word8
221maxProtocolNameSize = maxBound
222
223-- | The protocol name is used to identify to the local peer which
224-- version of BTP the remote peer uses.
225newtype ProtocolName = ProtocolName BS.ByteString
226 deriving (Eq, Ord, Typeable)
227
228-- | In BTP/1.0 the name is 'BitTorrent protocol'. If this string is
229-- different from the local peers own protocol name, then the
230-- connection is to be dropped.
231instance Default ProtocolName where
232 def = ProtocolName "BitTorrent protocol"
233
234instance Show ProtocolName where
235 show (ProtocolName bs) = show bs
236
237instance Pretty ProtocolName where
238 pPrint (ProtocolName bs) = PP.text $ BC.unpack bs
239
240instance IsString ProtocolName where
241 fromString str
242 | L.length str <= fromIntegral maxProtocolNameSize
243 = ProtocolName (fromString str)
244 | otherwise = error $ "fromString: ProtocolName too long: " ++ str
245
246instance Serialize ProtocolName where
247 put (ProtocolName bs) = do
248 putWord8 $ fromIntegral $ BS.length bs
249 putByteString bs
250
251 get = do
252 len <- getWord8
253 bs <- getByteString $ fromIntegral len
254 return (ProtocolName bs)
255
256-- | Handshake message is used to exchange all information necessary
257-- to establish connection between peers.
258--
259data Handshake = Handshake {
260 -- | Identifier of the protocol. This is usually equal to 'def'.
261 hsProtocol :: ProtocolName
262
263 -- | Reserved bytes used to specify supported BEP's.
264 , hsReserved :: Caps
265
266 -- | Info hash of the info part of the metainfo file. that is
267 -- transmitted in tracker requests. Info hash of the initiator
268 -- handshake and response handshake should match, otherwise
269 -- initiator should break the connection.
270 --
271 , hsInfoHash :: InfoHash
272
273 -- | Peer id of the initiator. This is usually the same peer id
274 -- that is transmitted in tracker requests.
275 --
276 , hsPeerId :: PeerId
277
278 } deriving (Show, Eq)
279
280instance Serialize Handshake where
281 put Handshake {..} = do
282 put hsProtocol
283 put hsReserved
284 put hsInfoHash
285 put hsPeerId
286 get = Handshake <$> get <*> get <*> get <*> get
287
288-- | Show handshake protocol string, caps and fingerprint.
289instance Pretty Handshake where
290 pPrint Handshake {..}
291 = pPrint hsProtocol $$
292 pPrint hsReserved $$
293 pPrint (fingerprint hsPeerId)
294
295-- | Get handshake message size in bytes from the length of protocol
296-- string.
297handshakeSize :: Word8 -> Int
298handshakeSize n = 1 + fromIntegral n + 8 + 20 + 20
299
300-- | Maximum size of handshake message in bytes.
301handshakeMaxSize :: Int
302handshakeMaxSize = handshakeSize maxProtocolNameSize
303
304-- | Handshake with default protocol string and reserved bitmask.
305defaultHandshake :: InfoHash -> PeerId -> Handshake
306defaultHandshake = Handshake def def
307
308handshakeStats :: Handshake -> ByteStats
309handshakeStats (Handshake (ProtocolName bs) _ _ _)
310 = ByteStats 1 (BS.length bs + 8 + 20 + 20) 0
311
312{-----------------------------------------------------------------------
313-- Stats
314-----------------------------------------------------------------------}
315
316-- | Number of bytes.
317type ByteCount = Int
318
319-- | Summary of encoded message byte layout can be used to collect
320-- stats about message flow in both directions. This data can be
321-- retrieved using 'stats' function.
322data ByteStats = ByteStats
323 { -- | Number of bytes used to help encode 'control' and 'payload'
324 -- bytes: message size, message ID's, etc
325 overhead :: {-# UNPACK #-} !ByteCount
326
327 -- | Number of bytes used to exchange peers state\/options: piece
328 -- and block indexes, infohash, port numbers, peer ID\/IP, etc.
329 , control :: {-# UNPACK #-} !ByteCount
330
331 -- | Number of payload bytes: torrent data blocks and infodict
332 -- metadata.
333 , payload :: {-# UNPACK #-} !ByteCount
334 } deriving Show
335
336instance Pretty ByteStats where
337 pPrint s @ ByteStats {..} = fsep
338 [ PP.int overhead, "overhead"
339 , PP.int control, "control"
340 , PP.int payload, "payload"
341 , "bytes"
342 ] $+$ fsep
343 [ PP.int (byteLength s), "total bytes"
344 ]
345
346-- | Empty byte sequences.
347instance Default ByteStats where
348 def = ByteStats 0 0 0
349
350-- | Monoid under addition.
351instance Monoid ByteStats where
352 mempty = def
353 mappend a b = ByteStats
354 { overhead = overhead a + overhead b
355 , control = control a + control b
356 , payload = payload a + payload b
357 }
358
359-- | Sum of the all byte sequences.
360byteLength :: ByteStats -> Int
361byteLength ByteStats {..} = overhead + control + payload
362
363{-----------------------------------------------------------------------
364-- Regular messages
365-----------------------------------------------------------------------}
366
367-- | Messages which can be sent after handshaking. Minimal complete
368-- definition: 'envelop'.
369class PeerMessage a where
370 -- | Construct a message to be /sent/. Note that if 'ExtendedCaps'
371 -- do not contain mapping for this message the default
372 -- 'ExtendedMessageId' is used.
373 envelop :: ExtendedCaps -- ^ The /receiver/ extended capabilities;
374 -> a -- ^ An regular message;
375 -> Message -- ^ Enveloped message to sent.
376
377 -- | Find out the extension this message belong to. Can be used to
378 -- check if this message is allowed to send\/recv in current
379 -- session.
380 requires :: a -> Maybe Extension
381 requires _ = Nothing
382
383 -- | Get sizes of overhead\/control\/payload byte sequences of
384 -- binary message representation without encoding message to binary
385 -- bytestring.
386 --
387 -- This function should obey one law:
388 --
389 -- * 'byteLength' ('stats' msg) == 'BL.length' ('encode' msg)
390 --
391 stats :: a -> ByteStats
392 stats _ = ByteStats 4 0 0
393
394{-----------------------------------------------------------------------
395-- Status messages
396-----------------------------------------------------------------------}
397
398-- | Notification that the sender have updated its
399-- 'Network.BitTorrent.Exchange.Status.PeerStatus'.
400data StatusUpdate
401 -- | Notification that the sender will not upload data to the
402 -- receiver until unchoking happen.
403 = Choking !Bool
404
405 -- | Notification that the sender is interested (or not interested)
406 -- in any of the receiver's data pieces.
407 | Interested !Bool
408 deriving (Show, Eq, Ord, Typeable)
409
410instance Pretty StatusUpdate where
411 pPrint (Choking False) = "not choking"
412 pPrint (Choking True ) = "choking"
413 pPrint (Interested False) = "not interested"
414 pPrint (Interested True ) = "interested"
415
416instance PeerMessage StatusUpdate where
417 envelop _ = Status
418 {-# INLINE envelop #-}
419
420 stats _ = ByteStats 4 1 0
421 {-# INLINE stats #-}
422
423{-----------------------------------------------------------------------
424-- Available messages
425-----------------------------------------------------------------------}
426
427-- | Messages used to inform receiver which pieces of the torrent
428-- sender have.
429data Available =
430 -- | Zero-based index of a piece that has just been successfully
431 -- downloaded and verified via the hash.
432 Have ! PieceIx
433
434 -- | The bitfield message may only be sent immediately after the
435 -- handshaking sequence is complete, and before any other message
436 -- are sent. If client have no pieces then bitfield need not to be
437 -- sent.
438 | Bitfield !Bitfield
439 deriving (Show, Eq)
440
441instance Pretty Available where
442 pPrint (Have ix ) = "Have" <+> int ix
443 pPrint (Bitfield _ ) = "Bitfield"
444
445instance PeerMessage Available where
446 envelop _ = Available
447 {-# INLINE envelop #-}
448
449 stats (Have _) = ByteStats (4 + 1) 4 0
450 stats (Bitfield bf) = ByteStats (4 + 1) (q + trailing) 0
451 where
452 trailing = if r == 0 then 0 else 1
453 (q, r) = quotRem (totalCount bf) 8
454
455{-----------------------------------------------------------------------
456-- Transfer messages
457-----------------------------------------------------------------------}
458
459-- | Messages used to transfer 'Block's.
460data Transfer
461 -- | Request for a particular block. If a client is requested a
462 -- block that another peer do not have the peer might not answer
463 -- at all.
464 = Request ! BlockIx
465
466 -- | Response to a request for a block.
467 | Piece !(Block BL.ByteString)
468
469 -- | Used to cancel block requests. It is typically used during
470 -- "End Game".
471 | Cancel !BlockIx
472 deriving (Show, Eq)
473
474instance Pretty Transfer where
475 pPrint (Request ix ) = "Request" <+> pPrint ix
476 pPrint (Piece blk) = "Piece" <+> pPrint blk
477 pPrint (Cancel i ) = "Cancel" <+> pPrint i
478
479instance PeerMessage Transfer where
480 envelop _ = Transfer
481 {-# INLINE envelop #-}
482
483 stats (Request _ ) = ByteStats (4 + 1) (3 * 4) 0
484 stats (Piece p ) = ByteStats (4 + 1) (4 + 4 + blockSize p) 0
485 stats (Cancel _ ) = ByteStats (4 + 1) (3 * 4) 0
486
487-- TODO increase
488-- | Max number of pending 'Request's inflight.
489defaultRequestQueueLength :: Int
490defaultRequestQueueLength = 1
491
492{-----------------------------------------------------------------------
493-- Fast messages
494-----------------------------------------------------------------------}
495
496-- | BEP6 messages.
497data FastMessage =
498 -- | If a peer have all pieces it might send the 'HaveAll' message
499 -- instead of 'Bitfield' message. Used to save bandwidth.
500 HaveAll
501
502 -- | If a peer have no pieces it might send 'HaveNone' message
503 -- intead of 'Bitfield' message. Used to save bandwidth.
504 | HaveNone
505
506 -- | This is an advisory message meaning "you might like to
507 -- download this piece." Used to avoid excessive disk seeks and
508 -- amount of IO.
509 | SuggestPiece !PieceIx
510
511 -- | Notifies a requesting peer that its request will not be
512 -- satisfied.
513 | RejectRequest !BlockIx
514
515 -- | This is an advisory messsage meaning \"if you ask for this
516 -- piece, I'll give it to you even if you're choked.\" Used to
517 -- shorten starting phase.
518 | AllowedFast !PieceIx
519 deriving (Show, Eq)
520
521instance Pretty FastMessage where
522 pPrint (HaveAll ) = "Have all"
523 pPrint (HaveNone ) = "Have none"
524 pPrint (SuggestPiece pix) = "Suggest" <+> int pix
525 pPrint (RejectRequest bix) = "Reject" <+> pPrint bix
526 pPrint (AllowedFast pix) = "Allowed fast" <+> int pix
527
528instance PeerMessage FastMessage where
529 envelop _ = Fast
530 {-# INLINE envelop #-}
531
532 requires _ = Just ExtFast
533 {-# INLINE requires #-}
534
535 stats HaveAll = ByteStats 4 1 0
536 stats HaveNone = ByteStats 4 1 0
537 stats (SuggestPiece _) = ByteStats 5 4 0
538 stats (RejectRequest _) = ByteStats 5 12 0
539 stats (AllowedFast _) = ByteStats 5 4 0
540
541{-----------------------------------------------------------------------
542-- Extension protocol
543-----------------------------------------------------------------------}
544
545{-----------------------------------------------------------------------
546-- Extended capabilities
547-----------------------------------------------------------------------}
548
549data ExtendedExtension
550 = ExtMetadata -- ^ BEP 9: Extension for Peers to Send Metadata Files
551 deriving (Show, Eq, Ord, Enum, Bounded, Typeable)
552
553instance IsString ExtendedExtension where
554 fromString = fromMaybe (error msg) . fromKey . fromString
555 where
556 msg = "fromString: could not parse ExtendedExtension"
557
558instance Pretty ExtendedExtension where
559 pPrint ExtMetadata = "Extension for Peers to Send Metadata Files"
560
561fromKey :: BKey -> Maybe ExtendedExtension
562fromKey "ut_metadata" = Just ExtMetadata
563fromKey _ = Nothing
564{-# INLINE fromKey #-}
565
566toKey :: ExtendedExtension -> BKey
567toKey ExtMetadata = "ut_metadata"
568{-# INLINE toKey #-}
569
570type ExtendedMessageId = Word8
571
572extId :: ExtendedExtension -> ExtendedMessageId
573extId ExtMetadata = 1
574{-# INLINE extId #-}
575
576type ExtendedMap = Map ExtendedExtension ExtendedMessageId
577
578-- | The extension IDs must be stored for every peer, because every
579-- peer may have different IDs for the same extension.
580--
581newtype ExtendedCaps = ExtendedCaps { extendedCaps :: ExtendedMap }
582 deriving (Show, Eq)
583
584instance Pretty ExtendedCaps where
585 pPrint = ppCaps
586 {-# INLINE pPrint #-}
587
588-- | The empty set.
589instance Default ExtendedCaps where
590 def = ExtendedCaps M.empty
591
592-- | Monoid under intersection:
593--
594-- * The 'mempty' caps includes all known extensions;
595--
596-- * the 'mappend' operation is NOT commutative: it return message
597-- id from the first caps for the extensions existing in both caps.
598--
599instance Monoid ExtendedCaps where
600 mempty = toCaps [minBound..maxBound]
601 mappend (ExtendedCaps a) (ExtendedCaps b) =
602 ExtendedCaps (M.intersection a b)
603
604appendBDict :: BDict -> ExtendedMap -> ExtendedMap
605appendBDict (Cons key val xs) caps
606 | Just ext <- fromKey key
607 , Right eid <- fromBEncode val = M.insert ext eid (appendBDict xs caps)
608 | otherwise = appendBDict xs caps
609appendBDict Nil caps = caps
610
611-- | Handshake compatible encoding.
612instance BEncode ExtendedCaps where
613 toBEncode = BDict . BE.fromAscList . L.sortBy (comparing fst)
614 . L.map (toKey *** toBEncode) . M.toList . extendedCaps
615
616 fromBEncode (BDict bd) = pure $ ExtendedCaps $ appendBDict bd M.empty
617 fromBEncode _ = decodingError "ExtendedCaps"
618
619instance Capabilities ExtendedCaps where
620 type Ext ExtendedCaps = ExtendedExtension
621
622 toCaps = ExtendedCaps . M.fromList . L.map (id &&& extId)
623
624 fromCaps = M.keys . extendedCaps
625 {-# INLINE fromCaps #-}
626
627 allowed e (ExtendedCaps caps) = M.member e caps
628 {-# INLINE allowed #-}
629
630remoteMessageId :: ExtendedExtension -> ExtendedCaps -> ExtendedMessageId
631remoteMessageId ext = fromMaybe (extId ext) . M.lookup ext . extendedCaps
632
633{-----------------------------------------------------------------------
634-- Extended handshake
635-----------------------------------------------------------------------}
636
637-- | This message should be sent immediately after the standard
638-- bittorrent handshake to any peer that supports this extension
639-- protocol. Extended handshakes can be sent more than once, however
640-- an implementation may choose to ignore subsequent handshake
641-- messages.
642--
643data ExtendedHandshake = ExtendedHandshake
644 { -- | If this peer has an IPv4 interface, this is the compact
645 -- representation of that address.
646 ehsIPv4 :: Maybe HostAddress
647
648 -- | If this peer has an IPv6 interface, this is the compact
649 -- representation of that address.
650 , ehsIPv6 :: Maybe HostAddress6
651
652 -- | Dictionary of supported extension messages which maps names
653 -- of extensions to an extended message ID for each extension
654 -- message.
655 , ehsCaps :: ExtendedCaps
656
657 -- | Size of 'Data.Torrent.InfoDict' in bytes. This field should
658 -- be added if 'ExtMetadata' is enabled in current session /and/
659 -- peer have the torrent file.
660 , ehsMetadataSize :: Maybe Int
661
662 -- | Local TCP /listen/ port. Allows each side to learn about the
663 -- TCP port number of the other side.
664 , ehsPort :: Maybe PortNumber
665
666 -- | Request queue the number of outstanding 'Request' messages
667 -- this client supports without dropping any.
668 , ehsQueueLength :: Maybe Int
669
670 -- | Client name and version.
671 , ehsVersion :: Maybe Text
672
673 -- | IP of the remote end
674 , ehsYourIp :: Maybe IP
675 } deriving (Show, Eq, Typeable)
676
677extHandshakeId :: ExtendedMessageId
678extHandshakeId = 0
679
680-- | Default 'Request' queue size.
681defaultQueueLength :: Int
682defaultQueueLength = 1
683
684-- | All fields are empty.
685instance Default ExtendedHandshake where
686 def = ExtendedHandshake def def def def def def def def
687
688instance Monoid ExtendedHandshake where
689 mempty = def { ehsCaps = mempty }
690 mappend old new = ExtendedHandshake {
691 ehsCaps = ehsCaps old <> ehsCaps new,
692 ehsIPv4 = ehsIPv4 old `mergeOld` ehsIPv4 new,
693 ehsIPv6 = ehsIPv6 old `mergeOld` ehsIPv6 new,
694 ehsMetadataSize = ehsMetadataSize old `mergeNew` ehsMetadataSize new,
695 ehsPort = ehsPort old `mergeOld` ehsPort new,
696 ehsQueueLength = ehsQueueLength old `mergeNew` ehsQueueLength new,
697 ehsVersion = ehsVersion old `mergeOld` ehsVersion new,
698 ehsYourIp = ehsYourIp old `mergeOld` ehsYourIp new
699 }
700 where
701 mergeOld mold mnew = mold <|> mnew
702 mergeNew mold mnew = mnew <|> mold
703
704
705instance BEncode ExtendedHandshake where
706 toBEncode ExtendedHandshake {..} = toDict $
707 "ipv4" .=? (S.encode <$> ehsIPv4)
708 .: "ipv6" .=? (S.encode <$> ehsIPv6)
709 .: "m" .=! ehsCaps
710 .: "metadata_size" .=? ehsMetadataSize
711 .: "p" .=? ehsPort
712 .: "reqq" .=? ehsQueueLength
713 .: "v" .=? ehsVersion
714 .: "yourip" .=? (runPut <$> either put put <$> toEither <$> ehsYourIp)
715 .: endDict
716 where
717 toEither (IPv4 v4) = Left v4
718 toEither (IPv6 v6) = Right v6
719
720 fromBEncode = fromDict $ ExtendedHandshake
721 <$>? "ipv4"
722 <*>? "ipv6"
723 <*>! "m"
724 <*>? "metadata_size"
725 <*>? "p"
726 <*>? "reqq"
727 <*>? "v"
728 <*> (opt "yourip" >>= getYourIp)
729
730getYourIp :: Maybe BValue -> BE.Get (Maybe IP)
731getYourIp f =
732 return $ do
733 BString ip <- f
734 either (const Nothing) Just $
735 case BS.length ip of
736 4 -> IPv4 <$> S.decode ip
737 16 -> IPv6 <$> S.decode ip
738 _ -> fail ""
739
740instance Pretty ExtendedHandshake where
741 pPrint = PP.text . show
742
743-- | NOTE: Approximated 'stats'.
744instance PeerMessage ExtendedHandshake where
745 envelop c = envelop c . EHandshake
746 {-# INLINE envelop #-}
747
748 requires _ = Just ExtExtended
749 {-# INLINE requires #-}
750
751 stats _ = ByteStats (4 + 1 + 1) 100 {- is it ok? -} 0 -- FIXME
752 {-# INLINE stats #-}
753
754-- | Set default values and the specified 'ExtendedCaps'.
755nullExtendedHandshake :: ExtendedCaps -> ExtendedHandshake
756nullExtendedHandshake caps = ExtendedHandshake
757 { ehsIPv4 = Nothing
758 , ehsIPv6 = Nothing
759 , ehsCaps = caps
760 , ehsMetadataSize = Nothing
761 , ehsPort = Nothing
762 , ehsQueueLength = Just defaultQueueLength
763 , ehsVersion = Just $ T.pack $ render $ pPrint libFingerprint
764 , ehsYourIp = Nothing
765 }
766
767{-----------------------------------------------------------------------
768-- Metadata exchange extension
769-----------------------------------------------------------------------}
770
771-- | A peer MUST verify that any piece it sends passes the info-hash
772-- verification. i.e. until the peer has the entire metadata, it
773-- cannot run SHA-1 to verify that it yields the same hash as the
774-- info-hash.
775--
776data ExtendedMetadata
777 -- | This message requests the a specified metadata piece. The
778 -- response to this message, from a peer supporting the extension,
779 -- is either a 'MetadataReject' or a 'MetadataData' message.
780 = MetadataRequest PieceIx
781
782 -- | If sender requested a valid 'PieceIx' and receiver have the
783 -- corresponding piece then receiver should respond with this
784 -- message.
785 | MetadataData
786 { -- | A piece of 'Data.Torrent.InfoDict'.
787 piece :: P.Piece BS.ByteString
788
789 -- | This key has the same semantics as the 'ehsMetadataSize' in
790 -- the 'ExtendedHandshake' — it is size of the torrent info
791 -- dict.
792 , totalSize :: Int
793 }
794
795 -- | Peers that do not have the entire metadata MUST respond with
796 -- a reject message to any metadata request.
797 --
798 -- Clients MAY implement flood protection by rejecting request
799 -- messages after a certain number of them have been
800 -- served. Typically the number of pieces of metadata times a
801 -- factor.
802 | MetadataReject PieceIx
803
804 -- | Reserved. By specification we should ignore unknown metadata
805 -- messages.
806 | MetadataUnknown BValue
807 deriving (Show, Eq, Typeable)
808
809-- | Extended metadata message id used in 'msg_type_key'.
810type MetadataId = Int
811
812msg_type_key, piece_key, total_size_key :: BKey
813msg_type_key = "msg_type"
814piece_key = "piece"
815total_size_key = "total_size"
816
817-- | BEP9 compatible encoding.
818instance BEncode ExtendedMetadata where
819 toBEncode (MetadataRequest pix) = toDict $
820 msg_type_key .=! (0 :: MetadataId)
821 .: piece_key .=! pix
822 .: endDict
823 toBEncode (MetadataData (P.Piece pix _) totalSize) = toDict $
824 msg_type_key .=! (1 :: MetadataId)
825 .: piece_key .=! pix
826 .: total_size_key .=! totalSize
827 .: endDict
828 toBEncode (MetadataReject pix) = toDict $
829 msg_type_key .=! (2 :: MetadataId)
830 .: piece_key .=! pix
831 .: endDict
832 toBEncode (MetadataUnknown bval) = bval
833
834 fromBEncode bval = (`fromDict` bval) $ do
835 mid <- field $ req msg_type_key
836 case mid :: MetadataId of
837 0 -> MetadataRequest <$>! piece_key
838 1 -> metadataData <$>! piece_key <*>! total_size_key
839 2 -> MetadataReject <$>! piece_key
840 _ -> pure (MetadataUnknown bval)
841 where
842 metadataData pix s = MetadataData (P.Piece pix BS.empty) s
843
844-- | Piece data bytes are omitted.
845instance Pretty ExtendedMetadata where
846 pPrint (MetadataRequest pix ) = "Request" <+> PP.int pix
847 pPrint (MetadataData p t) = "Data" <+> pPrint p <+> PP.int t
848 pPrint (MetadataReject pix ) = "Reject" <+> PP.int pix
849 pPrint (MetadataUnknown bval ) = "Unknown" <+> ppBEncode bval
850
851-- | NOTE: Approximated 'stats'.
852instance PeerMessage ExtendedMetadata where
853 envelop c = envelop c . EMetadata (remoteMessageId ExtMetadata c)
854 {-# INLINE envelop #-}
855
856 requires _ = Just ExtExtended
857 {-# INLINE requires #-}
858
859 stats (MetadataRequest _) = ByteStats (4 + 1 + 1) {- ~ -} 25 0
860 stats (MetadataData p _) = ByteStats (4 + 1 + 1) {- ~ -} 41 $
861 BS.length (P.pieceData p)
862 stats (MetadataReject _) = ByteStats (4 + 1 + 1) {- ~ -} 25 0
863 stats (MetadataUnknown _) = ByteStats (4 + 1 + 1) {- ? -} 0 0
864
865-- | All 'Piece's in 'MetadataData' messages MUST have size equal to
866-- this value. The last trailing piece can be shorter.
867metadataPieceSize :: PieceSize
868metadataPieceSize = 16 * 1024
869
870isLastPiece :: P.Piece a -> Int -> Bool
871isLastPiece P.Piece {..} total = succ pieceIndex == pcnt
872 where
873 pcnt = q + if r > 0 then 1 else 0
874 (q, r) = quotRem total metadataPieceSize
875
876-- TODO we can check if the piece payload bytestring have appropriate
877-- length; otherwise serialization MUST fail.
878isValidPiece :: P.Piece BL.ByteString -> Int -> Bool
879isValidPiece p @ P.Piece {..} total
880 | isLastPiece p total = pieceSize p <= metadataPieceSize
881 | otherwise = pieceSize p == metadataPieceSize
882
883setMetadataPayload :: BS.ByteString -> ExtendedMetadata -> ExtendedMetadata
884setMetadataPayload bs (MetadataData (P.Piece pix _) t) =
885 MetadataData (P.Piece pix bs) t
886setMetadataPayload _ msg = msg
887
888getMetadataPayload :: ExtendedMetadata -> Maybe BS.ByteString
889getMetadataPayload (MetadataData (P.Piece _ bs) _) = Just bs
890getMetadataPayload _ = Nothing
891
892-- | Metadata BDict usually contain only 'msg_type_key', 'piece_key'
893-- and 'total_size_key' fields so it normally should take less than
894-- 100 bytes. This limit is two order of magnitude larger to be
895-- permissive to 'MetadataUnknown' messages.
896--
897-- See 'maxMessageSize' for further explanation.
898--
899maxMetadataBDictSize :: Int
900maxMetadataBDictSize = 16 * 1024
901
902maxMetadataSize :: Int
903maxMetadataSize = maxMetadataBDictSize + metadataPieceSize
904
905-- to make MetadataData constructor fields a little bit prettier we
906-- cheat here: first we read empty 'pieceData' from bdict, but then we
907-- fill that field with the actual piece data — trailing bytes of
908-- the message
909getMetadata :: Int -> S.Get ExtendedMetadata
910getMetadata len
911 | len > maxMetadataSize = fail $ parseError "size exceeded limit"
912 | otherwise = do
913 bs <- getByteString len
914 parseRes $ BS.parse BE.parser bs
915 where
916 parseError reason = "unable to parse metadata message: " ++ reason
917
918 parseRes (BS.Fail _ _ m) = fail $ parseError $ "bdict: " ++ m
919 parseRes (BS.Partial _) = fail $ parseError "bdict: not enough bytes"
920 parseRes (BS.Done piece bvalueBS)
921 | BS.length piece > metadataPieceSize
922 = fail "infodict piece: size exceeded limit"
923 | otherwise = do
924 metadata <- either (fail . parseError) pure $ fromBEncode bvalueBS
925 return $ setMetadataPayload piece metadata
926
927putMetadata :: ExtendedMetadata -> BL.ByteString
928putMetadata msg
929 | Just bs <- getMetadataPayload msg = BE.encode msg <> BL.fromStrict bs
930 | otherwise = BE.encode msg
931
932-- | Allows a requesting peer to send 2 'MetadataRequest's for the
933-- each piece.
934--
935-- See 'Network.BitTorrent.Wire.Options.metadataFactor' for
936-- explanation why do we need this limit.
937defaultMetadataFactor :: Int
938defaultMetadataFactor = 2
939
940-- | Usually torrent size do not exceed 1MB. This value limit torrent
941-- /content/ size to about 8TB.
942--
943-- See 'Network.BitTorrent.Wire.Options.maxInfoDictSize' for
944-- explanation why do we need this limit.
945defaultMaxInfoDictSize :: Int
946defaultMaxInfoDictSize = 10 * 1024 * 1024
947
948{-----------------------------------------------------------------------
949-- Extension protocol messages
950-----------------------------------------------------------------------}
951
952-- | For more info see <http://www.bittorrent.org/beps/bep_0010.html>
953data ExtendedMessage
954 = EHandshake ExtendedHandshake
955 | EMetadata ExtendedMessageId ExtendedMetadata
956 | EUnknown ExtendedMessageId BS.ByteString
957 deriving (Show, Eq, Typeable)
958
959instance Pretty ExtendedMessage where
960 pPrint (EHandshake ehs) = pPrint ehs
961 pPrint (EMetadata _ msg) = "Metadata" <+> pPrint msg
962 pPrint (EUnknown mid _ ) = "Unknown" <+> PP.text (show mid)
963
964instance PeerMessage ExtendedMessage where
965 envelop _ = Extended
966 {-# INLINE envelop #-}
967
968 requires _ = Just ExtExtended
969 {-# INLINE requires #-}
970
971 stats (EHandshake hs) = stats hs
972 stats (EMetadata _ msg) = stats msg
973 stats (EUnknown _ msg) = ByteStats (4 + 1 + 1) (BS.length msg) 0
974
975{-----------------------------------------------------------------------
976-- The message datatype
977-----------------------------------------------------------------------}
978
979type MessageId = Word8
980
981-- | Messages used in communication between peers.
982--
983-- Note: If some extensions are disabled (not present in extension
984-- mask) and client receive message used by the disabled
985-- extension then the client MUST close the connection.
986--
987data Message
988 -- | Peers may close the TCP connection if they have not received
989 -- any messages for a given period of time, generally 2
990 -- minutes. Thus, the KeepAlive message is sent to keep the
991 -- connection between two peers alive, if no /other/ message has
992 -- been sent in a given period of time.
993 = KeepAlive
994 | Status !StatusUpdate -- ^ Messages used to update peer status.
995 | Available !Available -- ^ Messages used to inform availability.
996 | Transfer !Transfer -- ^ Messages used to transfer 'Block's.
997
998 -- | Peer receiving a handshake indicating the remote peer
999 -- supports the 'ExtDHT' should send a 'Port' message. Peers that
1000 -- receive this message should attempt to ping the node on the
1001 -- received port and IP address of the remote peer.
1002 | Port !PortNumber
1003 | Fast !FastMessage
1004 | Extended !ExtendedMessage
1005 deriving (Show, Eq)
1006
1007instance Default Message where
1008 def = KeepAlive
1009 {-# INLINE def #-}
1010
1011-- | Payload bytes are omitted.
1012instance Pretty Message where
1013 pPrint (KeepAlive ) = "Keep alive"
1014 pPrint (Status m) = "Status" <+> pPrint m
1015 pPrint (Available m) = pPrint m
1016 pPrint (Transfer m) = pPrint m
1017 pPrint (Port p) = "Port" <+> int (fromEnum p)
1018 pPrint (Fast m) = pPrint m
1019 pPrint (Extended m) = pPrint m
1020
1021instance PeerMessage Message where
1022 envelop _ = id
1023 {-# INLINE envelop #-}
1024
1025 requires KeepAlive = Nothing
1026 requires (Status _) = Nothing
1027 requires (Available _) = Nothing
1028 requires (Transfer _) = Nothing
1029 requires (Port _) = Just ExtDHT
1030 requires (Fast _) = Just ExtFast
1031 requires (Extended _) = Just ExtExtended
1032
1033 stats KeepAlive = ByteStats 4 0 0
1034 stats (Status m) = stats m
1035 stats (Available m) = stats m
1036 stats (Transfer m) = stats m
1037 stats (Port _) = ByteStats 5 2 0
1038 stats (Fast m) = stats m
1039 stats (Extended m) = stats m
1040
1041-- | PORT message.
1042instance PeerMessage PortNumber where
1043 envelop _ = Port
1044 {-# INLINE envelop #-}
1045
1046 requires _ = Just ExtDHT
1047 {-# INLINE requires #-}
1048
1049-- | How long /this/ peer should wait before dropping connection, in
1050-- seconds.
1051defaultKeepAliveTimeout :: Int
1052defaultKeepAliveTimeout = 2 * 60
1053
1054-- | How often /this/ peer should send 'KeepAlive' messages, in
1055-- seconds.
1056defaultKeepAliveInterval :: Int
1057defaultKeepAliveInterval = 60
1058
1059getInt :: S.Get Int
1060getInt = fromIntegral <$> S.getWord32be
1061{-# INLINE getInt #-}
1062
1063putInt :: S.Putter Int
1064putInt = S.putWord32be . fromIntegral
1065{-# INLINE putInt #-}
1066
1067-- | This limit should protect against "out-of-memory" attacks: if a
1068-- malicious peer have sent a long varlength message then receiver can
1069-- accumulate too long bytestring in the 'Get'.
1070--
1071-- Normal messages should never exceed this limits.
1072--
1073-- See also 'maxBitfieldSize', 'maxBlockSize' limits.
1074--
1075maxMessageSize :: Int
1076maxMessageSize = 20 + 1024 * 1024
1077
1078-- | This also limit max torrent size to:
1079--
1080-- max_bitfield_size * piece_ix_per_byte * max_piece_size =
1081-- 2 ^ 20 * 8 * 1MB =
1082-- 8TB
1083--
1084maxBitfieldSize :: Int
1085maxBitfieldSize = 1024 * 1024
1086
1087getBitfield :: Int -> S.Get Bitfield
1088getBitfield len
1089 | len > maxBitfieldSize = fail "BITFIELD message size exceeded limit"
1090 | otherwise = fromBitmap <$> getByteString len
1091
1092maxBlockSize :: Int
1093maxBlockSize = 4 * defaultTransferSize
1094
1095getBlock :: Int -> S.Get (Block BL.ByteString)
1096getBlock len
1097 | len > maxBlockSize = fail "BLOCK message size exceeded limit"
1098 | otherwise = Block <$> getInt <*> getInt
1099 <*> getLazyByteString (fromIntegral len)
1100{-# INLINE getBlock #-}
1101
1102instance Serialize Message where
1103 get = do
1104 len <- getInt
1105
1106 when (len > maxMessageSize) $ do
1107 fail "message body size exceeded the limit"
1108
1109 if len == 0 then return KeepAlive
1110 else do
1111 mid <- S.getWord8
1112 case mid of
1113 0x00 -> return $ Status (Choking True)
1114 0x01 -> return $ Status (Choking False)
1115 0x02 -> return $ Status (Interested True)
1116 0x03 -> return $ Status (Interested False)
1117 0x04 -> (Available . Have) <$> getInt
1118 0x05 -> (Available . Bitfield) <$> getBitfield (pred len)
1119 0x06 -> (Transfer . Request) <$> S.get
1120 0x07 -> (Transfer . Piece) <$> getBlock (len - 9)
1121 0x08 -> (Transfer . Cancel) <$> S.get
1122 0x09 -> Port <$> S.get
1123 0x0D -> (Fast . SuggestPiece) <$> getInt
1124 0x0E -> return $ Fast HaveAll
1125 0x0F -> return $ Fast HaveNone
1126 0x10 -> (Fast . RejectRequest) <$> S.get
1127 0x11 -> (Fast . AllowedFast) <$> getInt
1128 0x14 -> Extended <$> getExtendedMessage (pred len)
1129 _ -> do
1130 rm <- S.remaining >>= S.getBytes
1131 fail $ "unknown message ID: " ++ show mid ++ "\n"
1132 ++ "remaining available bytes: " ++ show rm
1133
1134 put KeepAlive = putInt 0
1135 put (Status msg) = putStatus msg
1136 put (Available msg) = putAvailable msg
1137 put (Transfer msg) = putTransfer msg
1138 put (Port p ) = putPort p
1139 put (Fast msg) = putFast msg
1140 put (Extended m ) = putExtendedMessage m
1141
1142statusUpdateId :: StatusUpdate -> MessageId
1143statusUpdateId (Choking choking) = fromIntegral (0 + fromEnum choking)
1144statusUpdateId (Interested choking) = fromIntegral (2 + fromEnum choking)
1145
1146putStatus :: Putter StatusUpdate
1147putStatus su = do
1148 putInt 1
1149 putWord8 (statusUpdateId su)
1150
1151putAvailable :: Putter Available
1152putAvailable (Have i) = do
1153 putInt 5
1154 putWord8 0x04
1155 putInt i
1156putAvailable (Bitfield (toBitmap -> bs)) = do
1157 putInt $ 1 + fromIntegral (BL.length bs)
1158 putWord8 0x05
1159 putLazyByteString bs
1160
1161putBlock :: Putter (Block BL.ByteString)
1162putBlock Block {..} = do
1163 putInt blkPiece
1164 putInt blkOffset
1165 putLazyByteString blkData
1166
1167putTransfer :: Putter Transfer
1168putTransfer (Request blk) = putInt 13 >> S.putWord8 0x06 >> S.put blk
1169putTransfer (Piece blk) = do
1170 putInt (9 + blockSize blk)
1171 putWord8 0x07
1172 putBlock blk
1173putTransfer (Cancel blk) = putInt 13 >> S.putWord8 0x08 >> S.put blk
1174
1175putPort :: Putter PortNumber
1176putPort p = do
1177 putInt 3
1178 putWord8 0x09
1179 put p
1180
1181putFast :: Putter FastMessage
1182putFast HaveAll = putInt 1 >> putWord8 0x0E
1183putFast HaveNone = putInt 1 >> putWord8 0x0F
1184putFast (SuggestPiece pix) = putInt 5 >> putWord8 0x0D >> putInt pix
1185putFast (RejectRequest i ) = putInt 13 >> putWord8 0x10 >> put i
1186putFast (AllowedFast i ) = putInt 5 >> putWord8 0x11 >> putInt i
1187
1188maxEHandshakeSize :: Int
1189maxEHandshakeSize = 16 * 1024
1190
1191getExtendedHandshake :: Int -> S.Get ExtendedHandshake
1192getExtendedHandshake messageSize
1193 | messageSize > maxEHandshakeSize
1194 = fail "extended handshake size exceeded limit"
1195 | otherwise = do
1196 bs <- getByteString messageSize
1197 either fail pure $ BE.decode bs
1198
1199maxEUnknownSize :: Int
1200maxEUnknownSize = 64 * 1024
1201
1202getExtendedUnknown :: Int -> S.Get BS.ByteString
1203getExtendedUnknown len
1204 | len > maxEUnknownSize = fail "unknown extended message size exceeded limit"
1205 | otherwise = getByteString len
1206
1207getExtendedMessage :: Int -> S.Get ExtendedMessage
1208getExtendedMessage messageSize = do
1209 msgId <- getWord8
1210 let msgBodySize = messageSize - 1
1211 case msgId of
1212 0 -> EHandshake <$> getExtendedHandshake msgBodySize
1213 1 -> EMetadata msgId <$> getMetadata msgBodySize
1214 _ -> EUnknown msgId <$> getExtendedUnknown msgBodySize
1215
1216-- | By spec.
1217extendedMessageId :: MessageId
1218extendedMessageId = 20
1219
1220putExt :: ExtendedMessageId -> BL.ByteString -> Put
1221putExt mid lbs = do
1222 putWord32be $ fromIntegral (1 + 1 + BL.length lbs)
1223 putWord8 extendedMessageId
1224 putWord8 mid
1225 putLazyByteString lbs
1226
1227-- NOTE: in contrast to getExtendedMessage this function put length
1228-- and message id too!
1229putExtendedMessage :: Putter ExtendedMessage
1230putExtendedMessage (EHandshake hs) = putExt extHandshakeId $ BE.encode hs
1231putExtendedMessage (EMetadata mid msg) = putExt mid $ putMetadata msg
1232putExtendedMessage (EUnknown mid bs) = putExt mid $ BL.fromStrict bs
diff --git a/src/Network/BitTorrent/Exchange/Session.hs b/src/Network/BitTorrent/Exchange/Session.hs
new file mode 100644
index 00000000..00886ccf
--- /dev/null
+++ b/src/Network/BitTorrent/Exchange/Session.hs
@@ -0,0 +1,586 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE DeriveDataTypeable #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE StandaloneDeriving #-}
5{-# LANGUAGE TemplateHaskell #-}
6{-# LANGUAGE TypeFamilies #-}
7module Network.BitTorrent.Exchange.Session
8 ( -- * Session
9 Session
10 , Event (..)
11 , LogFun
12 , sessionLogger
13
14 -- * Construction
15 , newSession
16 , closeSession
17 , withSession
18
19 -- * Connection Set
20 , connect
21 , connectSink
22 , establish
23
24 -- * Query
25 , waitMetadata
26 , takeMetadata
27 ) where
28
29import Control.Applicative
30import Control.Concurrent
31import Control.Concurrent.Chan.Split as CS
32import Control.Concurrent.STM
33import Control.Exception hiding (Handler)
34import Control.Lens
35import Control.Monad as M
36import Control.Monad.Logger
37import Control.Monad.Reader
38import Data.ByteString as BS
39import Data.ByteString.Lazy as BL
40import Data.Conduit as C (Sink, awaitForever, (=$=), ($=))
41import qualified Data.Conduit as C
42import Data.Conduit.List as C
43import Data.Map as M
44import Data.Monoid
45import Data.Set as S
46import Data.Text as T
47import Data.Typeable
48import Text.PrettyPrint hiding ((<>))
49import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
50import System.Log.FastLogger (LogStr, ToLogStr (..))
51
52import Data.BEncode as BE
53import Data.Torrent as Torrent
54import Network.BitTorrent.Internal.Types
55import Network.BitTorrent.Address
56import Network.BitTorrent.Exchange.Bitfield as BF
57import Network.BitTorrent.Exchange.Block as Block
58import Network.BitTorrent.Exchange.Connection
59import Network.BitTorrent.Exchange.Download as D
60import Network.BitTorrent.Exchange.Message as Message
61import System.Torrent.Storage
62
63#if !MIN_VERSION_iproute(1,2,12)
64deriving instance Ord IP
65#endif
66
67{-----------------------------------------------------------------------
68-- Exceptions
69-----------------------------------------------------------------------}
70
71data ExchangeError
72 = InvalidRequest BlockIx StorageFailure
73 | CorruptedPiece PieceIx
74 deriving (Show, Typeable)
75
76instance Exception ExchangeError
77
78packException :: Exception e => (e -> ExchangeError) -> IO a -> IO a
79packException f m = try m >>= either (throwIO . f) return
80
81{-----------------------------------------------------------------------
82-- Session state
83-----------------------------------------------------------------------}
84-- TODO unmap storage on zero connections
85
86data Cached a = Cached
87 { cachedValue :: !a
88 , cachedData :: BL.ByteString -- keep lazy
89 }
90
91cache :: BEncode a => a -> Cached a
92cache s = Cached s (BE.encode s)
93
94-- | Logger function.
95type LogFun = Loc -> LogSource -> LogLevel -> LogStr -> IO ()
96
97--data SessionStatus = Seeder | Leecher
98
99data SessionState
100 = WaitingMetadata
101 { metadataDownload :: MVar MetadataDownload
102 , metadataCompleted :: MVar InfoDict -- ^ used to unblock waiters
103 , contentRootPath :: FilePath
104 }
105 | HavingMetadata
106 { metadataCache :: Cached InfoDict
107 , contentDownload :: MVar ContentDownload
108 , contentStorage :: Storage
109 }
110
111newSessionState :: FilePath -> Either InfoHash InfoDict -> IO SessionState
112newSessionState rootPath (Left ih ) = do
113 WaitingMetadata <$> newMVar def <*> newEmptyMVar <*> pure rootPath
114newSessionState rootPath (Right dict) = do
115 storage <- openInfoDict ReadWriteEx rootPath dict
116 download <- newMVar $ D.contentDownload (BF.haveNone (totalPieces storage))
117 (piPieceLength (idPieceInfo dict))
118 storage
119 return $ HavingMetadata (cache dict) download storage
120
121closeSessionState :: SessionState -> IO ()
122closeSessionState WaitingMetadata {..} = return ()
123closeSessionState HavingMetadata {..} = close contentStorage
124
125haveMetadata :: InfoDict -> SessionState -> IO SessionState
126haveMetadata dict WaitingMetadata {..} = do
127 storage <- openInfoDict ReadWriteEx contentRootPath dict
128 download <- newMVar $ D.contentDownload (BF.haveNone (totalPieces storage))
129 (piPieceLength (idPieceInfo dict))
130 storage
131 return HavingMetadata
132 { metadataCache = cache dict
133 , contentDownload = download
134 , contentStorage = storage
135 }
136haveMetadata _ s = return s
137
138{-----------------------------------------------------------------------
139-- Session
140-----------------------------------------------------------------------}
141
142data Session = Session
143 { sessionPeerId :: !(PeerId)
144 , sessionTopic :: !(InfoHash)
145 , sessionLogger :: !(LogFun)
146 , sessionEvents :: !(SendPort (Event Session))
147
148 , sessionState :: !(MVar SessionState)
149
150------------------------------------------------------------------------
151 , connectionsPrefs :: !ConnectionPrefs
152
153 -- | Connections either waiting for TCP/uTP 'connect' or waiting
154 -- for BT handshake.
155 , connectionsPending :: !(TVar (Set (PeerAddr IP)))
156
157 -- | Connections successfully handshaked and data transfer can
158 -- take place.
159 , connectionsEstablished :: !(TVar (Map (PeerAddr IP) (Connection Session)))
160
161 -- | TODO implement choking mechanism
162 , connectionsUnchoked :: [PeerAddr IP]
163
164 -- | Messages written to this channel will be sent to the all
165 -- connections, including pending connections (but right after
166 -- handshake).
167 , connectionsBroadcast :: !(Chan Message)
168 }
169
170instance EventSource Session where
171 data Event Session
172 = ConnectingTo (PeerAddr IP)
173 | ConnectionEstablished (PeerAddr IP)
174 | ConnectionAborted
175 | ConnectionClosed (PeerAddr IP)
176 | SessionClosed
177 deriving Show
178
179 listen Session {..} = CS.listen sessionEvents
180
181newSession :: LogFun
182 -> PeerAddr (Maybe IP) -- ^ /external/ address of this peer;
183 -> FilePath -- ^ root directory for content files;
184 -> Either InfoHash InfoDict -- ^ torrent info dictionary;
185 -> IO Session
186newSession logFun addr rootPath source = do
187 let ih = either id idInfoHash source
188 pid <- maybe genPeerId return (peerId addr)
189 eventStream <- newSendPort
190 sState <- newSessionState rootPath source
191 sStateVar <- newMVar sState
192 pSetVar <- newTVarIO S.empty
193 eSetVar <- newTVarIO M.empty
194 chan <- newChan
195 return Session
196 { sessionPeerId = pid
197 , sessionTopic = ih
198 , sessionLogger = logFun
199 , sessionEvents = eventStream
200 , sessionState = sStateVar
201 , connectionsPrefs = def
202 , connectionsPending = pSetVar
203 , connectionsEstablished = eSetVar
204 , connectionsUnchoked = []
205 , connectionsBroadcast = chan
206 }
207
208closeSession :: Session -> IO ()
209closeSession Session {..} = do
210 s <- readMVar sessionState
211 closeSessionState s
212{-
213 hSet <- atomically $ do
214 pSet <- swapTVar connectionsPending S.empty
215 eSet <- swapTVar connectionsEstablished S.empty
216 return pSet
217 mapM_ kill hSet
218-}
219
220withSession :: ()
221withSession = error "withSession"
222
223{-----------------------------------------------------------------------
224-- Logging
225-----------------------------------------------------------------------}
226
227instance MonadLogger (Connected Session) where
228 monadLoggerLog loc src lvl msg = do
229 conn <- ask
230 ses <- asks connSession
231 addr <- asks connRemoteAddr
232 let addrSrc = src <> " @ " <> T.pack (render (pPrint addr))
233 liftIO $ sessionLogger ses loc addrSrc lvl (toLogStr msg)
234
235logMessage :: MonadLogger m => Message -> m ()
236logMessage msg = logDebugN $ T.pack (render (pPrint msg))
237
238logEvent :: MonadLogger m => Text -> m ()
239logEvent = logInfoN
240
241{-----------------------------------------------------------------------
242-- Connection set
243-----------------------------------------------------------------------}
244--- Connection status transition:
245---
246--- pending -> established -> finished -> closed
247--- | \|/ /|\
248--- \-------------------------------------|
249---
250--- Purpose of slots:
251--- 1) to avoid duplicates
252--- 2) connect concurrently
253---
254
255-- | Add connection to the pending set.
256pendingConnection :: PeerAddr IP -> Session -> STM Bool
257pendingConnection addr Session {..} = do
258 pSet <- readTVar connectionsPending
259 eSet <- readTVar connectionsEstablished
260 if (addr `S.member` pSet) || (addr `M.member` eSet)
261 then return False
262 else do
263 modifyTVar' connectionsPending (S.insert addr)
264 return True
265
266-- | Pending connection successfully established, add it to the
267-- established set.
268establishedConnection :: Connected Session ()
269establishedConnection = do
270 conn <- ask
271 addr <- asks connRemoteAddr
272 Session {..} <- asks connSession
273 liftIO $ atomically $ do
274 modifyTVar connectionsPending (S.delete addr)
275 modifyTVar connectionsEstablished (M.insert addr conn)
276
277-- | Either this or remote peer decided to finish conversation
278-- (conversation is alread /established/ connection), remote it from
279-- the established set.
280finishedConnection :: Connected Session ()
281finishedConnection = do
282 Session {..} <- asks connSession
283 addr <- asks connRemoteAddr
284 liftIO $ atomically $ do
285 modifyTVar connectionsEstablished $ M.delete addr
286
287-- | There are no state for this connection, remove it from the all
288-- sets.
289closedConnection :: PeerAddr IP -> Session -> STM ()
290closedConnection addr Session {..} = do
291 modifyTVar connectionsPending $ S.delete addr
292 modifyTVar connectionsEstablished $ M.delete addr
293
294getConnectionConfig :: Session -> IO (ConnectionConfig Session)
295getConnectionConfig s @ Session {..} = do
296 chan <- dupChan connectionsBroadcast
297 let sessionLink = SessionLink {
298 linkTopic = sessionTopic
299 , linkPeerId = sessionPeerId
300 , linkMetadataSize = Nothing
301 , linkOutputChan = Just chan
302 , linkSession = s
303 }
304 return ConnectionConfig
305 { cfgPrefs = connectionsPrefs
306 , cfgSession = sessionLink
307 , cfgWire = mainWire
308 }
309
310type Finalizer = IO ()
311type Runner = (ConnectionConfig Session -> IO ())
312
313runConnection :: Runner -> Finalizer -> PeerAddr IP -> Session -> IO ()
314runConnection runner finalize addr set @ Session {..} = do
315 _ <- forkIO (action `finally` cleanup)
316 return ()
317 where
318 action = do
319 notExist <- atomically $ pendingConnection addr set
320 when notExist $ do
321 cfg <- getConnectionConfig set
322 runner cfg
323
324 cleanup = do
325 finalize
326-- runStatusUpdates status (SS.resetPending addr)
327 -- TODO Metata.resetPending addr
328 atomically $ closedConnection addr set
329
330-- | Establish connection from scratch. If this endpoint is already
331-- connected, no new connections is created. This function do not block.
332connect :: PeerAddr IP -> Session -> IO ()
333connect addr = runConnection (connectWire addr) (return ()) addr
334
335-- | Establish connection with already pre-connected endpoint. If this
336-- endpoint is already connected, no new connections is created. This
337-- function do not block.
338--
339-- 'PendingConnection' will be closed automatically, you do not need
340-- to call 'closePending'.
341establish :: PendingConnection -> Session -> IO ()
342establish conn = runConnection (acceptWire conn) (closePending conn)
343 (pendingPeer conn)
344
345-- | Conduit version of 'connect'.
346connectSink :: MonadIO m => Session -> Sink [PeerAddr IPv4] m ()
347connectSink s = C.mapM_ (liftIO . connectBatch)
348 where
349 connectBatch = M.mapM_ (\ addr -> connect (IPv4 <$> addr) s)
350
351-- | Why do we need this message?
352type BroadcastMessage = ExtendedCaps -> Message
353
354broadcast :: BroadcastMessage -> Session -> IO ()
355broadcast = error "broadcast"
356
357{-----------------------------------------------------------------------
358-- Helpers
359-----------------------------------------------------------------------}
360
361waitMVar :: MVar a -> IO ()
362waitMVar m = withMVar m (const (return ()))
363
364-- This function appear in new GHC "out of box". (moreover it is atomic)
365tryReadMVar :: MVar a -> IO (Maybe a)
366tryReadMVar m = do
367 ma <- tryTakeMVar m
368 maybe (return ()) (putMVar m) ma
369 return ma
370
371readBlock :: BlockIx -> Storage -> IO (Block BL.ByteString)
372readBlock bix @ BlockIx {..} s = do
373 p <- packException (InvalidRequest bix) $ do readPiece ixPiece s
374 let chunk = BL.take (fromIntegral ixLength) $
375 BL.drop (fromIntegral ixOffset) (pieceData p)
376 if BL.length chunk == fromIntegral ixLength
377 then return $ Block ixPiece ixOffset chunk
378 else throwIO $ InvalidRequest bix (InvalidSize ixLength)
379
380-- |
381tryReadMetadataBlock :: PieceIx
382 -> Connected Session (Maybe (Torrent.Piece BS.ByteString, Int))
383tryReadMetadataBlock pix = do
384 Session {..} <- asks connSession
385 s <- liftIO (readMVar sessionState)
386 case s of
387 WaitingMetadata {..} -> error "tryReadMetadataBlock"
388 HavingMetadata {..} -> error "tryReadMetadataBlock"
389
390sendBroadcast :: PeerMessage msg => msg -> Wire Session ()
391sendBroadcast msg = do
392 Session {..} <- asks connSession
393 error "sendBroadcast"
394-- liftIO $ msg `broadcast` sessionConnections
395
396waitMetadata :: Session -> IO InfoDict
397waitMetadata Session {..} = do
398 s <- readMVar sessionState
399 case s of
400 WaitingMetadata {..} -> readMVar metadataCompleted
401 HavingMetadata {..} -> return (cachedValue metadataCache)
402
403takeMetadata :: Session -> IO (Maybe InfoDict)
404takeMetadata Session {..} = do
405 s <- readMVar sessionState
406 case s of
407 WaitingMetadata {..} -> return Nothing
408 HavingMetadata {..} -> return (Just (cachedValue metadataCache))
409
410{-----------------------------------------------------------------------
411-- Triggers
412-----------------------------------------------------------------------}
413
414-- | Trigger is the reaction of a handler at some event.
415type Trigger = Wire Session ()
416
417interesting :: Trigger
418interesting = do
419 addr <- asks connRemoteAddr
420 sendMessage (Interested True)
421 sendMessage (Choking False)
422 tryFillRequestQueue
423
424fillRequestQueue :: Trigger
425fillRequestQueue = do
426 maxN <- lift getMaxQueueLength
427 rbf <- use connBitfield
428 addr <- asks connRemoteAddr
429-- blks <- withStatusUpdates $ do
430-- n <- getRequestQueueLength addr
431-- scheduleBlocks addr rbf (maxN - n)
432-- mapM_ (sendMessage . Request) blks
433 return ()
434
435tryFillRequestQueue :: Trigger
436tryFillRequestQueue = do
437 allowed <- canDownload <$> use connStatus
438 when allowed $ do
439 fillRequestQueue
440
441{-----------------------------------------------------------------------
442-- Incoming message handling
443-----------------------------------------------------------------------}
444
445type Handler msg = msg -> Wire Session ()
446
447handleStatus :: Handler StatusUpdate
448handleStatus s = do
449 connStatus %= over remoteStatus (updateStatus s)
450 case s of
451 Interested _ -> return ()
452 Choking True -> do
453 addr <- asks connRemoteAddr
454-- withStatusUpdates (SS.resetPending addr)
455 return ()
456 Choking False -> tryFillRequestQueue
457
458handleAvailable :: Handler Available
459handleAvailable msg = do
460 connBitfield %= case msg of
461 Have ix -> BF.insert ix
462 Bitfield bf -> const bf
463
464 --thisBf <- getThisBitfield
465 thisBf <- undefined
466 case msg of
467 Have ix
468 | ix `BF.member` thisBf -> return ()
469 | otherwise -> interesting
470 Bitfield bf
471 | bf `BF.isSubsetOf` thisBf -> return ()
472 | otherwise -> interesting
473
474handleTransfer :: Handler Transfer
475handleTransfer (Request bix) = do
476 Session {..} <- asks connSession
477 s <- liftIO $ readMVar sessionState
478 case s of
479 WaitingMetadata {..} -> return ()
480 HavingMetadata {..} -> do
481 bitfield <- undefined -- getThisBitfield
482 upload <- canUpload <$> use connStatus
483 when (upload && ixPiece bix `BF.member` bitfield) $ do
484 blk <- liftIO $ readBlock bix contentStorage
485 sendMessage (Message.Piece blk)
486
487handleTransfer (Message.Piece blk) = do
488 Session {..} <- asks connSession
489 s <- liftIO $ readMVar sessionState
490 case s of
491 WaitingMetadata {..} -> return () -- TODO (?) break connection
492 HavingMetadata {..} -> do
493 isSuccess <- undefined -- withStatusUpdates (SS.pushBlock blk storage)
494 case isSuccess of
495 Nothing -> liftIO $ throwIO $ userError "block is not requested"
496 Just isCompleted -> do
497 when isCompleted $ do
498 sendBroadcast (Have (blkPiece blk))
499-- maybe send not interested
500 tryFillRequestQueue
501
502handleTransfer (Cancel bix) = filterQueue (not . (transferResponse bix))
503 where
504 transferResponse bix (Transfer (Message.Piece blk)) = blockIx blk == bix
505 transferResponse _ _ = False
506
507{-----------------------------------------------------------------------
508-- Metadata exchange
509-----------------------------------------------------------------------}
510-- TODO introduce new metadata exchange specific exceptions
511
512waitForMetadata :: Trigger
513waitForMetadata = do
514 Session {..} <- asks connSession
515 needFetch <- undefined --liftIO (isEmptyMVar infodict)
516 when needFetch $ do
517 canFetch <- allowed ExtMetadata <$> use connExtCaps
518 if canFetch
519 then tryRequestMetadataBlock
520 else undefined -- liftIO (waitMVar infodict)
521
522tryRequestMetadataBlock :: Trigger
523tryRequestMetadataBlock = do
524 mpix <- lift $ undefined --withMetadataUpdates Metadata.scheduleBlock
525 case mpix of
526 Nothing -> error "tryRequestMetadataBlock"
527 Just pix -> sendMessage (MetadataRequest pix)
528
529handleMetadata :: Handler ExtendedMetadata
530handleMetadata (MetadataRequest pix) =
531 lift (tryReadMetadataBlock pix) >>= sendMessage . mkResponse
532 where
533 mkResponse Nothing = MetadataReject pix
534 mkResponse (Just (piece, total)) = MetadataData piece total
535
536handleMetadata (MetadataData {..}) = do
537 ih <- asks connTopic
538 mdict <- lift $ undefined --withMetadataUpdates (Metadata.pushBlock piece ih)
539 case mdict of
540 Nothing -> tryRequestMetadataBlock -- not completed, need all blocks
541 Just dict -> do -- complete, wake up payload fetch
542 Session {..} <- asks connSession
543 liftIO $ modifyMVar_ sessionState (haveMetadata dict)
544
545handleMetadata (MetadataReject pix) = do
546 lift $ undefined -- withMetadataUpdates (Metadata.cancelPending pix)
547
548handleMetadata (MetadataUnknown _ ) = do
549 logInfoN "Unknown metadata message"
550
551{-----------------------------------------------------------------------
552-- Main entry point
553-----------------------------------------------------------------------}
554
555acceptRehandshake :: ExtendedHandshake -> Trigger
556acceptRehandshake ehs = error "acceptRehandshake"
557
558handleExtended :: Handler ExtendedMessage
559handleExtended (EHandshake ehs) = acceptRehandshake ehs
560handleExtended (EMetadata _ msg) = handleMetadata msg
561handleExtended (EUnknown _ _ ) = logWarnN "Unknown extension message"
562
563handleMessage :: Handler Message
564handleMessage KeepAlive = return ()
565handleMessage (Status s) = handleStatus s
566handleMessage (Available msg) = handleAvailable msg
567handleMessage (Transfer msg) = handleTransfer msg
568handleMessage (Port n) = error "handleMessage"
569handleMessage (Fast _) = error "handleMessage"
570handleMessage (Extended msg) = handleExtended msg
571
572exchange :: Wire Session ()
573exchange = do
574 waitForMetadata
575 bf <- undefined --getThisBitfield
576 sendMessage (Bitfield bf)
577 awaitForever handleMessage
578
579mainWire :: Wire Session ()
580mainWire = do
581 lift establishedConnection
582 Session {..} <- asks connSession
583-- lift $ resizeBitfield (totalPieces storage)
584 logEvent "Connection established"
585 iterM logMessage =$= exchange =$= iterM logMessage
586 lift finishedConnection
diff --git a/src/Network/BitTorrent/Internal/Cache.hs b/src/Network/BitTorrent/Internal/Cache.hs
new file mode 100644
index 00000000..8c74467a
--- /dev/null
+++ b/src/Network/BitTorrent/Internal/Cache.hs
@@ -0,0 +1,169 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2014
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Cached data for tracker responses.
9--
10module Network.BitTorrent.Internal.Cache
11 ( -- * Cache
12 Cached
13 , lastUpdated
14 , updateInterval
15 , minUpdateInterval
16
17 -- * Construction
18 , newCached
19 , newCached_
20
21 -- * Query
22 , isAlive
23 , isStalled
24 , isExpired
25 , canUpdate
26 , shouldUpdate
27
28 -- * Cached data
29 , tryTakeData
30 , unsafeTryTakeData
31 , takeData
32 ) where
33
34import Control.Applicative
35import Data.Monoid
36import Data.Default
37import Data.Time
38import Data.Time.Clock.POSIX
39import System.IO.Unsafe
40
41
42data Cached a = Cached
43 { -- | Time of resource creation.
44 lastUpdated :: !POSIXTime
45
46 -- | Minimum invalidation timeout.
47 , minUpdateInterval :: !NominalDiffTime
48
49 -- | Resource lifetime.
50 , updateInterval :: !NominalDiffTime
51
52 -- | Resource data.
53 , cachedData :: a
54 } deriving (Show, Eq)
55
56-- INVARIANT: minUpdateInterval <= updateInterval
57
58instance Default (Cached a) where
59 def = mempty
60
61instance Functor Cached where
62 fmap f (Cached t i m a) = Cached t i m (f a)
63
64posixEpoch :: NominalDiffTime
65posixEpoch = 1000000000000000000000000000000000000000000000000000000
66
67instance Applicative Cached where
68 pure = Cached 0 posixEpoch posixEpoch
69 f <*> c = Cached
70 { lastUpdated = undefined
71 , minUpdateInterval = undefined
72 , updateInterval = undefined
73 , cachedData = cachedData f (cachedData c)
74 }
75
76instance Alternative Cached where
77 empty = mempty
78 (<|>) = error "cached alternative instance: not implemented"
79
80instance Monad Cached where
81 return = pure
82 Cached {..} >>= f = Cached
83 { lastUpdated = undefined
84 , updateInterval = undefined
85 , minUpdateInterval = undefined
86 , cachedData = undefined
87 }
88
89instance Monoid (Cached a) where
90 mempty = Cached
91 { lastUpdated = 0
92 , minUpdateInterval = 0
93 , updateInterval = 0
94 , cachedData = error "cached mempty: impossible happen"
95 }
96
97 mappend a b
98 | expirationTime a > expirationTime b = a
99 | otherwise = b
100
101normalize :: NominalDiffTime -> NominalDiffTime
102 -> (NominalDiffTime, NominalDiffTime)
103normalize a b
104 | a < b = (a, b)
105 | otherwise = (b, a)
106{-# INLINE normalize #-}
107
108newCached :: NominalDiffTime -> NominalDiffTime -> a -> IO (Cached a)
109newCached minInterval interval x = do
110 t <- getPOSIXTime
111 let (mui, ui) = normalize minInterval interval
112 return Cached
113 { lastUpdated = t
114 , minUpdateInterval = mui
115 , updateInterval = ui
116 , cachedData = x
117 }
118
119newCached_ :: NominalDiffTime -> a -> IO (Cached a)
120newCached_ interval x = newCached interval interval x
121{-# INLINE newCached_ #-}
122
123expirationTime :: Cached a -> POSIXTime
124expirationTime Cached {..} = undefined
125
126isAlive :: Cached a -> IO Bool
127isAlive Cached {..} = do
128 currentTime <- getPOSIXTime
129 return $ lastUpdated + updateInterval > currentTime
130
131isExpired :: Cached a -> IO Bool
132isExpired Cached {..} = undefined
133
134isStalled :: Cached a -> IO Bool
135isStalled Cached {..} = undefined
136
137canUpdate :: Cached a -> IO (Maybe NominalDiffTime)
138canUpdate = undefined --isStaled
139
140shouldUpdate :: Cached a -> IO (Maybe NominalDiffTime)
141shouldUpdate = undefined -- isExpired
142
143tryTakeData :: Cached a -> IO (Maybe a)
144tryTakeData c = do
145 alive <- isAlive c
146 return $ if alive then Just (cachedData c) else Nothing
147
148unsafeTryTakeData :: Cached a -> Maybe a
149unsafeTryTakeData = unsafePerformIO . tryTakeData
150
151invalidateData :: Cached a -> IO a -> IO (Cached a)
152invalidateData Cached {..} action = do
153 t <- getPOSIXTime
154 x <- action
155 return Cached
156 { lastUpdated = t
157 , updateInterval = updateInterval
158 , minUpdateInterval = minUpdateInterval
159 , cachedData = x
160 }
161
162takeData :: Cached a -> IO a -> IO a
163takeData c action = do
164 mdata <- tryTakeData c
165 case mdata of
166 Just a -> return a
167 Nothing -> do
168 c' <- invalidateData c action
169 takeData c' action
diff --git a/src/Network/BitTorrent/Internal/Progress.hs b/src/Network/BitTorrent/Internal/Progress.hs
new file mode 100644
index 00000000..6ac889e2
--- /dev/null
+++ b/src/Network/BitTorrent/Internal/Progress.hs
@@ -0,0 +1,154 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- 'Progress' used to track amount downloaded\/left\/upload bytes
9-- either on per client or per torrent basis. This value is used to
10-- notify the tracker and usually shown to the user. To aggregate
11-- total progress you can use the Monoid instance.
12--
13{-# LANGUAGE TemplateHaskell #-}
14{-# LANGUAGE ViewPatterns #-}
15{-# OPTIONS -fno-warn-orphans #-}
16module Network.BitTorrent.Internal.Progress
17 ( -- * Progress
18 Progress (..)
19
20 -- * Lens
21 , left
22 , uploaded
23 , downloaded
24
25 -- * Construction
26 , startProgress
27 , downloadedProgress
28 , enqueuedProgress
29 , uploadedProgress
30 , dequeuedProgress
31
32 -- * Query
33 , canDownload
34 , canUpload
35 ) where
36
37import Control.Applicative
38import Control.Lens hiding ((%=))
39import Data.ByteString.Lazy.Builder as BS
40import Data.ByteString.Lazy.Builder.ASCII as BS
41import Data.Default
42import Data.Monoid
43import Data.Serialize as S
44import Data.Ratio
45import Data.Word
46import Network.HTTP.Types.QueryLike
47import Text.PrettyPrint as PP
48import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
49
50
51-- | Progress data is considered as dynamic within one client
52-- session. This data also should be shared across client application
53-- sessions (e.g. files), otherwise use 'startProgress' to get initial
54-- 'Progress' value.
55--
56data Progress = Progress
57 { _downloaded :: {-# UNPACK #-} !Word64 -- ^ Total amount of bytes downloaded;
58 , _left :: {-# UNPACK #-} !Word64 -- ^ Total amount of bytes left;
59 , _uploaded :: {-# UNPACK #-} !Word64 -- ^ Total amount of bytes uploaded.
60 } deriving (Show, Read, Eq)
61
62$(makeLenses ''Progress)
63
64-- | UDP tracker compatible encoding.
65instance Serialize Progress where
66 put Progress {..} = do
67 putWord64be $ fromIntegral _downloaded
68 putWord64be $ fromIntegral _left
69 putWord64be $ fromIntegral _uploaded
70
71 get = Progress
72 <$> (fromIntegral <$> getWord64be)
73 <*> (fromIntegral <$> getWord64be)
74 <*> (fromIntegral <$> getWord64be)
75
76instance Default Progress where
77 def = Progress 0 0 0
78 {-# INLINE def #-}
79
80-- | Can be used to aggregate total progress.
81instance Monoid Progress where
82 mempty = def
83 {-# INLINE mempty #-}
84
85 mappend (Progress da la ua) (Progress db lb ub) = Progress
86 { _downloaded = da + db
87 , _left = la + lb
88 , _uploaded = ua + ub
89 }
90 {-# INLINE mappend #-}
91
92instance QueryValueLike Builder where
93 toQueryValue = toQueryValue . BS.toLazyByteString
94
95instance QueryValueLike Word64 where
96 toQueryValue = toQueryValue . BS.word64Dec
97
98-- | HTTP Tracker protocol compatible encoding.
99instance QueryLike Progress where
100 toQuery Progress {..} =
101 [ ("uploaded" , toQueryValue _uploaded)
102 , ("left" , toQueryValue _left)
103 , ("downloaded", toQueryValue _downloaded)
104 ]
105
106instance Pretty Progress where
107 pPrint Progress {..} =
108 "/\\" <+> PP.text (show _uploaded) $$
109 "\\/" <+> PP.text (show _downloaded) $$
110 "left" <+> PP.text (show _left)
111
112-- | Initial progress is used when there are no session before.
113--
114-- Please note that tracker might penalize client some way if the do
115-- not accumulate progress. If possible and save 'Progress' between
116-- client sessions to avoid that.
117--
118startProgress :: Integer -> Progress
119startProgress = Progress 0 0 . fromIntegral
120{-# INLINE startProgress #-}
121
122-- | Used when the client download some data from /any/ peer.
123downloadedProgress :: Int -> Progress -> Progress
124downloadedProgress (fromIntegral -> amount)
125 = (left -~ amount)
126 . (downloaded +~ amount)
127{-# INLINE downloadedProgress #-}
128
129-- | Used when the client upload some data to /any/ peer.
130uploadedProgress :: Int -> Progress -> Progress
131uploadedProgress (fromIntegral -> amount) = uploaded +~ amount
132{-# INLINE uploadedProgress #-}
133
134-- | Used when leecher join client session.
135enqueuedProgress :: Integer -> Progress -> Progress
136enqueuedProgress amount = left +~ fromIntegral amount
137{-# INLINE enqueuedProgress #-}
138
139-- | Used when leecher leave client session.
140-- (e.g. user deletes not completed torrent)
141dequeuedProgress :: Integer -> Progress -> Progress
142dequeuedProgress amount = left -~ fromIntegral amount
143{-# INLINE dequeuedProgress #-}
144
145ri2rw64 :: Ratio Int -> Ratio Word64
146ri2rw64 x = fromIntegral (numerator x) % fromIntegral (denominator x)
147
148-- | Check global /download/ limit by uploaded \/ downloaded ratio.
149canDownload :: Ratio Int -> Progress -> Bool
150canDownload limit Progress {..} = _uploaded % _downloaded > ri2rw64 limit
151
152-- | Check global /upload/ limit by downloaded \/ uploaded ratio.
153canUpload :: Ratio Int -> Progress -> Bool
154canUpload limit Progress {..} = _downloaded % _uploaded > ri2rw64 limit
diff --git a/src/Network/BitTorrent/Internal/Types.hs b/src/Network/BitTorrent/Internal/Types.hs
new file mode 100644
index 00000000..d157db3e
--- /dev/null
+++ b/src/Network/BitTorrent/Internal/Types.hs
@@ -0,0 +1,10 @@
1{-# LANGUAGE TypeFamilies #-}
2module Network.BitTorrent.Internal.Types
3 ( EventSource (..)
4 ) where
5
6import Control.Concurrent.Chan.Split
7
8class EventSource source where
9 data Event source
10 listen :: source -> IO (ReceivePort (Event source))
diff --git a/src/Network/BitTorrent/Readme.md b/src/Network/BitTorrent/Readme.md
new file mode 100644
index 00000000..ebf9545e
--- /dev/null
+++ b/src/Network/BitTorrent/Readme.md
@@ -0,0 +1,10 @@
1Layout
2======
3
4| module group | can import | main purpose |
5|:-------------|:------------:|:--------------------------------------:|
6| Core | | common datatypes |
7| DHT | Core | centralized peer discovery |
8| Tracker | Core | decentralized peer discovery |
9| Exchange | Core | torrent content exchange |
10| Client | any other | core of bittorrent client application |
diff --git a/src/Network/BitTorrent/Tracker.hs b/src/Network/BitTorrent/Tracker.hs
new file mode 100644
index 00000000..6db67559
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker.hs
@@ -0,0 +1,50 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : non-portable
7--
8-- This module provides high level API for peer -> tracker
9-- communication. Tracker is used to discover other peers in the
10-- network using torrent info hash.
11--
12{-# LANGUAGE TemplateHaskell #-}
13module Network.BitTorrent.Tracker
14 ( -- * RPC Manager
15 PeerInfo (..)
16 , Options
17 , Manager
18 , newManager
19 , closeManager
20 , withManager
21
22 -- * Multitracker session
23 , trackerList
24 , Session
25 , Event (..)
26 , newSession
27 , closeSession
28 , withSession
29
30 -- ** Events
31 , AnnounceEvent (..)
32 , notify
33 , askPeers
34
35 -- ** Session state
36 , TrackerSession
37 , trackerPeers
38 , trackerScrape
39
40 , tryTakeData
41 , unsafeTryTakeData
42
43 , getSessionState
44 ) where
45
46import Network.BitTorrent.Internal.Cache (tryTakeData, unsafeTryTakeData)
47import Network.BitTorrent.Tracker.Message
48import Network.BitTorrent.Tracker.List
49import Network.BitTorrent.Tracker.RPC
50import Network.BitTorrent.Tracker.Session
diff --git a/src/Network/BitTorrent/Tracker/List.hs b/src/Network/BitTorrent/Tracker/List.hs
new file mode 100644
index 00000000..0eb11641
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker/List.hs
@@ -0,0 +1,193 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2014
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Multitracker Metadata Extension support.
9--
10-- For more info see: <http://www.bittorrent.org/beps/bep_0012.html>
11--
12{-# LANGUAGE FlexibleInstances #-}
13module Network.BitTorrent.Tracker.List
14 ( -- * Tracker list
15 TierEntry
16 , TrackerList
17
18 -- * Construction
19 , trackerList
20 , shuffleTiers
21 , mapWithURI
22 , Network.BitTorrent.Tracker.List.toList
23
24 -- * Traversals
25 , traverseAll
26 , traverseTiers
27 ) where
28
29import Prelude hiding (mapM, foldr)
30import Control.Arrow
31import Control.Applicative
32import Control.Exception
33import Data.Default
34import Data.List as L (map, elem, any, filter, null)
35import Data.Maybe
36import Data.Foldable
37import Data.Traversable
38import Network.URI
39import System.Random.Shuffle
40
41import Data.Torrent
42import Network.BitTorrent.Tracker.RPC as RPC
43
44{-----------------------------------------------------------------------
45-- Tracker list datatype
46-----------------------------------------------------------------------}
47
48type TierEntry a = (URI, a)
49type Tier a = [TierEntry a]
50
51-- | Tracker list is either a single tracker or list of tiers. All
52-- trackers in each tier must be checked before the client goes on to
53-- the next tier.
54data TrackerList a
55 = Announce (TierEntry a) -- ^ torrent file 'announce' field only
56 | TierList [Tier a] -- ^ torrent file 'announce-list' field only
57 deriving (Show, Eq)
58
59-- | Empty tracker list. Can be used for trackerless torrents.
60instance Default (TrackerList a) where
61 def = TierList []
62
63instance Functor TrackerList where
64 fmap f (Announce (uri, a)) = Announce (uri, f a)
65 fmap f (TierList a) = TierList (fmap (fmap (second f)) a)
66
67instance Foldable TrackerList where
68 foldr f z (Announce e ) = f (snd e) z
69 foldr f z (TierList xs) = foldr (flip (foldr (f . snd))) z xs
70
71_traverseEntry f (uri, a) = (,) uri <$> f a
72
73instance Traversable TrackerList where
74 traverse f (Announce e ) = Announce <$> _traverseEntry f e
75 traverse f (TierList xs) =
76 TierList <$> traverse (traverse (_traverseEntry f)) xs
77
78traverseWithURI :: Applicative f
79 => (TierEntry a -> f b) -> TrackerList a -> f (TrackerList b)
80traverseWithURI f (Announce (uri, a)) = (Announce . (,) uri) <$> f (uri, a)
81traverseWithURI f (TierList xxs ) =
82 TierList <$> traverse (traverse (traverseEntry f)) xxs
83 where
84 traverseEntry f (uri, a) = (,) uri <$> f (uri, a)
85
86{-----------------------------------------------------------------------
87-- List extraction
88-----------------------------------------------------------------------}
89-- BEP12 do not expose any restrictions for the content of
90-- 'announce-list' key - there are some /bad/ cases can happen with
91-- poorly designed or even malicious torrent creation software.
92--
93-- Bad case #1: announce-list is present, but empty.
94--
95-- { tAnnounce = Just "http://a.com"
96-- , tAnnounceList = Just [[]]
97-- }
98--
99-- Bad case #2: announce uri do not present in announce list.
100--
101-- { tAnnounce = Just "http://a.com"
102-- , tAnnounceList = Just [["udp://a.com"]]
103-- }
104--
105-- The addBackup function solves both problems by adding announce uri
106-- as backup tier.
107--
108addBackup :: [[URI]] -> URI -> [[URI]]
109addBackup tiers bkp
110 | L.any (L.elem bkp) tiers = tiers
111 | otherwise = tiers ++ [[bkp]]
112
113fixList :: Maybe [[URI]] -> Maybe URI -> Maybe [[URI]]
114fixList mxss mx = do
115 xss <- mxss
116 let xss' = L.filter (not . L.null) xss
117 return $ maybe xss' (addBackup xss') mx
118
119-- | Extract set of trackers from torrent file. The 'tAnnounce' key is
120-- only ignored if the 'tAnnounceList' key is present.
121trackerList :: Torrent -> TrackerList ()
122trackerList Torrent {..} = fromMaybe (TierList []) $ do
123 (TierList . tierList) <$> (tAnnounceList `fixList` tAnnounce)
124 <|> (Announce . nullEntry) <$> tAnnounce
125 where
126 nullEntry uri = (uri, ())
127 tierList = L.map (L.map nullEntry)
128
129-- | Shuffle /order of trackers/ in each tier, preserving original
130-- /order of tiers/. This can help to balance the load between the
131-- trackers.
132shuffleTiers :: TrackerList a -> IO (TrackerList a)
133shuffleTiers (Announce a ) = return (Announce a)
134shuffleTiers (TierList xs) = TierList <$> mapM shuffleM xs
135
136mapWithURI :: (URI -> a -> b) -> TrackerList a -> TrackerList b
137mapWithURI f (Announce (uri, a)) = Announce (uri, f uri a)
138mapWithURI f (TierList xs ) = TierList (L.map (L.map mapEntry) xs)
139 where
140 mapEntry (uri, a) = (uri, f uri a)
141
142toList :: TrackerList a -> [[TierEntry a]]
143toList (Announce e) = [[e]]
144toList (TierList xxs) = xxs
145
146{-----------------------------------------------------------------------
147-- Special traversals (suppressed RPC exceptions)
148-----------------------------------------------------------------------}
149
150catchRPC :: IO a -> IO a -> IO a
151catchRPC a b = catch a (f b)
152 where
153 f :: a -> RpcException -> a
154 f = const
155
156throwRPC :: String -> IO a
157throwRPC = throwIO . GenericException
158
159-- | Like 'traverse' but ignores 'RpcExceptions'.
160traverseAll :: (TierEntry a -> IO a) -> TrackerList a -> IO (TrackerList a)
161traverseAll action = traverseWithURI (action $?)
162 where
163 f $? x = catchRPC (f x) (return (snd x))
164
165-- | Like 'traverse' but put working trackers to the head of tiers.
166-- This can help to avoid exceessive requests to not available
167-- trackers at each reannounce. If no one action succeed then original
168-- list is returned.
169traverseTiers :: (TierEntry a -> IO a) -> TrackerList a -> IO (TrackerList a)
170traverseTiers action ts = catchRPC (goList ts) (return ts)
171 where
172 goList tl @ (Announce _ ) = traverseWithURI action tl
173 goList (TierList tiers) = TierList <$> goTiers (goTier []) tiers
174
175 goTiers _ [] = throwRPC "traverseTiers: no tiers"
176 goTiers f (x : xs) = catchRPC shortcut failback
177 where
178 shortcut = do
179 x' <- f x
180 return (x' : xs)
181
182 failback = do
183 xs' <- goTiers f xs
184 return (x : xs')
185
186 goTier _ [] = throwRPC "traverseTiers: no trackers in tier"
187 goTier failed ((uri, a) : as) = catchRPC shortcut failback
188 where
189 shortcut = do
190 a' <- action (uri, a)
191 return ((uri, a') : as ++ failed) -- failed trackers at the end
192
193 failback = goTier ((uri, a) : failed) as
diff --git a/src/Network/BitTorrent/Tracker/Message.hs b/src/Network/BitTorrent/Tracker/Message.hs
new file mode 100644
index 00000000..ddd86665
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker/Message.hs
@@ -0,0 +1,920 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- (c) Daniel Gröber 2013
4-- License : BSD3
5-- Maintainer : pxqr.sta@gmail.com
6-- Stability : experimental
7-- Portability : portable
8--
9-- Every tracker should support announce query. This query is used
10-- to discover peers within a swarm and have two-fold effect:
11--
12-- * peer doing announce discover other peers using peer list from
13-- the response to the announce query.
14--
15-- * tracker store peer information and use it in the succeeding
16-- requests made by other peers, until the peer info expires.
17--
18-- By convention most trackers support another form of request —
19-- scrape query — which queries the state of a given torrent (or
20-- a list of torrents) that the tracker is managing.
21--
22{-# LANGUAGE FlexibleContexts #-}
23{-# LANGUAGE FlexibleInstances #-}
24{-# LANGUAGE GeneralizedNewtypeDeriving #-}
25{-# LANGUAGE TemplateHaskell #-}
26{-# LANGUAGE DeriveDataTypeable #-}
27{-# LANGUAGE DeriveFunctor #-}
28{-# LANGUAGE ScopedTypeVariables #-}
29{-# LANGUAGE TypeFamilies #-}
30{-# OPTIONS -fno-warn-orphans #-}
31module Network.BitTorrent.Tracker.Message
32 ( -- * Announce
33 -- ** Query
34 AnnounceEvent (..)
35 , AnnounceQuery (..)
36 , renderAnnounceQuery
37 , ParamParseFailure
38 , parseAnnounceQuery
39
40 -- ** Info
41 , PeerList (..)
42 , getPeerList
43 , AnnounceInfo(..)
44 , defaultNumWant
45 , defaultMaxNumWant
46 , defaultReannounceInterval
47
48 -- * Scrape
49 -- ** Query
50 , ScrapeQuery
51 , renderScrapeQuery
52 , parseScrapeQuery
53
54 -- ** Info
55 , ScrapeEntry (..)
56 , ScrapeInfo
57
58 -- * HTTP specific
59 -- ** Routes
60 , PathPiece
61 , defaultAnnouncePath
62 , defaultScrapePath
63
64 -- ** Preferences
65 , AnnouncePrefs (..)
66 , renderAnnouncePrefs
67 , parseAnnouncePrefs
68
69 -- ** Request
70 , AnnounceRequest (..)
71 , parseAnnounceRequest
72 , renderAnnounceRequest
73
74 -- ** Response
75 , announceType
76 , scrapeType
77 , parseFailureStatus
78
79 -- ** Extra
80 , queryToSimpleQuery
81
82 -- * UDP specific
83 -- ** Connection
84 , ConnectionId
85 , initialConnectionId
86
87 -- ** Messages
88 , Request (..)
89 , Response (..)
90 , responseName
91
92 -- ** Transaction
93 , genTransactionId
94 , TransactionId
95 , Transaction (..)
96 )
97 where
98
99import Control.Applicative
100import Control.Monad
101import Data.BEncode as BE hiding (Result)
102import Data.BEncode.BDict as BE
103import Data.ByteString as BS
104import Data.ByteString.Char8 as BC
105import Data.Char as Char
106import Data.Convertible
107import Data.Default
108import Data.Either
109import Data.List as L
110import Data.Maybe
111import Data.Monoid
112import Data.Serialize as S hiding (Result)
113import Data.String
114import Data.Text (Text)
115import Data.Text.Encoding
116import Data.Typeable
117import Data.Word
118import Data.IP
119import Network
120import Network.HTTP.Types.QueryLike
121import Network.HTTP.Types.URI hiding (urlEncode)
122import Network.HTTP.Types.Status
123import Network.Socket hiding (Connected)
124import Numeric
125import System.Entropy
126import Text.Read (readMaybe)
127
128import Data.Torrent
129import Network.BitTorrent.Address
130import Network.BitTorrent.Internal.Progress
131
132{-----------------------------------------------------------------------
133-- Events
134-----------------------------------------------------------------------}
135
136-- | Events are used to specify which kind of announce query is performed.
137data AnnounceEvent
138 -- | For the first request: when download first begins.
139 = Started
140
141 -- | This peer stopped downloading /and/ uploading the torrent or
142 -- just shutting down.
143 | Stopped
144
145 -- | This peer completed downloading the torrent. This only happen
146 -- right after last piece have been verified. No 'Completed' is
147 -- sent if the file was completed when 'Started'.
148 | Completed
149 deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable)
150
151-- | HTTP tracker protocol compatible encoding.
152instance QueryValueLike AnnounceEvent where
153 toQueryValue e = toQueryValue (Char.toLower x : xs)
154 where
155 (x : xs) = show e -- INVARIANT: this is always nonempty list
156
157type EventId = Word32
158
159-- | UDP tracker encoding event codes.
160eventId :: AnnounceEvent -> EventId
161eventId Completed = 1
162eventId Started = 2
163eventId Stopped = 3
164
165-- TODO add Regular event
166putEvent :: Putter (Maybe AnnounceEvent)
167putEvent Nothing = putWord32be 0
168putEvent (Just e) = putWord32be (eventId e)
169
170getEvent :: S.Get (Maybe AnnounceEvent)
171getEvent = do
172 eid <- getWord32be
173 case eid of
174 0 -> return Nothing
175 1 -> return $ Just Completed
176 2 -> return $ Just Started
177 3 -> return $ Just Stopped
178 _ -> fail "unknown event id"
179
180{-----------------------------------------------------------------------
181 Announce query
182-----------------------------------------------------------------------}
183-- TODO add &ipv6= and &ipv4= params to AnnounceQuery
184-- http://www.bittorrent.org/beps/bep_0007.html#announce-parameter
185
186-- | A tracker request is HTTP GET request; used to include metrics
187-- from clients that help the tracker keep overall statistics about
188-- the torrent. The most important, requests are used by the tracker
189-- to keep track lists of active peer for a particular torrent.
190--
191data AnnounceQuery = AnnounceQuery
192 {
193 -- | Hash of info part of the torrent usually obtained from
194 -- 'Torrent' or 'Magnet'.
195 reqInfoHash :: !InfoHash
196
197 -- | ID of the peer doing request.
198 , reqPeerId :: !PeerId
199
200 -- | Port to listen to for connections from other
201 -- peers. Tracker should respond with this port when
202 -- some /other/ peer request the tracker with the same info hash.
203 -- Normally, this port is choosed from 'defaultPorts'.
204 , reqPort :: !PortNumber
205
206 -- | Current progress of peer doing request.
207 , reqProgress :: !Progress
208
209 -- | The peer IP. Needed only when client communicated with
210 -- tracker throught a proxy.
211 , reqIP :: Maybe HostAddress
212
213 -- | Number of peers that the peers wants to receive from. It is
214 -- optional for trackers to honor this limit. See note for
215 -- 'defaultNumWant'.
216 , reqNumWant :: Maybe Int
217
218 -- | If not specified, the request is regular periodic
219 -- request. Regular request should be sent
220 , reqEvent :: Maybe AnnounceEvent
221 } deriving (Show, Eq, Typeable)
222
223-- | UDP tracker protocol compatible encoding.
224instance Serialize AnnounceQuery where
225 put AnnounceQuery {..} = do
226 put reqInfoHash
227 put reqPeerId
228 put reqProgress
229 putEvent reqEvent
230 putWord32host $ fromMaybe 0 reqIP
231 putWord32be $ 0 -- TODO what the fuck is "key"?
232 putWord32be $ fromIntegral $ fromMaybe (-1) reqNumWant
233
234 put reqPort
235
236 get = do
237 ih <- get
238 pid <- get
239
240 progress <- get
241
242 ev <- getEvent
243 ip <- getWord32be
244-- key <- getWord32be -- TODO
245 want <- getWord32be
246
247 port <- get
248
249 return $ AnnounceQuery {
250 reqInfoHash = ih
251 , reqPeerId = pid
252 , reqPort = port
253 , reqProgress = progress
254 , reqIP = if ip == 0 then Nothing else Just ip
255 , reqNumWant = if want == -1 then Nothing
256 else Just (fromIntegral want)
257 , reqEvent = ev
258 }
259
260instance QueryValueLike PortNumber where
261 toQueryValue = toQueryValue . show . fromEnum
262
263instance QueryValueLike Word32 where
264 toQueryValue = toQueryValue . show
265
266instance QueryValueLike Int where
267 toQueryValue = toQueryValue . show
268
269-- | HTTP tracker protocol compatible encoding.
270instance QueryLike AnnounceQuery where
271 toQuery AnnounceQuery {..} =
272 toQuery reqProgress ++
273 [ ("info_hash", toQueryValue reqInfoHash) -- TODO use 'paramName'
274 , ("peer_id" , toQueryValue reqPeerId)
275 , ("port" , toQueryValue reqPort)
276 , ("ip" , toQueryValue reqIP)
277 , ("numwant" , toQueryValue reqNumWant)
278 , ("event" , toQueryValue reqEvent)
279 ]
280
281-- | Filter @param=value@ pairs with the unset value.
282queryToSimpleQuery :: Query -> SimpleQuery
283queryToSimpleQuery = catMaybes . L.map f
284 where
285 f (_, Nothing) = Nothing
286 f (a, Just b ) = Just (a, b)
287
288-- | Encode announce query to query string.
289renderAnnounceQuery :: AnnounceQuery -> SimpleQuery
290renderAnnounceQuery = queryToSimpleQuery . toQuery
291
292data QueryParam
293 -- announce query
294 = ParamInfoHash
295 | ParamPeerId
296 | ParamPort
297 | ParamUploaded
298 | ParamLeft
299 | ParamDownloaded
300 | ParamIP
301 | ParamNumWant
302 | ParamEvent
303 -- announce query ext
304 | ParamCompact
305 | ParamNoPeerId
306 deriving (Show, Eq, Ord, Enum)
307
308paramName :: QueryParam -> BS.ByteString
309paramName ParamInfoHash = "info_hash"
310paramName ParamPeerId = "peer_id"
311paramName ParamPort = "port"
312paramName ParamUploaded = "uploaded"
313paramName ParamLeft = "left"
314paramName ParamDownloaded = "downloaded"
315paramName ParamIP = "ip"
316paramName ParamNumWant = "numwant"
317paramName ParamEvent = "event"
318paramName ParamCompact = "compact"
319paramName ParamNoPeerId = "no_peer_id"
320{-# INLINE paramName #-}
321
322class FromParam a where
323 fromParam :: BS.ByteString -> Maybe a
324
325instance FromParam Bool where
326 fromParam "0" = Just False
327 fromParam "1" = Just True
328 fromParam _ = Nothing
329
330instance FromParam InfoHash where
331 fromParam = either (const Nothing) pure . safeConvert
332
333instance FromParam PeerId where
334 fromParam = either (const Nothing) pure . safeConvert
335
336instance FromParam Word32 where
337 fromParam = readMaybe . BC.unpack
338
339instance FromParam Word64 where
340 fromParam = readMaybe . BC.unpack
341
342instance FromParam Int where
343 fromParam = readMaybe . BC.unpack
344
345instance FromParam PortNumber where
346 fromParam bs = fromIntegral <$> (fromParam bs :: Maybe Word32)
347
348instance FromParam AnnounceEvent where
349 fromParam bs = do
350 (x, xs) <- BC.uncons bs
351 readMaybe $ BC.unpack $ BC.cons (Char.toUpper x) xs
352
353-- | 'ParamParseFailure' represent errors can occur while parsing HTTP
354-- tracker requests. In case of failure, this can be used to provide
355-- more informative 'statusCode' and 'statusMessage' in tracker
356-- responses.
357--
358data ParamParseFailure
359 = Missing QueryParam -- ^ param not found in query string;
360 | Invalid QueryParam BS.ByteString -- ^ param present but not valid.
361 deriving (Show, Eq)
362
363type ParseResult = Either ParamParseFailure
364
365withError :: ParamParseFailure -> Maybe a -> ParseResult a
366withError e = maybe (Left e) Right
367
368reqParam :: FromParam a => QueryParam -> SimpleQuery -> ParseResult a
369reqParam param xs = do
370 val <- withError (Missing param) $ L.lookup (paramName param) xs
371 withError (Invalid param val) (fromParam val)
372
373optParam :: FromParam a => QueryParam -> SimpleQuery -> ParseResult (Maybe a)
374optParam param ps
375 | Just x <- L.lookup (paramName param) ps
376 = pure <$> withError (Invalid param x) (fromParam x)
377 | otherwise = pure Nothing
378
379parseProgress :: SimpleQuery -> ParseResult Progress
380parseProgress params = Progress
381 <$> reqParam ParamDownloaded params
382 <*> reqParam ParamLeft params
383 <*> reqParam ParamUploaded params
384
385-- | Parse announce request from a query string.
386parseAnnounceQuery :: SimpleQuery -> ParseResult AnnounceQuery
387parseAnnounceQuery params = AnnounceQuery
388 <$> reqParam ParamInfoHash params
389 <*> reqParam ParamPeerId params
390 <*> reqParam ParamPort params
391 <*> parseProgress params
392 <*> optParam ParamIP params
393 <*> optParam ParamNumWant params
394 <*> optParam ParamEvent params
395
396{-----------------------------------------------------------------------
397-- Announce Info
398-----------------------------------------------------------------------}
399-- TODO check if announceinterval/complete/incomplete is positive ints
400
401-- | Tracker can return peer list in either compact(BEP23) or not
402-- compact form.
403--
404-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
405--
406data PeerList ip
407 = PeerList [PeerAddr IP]
408 | CompactPeerList [PeerAddr ip]
409 deriving (Show, Eq, Typeable, Functor)
410
411-- | The empty non-compact peer list.
412instance Default (PeerList IP) where
413 def = PeerList []
414 {-# INLINE def #-}
415
416getPeerList :: PeerList IP -> [PeerAddr IP]
417getPeerList (PeerList xs) = xs
418getPeerList (CompactPeerList xs) = xs
419
420instance Serialize a => BEncode (PeerList a) where
421 toBEncode (PeerList xs) = toBEncode xs
422 toBEncode (CompactPeerList xs) = toBEncode $ runPut (mapM_ put xs)
423
424 fromBEncode (BList l ) = PeerList <$> fromBEncode (BList l)
425 fromBEncode (BString s ) = CompactPeerList <$> runGet (many get) s
426 fromBEncode _ = decodingError "PeerList: should be a BString or BList"
427
428-- | The tracker response includes a peer list that helps the client
429-- participate in the torrent. The most important is 'respPeer' list
430-- used to join the swarm.
431--
432data AnnounceInfo =
433 Failure !Text -- ^ Failure reason in human readable form.
434 | AnnounceInfo {
435 -- | Number of peers completed the torrent. (seeders)
436 respComplete :: !(Maybe Int)
437
438 -- | Number of peers downloading the torrent. (leechers)
439 , respIncomplete :: !(Maybe Int)
440
441 -- | Recommended interval to wait between requests, in seconds.
442 , respInterval :: !Int
443
444 -- | Minimal amount of time between requests, in seconds. A
445 -- peer /should/ make timeout with at least 'respMinInterval'
446 -- value, otherwise tracker might not respond. If not specified
447 -- the same applies to 'respInterval'.
448 , respMinInterval :: !(Maybe Int)
449
450 -- | Peers that must be contacted.
451 , respPeers :: !(PeerList IP)
452
453 -- | Human readable warning.
454 , respWarning :: !(Maybe Text)
455 } deriving (Show, Eq, Typeable)
456
457-- | Empty peer list with default reannounce interval.
458instance Default AnnounceInfo where
459 def = AnnounceInfo
460 { respComplete = Nothing
461 , respIncomplete = Nothing
462 , respInterval = defaultReannounceInterval
463 , respMinInterval = Nothing
464 , respPeers = def
465 , respWarning = Nothing
466 }
467
468-- | HTTP tracker protocol compatible encoding.
469instance BEncode AnnounceInfo where
470 toBEncode (Failure t) = toDict $
471 "failure reason" .=! t
472 .: endDict
473
474 toBEncode AnnounceInfo {..} = toDict $
475 "complete" .=? respComplete
476 .: "incomplete" .=? respIncomplete
477 .: "interval" .=! respInterval
478 .: "min interval" .=? respMinInterval
479 .: "peers" .=! peers
480 .: "peers6" .=? peers6
481 .: "warning message" .=? respWarning
482 .: endDict
483 where
484 (peers, peers6) = prttn respPeers
485
486 prttn :: PeerList IP -> (PeerList IPv4, Maybe (PeerList IPv6))
487 prttn (PeerList xs) = (PeerList xs, Nothing)
488 prttn (CompactPeerList xs) = mk $ partitionEithers $ toEither <$> xs
489 where
490 mk (v4s, v6s)
491 | L.null v6s = (CompactPeerList v4s, Nothing)
492 | otherwise = (CompactPeerList v4s, Just (CompactPeerList v6s))
493
494 toEither :: PeerAddr IP -> Either (PeerAddr IPv4) (PeerAddr IPv6)
495 toEither PeerAddr {..} = case peerHost of
496 IPv4 ipv4 -> Left $ PeerAddr peerId ipv4 peerPort
497 IPv6 ipv6 -> Right $ PeerAddr peerId ipv6 peerPort
498
499 fromBEncode (BDict d)
500 | Just t <- BE.lookup "failure reason" d = Failure <$> fromBEncode t
501 | otherwise = (`fromDict` (BDict d)) $
502 AnnounceInfo
503 <$>? "complete"
504 <*>? "incomplete"
505 <*>! "interval"
506 <*>? "min interval"
507 <*> (uncurry merge =<< (,) <$>! "peers" <*>? "peers6")
508 <*>? "warning message"
509 where
510 merge :: PeerList IPv4 -> Maybe (PeerList IPv6) -> BE.Get (PeerList IP)
511 merge (PeerList ips) Nothing = pure (PeerList ips)
512 merge (PeerList _ ) (Just _)
513 = fail "PeerList: non-compact peer list provided, \
514 \but the `peers6' field present"
515
516 merge (CompactPeerList ipv4s) Nothing
517 = pure $ CompactPeerList (fmap IPv4 <$> ipv4s)
518
519 merge (CompactPeerList _ ) (Just (PeerList _))
520 = fail "PeerList: the `peers6' field value \
521 \should contain *compact* peer list"
522
523 merge (CompactPeerList ipv4s) (Just (CompactPeerList ipv6s))
524 = pure $ CompactPeerList $
525 (fmap IPv4 <$> ipv4s) <> (fmap IPv6 <$> ipv6s)
526
527 fromBEncode _ = decodingError "Announce info"
528
529-- | UDP tracker protocol compatible encoding.
530instance Serialize AnnounceInfo where
531 put (Failure msg) = put $ encodeUtf8 msg
532 put AnnounceInfo {..} = do
533 putWord32be $ fromIntegral respInterval
534 putWord32be $ fromIntegral $ fromMaybe 0 respIncomplete
535 putWord32be $ fromIntegral $ fromMaybe 0 respComplete
536 forM_ (fmap ipv4 <$> getPeerList respPeers) put
537
538 get = do
539 interval <- getWord32be
540 leechers <- getWord32be
541 seeders <- getWord32be
542 peers <- many $ fmap IPv4 <$> get
543
544 return $ AnnounceInfo {
545 respWarning = Nothing
546 , respInterval = fromIntegral interval
547 , respMinInterval = Nothing
548 , respIncomplete = Just $ fromIntegral leechers
549 , respComplete = Just $ fromIntegral seeders
550 , respPeers = PeerList peers
551 }
552
553-- | Decodes announce response from bencoded string, for debugging only.
554instance IsString AnnounceInfo where
555 fromString str = either (error . format) id $ BE.decode (fromString str)
556 where
557 format msg = "fromString: unable to decode AnnounceInfo: " ++ msg
558
559-- | Above 25, new peers are highly unlikely to increase download
560-- speed. Even 30 peers is /plenty/, the official client version 3
561-- in fact only actively forms new connections if it has less than
562-- 30 peers and will refuse connections if it has 55.
563--
564-- <https://wiki.theory.org/BitTorrent_Tracker_Protocol#Basic_Tracker_Announce_Request>
565--
566defaultNumWant :: Int
567defaultNumWant = 50
568
569-- | Reasonable upper bound of numwant parameter.
570defaultMaxNumWant :: Int
571defaultMaxNumWant = 200
572
573-- | Widely used reannounce interval. Note: tracker clients should not
574-- use this value!
575defaultReannounceInterval :: Int
576defaultReannounceInterval = 30 * 60
577
578{-----------------------------------------------------------------------
579 Scrape message
580-----------------------------------------------------------------------}
581
582-- | Scrape query used to specify a set of torrent to scrape.
583-- If list is empty then tracker should return scrape info about each
584-- torrent.
585type ScrapeQuery = [InfoHash]
586
587-- TODO
588-- data ScrapeQuery
589-- = ScrapeAll
590-- | ScrapeSingle InfoHash
591-- | ScrapeMulti (HashSet InfoHash)
592-- deriving (Show)
593--
594-- data ScrapeInfo
595-- = ScrapeAll (HashMap InfoHash ScrapeEntry)
596-- | ScrapeSingle InfoHash ScrapeEntry
597-- | ScrapeMulti (HashMap InfoHash ScrapeEntry)
598--
599
600scrapeParam :: BS.ByteString
601scrapeParam = "info_hash"
602
603isScrapeParam :: BS.ByteString -> Bool
604isScrapeParam = (==) scrapeParam
605
606-- | Parse scrape query to query string.
607parseScrapeQuery :: SimpleQuery -> ScrapeQuery
608parseScrapeQuery
609 = catMaybes . L.map (fromParam . snd) . L.filter (isScrapeParam . fst)
610
611-- | Render scrape query to query string.
612renderScrapeQuery :: ScrapeQuery -> SimpleQuery
613renderScrapeQuery = queryToSimpleQuery . L.map mkPair
614 where
615 mkPair ih = (scrapeParam, toQueryValue ih)
616
617-- | Overall information about particular torrent.
618data ScrapeEntry = ScrapeEntry {
619 -- | Number of seeders - peers with the entire file.
620 siComplete :: {-# UNPACK #-} !Int
621
622 -- | Total number of times the tracker has registered a completion.
623 , siDownloaded :: {-# UNPACK #-} !Int
624
625 -- | Number of leechers.
626 , siIncomplete :: {-# UNPACK #-} !Int
627
628 -- | Name of the torrent file, as specified by the "name"
629 -- file in the info section of the .torrent file.
630 , siName :: !(Maybe Text)
631 } deriving (Show, Eq, Typeable)
632
633-- | HTTP tracker protocol compatible encoding.
634instance BEncode ScrapeEntry where
635 toBEncode ScrapeEntry {..} = toDict $
636 "complete" .=! siComplete
637 .: "downloaded" .=! siDownloaded
638 .: "incomplete" .=! siIncomplete
639 .: "name" .=? siName
640 .: endDict
641
642 fromBEncode = fromDict $ ScrapeEntry
643 <$>! "complete"
644 <*>! "downloaded"
645 <*>! "incomplete"
646 <*>? "name"
647
648-- | UDP tracker protocol compatible encoding.
649instance Serialize ScrapeEntry where
650 put ScrapeEntry {..} = do
651 putWord32be $ fromIntegral siComplete
652 putWord32be $ fromIntegral siDownloaded
653 putWord32be $ fromIntegral siIncomplete
654
655 get = ScrapeEntry
656 <$> (fromIntegral <$> getWord32be)
657 <*> (fromIntegral <$> getWord32be)
658 <*> (fromIntegral <$> getWord32be)
659 <*> pure Nothing
660
661-- | Scrape info about a set of torrents.
662type ScrapeInfo = [(InfoHash, ScrapeEntry)]
663
664{-----------------------------------------------------------------------
665-- HTTP specific
666-----------------------------------------------------------------------}
667
668-- | Some HTTP trackers allow to choose prefered representation of the
669-- 'AnnounceInfo'. It's optional for trackers to honor any of this
670-- options.
671data AnnouncePrefs = AnnouncePrefs
672 { -- | If specified, "compact" parameter is used to advise the
673 -- tracker to send peer id list as:
674 --
675 -- * bencoded list (extCompact = Just False);
676 -- * or more compact binary string (extCompact = Just True).
677 --
678 -- The later is prefered since compact peer list will reduce the
679 -- size of tracker responses. Hovewer, if tracker do not support
680 -- this extension then it can return peer list in either form.
681 --
682 -- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
683 --
684 extCompact :: !(Maybe Bool)
685
686 -- | If specified, "no_peer_id" parameter is used advise tracker
687 -- to either send or not to send peer id in tracker response.
688 -- Tracker may not support this extension as well.
689 --
690 -- For more info see:
691 -- <http://permalink.gmane.org/gmane.network.bit-torrent.general/4030>
692 --
693 , extNoPeerId :: !(Maybe Bool)
694 } deriving (Show, Eq, Typeable)
695
696instance Default AnnouncePrefs where
697 def = AnnouncePrefs Nothing Nothing
698
699instance QueryLike AnnouncePrefs where
700 toQuery AnnouncePrefs {..} =
701 [ ("compact", toQueryFlag <$> extCompact) -- TODO use 'paramName'
702 , ("no_peer_id", toQueryFlag <$> extNoPeerId)
703 ]
704 where
705 toQueryFlag False = "0"
706 toQueryFlag True = "1"
707
708-- | Parse announce query extended part from query string.
709parseAnnouncePrefs :: SimpleQuery -> AnnouncePrefs
710parseAnnouncePrefs params = either (const def) id $
711 AnnouncePrefs
712 <$> optParam ParamCompact params
713 <*> optParam ParamNoPeerId params
714
715-- | Render announce preferences to query string.
716renderAnnouncePrefs :: AnnouncePrefs -> SimpleQuery
717renderAnnouncePrefs = queryToSimpleQuery . toQuery
718
719-- | HTTP tracker request with preferences.
720data AnnounceRequest = AnnounceRequest
721 { announceQuery :: AnnounceQuery -- ^ Request query params.
722 , announcePrefs :: AnnouncePrefs -- ^ Optional advises to the tracker.
723 } deriving (Show, Eq, Typeable)
724
725instance QueryLike AnnounceRequest where
726 toQuery AnnounceRequest{..} =
727 toQuery announcePrefs <>
728 toQuery announceQuery
729
730-- | Parse announce request from query string.
731parseAnnounceRequest :: SimpleQuery -> ParseResult AnnounceRequest
732parseAnnounceRequest params = AnnounceRequest
733 <$> parseAnnounceQuery params
734 <*> pure (parseAnnouncePrefs params)
735
736-- | Render announce request to query string.
737renderAnnounceRequest :: AnnounceRequest -> SimpleQuery
738renderAnnounceRequest = queryToSimpleQuery . toQuery
739
740type PathPiece = BS.ByteString
741
742defaultAnnouncePath :: PathPiece
743defaultAnnouncePath = "announce"
744
745defaultScrapePath :: PathPiece
746defaultScrapePath = "scrape"
747
748missingOffset :: Int
749missingOffset = 101
750
751invalidOffset :: Int
752invalidOffset = 150
753
754parseFailureCode :: ParamParseFailure -> Int
755parseFailureCode (Missing param ) = missingOffset + fromEnum param
756parseFailureCode (Invalid param _) = invalidOffset + fromEnum param
757
758parseFailureMessage :: ParamParseFailure -> BS.ByteString
759parseFailureMessage e = BS.concat $ case e of
760 Missing p -> ["Missing parameter: ", paramName p]
761 Invalid p v -> ["Invalid parameter: ", paramName p, " = ", v]
762
763-- | HTTP response /content type/ for announce info.
764announceType :: ByteString
765announceType = "text/plain"
766
767-- | HTTP response /content type/ for scrape info.
768scrapeType :: ByteString
769scrapeType = "text/plain"
770
771-- | Get HTTP response status from a announce params parse failure.
772--
773-- For more info see:
774-- <https://wiki.theory.org/BitTorrent_Tracker_Protocol#Response_Codes>
775--
776parseFailureStatus :: ParamParseFailure -> Status
777parseFailureStatus = mkStatus <$> parseFailureCode <*> parseFailureMessage
778
779{-----------------------------------------------------------------------
780-- UDP specific message types
781-----------------------------------------------------------------------}
782
783genToken :: IO Word64
784genToken = do
785 bs <- getEntropy 8
786 either err return $ runGet getWord64be bs
787 where
788 err = error "genToken: impossible happen"
789
790-- | Connection Id is used for entire tracker session.
791newtype ConnectionId = ConnectionId Word64
792 deriving (Eq, Serialize)
793
794instance Show ConnectionId where
795 showsPrec _ (ConnectionId cid) = showString "0x" <> showHex cid
796
797initialConnectionId :: ConnectionId
798initialConnectionId = ConnectionId 0x41727101980
799
800-- | Transaction Id is used within a UDP RPC.
801newtype TransactionId = TransactionId Word32
802 deriving (Eq, Ord, Enum, Bounded, Serialize)
803
804instance Show TransactionId where
805 showsPrec _ (TransactionId tid) = showString "0x" <> showHex tid
806
807genTransactionId :: IO TransactionId
808genTransactionId = (TransactionId . fromIntegral) <$> genToken
809
810data Request
811 = Connect
812 | Announce AnnounceQuery
813 | Scrape ScrapeQuery
814 deriving Show
815
816data Response
817 = Connected ConnectionId
818 | Announced AnnounceInfo
819 | Scraped [ScrapeEntry]
820 | Failed Text
821 deriving Show
822
823responseName :: Response -> String
824responseName (Connected _) = "connected"
825responseName (Announced _) = "announced"
826responseName (Scraped _) = "scraped"
827responseName (Failed _) = "failed"
828
829data family Transaction a
830data instance Transaction Request = TransactionQ
831 { connIdQ :: {-# UNPACK #-} !ConnectionId
832 , transIdQ :: {-# UNPACK #-} !TransactionId
833 , request :: !Request
834 } deriving Show
835data instance Transaction Response = TransactionR
836 { transIdR :: {-# UNPACK #-} !TransactionId
837 , response :: !Response
838 } deriving Show
839
840-- TODO newtype
841newtype MessageId = MessageId Word32
842 deriving (Show, Eq, Num, Serialize)
843
844connectId, announceId, scrapeId, errorId :: MessageId
845connectId = 0
846announceId = 1
847scrapeId = 2
848errorId = 3
849
850instance Serialize (Transaction Request) where
851 put TransactionQ {..} = do
852 case request of
853 Connect -> do
854 put initialConnectionId
855 put connectId
856 put transIdQ
857
858 Announce ann -> do
859 put connIdQ
860 put announceId
861 put transIdQ
862 put ann
863
864 Scrape hashes -> do
865 put connIdQ
866 put scrapeId
867 put transIdQ
868 forM_ hashes put
869
870 get = do
871 cid <- get
872 mid <- get
873 TransactionQ cid <$> S.get <*> getBody mid
874 where
875 getBody :: MessageId -> S.Get Request
876 getBody msgId
877 | msgId == connectId = pure Connect
878 | msgId == announceId = Announce <$> get
879 | msgId == scrapeId = Scrape <$> many get
880 | otherwise = fail errMsg
881 where
882 errMsg = "unknown request: " ++ show msgId
883
884instance Serialize (Transaction Response) where
885 put TransactionR {..} = do
886 case response of
887 Connected conn -> do
888 put connectId
889 put transIdR
890 put conn
891
892 Announced info -> do
893 put announceId
894 put transIdR
895 put info
896
897 Scraped infos -> do
898 put scrapeId
899 put transIdR
900 forM_ infos put
901
902 Failed info -> do
903 put errorId
904 put transIdR
905 put (encodeUtf8 info)
906
907
908 get = do
909 mid <- get
910 TransactionR <$> get <*> getBody mid
911 where
912 getBody :: MessageId -> S.Get Response
913 getBody msgId
914 | msgId == connectId = Connected <$> get
915 | msgId == announceId = Announced <$> get
916 | msgId == scrapeId = Scraped <$> many get
917 | msgId == errorId = (Failed . decodeUtf8) <$> get
918 | otherwise = fail msg
919 where
920 msg = "unknown response: " ++ show msgId
diff --git a/src/Network/BitTorrent/Tracker/RPC.hs b/src/Network/BitTorrent/Tracker/RPC.hs
new file mode 100644
index 00000000..6fd22b25
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker/RPC.hs
@@ -0,0 +1,175 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This module provides unified RPC interface to BitTorrent
9-- trackers. The tracker is an UDP/HTTP/HTTPS service used to
10-- discovery peers for a particular existing torrent and keep
11-- statistics about the swarm. This module also provides a way to
12-- request scrape info for a particular torrent list.
13--
14{-# LANGUAGE DeriveDataTypeable #-}
15module Network.BitTorrent.Tracker.RPC
16 ( PeerInfo (..)
17
18 -- * Manager
19 , Options (..)
20 , Manager
21 , newManager
22 , closeManager
23 , withManager
24
25 -- * RPC
26 , SAnnounceQuery (..)
27 , RpcException (..)
28 , Network.BitTorrent.Tracker.RPC.announce
29 , scrape
30 ) where
31
32import Control.Exception
33import Data.Default
34import Data.Typeable
35import Network
36import Network.URI
37import Network.Socket (HostAddress)
38
39import Data.Torrent
40import Network.BitTorrent.Address
41import Network.BitTorrent.Internal.Progress
42import Network.BitTorrent.Tracker.Message
43import qualified Network.BitTorrent.Tracker.RPC.HTTP as HTTP
44import qualified Network.BitTorrent.Tracker.RPC.UDP as UDP
45
46
47{-----------------------------------------------------------------------
48-- Simplified announce
49-----------------------------------------------------------------------}
50
51-- | Info to advertise to trackers.
52data PeerInfo = PeerInfo
53 { peerId :: !PeerId
54 , peerIP :: !(Maybe HostAddress)
55 , peerPort :: !PortNumber
56 } deriving (Show, Eq)
57
58instance Default PeerInfo where
59 def = PeerInfo def Nothing 6881
60
61-- | Simplified announce query.
62data SAnnounceQuery = SAnnounceQuery
63 { sInfoHash :: InfoHash
64 , sProgress :: Progress
65 , sNumWant :: Maybe Int
66 , sEvent :: Maybe AnnounceEvent
67 }
68
69fillAnnounceQuery :: PeerInfo -> SAnnounceQuery -> AnnounceQuery
70fillAnnounceQuery PeerInfo{..} SAnnounceQuery {..} = AnnounceQuery
71 { reqInfoHash = sInfoHash
72 , reqPeerId = peerId
73 , reqPort = peerPort
74 , reqProgress = sProgress
75 , reqIP = peerIP
76 , reqNumWant = sNumWant
77 , reqEvent = sEvent
78 }
79
80{-----------------------------------------------------------------------
81-- RPC manager
82-----------------------------------------------------------------------}
83
84-- | Tracker manager settings.
85data Options = Options
86 { -- | HTTP tracker protocol specific options.
87 optHttpRPC :: !HTTP.Options
88
89 -- | UDP tracker protocol specific options.
90 , optUdpRPC :: !UDP.Options
91
92 -- | Whether to use multitracker extension.
93 , optMultitracker :: !Bool
94 }
95
96instance Default Options where
97 def = Options
98 { optHttpRPC = def
99 , optUdpRPC = def
100 , optMultitracker = True
101 }
102
103-- | Tracker RPC Manager.
104data Manager = Manager
105 { options :: !Options
106 , peerInfo :: !PeerInfo
107 , httpMgr :: !HTTP.Manager
108 , udpMgr :: !UDP.Manager
109 }
110
111-- | Create a new 'Manager'. You /must/ manually 'closeManager'
112-- otherwise resource leakage is possible. Normally, a bittorrent
113-- client need a single RPC manager only.
114--
115-- This function can throw 'IOException' on invalid 'Options'.
116--
117newManager :: Options -> PeerInfo -> IO Manager
118newManager opts info = do
119 h <- HTTP.newManager (optHttpRPC opts)
120 u <- UDP.newManager (optUdpRPC opts) `onException` HTTP.closeManager h
121 return $ Manager opts info h u
122
123-- | Close all pending RPCs. Behaviour of currently in-flight RPCs can
124-- differ depending on underlying protocol used. No rpc calls should
125-- be performed after manager becomes closed.
126closeManager :: Manager -> IO ()
127closeManager Manager {..} = do
128 UDP.closeManager udpMgr `finally` HTTP.closeManager httpMgr
129
130-- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'.
131withManager :: Options -> PeerInfo -> (Manager -> IO a) -> IO a
132withManager opts info = bracket (newManager opts info) closeManager
133
134{-----------------------------------------------------------------------
135-- Exceptions
136-----------------------------------------------------------------------}
137-- TODO Catch IO exceptions on rpc calls (?)
138
139data RpcException
140 = UdpException UDP.RpcException -- ^ UDP RPC driver failure;
141 | HttpException HTTP.RpcException -- ^ HTTP RPC driver failure;
142 | UnrecognizedScheme String -- ^ unsupported scheme in announce URI;
143 | GenericException String -- ^ for furter extensibility.
144 deriving (Show, Typeable)
145
146instance Exception RpcException
147
148packException :: Exception e => (e -> RpcException) -> IO a -> IO a
149packException f m = try m >>= either (throwIO . f) return
150{-# INLINE packException #-}
151
152{-----------------------------------------------------------------------
153-- RPC calls
154-----------------------------------------------------------------------}
155
156dispatch :: URI -> IO a -> IO a -> IO a
157dispatch URI {..} http udp
158 | uriScheme == "http:" ||
159 uriScheme == "https:" = packException HttpException http
160 | uriScheme == "udp:" = packException UdpException udp
161 | otherwise = throwIO $ UnrecognizedScheme uriScheme
162
163announce :: Manager -> URI -> SAnnounceQuery -> IO AnnounceInfo
164announce Manager {..} uri simpleQuery
165 = dispatch uri
166 (HTTP.announce httpMgr uri annQ)
167 ( UDP.announce udpMgr uri annQ)
168 where
169 annQ = fillAnnounceQuery peerInfo simpleQuery
170
171scrape :: Manager -> URI -> ScrapeQuery -> IO ScrapeInfo
172scrape Manager {..} uri q
173 = dispatch uri
174 (HTTP.scrape httpMgr uri q)
175 ( UDP.scrape udpMgr uri q)
diff --git a/src/Network/BitTorrent/Tracker/RPC/HTTP.hs b/src/Network/BitTorrent/Tracker/RPC/HTTP.hs
new file mode 100644
index 00000000..de3fc5f5
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker/RPC/HTTP.hs
@@ -0,0 +1,191 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : provisional
6-- Portability : portable
7--
8-- This module implement HTTP tracker protocol.
9--
10-- For more information see:
11-- <https://wiki.theory.org/BitTorrentSpecification#Tracker_HTTP.2FHTTPS_Protocol>
12--
13{-# LANGUAGE DeriveDataTypeable #-}
14module Network.BitTorrent.Tracker.RPC.HTTP
15 ( -- * Manager
16 Options (..)
17 , Manager
18 , newManager
19 , closeManager
20 , withManager
21
22 -- * RPC
23 , RpcException (..)
24 , announce
25 , scrape
26 , scrapeOne
27 ) where
28
29import Control.Applicative
30import Control.Exception
31import Control.Monad
32import Control.Monad.Trans.Resource
33import Data.BEncode as BE
34import Data.ByteString as BS
35import Data.ByteString.Char8 as BC
36import Data.ByteString.Lazy as BL
37import Data.Default
38import Data.List as L
39import Data.Monoid
40import Data.Typeable hiding (Proxy)
41import Network.URI
42import Network.HTTP.Conduit hiding
43 (Manager, newManager, closeManager, withManager)
44import Network.HTTP.Client (defaultManagerSettings)
45import Network.HTTP.Client.Internal (setUri)
46import qualified Network.HTTP.Conduit as HTTP
47import Network.HTTP.Types.Header (hUserAgent)
48import Network.HTTP.Types.URI (SimpleQuery, renderSimpleQuery)
49
50import Data.Torrent (InfoHash)
51import Network.BitTorrent.Address (libUserAgent)
52import Network.BitTorrent.Tracker.Message hiding (Request, Response)
53
54{-----------------------------------------------------------------------
55-- Exceptions
56-----------------------------------------------------------------------}
57
58data RpcException
59 = RequestFailed HttpException -- ^ failed HTTP request.
60 | ParserFailure String -- ^ unable to decode tracker response;
61 | ScrapelessTracker -- ^ tracker do not support scraping;
62 | BadScrape -- ^ unable to find info hash in response dict;
63 deriving (Show, Typeable)
64
65instance Exception RpcException
66
67packHttpException :: IO a -> IO a
68packHttpException m = try m >>= either (throwIO . RequestFailed) return
69
70{-----------------------------------------------------------------------
71-- Manager
72-----------------------------------------------------------------------}
73
74-- | HTTP tracker specific RPC options.
75data Options = Options
76 { -- | Global HTTP announce query preferences.
77 optAnnouncePrefs :: !AnnouncePrefs
78
79 -- | Whether to use HTTP proxy for HTTP tracker requests.
80 , optHttpProxy :: !(Maybe Proxy)
81
82 -- | Value to put in HTTP user agent header.
83 , optUserAgent :: !BS.ByteString
84
85 -- | HTTP manager options.
86 , optHttpOptions :: !ManagerSettings
87 }
88
89instance Default Options where
90 def = Options
91 { optAnnouncePrefs = def
92 , optHttpProxy = Nothing
93 , optUserAgent = BC.pack libUserAgent
94 , optHttpOptions = defaultManagerSettings
95 }
96
97-- | HTTP tracker manager.
98data Manager = Manager
99 { options :: !Options
100 , httpMgr :: !HTTP.Manager
101 }
102
103-- |
104newManager :: Options -> IO Manager
105newManager opts = Manager opts <$> HTTP.newManager (optHttpOptions opts)
106
107-- |
108closeManager :: Manager -> IO ()
109closeManager Manager {..} = HTTP.closeManager httpMgr
110
111-- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'.
112withManager :: Options -> (Manager -> IO a) -> IO a
113withManager opts = bracket (newManager opts) closeManager
114
115{-----------------------------------------------------------------------
116-- Queries
117-----------------------------------------------------------------------}
118
119fillRequest :: Options -> SimpleQuery -> Request -> Request
120fillRequest Options {..} q r = r
121 { queryString = joinQuery (queryString r) (renderSimpleQuery False q)
122 , requestHeaders = (hUserAgent, optUserAgent) : requestHeaders r
123 , proxy = optHttpProxy
124 }
125 where
126 joinQuery a b
127 | BS.null a = b
128 | otherwise = a <> "&" <> b
129
130httpTracker :: BEncode a => Manager -> URI -> SimpleQuery -> IO a
131httpTracker Manager {..} uri q = packHttpException $ do
132 request <- fillRequest options q <$> setUri def uri
133 response <- runResourceT $ httpLbs request httpMgr
134 case BE.decode $ BL.toStrict $ responseBody response of
135 Left msg -> throwIO (ParserFailure msg)
136 Right info -> return info
137
138{-----------------------------------------------------------------------
139-- RPC
140-----------------------------------------------------------------------}
141
142-- | Send request and receive response from the tracker specified in
143-- announce list.
144--
145-- This function can throw 'RpcException'.
146--
147announce :: Manager -> URI -> AnnounceQuery -> IO AnnounceInfo
148announce mgr uri q = httpTracker mgr uri (renderAnnounceRequest uriQ)
149 where
150 uriQ = AnnounceRequest
151 { announceQuery = q
152 , announcePrefs = optAnnouncePrefs (options mgr)
153 }
154
155-- | Trying to convert /announce/ URL to /scrape/ URL. If 'scrapeURL'
156-- gives 'Nothing' then tracker do not support scraping.
157--
158scrapeURL :: URI -> Maybe URI
159scrapeURL uri = do
160 newPath <- replace (BC.pack (uriPath uri))
161 return uri { uriPath = BC.unpack newPath }
162 where
163 replace p = do
164 let ps = BC.splitWith (== '/') p
165 guard (not (L.null ps))
166 guard ("announce" `BS.isPrefixOf` L.last ps)
167 let newSuff = "scrape" <> BS.drop (BS.length "announce") (L.last ps)
168 return (BS.intercalate "/" (L.init ps ++ [newSuff]))
169
170-- | For each 'InfoHash' of torrents request scrape info from the tracker.
171-- However if the info hash list is 'null', the tracker should list
172-- all available torrents.
173--
174-- This function can throw 'RpcException'.
175--
176scrape :: Manager -> URI -> ScrapeQuery -> IO ScrapeInfo
177scrape m u q = do
178 case scrapeURL u of
179 Nothing -> throwIO ScrapelessTracker
180 Just uri -> httpTracker m uri (renderScrapeQuery q)
181
182-- | More particular version of 'scrape', just for one torrent.
183--
184-- This function can throw 'RpcException'.
185--
186scrapeOne :: Manager -> URI -> InfoHash -> IO ScrapeEntry
187scrapeOne m uri ih = do
188 xs <- scrape m uri [ih]
189 case L.lookup ih xs of
190 Nothing -> throwIO BadScrape
191 Just a -> return a
diff --git a/src/Network/BitTorrent/Tracker/RPC/UDP.hs b/src/Network/BitTorrent/Tracker/RPC/UDP.hs
new file mode 100644
index 00000000..31b6b870
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker/RPC/UDP.hs
@@ -0,0 +1,454 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013-2014
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : provisional
6-- Portability : portable
7--
8-- This module implement UDP tracker protocol.
9--
10-- For protocol details and uri scheme see:
11-- <http://www.bittorrent.org/beps/bep_0015.html>,
12-- <https://www.iana.org/assignments/uri-schemes/prov/udp>
13--
14{-# LANGUAGE RecordWildCards #-}
15{-# LANGUAGE FlexibleInstances #-}
16{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17{-# LANGUAGE DeriveDataTypeable #-}
18module Network.BitTorrent.Tracker.RPC.UDP
19 ( -- * Manager
20 Options (..)
21 , Manager
22 , newManager
23 , closeManager
24 , withManager
25
26 -- * RPC
27 , RpcException (..)
28 , announce
29 , scrape
30 ) where
31
32import Control.Applicative
33import Control.Concurrent
34import Control.Exception
35import Control.Monad
36import Data.Default
37import Data.IORef
38import Data.List as L
39import Data.Map as M
40import Data.Maybe
41import Data.Serialize
42import Data.Text as T
43import Data.Time
44import Data.Time.Clock.POSIX
45import Data.Traversable
46import Data.Typeable
47import Text.Read (readMaybe)
48import Network.Socket hiding (Connected, connect, listen)
49import Network.Socket.ByteString as BS
50import Network.URI
51import System.Timeout
52
53import Network.BitTorrent.Tracker.Message
54
55{-----------------------------------------------------------------------
56-- Options
57-----------------------------------------------------------------------}
58
59-- | 'System.Timeout.timeout' specific.
60sec :: Int
61sec = 1000000
62
63-- | See <http://www.bittorrent.org/beps/bep_0015.html#time-outs>
64defMinTimeout :: Int
65defMinTimeout = 15
66
67-- | See <http://www.bittorrent.org/beps/bep_0015.html#time-outs>
68defMaxTimeout :: Int
69defMaxTimeout = 15 * 2 ^ (8 :: Int)
70
71-- | See: <http://www.bittorrent.org/beps/bep_0015.html#time-outs>
72defMultiplier :: Int
73defMultiplier = 2
74
75-- TODO why 98?
76defMaxPacketSize :: Int
77defMaxPacketSize = 98
78
79-- | Manager configuration.
80data Options = Options
81 { -- | Max size of a /response/ packet.
82 --
83 -- 'optMaxPacketSize' /must/ be a positive value.
84 --
85 optMaxPacketSize :: {-# UNPACK #-} !Int
86
87 -- | Starting timeout interval in seconds. If a response is not
88 -- received after 'optMinTimeout' then 'Manager' repeat RPC with
89 -- timeout interval multiplied by 'optMultiplier' and so on until
90 -- timeout interval reach 'optMaxTimeout'.
91 --
92 -- 'optMinTimeout' /must/ be a positive value.
93 --
94 , optMinTimeout :: {-# UNPACK #-} !Int
95
96 -- | Final timeout interval in seconds. After 'optMaxTimeout'
97 -- reached and tracker still not responding both 'announce' and
98 -- 'scrape' functions will throw 'TimeoutExpired' exception.
99 --
100 -- 'optMaxTimeout' /must/ be greater than 'optMinTimeout'.
101 --
102 , optMaxTimeout :: {-# UNPACK #-} !Int
103
104 -- | 'optMultiplier' /must/ be a positive value.
105 , optMultiplier :: {-# UNPACK #-} !Int
106 } deriving (Show, Eq)
107
108-- | Options suitable for bittorrent client.
109instance Default Options where
110 def = Options
111 { optMaxPacketSize = defMaxPacketSize
112 , optMinTimeout = defMinTimeout
113 , optMaxTimeout = defMaxTimeout
114 , optMultiplier = defMultiplier
115 }
116
117checkOptions :: Options -> IO ()
118checkOptions Options {..} = do
119 unless (optMaxPacketSize > 0) $ do
120 throwIO $ userError "optMaxPacketSize must be positive"
121
122 unless (optMinTimeout > 0) $ do
123 throwIO $ userError "optMinTimeout must be positive"
124
125 unless (optMaxTimeout > 0) $ do
126 throwIO $ userError "optMaxTimeout must be positive"
127
128 unless (optMultiplier > 0) $ do
129 throwIO $ userError "optMultiplier must be positive"
130
131 unless (optMaxTimeout > optMinTimeout) $ do
132 throwIO $ userError "optMaxTimeout must be greater than optMinTimeout"
133
134
135{-----------------------------------------------------------------------
136-- Manager state
137-----------------------------------------------------------------------}
138
139type ConnectionCache = Map SockAddr Connection
140
141type PendingResponse = MVar (Either RpcException Response)
142type PendingTransactions = Map TransactionId PendingResponse
143type PendingQueries = Map SockAddr PendingTransactions
144
145-- | UDP tracker manager.
146data Manager = Manager
147 { options :: !Options
148 , sock :: !Socket
149-- , dnsCache :: !(IORef (Map URI SockAddr))
150 , connectionCache :: !(IORef ConnectionCache)
151 , pendingResps :: !(MVar PendingQueries)
152 , listenerThread :: !(MVar ThreadId)
153 }
154
155initManager :: Options -> IO Manager
156initManager opts = Manager opts
157 <$> socket AF_INET Datagram defaultProtocol
158 <*> newIORef M.empty
159 <*> newMVar M.empty
160 <*> newEmptyMVar
161
162unblockAll :: PendingQueries -> IO ()
163unblockAll m = traverse (traverse unblockCall) m >> return ()
164 where
165 unblockCall ares = putMVar ares (Left ManagerClosed)
166
167resetState :: Manager -> IO ()
168resetState Manager {..} = do
169 writeIORef connectionCache err
170 m <- swapMVar pendingResps err
171 unblockAll m
172 mtid <- tryTakeMVar listenerThread
173 case mtid of
174 Nothing -> return () -- thread killed by 'closeManager'
175 Just _ -> return () -- thread killed by exception from 'listen'
176 return ()
177 where
178 err = error "UDP tracker manager closed"
179
180-- | This function will throw 'IOException' on invalid 'Options'.
181newManager :: Options -> IO Manager
182newManager opts = do
183 checkOptions opts
184 mgr <- initManager opts
185 tid <- forkIO (listen mgr `finally` resetState mgr)
186 putMVar (listenerThread mgr) tid
187 return mgr
188
189-- | Unblock all RPCs by throwing 'ManagerClosed' exception. No rpc
190-- calls should be performed after manager becomes closed.
191closeManager :: Manager -> IO ()
192closeManager Manager {..} = do
193 close sock
194 mtid <- tryTakeMVar listenerThread
195 case mtid of
196 Nothing -> return ()
197 Just tid -> killThread tid
198
199-- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'.
200withManager :: Options -> (Manager -> IO a) -> IO a
201withManager opts = bracket (newManager opts) closeManager
202
203{-----------------------------------------------------------------------
204-- Exceptions
205-----------------------------------------------------------------------}
206
207data RpcException
208 -- | Unable to lookup hostname;
209 = HostUnknown
210
211 -- | Unable to lookup hostname;
212 | HostLookupFailed
213
214 -- | Expecting 'udp:', but some other scheme provided.
215 | UnrecognizedScheme String
216
217 -- | Tracker exists but not responding for specific number of seconds.
218 | TimeoutExpired Int
219
220 -- | Tracker responded with unexpected message type.
221 | UnexpectedResponse
222 { expectedMsg :: String
223 , actualMsg :: String
224 }
225
226 -- | RPC succeed, but tracker responded with error code.
227 | QueryFailed Text
228
229 -- | RPC manager closed while waiting for response.
230 | ManagerClosed
231 deriving (Eq, Show, Typeable)
232
233instance Exception RpcException
234
235{-----------------------------------------------------------------------
236-- Host Addr resolution
237-----------------------------------------------------------------------}
238
239setPort :: PortNumber -> SockAddr -> SockAddr
240setPort p (SockAddrInet _ h) = SockAddrInet p h
241setPort p (SockAddrInet6 _ f h s) = SockAddrInet6 p f h s
242setPort _ addr = addr
243
244resolveURI :: URI -> IO SockAddr
245resolveURI URI { uriAuthority = Just (URIAuth {..}) } = do
246 infos <- getAddrInfo Nothing (Just uriRegName) Nothing
247 let port = fromMaybe 0 (readMaybe (L.drop 1 uriPort) :: Maybe Int)
248 case infos of
249 AddrInfo {..} : _ -> return $ setPort (fromIntegral port) addrAddress
250 _ -> throwIO HostLookupFailed
251resolveURI _ = throwIO HostUnknown
252
253-- TODO caching?
254getTrackerAddr :: Manager -> URI -> IO SockAddr
255getTrackerAddr _ uri
256 | uriScheme uri == "udp:" = resolveURI uri
257 | otherwise = throwIO (UnrecognizedScheme (uriScheme uri))
258
259{-----------------------------------------------------------------------
260 Connection
261-----------------------------------------------------------------------}
262
263connectionLifetime :: NominalDiffTime
264connectionLifetime = 60
265
266data Connection = Connection
267 { connectionId :: ConnectionId
268 , connectionTimestamp :: UTCTime
269 } deriving Show
270
271-- placeholder for the first 'connect'
272initialConnection :: Connection
273initialConnection = Connection initialConnectionId (posixSecondsToUTCTime 0)
274
275establishedConnection :: ConnectionId -> IO Connection
276establishedConnection cid = Connection cid <$> getCurrentTime
277
278isExpired :: Connection -> IO Bool
279isExpired Connection {..} = do
280 currentTime <- getCurrentTime
281 let timeDiff = diffUTCTime currentTime connectionTimestamp
282 return $ timeDiff > connectionLifetime
283
284{-----------------------------------------------------------------------
285-- Transactions
286-----------------------------------------------------------------------}
287
288-- | Sometimes 'genTransactionId' may return already used transaction
289-- id. We use a good entropy source but the issue /still/ (with very
290-- small probabality) may happen. If the collision happen then this
291-- function tries to find nearest unused slot, otherwise pending
292-- transactions table is full.
293firstUnused :: SockAddr -> TransactionId -> PendingQueries -> TransactionId
294firstUnused addr rid m = do
295 case M.splitLookup rid <$> M.lookup addr m of
296 Nothing -> rid
297 Just (_ , Nothing, _ ) -> rid
298 Just (lt, Just _ , gt) ->
299 case backwardHole (keys lt) rid <|> forwardHole rid (keys gt) of
300 Nothing -> error "firstUnused: table is full" -- impossible
301 Just tid -> tid
302 where
303 forwardHole a []
304 | a == maxBound = Nothing
305 | otherwise = Just (succ a)
306 forwardHole a (b : xs)
307 | succ a == b = forwardHole b xs
308 | otherwise = Just (succ a)
309
310 backwardHole [] a
311 | a == minBound = Nothing
312 | otherwise = Just (pred a)
313 backwardHole (b : xs) a
314 | b == pred a = backwardHole xs b
315 | otherwise = Just (pred a)
316
317register :: SockAddr -> TransactionId -> PendingResponse
318 -> PendingQueries -> PendingQueries
319register addr tid ares = M.alter insertId addr
320 where
321 insertId Nothing = Just (M.singleton tid ares)
322 insertId (Just m) = Just (M.insert tid ares m)
323
324unregister :: SockAddr -> TransactionId
325 -> PendingQueries -> PendingQueries
326unregister addr tid = M.update deleteId addr
327 where
328 deleteId m
329 | M.null m' = Nothing
330 | otherwise = Just m'
331 where
332 m' = M.delete tid m
333
334-- | Generate a new unused transaction id and register as pending.
335allocTransaction :: Manager -> SockAddr -> PendingResponse -> IO TransactionId
336allocTransaction Manager {..} addr ares =
337 modifyMVar pendingResps $ \ m -> do
338 rndId <- genTransactionId
339 let tid = firstUnused addr rndId m
340 return (register addr tid ares m, tid)
341
342-- | Wake up blocked thread and return response back.
343commitTransaction :: Manager -> SockAddr -> TransactionId -> Response -> IO ()
344commitTransaction Manager {..} addr tid resp =
345 modifyMVarMasked_ pendingResps $ \ m -> do
346 case M.lookup tid =<< M.lookup addr m of
347 Nothing -> return m -- tracker responded after 'cancelTransaction' fired
348 Just ares -> do
349 putMVar ares (Right resp)
350 return $ unregister addr tid m
351
352-- | Abort transaction forcefully.
353cancelTransaction :: Manager -> SockAddr -> TransactionId -> IO ()
354cancelTransaction Manager {..} addr tid =
355 modifyMVarMasked_ pendingResps $ \m ->
356 return $ unregister addr tid m
357
358-- | Handle responses from trackers.
359listen :: Manager -> IO ()
360listen mgr @ Manager {..} = do
361 forever $ do
362 (bs, addr) <- BS.recvFrom sock (optMaxPacketSize options)
363 case decode bs of
364 Left _ -> return () -- parser failed, ignoring
365 Right (TransactionR {..}) -> commitTransaction mgr addr transIdR response
366
367-- | Perform RPC transaction. If the action interrupted transaction
368-- will be aborted.
369transaction :: Manager -> SockAddr -> Connection -> Request -> IO Response
370transaction mgr @ Manager {..} addr conn request = do
371 ares <- newEmptyMVar
372 tid <- allocTransaction mgr addr ares
373 performTransaction tid ares
374 `onException` cancelTransaction mgr addr tid
375 where
376 performTransaction tid ares = do
377 let trans = TransactionQ (connectionId conn) tid request
378 BS.sendAllTo sock (encode trans) addr
379 takeMVar ares >>= either throwIO return
380
381{-----------------------------------------------------------------------
382-- Connection cache
383-----------------------------------------------------------------------}
384
385connect :: Manager -> SockAddr -> Connection -> IO ConnectionId
386connect m addr conn = do
387 resp <- transaction m addr conn Connect
388 case resp of
389 Connected cid -> return cid
390 Failed msg -> throwIO $ QueryFailed msg
391 _ -> throwIO $ UnexpectedResponse "connected" (responseName resp)
392
393newConnection :: Manager -> SockAddr -> IO Connection
394newConnection m addr = do
395 connId <- connect m addr initialConnection
396 establishedConnection connId
397
398refreshConnection :: Manager -> SockAddr -> Connection -> IO Connection
399refreshConnection mgr addr conn = do
400 expired <- isExpired conn
401 if expired
402 then do
403 connId <- connect mgr addr conn
404 establishedConnection connId
405 else do
406 return conn
407
408withCache :: Manager -> SockAddr
409 -> (Maybe Connection -> IO Connection) -> IO Connection
410withCache mgr addr action = do
411 cache <- readIORef (connectionCache mgr)
412 conn <- action (M.lookup addr cache)
413 writeIORef (connectionCache mgr) (M.insert addr conn cache)
414 return conn
415
416getConnection :: Manager -> SockAddr -> IO Connection
417getConnection mgr addr = withCache mgr addr $
418 maybe (newConnection mgr addr) (refreshConnection mgr addr)
419
420{-----------------------------------------------------------------------
421-- RPC
422-----------------------------------------------------------------------}
423
424retransmission :: Options -> IO a -> IO a
425retransmission Options {..} action = go optMinTimeout
426 where
427 go curTimeout
428 | curTimeout > optMaxTimeout = throwIO $ TimeoutExpired curTimeout
429 | otherwise = do
430 r <- timeout (curTimeout * sec) action
431 maybe (go (optMultiplier * curTimeout)) return r
432
433queryTracker :: Manager -> URI -> Request -> IO Response
434queryTracker mgr uri req = do
435 addr <- getTrackerAddr mgr uri
436 retransmission (options mgr) $ do
437 conn <- getConnection mgr addr
438 transaction mgr addr conn req
439
440-- | This function can throw 'RpcException'.
441announce :: Manager -> URI -> AnnounceQuery -> IO AnnounceInfo
442announce mgr uri q = do
443 resp <- queryTracker mgr uri (Announce q)
444 case resp of
445 Announced info -> return info
446 _ -> throwIO $ UnexpectedResponse "announce" (responseName resp)
447
448-- | This function can throw 'RpcException'.
449scrape :: Manager -> URI -> ScrapeQuery -> IO ScrapeInfo
450scrape mgr uri ihs = do
451 resp <- queryTracker mgr uri (Scrape ihs)
452 case resp of
453 Scraped info -> return $ L.zip ihs info
454 _ -> throwIO $ UnexpectedResponse "scrape" (responseName resp)
diff --git a/src/Network/BitTorrent/Tracker/Session.hs b/src/Network/BitTorrent/Tracker/Session.hs
new file mode 100644
index 00000000..cef7d665
--- /dev/null
+++ b/src/Network/BitTorrent/Tracker/Session.hs
@@ -0,0 +1,306 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2014
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Multitracker sessions.
9--
10{-# LANGUAGE FlexibleInstances #-}
11{-# LANGUAGE TypeFamilies #-}
12{-# LANGUAGE TypeSynonymInstances #-}
13{-# LANGUAGE TemplateHaskell #-}
14module Network.BitTorrent.Tracker.Session
15 ( -- * Session
16 Session
17 , Event (..)
18 , newSession
19 , closeSession
20 , withSession
21
22 -- * Client send notifications
23 , notify
24 , askPeers
25
26 -- * Session state
27 -- ** Status
28 , Status (..)
29 , getStatus
30
31 -- ** Single tracker sessions
32 , LastScrape (..)
33 , TrackerSession
34 , trackerPeers
35 , trackerScrape
36 , getSessionState
37
38 -- * Tracker Exchange
39 -- | BEP28: <http://www.bittorrent.org/beps/bep_0028.html>
40 , addTracker
41 , removeTracker
42 , getTrustedTrackers
43 ) where
44
45import Control.Applicative
46import Control.Exception
47import Control.Concurrent
48import Control.Concurrent.Chan.Split as CS
49import Control.Monad
50import Data.Default
51import Data.Fixed
52import Data.Foldable as F
53import Data.IORef
54import Data.List as L
55import Data.Maybe
56import Data.Time
57import Data.Traversable
58import Network.URI
59
60import Data.Torrent
61import Network.BitTorrent.Address
62import Network.BitTorrent.Internal.Cache
63import Network.BitTorrent.Internal.Types
64import Network.BitTorrent.Tracker.List as TL
65import Network.BitTorrent.Tracker.Message
66import Network.BitTorrent.Tracker.RPC as RPC
67
68{-----------------------------------------------------------------------
69-- Single tracker session
70-----------------------------------------------------------------------}
71
72-- | Status of this client.
73data Status
74 = Running -- ^ This client is announced and listenning for incoming
75 -- connections.
76 | Paused -- ^ This client does not expecting incoming connections.
77 deriving (Show, Eq, Bounded, Enum)
78
79-- | Client starting in the paused state.
80instance Default Status where
81 def = Paused
82
83-- | Tracker session starts with scrape unknown.
84instance Default LastScrape where
85 def = LastScrape Nothing Nothing
86
87data LastScrape = LastScrape
88 { -- | Count of leechers the tracker aware of.
89 scrapeLeechers :: Maybe Int
90
91 -- | Count of seeders the tracker aware of.
92 , scrapeSeeders :: Maybe Int
93 } deriving (Show, Eq)
94
95-- | Single tracker session.
96data TrackerSession = TrackerSession
97 { -- | Used to notify 'Stopped' and 'Completed' events.
98 statusSent :: !(Maybe Status)
99
100 -- | Can be used to retrieve peer set.
101 , trackerPeers :: Cached [PeerAddr IP]
102
103 -- | Can be used to show brief swarm stats in client GUI.
104 , trackerScrape :: Cached LastScrape
105 }
106
107-- | Not contacted.
108instance Default TrackerSession where
109 def = TrackerSession Nothing def def
110
111-- | Do we need to notify this /specific/ tracker?
112needNotify :: AnnounceEvent -> Maybe Status -> Maybe Bool
113needNotify Started Nothing = Just True
114needNotify Stopped Nothing = Just False
115needNotify Completed Nothing = Just False
116needNotify Started (Just Running) = Nothing
117needNotify Stopped (Just Running) = Just True
118needNotify Completed (Just Running) = Just True
119needNotify Started (Just Paused ) = Just True
120needNotify Stopped (Just Paused ) = Just False
121needNotify Completed (Just Paused ) = Just True
122
123-- | Client status after event announce succeed.
124nextStatus :: AnnounceEvent -> Maybe Status
125nextStatus Started = Just Running
126nextStatus Stopped = Just Paused
127nextStatus Completed = Nothing -- must keep previous status
128
129seconds :: Int -> NominalDiffTime
130seconds n = realToFrac (toEnum n :: Uni)
131
132cachePeers :: AnnounceInfo -> IO (Cached [PeerAddr IP])
133cachePeers AnnounceInfo {..} =
134 newCached (seconds respInterval)
135 (seconds (fromMaybe respInterval respMinInterval))
136 (getPeerList respPeers)
137
138cacheScrape :: AnnounceInfo -> IO (Cached LastScrape)
139cacheScrape AnnounceInfo {..} =
140 newCached (seconds respInterval)
141 (seconds (fromMaybe respInterval respMinInterval))
142 LastScrape
143 { scrapeSeeders = respComplete
144 , scrapeLeechers = respIncomplete
145 }
146
147-- | Make announce request to specific tracker returning new state.
148notifyTo :: Manager -> Session -> AnnounceEvent
149 -> TierEntry TrackerSession -> IO TrackerSession
150notifyTo mgr s @ Session {..} event (uri, entry @ TrackerSession {..}) = do
151 let shouldNotify = needNotify event statusSent
152 mustNotify <- maybe (isExpired trackerPeers) return shouldNotify
153 if not mustNotify
154 then return entry
155 else do
156 let q = SAnnounceQuery sessionTopic def Nothing (Just event)
157 res <- RPC.announce mgr uri q
158 when (statusSent == Nothing) $ do
159 send sessionEvents (TrackerConfirmed uri)
160 send sessionEvents (AnnouncedTo uri)
161 let status' = nextStatus event <|> statusSent
162 TrackerSession status' <$> cachePeers res <*> cacheScrape res
163
164{-----------------------------------------------------------------------
165-- Multitracker Session
166-----------------------------------------------------------------------}
167
168-- | Multitracker session.
169data Session = Session
170 { -- | Infohash to announce at each 'announce' request.
171 sessionTopic :: !InfoHash
172
173 -- | Current status of this client is used to filter duplicated
174 -- notifications, for e.g. we don't want to notify a tracker with
175 -- ['Stopped', 'Stopped'], the last should be ignored.
176 , sessionStatus :: !(IORef Status)
177
178 -- | A set of single-tracker sessions. Any request to a tracker
179 -- must take a lock.
180 , sessionTrackers :: !(MVar (TrackerList TrackerSession))
181
182 , sessionEvents :: !(SendPort (Event Session))
183 }
184
185instance EventSource Session where
186 data Event Session
187 = TrackerAdded URI
188 | TrackerConfirmed URI
189 | TrackerRemoved URI
190 | AnnouncedTo URI
191 | SessionClosed
192
193 listen Session {..} = CS.listen sessionEvents
194
195
196-- | Create a new multitracker session in paused state. Tracker list
197-- must contant only /trusted/ tracker uris. To start announcing
198-- client presence use 'notify'.
199newSession :: InfoHash -> TrackerList () -> IO Session
200newSession ih origUris = do
201 urisList <- shuffleTiers origUris
202 statusRef <- newIORef def
203 entriesVar <- newMVar (fmap (const def) urisList)
204 eventStream <- newSendPort
205 return Session
206 { sessionTopic = ih
207 , sessionStatus = statusRef
208 , sessionTrackers = entriesVar
209 , sessionEvents = eventStream
210 }
211
212-- | Release scarce resources associated with the given session. This
213-- function block until all trackers tied with this peer notified with
214-- 'Stopped' event.
215closeSession :: Manager -> Session -> IO ()
216closeSession m s @ Session {..} = do
217 notify m s Stopped
218 send sessionEvents SessionClosed
219
220{-----------------------------------------------------------------------
221-- Operations
222-----------------------------------------------------------------------}
223
224-- | Normally you need to use 'Control.Monad.Trans.Resource.alloc'.
225withSession :: Manager -> InfoHash -> TrackerList ()
226 -> (Session -> IO ()) -> IO ()
227withSession m ih uris = bracket (newSession ih uris) (closeSession m)
228
229-- | Get last announced status. The only action can alter this status
230-- is 'notify'.
231getStatus :: Session -> IO Status
232getStatus Session {..} = readIORef sessionStatus
233
234getSessionState :: Session -> IO [[TierEntry TrackerSession]]
235getSessionState Session {..} = TL.toList <$> readMVar sessionTrackers
236
237-- | Do we need to sent this event to a first working tracker or to
238-- the all known good trackers?
239allNotify :: AnnounceEvent -> Bool
240allNotify Started = False
241allNotify Stopped = True
242allNotify Completed = True
243
244notifyAll :: Manager -> Session -> AnnounceEvent -> IO ()
245notifyAll mgr s @ Session {..} event = do
246 modifyMVar_ sessionTrackers $
247 (traversal (notifyTo mgr s event))
248 where
249 traversal
250 | allNotify event = traverseAll
251 | otherwise = traverseTiers
252
253-- TODO send notifications to tracker periodically.
254-- |
255--
256-- This function /may/ block until tracker query proceed.
257notify :: Manager -> Session -> AnnounceEvent -> IO ()
258notify mgr ses event = do
259 prevStatus <- atomicModifyIORef (sessionStatus ses) $ \ s ->
260 (fromMaybe s (nextStatus event), s)
261 when (needNotify event (Just prevStatus) == Just True) $ do
262 notifyAll mgr ses event
263
264-- TODO run announce if sesion have no peers
265-- | The returned list of peers can have duplicates.
266-- This function /may/ block. Use async if needed.
267askPeers :: Manager -> Session -> IO [PeerAddr IP]
268askPeers _mgr ses = do
269 list <- readMVar (sessionTrackers ses)
270 L.concat <$> collect (tryTakeData . trackerPeers) list
271
272collect :: (a -> IO (Maybe b)) -> TrackerList a -> IO [b]
273collect f lst = (catMaybes . F.toList) <$> traverse f lst
274
275--sourcePeers :: Session -> Source (PeerAddr IP)
276--sourcePeers
277
278{-----------------------------------------------------------------------
279-- Tracker exchange
280-----------------------------------------------------------------------}
281
282-- Trackers discovered through this protocol SHOULD be treated with a
283-- certain amount of suspicion. Since the source of a tracker exchange
284-- message cannot be trusted, an implementation SHOULD have a lower
285-- number of retries before giving up entirely.
286
287addTracker :: Session -> URI -> IO ()
288addTracker Session {..} uri = do
289 undefined
290 send sessionEvents (TrackerAdded uri)
291
292removeTracker :: Manager -> Session -> URI -> IO ()
293removeTracker m Session {..} uri = do
294 send sessionEvents (TrackerRemoved uri)
295
296-- Also, as specified under the definitions section, a tracker that
297-- has not worked should never be propagated to other peers over the
298-- tracker exchange protocol.
299
300-- | Return all known trackers.
301getTrackers :: Session -> IO [URI]
302getTrackers = undefined
303
304-- | Return trackers from torrent file and
305getTrustedTrackers :: Session -> IO [URI]
306getTrustedTrackers = undefined