summaryrefslogtreecommitdiff
path: root/src/Network/BitTorrent/DHT.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Network/BitTorrent/DHT.hs')
-rw-r--r--src/Network/BitTorrent/DHT.hs362
1 files changed, 0 insertions, 362 deletions
diff --git a/src/Network/BitTorrent/DHT.hs b/src/Network/BitTorrent/DHT.hs
deleted file mode 100644
index 1a67c7c4..00000000
--- a/src/Network/BitTorrent/DHT.hs
+++ /dev/null
@@ -1,362 +0,0 @@
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 FlexibleContexts #-}
19{-# LANGUAGE TemplateHaskell #-}
20{-# LANGUAGE TypeOperators #-}
21{-# LANGUAGE ScopedTypeVariables #-}
22{-# LANGUAGE CPP #-}
23module Network.BitTorrent.DHT
24 ( -- * Distributed Hash Table
25 DHT
26 , Options (..)
27 , fullLogging
28 , dht
29
30 -- * Bootstrapping
31 -- $bootstrapping-terms
32 , tNodes
33 , defaultBootstrapNodes
34 , resolveHostName
35 , bootstrap
36 , isBootstrapped
37
38 -- * Initialization
39 , snapshot
40
41 -- * Operations
42 -- , Network.BitTorrent.DHT.lookup
43 , Network.BitTorrent.DHT.insert
44 , Network.BitTorrent.DHT.delete
45
46 -- * Embedding
47 -- ** Session
48 , LogFun
49 , Node
50 , defaultHandlers
51 , newNode
52 , closeNode
53
54 -- ** Monad
55 -- , MonadDHT (..)
56 , runDHT
57 ) where
58
59import Control.Monad.Logger
60import Control.Monad.Reader
61import Control.Exception
62import qualified Data.ByteString as BS
63import Data.Conduit as C
64import qualified Data.Conduit.List as C
65import Data.Serialize
66import Network.Socket
67import Text.PrettyPrint.HughesPJClass as PP (pPrint,render)
68
69import Data.Torrent
70import Network.Address
71import Network.BitTorrent.DHT.Query
72import Network.BitTorrent.DHT.Session
73import Network.DHT.Routing as T hiding (null)
74import qualified Data.Text as Text
75import Data.Typeable
76import Data.Monoid
77import Network.DatagramServer.Mainline (KMessageOf)
78import qualified Network.DatagramServer as KRPC (listen, Protocol(..))
79import Network.DatagramServer.Types
80import Network.DHT.Types
81import Data.Bits
82import Data.Default
83import Text.PrettyPrint.HughesPJClass hiding (($$), (<>))
84import Network.KRPC.Method
85
86{-----------------------------------------------------------------------
87-- DHT types
88-----------------------------------------------------------------------}
89
90#if 0
91class MonadDHT m where
92 liftDHT :: DHT raw dht u IPv4 a -> m a
93
94instance MonadDHT (DHT raw dht u IPv4) where
95 liftDHT = id
96#endif
97
98-- | Convenience method. Pass this to 'dht' to enable full logging.
99fullLogging :: LogSource -> LogLevel -> Bool
100fullLogging _ _ = True
101
102-- | Run DHT on specified port. <add note about resources>
103dht ::
104 ( Ord ip
105 , Address ip
106 , Functor dht
107 , Ord (NodeId dht)
108 , FiniteBits (NodeId dht)
109 , Serialize (NodeId dht)
110 , Show (NodeId dht)
111 , SerializableTo raw (Response dht (Ping dht))
112 , SerializableTo raw (Query dht (Ping dht))
113 , SerializableTo raw (Response dht (NodeFound dht ip))
114 , SerializableTo raw (Query dht (FindNode dht ip))
115 , Ord (TransactionID dht)
116 , Serialize (TransactionID dht)
117 , Eq (QueryMethod dht)
118 , Show (QueryMethod dht)
119 , Pretty (NodeInfo dht ip u)
120 , Kademlia dht
121 , KRPC dht (Query dht (FindNode dht ip)) (Response dht (NodeFound dht ip))
122 , KRPC dht (Query dht (Ping dht)) (Response dht (Ping dht))
123 , DataHandlers raw dht
124 , WireFormat raw dht
125 , Show u
126 , Default u
127 , Typeable dht
128 )
129 => Options -- ^ normally you need to use 'Data.Default.def';
130 -> NodeAddr ip -- ^ address to bind this node;
131 -> (LogSource -> LogLevel -> Bool) -- ^ use 'fullLogging' as a noisy default
132 -> DHT raw dht u ip a -- ^ actions to run: 'bootstrap', 'lookup', etc;
133 -> IO a -- ^ result.
134dht opts addr logfilter action = do
135 runStderrLoggingT $ filterLogger logfilter $ LoggingT $ \ logger -> do
136 bracket (newNode opts addr logger Nothing) closeNode $
137 \ node -> runDHT node $ do
138 hs <- defaultHandlers logger
139 m <- asks manager
140 liftIO $ KRPC.listen m hs (KRPC.Protocol Proxy Proxy)
141 action
142{-# INLINE dht #-}
143
144{-----------------------------------------------------------------------
145-- Bootstrapping
146-----------------------------------------------------------------------}
147-- $bootstrapping-terms
148--
149-- [@Bootstrapping@] DHT @bootstrapping@ is the process of filling
150-- routing 'Table' by /good/ nodes.
151--
152-- [@Bootstrapping time@] Bootstrapping process can take up to 5
153-- minutes. Bootstrapping should only happen at first application
154-- startup, if possible you should use 'snapshot' & 'restore'
155-- mechanism which must work faster.
156--
157-- [@Bootstrap nodes@] DHT @bootstrap node@ is either:
158--
159-- * a specialized high performance node maintained by bittorrent
160-- software authors\/maintainers, like those listed in
161-- 'defaultBootstrapNodes'. /Specialized/ means that those nodes
162-- may not support 'insert' queries and is running for the sake of
163-- bootstrapping only.
164--
165-- * an ordinary bittorrent client running DHT node. The list of
166-- such bootstrapping nodes usually obtained from
167-- 'Data.Torrent.tNodes' field or
168-- 'Network.BitTorrent.Exchange.Message.Port' messages.
169
170-- Do not include the following hosts in the default bootstrap nodes list:
171--
172-- * "dht.aelitis.com" and "dht6.azureusplatform.com" - since
173-- Azureus client have a different (and probably incompatible) DHT
174-- protocol implementation.
175--
176-- * "router.utorrent.com" since it is just an alias to
177-- "router.bittorrent.com".
178-- XXX: ignoring this advise as it resolves to a different
179-- ip address for me.
180
181-- | List of bootstrap nodes maintained by different bittorrent
182-- software authors.
183defaultBootstrapNodes :: [NodeAddr HostName]
184defaultBootstrapNodes =
185 [ NodeAddr "router.bittorrent.com" 6881 -- by BitTorrent Inc.
186
187 -- doesn't work at the moment (use git blame) of commit
188 , NodeAddr "dht.transmissionbt.com" 6881 -- by Transmission project
189
190 , NodeAddr "router.utorrent.com" 6881
191 ]
192
193-- TODO Multihomed hosts
194
195-- | Resolve either a numeric network address or a hostname to a
196-- numeric IP address of the node. Usually used to resolve
197-- 'defaultBootstrapNodes' or 'Data.Torrent.tNodes' lists.
198resolveHostName :: NodeAddr HostName -> IO (NodeAddr IPv4)
199resolveHostName NodeAddr {..} = do
200 let hints = defaultHints { addrFamily = AF_INET, addrSocketType = Datagram }
201 -- getAddrInfo throws exception on empty list, so the pattern matching never fail
202 info : _ <- getAddrInfo (Just hints) (Just nodeHost) (Just (show nodePort))
203 case fromSockAddr (addrAddress info) of
204 Nothing -> error "resolveNodeAddr: impossible"
205 Just addr -> return addr
206
207-- | One good node may be sufficient.
208--
209-- This operation do block, use
210-- 'Control.Concurrent.Async.Lifted.async' if needed.
211bootstrap :: forall raw dht u ip.
212 ( Ord ip
213 , Address ip
214 , Functor dht
215 , Ord (NodeId dht)
216 , FiniteBits (NodeId dht)
217 , Serialize (NodeId dht)
218 , Show (NodeId dht)
219 , Pretty (NodeId dht)
220 , SerializableTo raw (Response dht (Ping dht))
221 , SerializableTo raw (Query dht (Ping dht))
222 , SerializableTo raw (Response dht (NodeFound dht ip))
223 , SerializableTo raw (Query dht (FindNode dht ip))
224 , Ord (TransactionID dht)
225 , Serialize (TransactionID dht)
226 , Eq (QueryMethod dht)
227 , Show (QueryMethod dht)
228 , Pretty (NodeInfo dht ip u)
229 , Kademlia dht
230 , KRPC dht (Query dht (FindNode dht ip)) (Response dht (NodeFound dht ip))
231 , KRPC dht (Query dht (Ping dht)) (Response dht (Ping dht))
232 , DataHandlers raw dht
233 , WireFormat raw dht
234 , Show u
235 , Default u
236 , Serialize u
237 ) => Maybe BS.ByteString -> [PacketDestination dht] -> DHT raw dht u ip ()
238bootstrap mbs startNodes = do
239 restored <-
240 case decode <$> mbs of
241 Just (Right tbl) -> return (T.toList tbl)
242 Just (Left e) -> do $(logWarnS) "restore" (Text.pack e)
243 return []
244 Nothing -> return []
245
246 $(logInfoS) "bootstrap" "Start node bootstrapping"
247 let searchAll aliveNodes = do
248 nid <- myNodeIdAccordingTo (error "FIXME")
249 ns <- bgsearch ioFindNodes nid
250 return ( ns :: [NodeInfo dht ip u] )
251 input_nodes <- (restored ++) . T.toList <$> getTable
252 -- Step 1: Use iterative searches to flesh out the table..
253 do let knowns = map (map $ fst) input_nodes
254 -- Below, we reverse the nodes since the table serialization puts the
255 -- nearest nodes last and we want to choose a similar node id to bootstrap
256 -- faster.
257 (alive_knowns,_) <- unzip <$> queryParallel (pingQ <$> reverse (concat knowns))
258 b <- isBootstrapped
259 -- If our cached nodes are alive and our IP address did not change, it's possible
260 -- we are already bootsrapped, so no need to do any searches.
261 when (not b) $ do
262 ns <- searchAll $ take 2 alive_knowns
263 -- We only use the supplied bootstrap nodes when we don't know of any
264 -- others to try.
265 when (null ns) $ do
266 -- TODO filter duplicated in startNodes list
267 -- TODO retransmissions for startNodes
268 (aliveNodes,_) <- unzip <$> queryParallel (coldPingQ <$> startNodes)
269 _ <- searchAll $ take 2 aliveNodes
270 return ()
271 -- Step 2: Repeatedly refresh incomplete buckets until the table is full.
272 maxbuckets <- asks $ optBucketCount . options
273 flip fix 0 $ \loop icnt -> do
274 tbl <- getTable
275 let unfull = filter ((/=defaultBucketSize) . snd)
276 us = zip
277 -- is_last = True for the last bucket
278 (True:repeat False)
279 -- Only non-full buckets unless it is the last one and the
280 -- maximum number of buckets has not been reached.
281 $ case reverse $ zip [0..] $ T.shape tbl of
282 p@(n,_):ps | n+1==maxbuckets -> unfull (p:ps)
283 p:ps -> p:unfull ps
284 [] -> []
285 forM_ us $ \(is_last,(index,_)) -> do
286 nid <- myNodeIdAccordingTo (error "FIXME")
287 sample <- liftIO $ genBucketSample nid (bucketRange index is_last)
288 $(logDebugS) "bootstrapping"
289 $ "BOOTSTRAP sample"
290 <> Text.pack (show (is_last,index,T.shape tbl))
291 <> " " <> Text.pack (render $ pPrint sample)
292 refreshNodes sample
293 $(logDebugS) "bootstrapping"
294 $ "BOOTSTRAP finished iteration "
295 <> Text.pack (show (icnt,T.shape tbl,us,defaultBucketSize))
296 when (not (null us) && icnt < div (3*maxbuckets) 2)
297 $ loop (succ icnt)
298 $(logInfoS) "bootstrap" "Node bootstrapping finished"
299
300-- | Check if this node is already bootstrapped.
301-- @bootstrap [good_node] >> isBootstrapped@@ should always return 'True'.
302--
303-- This operation do not block.
304--
305isBootstrapped :: Eq ip => DHT raw dht u ip Bool
306isBootstrapped = T.full <$> getTable
307
308{-----------------------------------------------------------------------
309-- Initialization
310-----------------------------------------------------------------------}
311
312-- | Serialize current DHT session to byte string.
313--
314-- This is blocking operation, use
315-- 'Control.Concurrent.Async.Lifted.async' if needed.
316snapshot :: ( Address ip
317 , Ord (NodeId dht)
318 , Serialize u
319 , Serialize (NodeId dht)
320 ) => DHT raw dht u ip BS.ByteString
321snapshot = do
322 tbl <- getTable
323 return $ encode tbl
324
325{-----------------------------------------------------------------------
326-- Operations
327-----------------------------------------------------------------------}
328
329#if 0
330
331-- | Get list of peers which downloading this torrent.
332--
333-- This operation is incremental and do block.
334--
335lookup :: Address ip => InfoHash -> DHT raw dht u ip `C.Source` [PeerAddr ip]
336lookup topic = do -- TODO retry getClosest if bucket is empty
337 closest <- lift $ getClosest topic
338 C.sourceList [closest] $= search topic (getPeersQ topic)
339
340#endif
341
342-- TODO do not republish if the topic is already in announceSet
343
344-- | Announce that /this/ peer may have some pieces of the specified
345-- torrent. DHT will reannounce this data periodically using
346-- 'optReannounce' interval.
347--
348-- This operation is synchronous and do block, use
349-- 'Control.Concurrent.Async.Lifted.async' if needed.
350--
351insert :: Address ip => InfoHash -> PortNumber -> DHT raw dht u ip ()
352insert ih p = do
353 publish ih p
354 insertTopic ih p
355
356-- | Stop announcing /this/ peer for the specified torrent.
357--
358-- This operation is atomic and may block for a while.
359--
360delete :: InfoHash -> PortNumber -> DHT raw dht u ip ()
361delete = deleteTopic
362{-# INLINE delete #-}