summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorjoe <joe@jerkface.net>2017-01-22 05:31:14 -0500
committerjoe <joe@jerkface.net>2017-01-22 05:31:14 -0500
commit8cf4de73d77197032fd8ebfc4e4f3a00b287e0e7 (patch)
tree6ee5d529caf714851223d2da9f22eb1510d5cfee /examples
parent1c8cbe8fc66466936b4f889b3893ca3c23478631 (diff)
New: DHT deamon and command-line interface.
Diffstat (limited to 'examples')
-rw-r--r--examples/dht.hs80
-rw-r--r--examples/dhtd.hs192
2 files changed, 272 insertions, 0 deletions
diff --git a/examples/dht.hs b/examples/dht.hs
new file mode 100644
index 00000000..feeee9ff
--- /dev/null
+++ b/examples/dht.hs
@@ -0,0 +1,80 @@
1{-# LANGUAGE NondecreasingIndentation #-}
2import Control.Monad
3import Control.Monad.Fix
4import Control.Monad.IO.Class
5import Data.Char
6import Network.Socket as Socket
7import System.Console.Haskeline
8import System.Environment
9import System.Exit
10import System.IO
11import System.IO.Unsafe
12import qualified Data.ByteString as B
13
14-- | Reads one character. If it is not a digit,
15-- then it is discarded and 'Nothing' is returned.
16hReadDigit :: Handle -> IO (Maybe Char)
17hReadDigit h = do c <- hGetChar h
18 return $ guard (isDigit c) >> pure c
19
20-- | Expected input: "nnn:..."
21-- Here we read the digit sequence "nnn" and drop the colon
22-- as it is the first non-digit.
23hReadInt :: Handle -> IO Int
24hReadInt h = do
25 nstr <- fix $ \readDigits ->
26 maybe (return []) -- dropped non-digit character
27 (($ unsafeInterleaveIO readDigits) . fmap . (:))
28 =<< hReadDigit h
29 readIO nstr :: IO Int
30
31
32-- | Read a length prefixed string from a handle.
33-- The format is "nnn:..." where /nnn/ is an ascii-encoded character count
34-- and /.../ is the sequence of characters
35--
36-- Note: The first byte after the count is ignored and discarded.
37readResponse :: Handle -> IO String
38readResponse h = do
39 n <- hReadInt h
40 sequence $ replicate n (hGetChar h)
41
42-- | Send a command to the dhtd daemon and then print the response.
43sendCommand :: Handle -> String -> InputT IO ()
44sendCommand h cmd = do resp <- liftIO $ do hPutStrLn h cmd
45 readResponse h
46 outputStrLn resp
47
48-- | Get one line of input and send it to the daemon, then run the
49-- passed continuation if it wasn't "quit".
50interactiveMode :: Handle -> InputT IO () -> InputT IO ()
51interactiveMode h repl = do
52 minput <- getInputLine "dht> "
53 case minput of
54 Nothing -> return ()
55 Just "quit" -> sendCommand h "quit" >> return ()
56 Just cmd -> sendCommand h cmd >> repl
57
58main :: IO ()
59main = do
60 -- Open the control socket to the daemon.
61 h <- liftIO $ handle (\e -> do hPutStrLn stderr (show (e ::IOError))
62 exitFailure)
63 $ do sock <- socket AF_UNIX Stream defaultProtocol
64 connect sock (SockAddrUnix "dht.sock")
65 socketToHandle sock ReadWriteMode
66
67 -- Haskeline's default looks only at our stdin and not our stdout.
68 -- That's a bad idea because we can take input from the command line.
69 behavior <- do
70 useTerminal <- and <$> mapM hIsTerminalDevice [stdin,stdout]
71 return $ if useTerminal then preferTerm else useFileHandle stdin
72
73 runInputTBehaviorWithPrefs behavior defaultPrefs defaultSettings $ do
74
75 -- A command may be specified on the command line
76 -- or else we enter an interactive shell.
77 args <- dropWhile isSpace . unwords <$> liftIO getArgs
78 case args of
79 (_:_) -> sendCommand h args >> sendCommand h "quit"
80 _ -> fix $ interactiveMode h
diff --git a/examples/dhtd.hs b/examples/dhtd.hs
new file mode 100644
index 00000000..6bf48595
--- /dev/null
+++ b/examples/dhtd.hs
@@ -0,0 +1,192 @@
1{-# LANGUAGE NondecreasingIndentation #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE LambdaCase #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE TupleSections #-}
7
8import Control.Arrow;
9import Control.Concurrent
10import Control.Exception.Lifted as Lifted
11import Control.Monad
12import Control.Monad.Logger
13import Control.Monad.Reader
14import Data.Char
15import Data.Default
16import Data.List as L
17import Data.Maybe
18import qualified Data.ByteString as B (ByteString,writeFile,readFile)
19 ; import Data.ByteString (ByteString)
20import System.IO
21import System.IO.Error
22import Text.PrettyPrint.HughesPJClass
23import Text.Printf
24import Control.Monad.Reader.Class
25
26import Network.BitTorrent.Address
27import Network.BitTorrent.DHT
28import qualified Network.BitTorrent.DHT.Routing as R
29import Network.BitTorrent.DHT.Session
30import Network.SocketLike
31import Network.StreamServer
32
33mkNodeAddr :: SockAddr -> NodeAddr IPv4
34mkNodeAddr addr = NodeAddr (fromJust $ fromSockAddr addr)
35 (fromMaybe 0 $ sockAddrPort addr) -- FIXME
36
37btBindAddr :: String -> Bool -> IO (NodeAddr IPv4)
38btBindAddr s b = mkNodeAddr <$> getBindAddress s b
39
40printReport :: MonadIO m => [(String,String)] -> m ()
41printReport kvs = liftIO $ do
42 putStrLn (showReport kvs)
43 hFlush stdout
44
45showReport :: [(String,String)] -> String
46showReport kvs = do
47 let colwidth = maximum $ map (length . fst) kvs
48 (k,v) <- kvs
49 concat [ printf " %-*s" (colwidth+1) k, v, "\n" ]
50
51showEnry :: Show a => (NodeInfo a, t) -> [Char]
52showEnry (n,_) = intercalate " "
53 [ show $ pPrint (nodeId n)
54 , show $ nodeAddr n
55 ]
56
57printTable :: DHT IPv4 ()
58printTable = do
59 t <- showTable
60 liftIO $ do
61 putStrLn t
62 hFlush stdout
63
64showTable :: DHT IPv4 String
65showTable = do
66 nodes <- R.toList <$> getTable
67 return $ showReport
68 $ map (show *** showEnry)
69 $ concat $ zipWith map (map (,) [0::Int ..]) nodes
70
71bootstrapNodes :: IO [NodeAddr IPv4]
72bootstrapNodes = mapMaybe fromAddr
73 <$> mapM resolveHostName defaultBootstrapNodes
74
75-- ExtendedCaps (Map.singleton
76
77noDebugPrints :: LogSource -> LogLevel -> Bool
78noDebugPrints _ = \case LevelDebug -> False
79 _ -> True
80
81noLogging :: LogSource -> LogLevel -> Bool
82noLogging _ _ = False
83
84
85resume :: DHT IPv4 (Maybe B.ByteString)
86resume = do
87 restore_attempt <- liftIO $ tryIOError $ B.readFile "dht-nodes.dat"
88 saved_nodes <-
89 either (const $ do liftIO $ putStrLn "Error reading dht-nodes.dat"
90 return Nothing)
91 (return . Just)
92 restore_attempt
93 return saved_nodes
94
95godht :: forall b. (NodeAddr IPv4 -> NodeId -> DHT IPv4 b) -> IO b
96godht f = do
97 a <- btBindAddr "8008" False
98 dht def { optTimeout = 5 } a (const $ const True) $ do
99 me0 <- asks tentativeNodeId
100 printReport [("tentative node-id",show $ pPrint me0)
101 ,("listen-address", show a)
102 ]
103 f a me0
104
105marshalForClient :: String -> String
106marshalForClient s = show (length s) ++ ":" ++ s
107
108hPutClient :: Handle -> String -> IO ()
109hPutClient h s = hPutStr h (marshalForClient s)
110
111clientSession :: Node IPv4 -> MVar () -> RestrictedSocket -> Int -> Handle -> IO ()
112clientSession st signalQuit sock n h = do
113 line <- map toLower . dropWhile isSpace <$> hGetLine h
114 let cmd action = action >> clientSession st signalQuit sock n h
115 case line of
116
117 "quit" -> hPutClient h "goodbye." >> hClose h
118
119 "stop" -> do hPutClient h "Terminating DHT Daemon."
120 hClose h
121 putMVar signalQuit ()
122
123 "ls" -> cmd $ join $ runDHT st $ do
124 tbl <- getTable
125 t <- showTable
126 me <- myNodeIdAccordingTo (read "8.8.8.8:6881")
127 ip <- routableAddress
128 return $ do
129 hPutClient h $ unlines
130 [ t
131 , showReport
132 [ ("node-id", show $ pPrint me)
133 , ("internet address", show ip)
134 , ("buckets", show $ R.shape tbl)]
135 ]
136
137 _ -> cmd $ hPutClient h "error."
138
139main :: IO ()
140main = do
141 godht $ \a me0 -> do
142 printTable
143 bs <- liftIO bootstrapNodes
144 `onException`
145 (Lifted.ioError $ userError "unable to resolve bootstrap nodes")
146 saved_nodes <- resume
147
148 when (isJust saved_nodes) $ do
149 b <- isBootstrapped
150 tbl <- getTable
151 bc <- optBucketCount <$> asks options
152 printTable
153 me <- case concat $ R.toList tbl of
154 (n,_):_ -> myNodeIdAccordingTo (nodeAddr n)
155 _ -> return me0
156 printReport [("node-id",show $ pPrint me)
157 ,("listen-address", show a)
158 ,("bootstrapped", show b)
159 ,("buckets", show $ R.shape tbl)
160 ,("optBucketCount", show bc)
161 ,("dht-nodes.dat", "Running bootstrap...")
162 ]
163
164 st <- ask
165 waitForSignal <- liftIO $ do
166 signalQuit <- newEmptyMVar
167 srv <- streamServer (withSession $ clientSession st signalQuit) (SockAddrUnix "dht.sock")
168 return $ liftIO $ do
169 () <- takeMVar signalQuit
170 quitListening srv
171
172 bootstrap saved_nodes bs
173
174 b <- isBootstrapped
175 tbl <- getTable
176 bc <- optBucketCount <$> asks options
177 printTable
178 ip <- routableAddress
179 me <- case concat $ R.toList tbl of
180 (n,_):_ -> myNodeIdAccordingTo (nodeAddr n)
181 _ -> return me0
182 printReport [("node-id",show $ pPrint me)
183 ,("internet address", show ip)
184 ,("listen-address", show a)
185 ,("bootstrapped", show b)
186 ,("buckets", show $ R.shape tbl)
187 ,("optBucketCount", show bc)
188 ]
189 snapshot >>= liftIO . B.writeFile "dht-nodes.dat"
190
191 waitForSignal
192