diff options
Diffstat (limited to 'src/Network/BitTorrent/DHT.hs')
-rw-r--r-- | src/Network/BitTorrent/DHT.hs | 362 |
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 #-} | ||
23 | module 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 | |||
59 | import Control.Monad.Logger | ||
60 | import Control.Monad.Reader | ||
61 | import Control.Exception | ||
62 | import qualified Data.ByteString as BS | ||
63 | import Data.Conduit as C | ||
64 | import qualified Data.Conduit.List as C | ||
65 | import Data.Serialize | ||
66 | import Network.Socket | ||
67 | import Text.PrettyPrint.HughesPJClass as PP (pPrint,render) | ||
68 | |||
69 | import Data.Torrent | ||
70 | import Network.Address | ||
71 | import Network.BitTorrent.DHT.Query | ||
72 | import Network.BitTorrent.DHT.Session | ||
73 | import Network.DHT.Routing as T hiding (null) | ||
74 | import qualified Data.Text as Text | ||
75 | import Data.Typeable | ||
76 | import Data.Monoid | ||
77 | import Network.DatagramServer.Mainline (KMessageOf) | ||
78 | import qualified Network.DatagramServer as KRPC (listen, Protocol(..)) | ||
79 | import Network.DatagramServer.Types | ||
80 | import Network.DHT.Types | ||
81 | import Data.Bits | ||
82 | import Data.Default | ||
83 | import Text.PrettyPrint.HughesPJClass hiding (($$), (<>)) | ||
84 | import Network.KRPC.Method | ||
85 | |||
86 | {----------------------------------------------------------------------- | ||
87 | -- DHT types | ||
88 | -----------------------------------------------------------------------} | ||
89 | |||
90 | #if 0 | ||
91 | class MonadDHT m where | ||
92 | liftDHT :: DHT raw dht u IPv4 a -> m a | ||
93 | |||
94 | instance MonadDHT (DHT raw dht u IPv4) where | ||
95 | liftDHT = id | ||
96 | #endif | ||
97 | |||
98 | -- | Convenience method. Pass this to 'dht' to enable full logging. | ||
99 | fullLogging :: LogSource -> LogLevel -> Bool | ||
100 | fullLogging _ _ = True | ||
101 | |||
102 | -- | Run DHT on specified port. <add note about resources> | ||
103 | dht :: | ||
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. | ||
134 | dht 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. | ||
183 | defaultBootstrapNodes :: [NodeAddr HostName] | ||
184 | defaultBootstrapNodes = | ||
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. | ||
198 | resolveHostName :: NodeAddr HostName -> IO (NodeAddr IPv4) | ||
199 | resolveHostName 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. | ||
211 | bootstrap :: 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 () | ||
238 | bootstrap 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 | -- | ||
305 | isBootstrapped :: Eq ip => DHT raw dht u ip Bool | ||
306 | isBootstrapped = 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. | ||
316 | snapshot :: ( Address ip | ||
317 | , Ord (NodeId dht) | ||
318 | , Serialize u | ||
319 | , Serialize (NodeId dht) | ||
320 | ) => DHT raw dht u ip BS.ByteString | ||
321 | snapshot = 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 | -- | ||
335 | lookup :: Address ip => InfoHash -> DHT raw dht u ip `C.Source` [PeerAddr ip] | ||
336 | lookup 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 | -- | ||
351 | insert :: Address ip => InfoHash -> PortNumber -> DHT raw dht u ip () | ||
352 | insert 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 | -- | ||
360 | delete :: InfoHash -> PortNumber -> DHT raw dht u ip () | ||
361 | delete = deleteTopic | ||
362 | {-# INLINE delete #-} | ||