diff options
Diffstat (limited to 'src/Network/BitTorrent/DHT.hs')
-rw-r--r-- | src/Network/BitTorrent/DHT.hs | 285 |
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 #-} | ||
20 | module 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 | |||
56 | import Control.Monad.Logger | ||
57 | import Control.Monad.Reader | ||
58 | import Control.Exception | ||
59 | import qualified Data.ByteString as BS | ||
60 | import Data.Conduit as C | ||
61 | import qualified Data.Conduit.List as C | ||
62 | import Data.Serialize | ||
63 | import Network.Socket | ||
64 | import Text.PrettyPrint.HughesPJClass as PP (pPrint,render) | ||
65 | |||
66 | import Data.Torrent | ||
67 | import Network.BitTorrent.Address | ||
68 | import Network.BitTorrent.DHT.Query | ||
69 | import Network.BitTorrent.DHT.Session | ||
70 | import Network.BitTorrent.DHT.Routing as T hiding (null) | ||
71 | import qualified Data.Text as Text | ||
72 | import Data.Monoid | ||
73 | |||
74 | |||
75 | {----------------------------------------------------------------------- | ||
76 | -- DHT types | ||
77 | -----------------------------------------------------------------------} | ||
78 | |||
79 | class MonadDHT m where | ||
80 | liftDHT :: DHT IPv4 a -> m a | ||
81 | |||
82 | instance MonadDHT (DHT IPv4) where | ||
83 | liftDHT = id | ||
84 | |||
85 | -- | Convenience method. Pass this to 'dht' to enable full logging. | ||
86 | fullLogging :: LogSource -> LogLevel -> Bool | ||
87 | fullLogging _ _ = True | ||
88 | |||
89 | -- | Run DHT on specified port. <add note about resources> | ||
90 | dht :: 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. | ||
96 | dht 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. | ||
141 | defaultBootstrapNodes :: [NodeAddr HostName] | ||
142 | defaultBootstrapNodes = | ||
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. | ||
156 | resolveHostName :: NodeAddr HostName -> IO (NodeAddr IPv4) | ||
157 | resolveHostName 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. | ||
169 | bootstrap :: Address ip => Maybe BS.ByteString -> [NodeAddr ip] -> DHT ip () | ||
170 | bootstrap 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 | -- | ||
236 | isBootstrapped :: Eq ip => DHT ip Bool | ||
237 | isBootstrapped = 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. | ||
247 | snapshot :: Address ip => DHT ip BS.ByteString | ||
248 | snapshot = 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 | -- | ||
260 | lookup :: Address ip => InfoHash -> DHT ip `C.Source` [PeerAddr ip] | ||
261 | lookup 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 | -- | ||
274 | insert :: Address ip => InfoHash -> PortNumber -> DHT ip () | ||
275 | insert 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 | -- | ||
283 | delete :: InfoHash -> PortNumber -> DHT ip () | ||
284 | delete = deleteTopic | ||
285 | {-# INLINE delete #-} | ||