summaryrefslogtreecommitdiff
path: root/src/Network/BitTorrent/DHT
diff options
context:
space:
mode:
Diffstat (limited to 'src/Network/BitTorrent/DHT')
-rw-r--r--src/Network/BitTorrent/DHT/Protocol.hs339
1 files changed, 339 insertions, 0 deletions
diff --git a/src/Network/BitTorrent/DHT/Protocol.hs b/src/Network/BitTorrent/DHT/Protocol.hs
new file mode 100644
index 00000000..5267a916
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/Protocol.hs
@@ -0,0 +1,339 @@
1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE RecordWildCards #-}
3module Network.BitTorrent.DHT.Protocol
4 (
5 newNodeSession
6
7 -- * Tracker
8 , ping
9 , findNode
10 , getPeers
11 , announcePeer
12
13 -- * Server
14 , dhtServer
15 ) where
16
17import Control.Applicative
18import Control.Concurrent
19import Control.Concurrent.STM
20import Control.Monad
21import Control.Exception
22import Data.ByteString
23import Data.Serialize as S
24import Data.Function
25import Data.Ord
26import Data.Maybe
27import Data.List as L
28import Data.Map as M
29import Data.HashMap.Strict as HM
30import Network
31import Network.Socket
32import System.Entropy
33
34import Remote.KRPC
35import Remote.KRPC.Protocol
36import Data.BEncode
37import Data.Torrent
38import Network.BitTorrent.Peer
39
40{-----------------------------------------------------------------------
41 Node
42-----------------------------------------------------------------------}
43
44type NodeId = ByteString
45
46-- WARN is the 'system' random suitable for this?
47-- | Generate random NodeID used for the entire session.
48-- Distribution of ID's should be as uniform as possible.
49--
50genNodeId :: IO NodeId
51genNodeId = getEntropy 20
52
53instance Serialize PortNumber where
54 get = fromIntegral <$> getWord16be
55 put = putWord16be . fromIntegral
56
57
58data NodeAddr = NodeAddr {
59 nodeIP :: {-# UNPACK #-} !HostAddress
60 , nodePort :: {-# UNPACK #-} !PortNumber
61 } deriving (Show, Eq)
62
63instance Serialize NodeAddr where
64 get = NodeAddr <$> getWord32be <*> get
65 put NodeAddr {..} = do
66 putWord32be nodeIP
67 put nodePort
68
69
70data NodeInfo = NodeInfo {
71 nodeID :: !NodeId
72 , nodeAddr :: !NodeAddr
73 } deriving (Show, Eq)
74
75instance Serialize NodeInfo where
76 get = NodeInfo <$> getByteString 20 <*> get
77 put NodeInfo {..} = put nodeID >> put nodeAddr
78
79type CompactInfo = ByteString
80
81decodeCompact :: CompactInfo -> [NodeInfo]
82decodeCompact = either (const []) id . S.runGet (many get)
83
84encodeCompact :: [NodeId] -> CompactInfo
85encodeCompact = S.runPut . mapM_ put
86
87decodePeerList :: [BEncode] -> [PeerAddr]
88decodePeerList = undefined
89
90encodePeerList :: [PeerAddr] -> [BEncode]
91encodePeerList = undefined
92
93type Distance = NodeId
94
95{-----------------------------------------------------------------------
96 Tokens
97-----------------------------------------------------------------------}
98
99type Secret = Int
100
101genSecret :: IO Secret
102genSecret = error "secret"
103
104-- | Instead of periodically loop over the all nodes in the routing
105-- table with some given interval (or some other tricky method
106-- e.g. using timeouts) we can just update tokens on demand - if no
107-- one asks for a token then the token _should_ not change at all.
108--
109type Token = ByteString
110
111defaultToken :: Token
112defaultToken = "0xdeadbeef"
113
114genToken :: NodeAddr -> Secret -> Token
115genToken _ _ = defaultToken
116
117{-----------------------------------------------------------------------
118 Routing table
119-----------------------------------------------------------------------}
120
121type ContactInfo = HashMap InfoHash [PeerAddr]
122
123insertPeer :: InfoHash -> PeerAddr -> ContactInfo -> ContactInfo
124insertPeer ih addr = HM.insertWith (++) ih [addr]
125
126lookupPeers :: InfoHash -> ContactInfo -> [PeerAddr]
127lookupPeers ih = fromMaybe [] . HM.lookup ih
128
129-- TODO use more compact routing table
130type RoutingTable = HashMap NodeId NodeAddr
131
132insertNode :: NodeId -> NodeAddr -> RoutingTable -> RoutingTable
133insertNode = HM.insert
134
135type Alpha = Int
136
137defaultAlpha :: Alpha
138defaultAlpha = 8
139
140-- TODO
141kclosest :: Int -> NodeId -> RoutingTable -> [NodeId]
142kclosest = undefined
143
144{-----------------------------------------------------------------------
145 Node session
146-----------------------------------------------------------------------}
147
148data NodeSession = NodeSession {
149 nodeId :: !NodeId
150 , routingTable :: !(TVar RoutingTable)
151 , contactInfo :: !(TVar ContactInfo)
152-- , currentSecret :: !(TVar Secret)
153-- , secretTimestamp :: !(TVar Timestamp)
154 , alpha :: !Alpha
155 , listenerPort :: !PortNumber
156 }
157
158instance Eq NodeSession where
159 (==) = (==) `on` nodeId
160
161instance Ord NodeSession where
162 compare = comparing nodeId
163
164newNodeSession :: PortNumber -> IO NodeSession
165newNodeSession lport
166 = NodeSession
167 <$> genNodeId
168 <*> newTVarIO HM.empty
169 <*> newTVarIO HM.empty
170 <*> pure defaultAlpha
171 <*> pure lport
172
173assignToken :: NodeSession -> NodeId -> IO Token
174assignToken _ _ = return ""
175
176-- TODO
177checkToken :: NodeId -> Token -> NodeSession -> IO Bool
178checkToken nid token _ = return True
179
180updateTimestamp :: NodeSession -> NodeId -> IO ()
181updateTimestamp = error "updateTimestamp"
182
183updateToken :: NodeSession -> NodeId -> Token -> IO ()
184updateToken _ _ _ = error "updateToken"
185
186{-----------------------------------------------------------------------
187 DHT Queries
188-----------------------------------------------------------------------}
189
190pingM :: Method NodeId NodeId
191pingM = method "ping" ["id"] ["id"]
192
193findNodeM :: Method (NodeId, NodeId) (NodeId, CompactInfo)
194findNodeM = method "find_node" ["id", "target"] ["id", "nodes"]
195
196-- | Lookup peers by a torrent infohash. This method might return
197-- different kind of responses depending on the routing table of
198-- queried node:
199--
200-- * If quieried node contains a peer list for the given infohash
201-- then the node should return the list in a "value" key. Note that
202-- list is encoded as compact peer address, not a compact node info.
203-- The result of 'get_peers' method have the following scheme:
204--
205-- > { "id" : "dht_server_node_id"
206-- > , "token" : "assigned_token"
207-- > , "values" : ["_IP_PO", "_ip_po"]
208-- > }
209--
210-- * If quieried node does not contain a list of peers associated
211-- with the given infohash, then node should return
212--
213-- > { "id" : "dht_server_node_id"
214-- > , "token" : "assigned_token"
215-- > , "nodes" : "compact_nodes_info"
216-- > }
217--
218-- The resulting dictionaries might differ only in a values\/nodes
219-- keys.
220--
221getPeersM :: Method (NodeId, InfoHash) BEncode
222getPeersM = method "get_peers" ["id", "info_hash"] []
223
224-- | Used to announce that the peer, controlling the quering node is
225-- downloading a torrent on a port.
226announcePeerM :: Method (NodeId, InfoHash, PortNumber, Token) NodeId
227announcePeerM = method "announce_peer" ["id", "info_hash", "port", "token"] ["id"]
228
229{-----------------------------------------------------------------------
230 DHT Tracker
231-----------------------------------------------------------------------}
232-- TODO: update node timestamp on each successful call
233
234-- | Note that tracker side query functions could throw RPCException.
235type DHT a b = NodeSession -> NodeAddr -> a -> IO b
236
237ping :: DHT () ()
238ping NodeSession {..} addr @ NodeAddr {..} () = do
239 nid <- call (nodeIP, nodePort) pingM nodeId
240 atomically $ modifyTVar' routingTable $ HM.insert nid addr
241
242findNode :: DHT NodeId [NodeInfo]
243findNode ses @ NodeSession {..} NodeAddr {..} qnid = do
244 (nid, info) <- call (nodeIP, nodePort) findNodeM (nodeId, qnid)
245 updateTimestamp ses nid
246 return (decodeCompact info)
247
248getPeers :: DHT InfoHash (Either [NodeInfo] [PeerAddr])
249getPeers ses @ NodeSession {..} NodeAddr {..} ih = do
250 resp <- call (nodeIP, nodePort) getPeersM (nodeId, ih)
251 (nid, tok, res) <- extrResp resp
252 updateTimestamp ses nid
253 updateToken ses nid tok
254 return res
255 where
256 extrResp (BDict d)
257 | Just (BString nid ) <- M.lookup "id" d
258 , Just (BString tok ) <- M.lookup "token" d
259 , Just (BList values) <- M.lookup "values" d
260 = return $ (nid, tok, Right $ decodePeerList values)
261
262 | Just (BString nid ) <- M.lookup "id" d
263 , Just (BString tok ) <- M.lookup "token" d
264 , Just (BString nodes) <- M.lookup "nodes" d
265 = return (nid, tok, Left $ decodeCompact nodes)
266
267 extrResp _ = throw $ RPCException msg
268 where msg = ProtocolError "unable to extract getPeers resp"
269
270-- remove token from signature, handle the all token stuff by NodeSession
271
272-- | Note that before ever calling this method you should call the
273-- getPeerList.
274announcePeer :: DHT (InfoHash, Token) NodeId
275announcePeer ses @ NodeSession {..} NodeAddr {..} (ih, tok) = do
276 nid <- call (nodeIP, nodePort) announcePeerM (nodeId, ih, listenerPort, tok)
277 updateTimestamp ses nid
278 return nid
279
280{-----------------------------------------------------------------------
281 DHT Server
282-----------------------------------------------------------------------}
283-- TODO: update node timestamp on each successful call
284-- NOTE: ensure all server operations run in O(1)
285
286type ServerHandler a b = NodeSession -> NodeAddr -> a -> IO b
287
288pingS :: ServerHandler NodeId NodeId
289pingS NodeSession {..} addr nid = do
290 atomically $ modifyTVar' routingTable $ insertNode nid addr
291 return nodeId
292
293findNodeS :: ServerHandler (NodeId, NodeId) (NodeId, CompactInfo)
294findNodeS ses @ NodeSession {..} _ (nid, qnid) = do
295 updateTimestamp ses nid
296 rt <- atomically $ readTVar routingTable
297 return (nodeId, encodeCompact $ kclosest alpha qnid rt)
298
299getPeersS :: ServerHandler (NodeId, InfoHash) BEncode
300getPeersS ses @ NodeSession {..} _ (nid, ih) = do
301 updateTimestamp ses nid
302 mkResp <$> assignToken ses nid <*> findPeers
303 where
304 findPeers = do
305 list <- lookupPeers ih <$> readTVarIO contactInfo
306 if not (L.null list)
307 then return $ Right list
308 else do
309 rt <- readTVarIO routingTable
310 let nodes = kclosest alpha (getInfoHash ih) rt
311 return $ Left nodes
312
313 mkDict tok res = [("id",BString nodeId), ("token", BString tok), res]
314 mkResult (Left nodes ) = ("nodes", BString $ encodeCompact nodes)
315 mkResult (Right values) = ("values", BList $ encodePeerList values)
316 mkResp tok = BDict . M.fromList . mkDict tok . mkResult
317
318announcePeerS :: ServerHandler (NodeId, InfoHash, PortNumber, Token) NodeId
319announcePeerS ses @ NodeSession {..} NodeAddr {..} (nid, ih, port, token) = do
320 updateTimestamp ses nid
321 registered <- checkToken nid token ses
322 when registered $ do
323 atomically $ do
324 let peerAddr = PeerAddr Nothing nodeIP port
325 modifyTVar contactInfo $ insertPeer ih peerAddr
326 return nodeId
327
328dhtTracker :: NodeSession -> InfoHash -> Chan PeerAddr -> IO ()
329dhtTracker = undefined
330
331dhtServer :: NodeSession -> PortNumber -> IO ()
332dhtServer s p = server p methods
333 where
334 methods =
335 [ pingM ==> pingS s undefined
336 , findNodeM ==> findNodeS s undefined
337 , getPeersM ==> getPeersS s undefined
338 , announcePeerM ==> announcePeerS s undefined
339 ] \ No newline at end of file