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.hs285
1 files changed, 285 insertions, 0 deletions
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 #-}